Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,6 +11,7 @@ old_code
|
||||
created_ncas
|
||||
assets/romfs/shaders
|
||||
.vscode/settings.json
|
||||
.idea
|
||||
info/
|
||||
romfs/shaders
|
||||
assets/unused
|
||||
|
||||
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
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -66,7 +71,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 +107,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 direito (R)",
|
||||
|
||||
"FileBrowser": "Arquivos",
|
||||
"%zd files": "%zd arquivo(s)",
|
||||
@@ -115,6 +122,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",
|
||||
@@ -132,7 +140,7 @@
|
||||
"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!",
|
||||
@@ -164,8 +172,16 @@
|
||||
"app_dls: %s": "downloads: %s",
|
||||
"More by Author": "Mais deste autor",
|
||||
"Leave Feedback": "Deixar um feedback",
|
||||
|
||||
"Game Options": "Opções de jogo",
|
||||
"Launch random game": "Iniciar jogo aleatório",
|
||||
"List meta records": "Listar registro de metadados",
|
||||
"Entries": "Entradas",
|
||||
"Delete entity": "Excluir entidade",
|
||||
"Hide forwarders": "Ocultar atalhos (forwarders)",
|
||||
|
||||
"Irs": "Sensor infravermelho",
|
||||
"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,6 +232,58 @@
|
||||
"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",
|
||||
"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 base",
|
||||
"Skip patch": "Pular patch",
|
||||
"Skip dlc": "Pular DLC",
|
||||
"Skip data patch": "Pular patch de dados",
|
||||
"Skip ticket": "Pular ticket",
|
||||
"skip NCA hash verify": "Pular verificação de hash NCA",
|
||||
"Skip RSA header verify": "Pular verificação de header RSA",
|
||||
"Skip RSA NPDM verify": "Pular verificação 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 ",
|
||||
@@ -235,7 +303,7 @@
|
||||
"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 ",
|
||||
@@ -254,4 +322,5 @@
|
||||
"Are you sure you wish to cancel?": "Você tem certeza que quer cancelar?",
|
||||
"Audio disabled due to suspended game": "Áudio desativado devido ao software suspenso",
|
||||
"If this message appears repeatedly, please open an issue.": "Se esta mensagem aparecer repetidamente, abra um issue."
|
||||
|
||||
}
|
||||
|
||||
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.8.0)
|
||||
set(sphaira_VERSION 0.10.2)
|
||||
|
||||
project(sphaira
|
||||
VERSION ${sphaira_VERSION}
|
||||
@@ -49,6 +49,8 @@ add_executable(sphaira
|
||||
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
|
||||
@@ -60,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
|
||||
@@ -68,16 +70,21 @@ 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
|
||||
@@ -98,6 +105,7 @@ add_executable(sphaira
|
||||
target_compile_definitions(sphaira PRIVATE
|
||||
-DAPP_VERSION="${sphaira_VERSION}"
|
||||
-DAPP_VERSION_HASH="${sphaira_VERSION_HASH}"
|
||||
-DCURL_NO_OLDIES=1
|
||||
)
|
||||
|
||||
target_compile_options(sphaira PRIVATE
|
||||
@@ -153,7 +161,7 @@ FetchContent_Declare(ftpsrv
|
||||
|
||||
FetchContent_Declare(libhaze
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git
|
||||
GIT_TAG 3244b9e
|
||||
GIT_TAG 04f1526
|
||||
)
|
||||
|
||||
FetchContent_Declare(libpulsar
|
||||
@@ -277,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})
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "owo.hpp"
|
||||
#include "option.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
#include <switch.h>
|
||||
#include <vector>
|
||||
@@ -77,6 +78,8 @@ 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 GetThemeMusicEnable() -> bool;
|
||||
@@ -89,7 +92,8 @@ 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 SetThemeMusicEnable(bool enable);
|
||||
@@ -123,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;
|
||||
@@ -142,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";
|
||||
@@ -184,11 +207,14 @@ public:
|
||||
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
|
||||
option::OptionBool m_12hour_time{INI_SECTION, "12hour_time", false};
|
||||
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto
|
||||
option::OptionString m_right_side_menu{INI_SECTION, "right_side_menu", "Appstore"};
|
||||
|
||||
// install options
|
||||
option::OptionBool m_install{INI_SECTION, "install", false};
|
||||
option::OptionBool m_install_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};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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,6 +5,11 @@
|
||||
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(bool touch, s64 index)>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -124,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;
|
||||
@@ -135,8 +136,12 @@ struct Menu final : MenuBase {
|
||||
private:
|
||||
void SetIndex(s64 index);
|
||||
void InstallForwarder();
|
||||
void InstallFile(const FileEntry& target);
|
||||
void InstallFiles(const std::vector<FileEntry>& targets);
|
||||
|
||||
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);
|
||||
@@ -156,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,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;
|
||||
|
||||
@@ -38,13 +38,14 @@ 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;
|
||||
std::shared_ptr<StreamFtp> m_source{};
|
||||
Thread m_thread{};
|
||||
Mutex m_mutex{};
|
||||
// the below are shared across threads, lock with the above mutex!
|
||||
@@ -54,6 +55,7 @@ struct Menu final : MenuBase {
|
||||
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
|
||||
@@ -10,14 +10,16 @@
|
||||
namespace sphaira::ui::menu::gc {
|
||||
|
||||
struct GcCollection : yati::container::CollectionEntry {
|
||||
GcCollection(const char* _name, s64 _size, u8 _type) {
|
||||
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>;
|
||||
@@ -41,13 +43,16 @@ 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();
|
||||
@@ -57,6 +62,8 @@ 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{};
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -8,8 +8,10 @@ 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,
|
||||
Connected_StartingTransfer,
|
||||
// set whilst transfer is in progress.
|
||||
Progress,
|
||||
// set when the transfer is finished.
|
||||
@@ -22,6 +24,7 @@ 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;
|
||||
@@ -35,9 +38,8 @@ struct Menu final : MenuBase {
|
||||
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{};
|
||||
u32 m_usb_speed{};
|
||||
u32 m_usb_count{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::usb
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "nanovg.h"
|
||||
#include "ui/types.hpp"
|
||||
#include "ui/scrolling_text.hpp"
|
||||
|
||||
namespace sphaira::ui::gfx {
|
||||
|
||||
@@ -18,6 +19,9 @@ void drawRect(NVGcontext*, const Vec4& v, const NVGpaint& p, float rounding = 0.
|
||||
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);
|
||||
@@ -35,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{};
|
||||
|
||||
@@ -17,7 +17,7 @@ struct ProgressBox final : Widget {
|
||||
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();
|
||||
|
||||
@@ -38,11 +38,17 @@ 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;
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
@@ -2,12 +2,16 @@
|
||||
|
||||
#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
|
||||
|
||||
@@ -69,7 +69,25 @@ struct EticketRsaDeviceKey {
|
||||
static_assert(sizeof(EticketRsaDeviceKey) == 0x240);
|
||||
|
||||
// es functions.
|
||||
Result ImportTicket(Service* srv, const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size);
|
||||
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);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
#include "fs.hpp"
|
||||
#include "keys.hpp"
|
||||
#include "ncm.hpp"
|
||||
#include <switch.h>
|
||||
#include <vector>
|
||||
|
||||
namespace sphaira::nca {
|
||||
|
||||
@@ -41,6 +44,7 @@ enum KeyGeneration {
|
||||
KeyGeneration_1700 = 0x11,
|
||||
KeyGeneration_1800 = 0x12,
|
||||
KeyGeneration_1900 = 0x13,
|
||||
KeyGeneration_2000 = 0x14,
|
||||
KeyGeneration_Invalid = 0xFF,
|
||||
};
|
||||
|
||||
@@ -175,7 +179,7 @@ struct Header {
|
||||
u8 old_key_gen; // see KeyGenerationOld.
|
||||
u8 kaek_index; // see KeyAreaEncryptionKeyIndex.
|
||||
u64 size;
|
||||
u64 title_id;
|
||||
u64 program_id;
|
||||
u32 context_id;
|
||||
u32 sdk_version;
|
||||
u8 key_gen; // see KeyGeneration.
|
||||
@@ -215,6 +219,10 @@ 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
|
||||
|
||||
@@ -31,10 +31,16 @@ union ExtendedHeader {
|
||||
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);
|
||||
|
||||
|
||||
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), ())
|
||||
@@ -14,6 +14,10 @@ struct Base {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual void SignalCancel() {
|
||||
|
||||
}
|
||||
|
||||
Result GetOpenResult() const {
|
||||
return m_open_result;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include "base.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "usb/usbds.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
@@ -21,32 +25,26 @@ struct Usb final : Base {
|
||||
~Usb();
|
||||
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||
Result Finished() const;
|
||||
Result Finished(u64 timeout);
|
||||
|
||||
Result Init();
|
||||
Result WaitForConnection(u64 timeout, u32& speed, u32& count);
|
||||
Result GetFileInfo(std::string& name_out, u64& size_out);
|
||||
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:
|
||||
enum UsbSessionEndpoint {
|
||||
UsbSessionEndpoint_In = 0,
|
||||
UsbSessionEndpoint_Out = 1,
|
||||
};
|
||||
|
||||
Result SendCommand(s64 off, s64 size) const;
|
||||
Result InternalRead(void* buf, s64 off, s64 size) const;
|
||||
|
||||
bool GetConfigured() const;
|
||||
Event *GetCompletionEvent(UsbSessionEndpoint ep) const;
|
||||
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) const;
|
||||
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) const;
|
||||
Result GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) const;
|
||||
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) const;
|
||||
Result SendCmdHeader(u32 cmdId, size_t dataSize, u64 timeout);
|
||||
Result SendFileRangeCmd(u64 offset, u64 size, u64 timeout);
|
||||
|
||||
private:
|
||||
UsbDsInterface* m_interface{};
|
||||
UsbDsEndpoint* m_endpoints[2]{};
|
||||
u64 m_transfer_timeout{};
|
||||
std::unique_ptr<usb::UsbDs> m_usb;
|
||||
std::string m_transfer_file_name{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include "fs.hpp"
|
||||
#include "source/base.hpp"
|
||||
#include "container/base.hpp"
|
||||
#include "nx/ncm.hpp"
|
||||
#include "ui/progress_box.hpp"
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
@@ -71,6 +70,10 @@ enum : Result {
|
||||
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{};
|
||||
|
||||
@@ -132,7 +135,4 @@ Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> so
|
||||
Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr<container::Base> container, const ConfigOverride& override = {});
|
||||
Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const container::Collections& collections, const ConfigOverride& override = {});
|
||||
|
||||
Result ParseCnmtNca(const fs::FsPath& path, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos);
|
||||
Result ParseControlNca(const fs::FsPath& path, u64 id, void* nacp_out = nullptr, s64 nacp_size = 0, std::vector<u8>* icon_out = nullptr);
|
||||
|
||||
} // namespace sphaira::yati
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "ui/option_box.hpp"
|
||||
#include "ui/bubbles.hpp"
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/popup_list.hpp"
|
||||
#include "ui/option_box.hpp"
|
||||
@@ -7,12 +6,6 @@
|
||||
#include "ui/error_box.hpp"
|
||||
|
||||
#include "ui/menus/main_menu.hpp"
|
||||
#include "ui/menus/irs_menu.hpp"
|
||||
#include "ui/menus/themezer.hpp"
|
||||
#include "ui/menus/ghdl.hpp"
|
||||
#include "ui/menus/usb_menu.hpp"
|
||||
#include "ui/menus/ftp_menu.hpp"
|
||||
#include "ui/menus/gc_menu.hpp"
|
||||
|
||||
#include "app.hpp"
|
||||
#include "log.hpp"
|
||||
@@ -26,7 +19,6 @@
|
||||
#include "defines.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "ftpsrv_helper.hpp"
|
||||
#include "web.hpp"
|
||||
|
||||
#include <nanovg_dk.h>
|
||||
#include <minIni.h>
|
||||
@@ -46,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]{};
|
||||
};
|
||||
|
||||
@@ -283,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;
|
||||
@@ -339,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")) {
|
||||
@@ -581,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 {
|
||||
@@ -687,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 {
|
||||
@@ -737,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) {
|
||||
@@ -762,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();
|
||||
}
|
||||
@@ -1013,7 +1049,6 @@ void App::Draw() {
|
||||
}
|
||||
|
||||
m_notif_manager.Draw(vg, &m_theme);
|
||||
ui::bubble::Draw(vg, &m_theme);
|
||||
|
||||
nvgResetTransform(vg);
|
||||
nvgEndFrame(this->vg);
|
||||
@@ -1116,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) {
|
||||
@@ -1146,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1193,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)) {
|
||||
@@ -1221,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()) {
|
||||
@@ -1312,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");
|
||||
@@ -1362,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;
|
||||
@@ -1395,36 +1444,8 @@ App::App(const char* argv0) {
|
||||
}
|
||||
}
|
||||
|
||||
struct EventDay {
|
||||
u8 day;
|
||||
u8 month;
|
||||
};
|
||||
|
||||
static constexpr EventDay event_days[] = {
|
||||
{ .day = 1, .month = 1 }, // New years
|
||||
|
||||
{ .day = 3, .month = 3 }, // March 3 (switch 1)
|
||||
{ .day = 10, .month = 5 }, // June 10 (switch 2)
|
||||
{ .day = 15, .month = 5 }, // June 15
|
||||
|
||||
{ .day = 25, .month = 12 }, // Christmas
|
||||
{ .day = 26, .month = 12 },
|
||||
{ .day = 27, .month = 12 },
|
||||
{ .day = 28, .month = 12 },
|
||||
};
|
||||
|
||||
const auto time = std::time(nullptr);
|
||||
const auto tm = std::localtime(&time);
|
||||
|
||||
for (auto e : event_days) {
|
||||
if (e.day == tm->tm_mday && e.month == (tm->tm_mon + 1)) {
|
||||
ui::bubble::Init();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
App::Push(std::make_shared<ui::menu::main::MainMenu>());
|
||||
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) {
|
||||
@@ -1450,17 +1471,34 @@ void App::DisplayThemeOptions(bool left_side) {
|
||||
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, [theme_items](s64& index_out){
|
||||
options->Add(std::make_shared<ui::SidebarEntryArray>("Select Theme"_i18n, theme_items, [](s64& index_out){
|
||||
App::SetTheme(index_out);
|
||||
}, App::GetThemeIndex()));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [](bool& enable){
|
||||
App::SetThemeMusicEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [](bool& enable){
|
||||
App::Set12HourTimeEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("Download Default Music"_i18n, [](){
|
||||
// check if we already have music
|
||||
if (fs::FileExists(DEFAULT_MUSIC_PATH)) {
|
||||
App::Push(std::make_shared<ui::OptionBox>(
|
||||
"Overwrite current default music?"_i18n,
|
||||
"No"_i18n, "Yes"_i18n, 0, [](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
download_default_music();
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
} else {
|
||||
download_default_music();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void App::DisplayNetworkOptions(bool left_side) {
|
||||
@@ -1471,37 +1509,17 @@ void App::DisplayMiscOptions(bool left_side) {
|
||||
auto options = std::make_shared<ui::Sidebar>("Misc Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("Themezer"_i18n, [](){
|
||||
App::Push(std::make_shared<ui::menu::themezer::Menu>());
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("GitHub"_i18n, [](){
|
||||
App::Push(std::make_shared<ui::menu::gh::Menu>());
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("Irs"_i18n, [](){
|
||||
App::Push(std::make_shared<ui::menu::irs::Menu>());
|
||||
}));
|
||||
|
||||
if (App::IsApplication()) {
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("Web"_i18n, [](){
|
||||
WebShow("https://lite.duckduckgo.com/lite");
|
||||
}));
|
||||
}
|
||||
|
||||
if (App::GetApp()->m_install.Get()) {
|
||||
if (App::GetFtpEnable()) {
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("Ftp Install"_i18n, [](){
|
||||
App::Push(std::make_shared<ui::menu::ftp::Menu>());
|
||||
}));
|
||||
for (auto& e : ui::menu::main::GetMiscMenuEntries()) {
|
||||
if (e.name == g_app->m_right_side_menu.Get()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("Usb Install"_i18n, [](){
|
||||
App::Push(std::make_shared<ui::menu::usb::Menu>());
|
||||
}));
|
||||
if (e.IsInstall() && !App::GetInstallEnable()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("GameCard Install"_i18n, [](){
|
||||
App::Push(std::make_shared<ui::menu::gc::Menu>());
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>(i18n::get(e.title), [e](){
|
||||
App::Push(e.func());
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1515,17 +1533,52 @@ void App::DisplayAdvancedOptions(bool left_side) {
|
||||
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);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [](bool& enable){
|
||||
App::SetReplaceHbmenuEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [](s64& index_out){
|
||||
App::SetTextScrollSpeed(index_out);
|
||||
}, (s64)App::GetTextScrollSpeed()));
|
||||
}, 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);
|
||||
@@ -1540,77 +1593,85 @@ void App::DisplayInstallOptions(bool left_side) {
|
||||
install_items.push_back("System memory"_i18n);
|
||||
install_items.push_back("microSD card"_i18n);
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Enable"_i18n, App::GetInstallEnable(), [](bool& enable){
|
||||
App::SetInstallEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_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>("Show install warning"_i18n, App::GetInstallPrompt(), [](bool& enable){
|
||||
App::SetInstallPrompt(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
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);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip if already installed"_i18n, App::GetApp()->m_skip_if_already_installed.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_if_already_installed.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Ticket only"_i18n, App::GetApp()->m_ticket_only.Get(), [](bool& enable){
|
||||
App::GetApp()->m_ticket_only.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip base"_i18n, App::GetApp()->m_skip_base.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_base.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip Patch"_i18n, App::GetApp()->m_skip_patch.Get(), [](bool& enable){
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip patch"_i18n, App::GetApp()->m_skip_patch.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_patch.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip addon"_i18n, App::GetApp()->m_skip_addon.Get(), [](bool& 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);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip data patch"_i18n, App::GetApp()->m_skip_data_patch.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_data_patch.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip ticket"_i18n, App::GetApp()->m_skip_ticket.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_ticket.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_nca_hash_verify.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip RSA header verify"_i18n, App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_rsa_header_fixed_key_verify.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip RSA NPDM verify"_i18n, App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Ignore distribution bit"_i18n, App::GetApp()->m_ignore_distribution_bit.Get(), [](bool& enable){
|
||||
App::GetApp()->m_ignore_distribution_bit.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [](bool& enable){
|
||||
App::GetApp()->m_convert_to_standard_crypto.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Lower master key"_i18n, App::GetApp()->m_lower_master_key.Get(), [](bool& enable){
|
||||
App::GetApp()->m_lower_master_key.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Lower system version"_i18n, App::GetApp()->m_lower_system_version.Get(), [](bool& enable){
|
||||
App::GetApp()->m_lower_system_version.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
}
|
||||
|
||||
App::~App() {
|
||||
@@ -1619,8 +1680,6 @@ App::~App() {
|
||||
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();
|
||||
@@ -1685,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 {
|
||||
@@ -1723,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
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -715,7 +715,7 @@ void write_nca_header_encypted(nca::Header& nca_header, u64 tid, const keys::Key
|
||||
nca_header.magic = NCA3_MAGIC;
|
||||
nca_header.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();
|
||||
|
||||
@@ -857,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;
|
||||
|
||||
@@ -980,6 +981,12 @@ 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 = ns::DeleteApplicationRecord(srv_ptr, tid);
|
||||
|
||||
@@ -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,16 +31,140 @@ 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 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;
|
||||
|
||||
@@ -105,7 +235,31 @@ void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 index, s64 cou
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -114,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()) {
|
||||
@@ -143,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,7 +235,7 @@ 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 ? 15 : 0);
|
||||
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;
|
||||
@@ -244,7 +243,7 @@ void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, float x, f
|
||||
nvgIntersectScissor(vg, x, y, w, h);
|
||||
}
|
||||
|
||||
gfx::drawImage(vg, ix, iy, iw, ih, i.image, rounded_image ? 15 : 0);
|
||||
gfx::drawImage(vg, ix, iy, iw, ih, i.image, rounded_image ? 5 : 0);
|
||||
if (crop) {
|
||||
nvgRestore(vg);
|
||||
}
|
||||
@@ -590,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)
|
||||
@@ -838,19 +845,42 @@ 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::B, Action{"Back"_i18n, [this](){
|
||||
if (m_is_author) {
|
||||
m_is_author = false;
|
||||
if (m_is_search) {
|
||||
SetSearch(m_search_term);
|
||||
} else {
|
||||
SetFilter();
|
||||
}
|
||||
|
||||
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](){
|
||||
if (m_entries_current.empty()) {
|
||||
// log_write("pushing A when empty: size: %zu count: %zu\n", repo_json.size(), m_entries_current.size());
|
||||
@@ -882,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;
|
||||
@@ -922,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() {
|
||||
@@ -1024,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::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image, 15);
|
||||
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image, 20);
|
||||
break;
|
||||
case EntryStatus::Installed:
|
||||
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image, 15);
|
||||
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image, 20);
|
||||
break;
|
||||
case EntryStatus::Local:
|
||||
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image, 15);
|
||||
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image, 20);
|
||||
break;
|
||||
case EntryStatus::Update:
|
||||
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image, 15);
|
||||
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image, 20);
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -1096,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;
|
||||
}
|
||||
}
|
||||
@@ -1185,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];
|
||||
|
||||
@@ -1211,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;
|
||||
@@ -1224,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;
|
||||
@@ -1233,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;
|
||||
@@ -1254,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();
|
||||
}
|
||||
@@ -1293,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);
|
||||
@@ -1327,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);
|
||||
|
||||
@@ -18,10 +18,13 @@
|
||||
#include "owo.hpp"
|
||||
#include "swkbd.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "location.hpp"
|
||||
|
||||
#include "yati/yati.hpp"
|
||||
#include "yati/source/file.hpp"
|
||||
|
||||
#include <minIni.h>
|
||||
#include <minizip/zip.h>
|
||||
#include <minizip/unzip.h>
|
||||
#include <dirent.h>
|
||||
#include <cstring>
|
||||
@@ -57,6 +60,14 @@ constexpr std::string_view IMAGE_EXTENSIONS[] = {
|
||||
constexpr std::string_view INSTALL_EXTENSIONS[] = {
|
||||
"nsp", "xci", "nsz", "xcz",
|
||||
};
|
||||
// these are files that are already compressed or encrypted and should
|
||||
// be stored raw in a zip file.
|
||||
constexpr std::string_view COMPRESSED_EXTENSIONS[] = {
|
||||
"zip", "xz", "7z", "rar", "tar", "nca", "nsp", "xci", "nsz", "xcz"
|
||||
};
|
||||
constexpr std::string_view ZIP_EXTENSIONS[] = {
|
||||
"zip",
|
||||
};
|
||||
|
||||
|
||||
struct RomDatabaseEntry {
|
||||
@@ -267,6 +278,25 @@ auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, const
|
||||
|
||||
Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i18n}, m_nro_entries{nro_entries} {
|
||||
this->SetActions(
|
||||
std::make_pair(Button::L2, Action{[this](){
|
||||
if (!m_selected_files.empty()) {
|
||||
ResetSelection();
|
||||
}
|
||||
|
||||
const auto set = m_selected_count != m_entries_current.size();
|
||||
|
||||
for (u32 i = 0; i < m_entries_current.size(); i++) {
|
||||
auto& e = GetEntry(i);
|
||||
if (e.selected != set) {
|
||||
e.selected = set;
|
||||
if (set) {
|
||||
m_selected_count++;
|
||||
} else {
|
||||
m_selected_count--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::R2, Action{[this](){
|
||||
if (!m_selected_files.empty()) {
|
||||
ResetSelection();
|
||||
@@ -309,7 +339,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
||||
}
|
||||
}));
|
||||
} else if (App::GetInstallEnable() && IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) {
|
||||
InstallFile(GetEntry());
|
||||
InstallFiles();
|
||||
} else {
|
||||
const auto assoc_list = FindFileAssocFor();
|
||||
if (!assoc_list.empty()) {
|
||||
@@ -384,42 +414,31 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Show Hidden"_i18n, m_show_hidden.Get(), [this](bool& v_out){
|
||||
m_show_hidden.Set(v_out);
|
||||
SortAndFindLastFile();
|
||||
}, "Yes"_i18n, "No"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Folders First"_i18n, m_folders_first.Get(), [this](bool& v_out){
|
||||
m_folders_first.Set(v_out);
|
||||
SortAndFindLastFile();
|
||||
}, "Yes"_i18n, "No"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Hidden Last"_i18n, m_hidden_last.Get(), [this](bool& v_out){
|
||||
m_hidden_last.Set(v_out);
|
||||
SortAndFindLastFile();
|
||||
}, "Yes"_i18n, "No"_i18n));
|
||||
}));
|
||||
}));
|
||||
|
||||
if (m_entries_current.size()) {
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Cut"_i18n, [this](){
|
||||
if (!m_selected_count) {
|
||||
AddCurrentFileToSelection(SelectedType::Cut);
|
||||
} else {
|
||||
AddSelectedEntries(SelectedType::Cut);
|
||||
}
|
||||
AddSelectedEntries(SelectedType::Cut);
|
||||
}, true));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Copy"_i18n, [this](){
|
||||
if (!m_selected_count) {
|
||||
AddCurrentFileToSelection(SelectedType::Copy);
|
||||
} else {
|
||||
AddSelectedEntries(SelectedType::Copy);
|
||||
}
|
||||
AddSelectedEntries(SelectedType::Copy);
|
||||
}, true));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Delete"_i18n, [this](){
|
||||
if (!m_selected_count) {
|
||||
AddCurrentFileToSelection(SelectedType::Delete);
|
||||
} else {
|
||||
AddSelectedEntries(SelectedType::Delete);
|
||||
}
|
||||
AddSelectedEntries(SelectedType::Delete);
|
||||
|
||||
log_write("clicked on delete\n");
|
||||
App::Push(std::make_shared<OptionBox>(
|
||||
"Delete Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 0, [this](auto op_index){
|
||||
@@ -475,32 +494,96 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
||||
}));
|
||||
}
|
||||
|
||||
// if install is enabled, check if all currently selected files are installable.
|
||||
if (m_entries_current.size() && App::GetInstallEnable()) {
|
||||
bool should_install = true;
|
||||
// returns true if all entries match the ext array.
|
||||
const auto check_all_ext = [this](auto& exts){
|
||||
if (!m_selected_count) {
|
||||
should_install = IsExtension(GetEntry().GetExtension(), INSTALL_EXTENSIONS);
|
||||
return IsExtension(GetEntry().GetExtension(), exts);
|
||||
} else {
|
||||
const auto entries = GetSelectedEntries();
|
||||
for (auto&e : entries) {
|
||||
if (!IsExtension(e.GetExtension(), INSTALL_EXTENSIONS)) {
|
||||
should_install = false;
|
||||
break;
|
||||
if (!IsExtension(e.GetExtension(), exts)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (should_install) {
|
||||
// if install is enabled, check if all currently selected files are installable.
|
||||
if (m_entries_current.size() && App::GetInstallEnable()) {
|
||||
if (check_all_ext(INSTALL_EXTENSIONS)) {
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Install"_i18n, [this](){
|
||||
if (!m_selected_count) {
|
||||
InstallFile(GetEntry());
|
||||
InstallFiles();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (m_fs_type == FsType::Sd && m_entries_current.size()) {
|
||||
if (App::GetInstallEnable() && HasTypeInSelectedEntries(FsDirEntryType_File) && !m_selected_count && (GetEntry().GetExtension() == "nro" || !FindFileAssocFor().empty())) {
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Install Forwarder"_i18n, [this](){;
|
||||
if (App::GetInstallPrompt()) {
|
||||
App::Push(std::make_shared<OptionBox>(
|
||||
"WARNING: Installing forwarders will lead to a ban!"_i18n,
|
||||
"Back"_i18n, "Install"_i18n, 0, [this](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
InstallForwarder();
|
||||
}
|
||||
}
|
||||
));
|
||||
} else {
|
||||
InstallFiles(GetSelectedEntries());
|
||||
InstallForwarder();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (m_entries_current.size()) {
|
||||
if (check_all_ext(ZIP_EXTENSIONS)) {
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Extract zip"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Extract Options"_i18n, Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Extract here"_i18n, [this](){
|
||||
UnzipFiles("");
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Extract to root"_i18n, [this](){
|
||||
App::Push(std::make_shared<OptionBox>("Are you sure you want to extract to root?"_i18n,
|
||||
"No"_i18n, "Yes"_i18n, 0, [this](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
UnzipFiles("/");
|
||||
}
|
||||
}));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Extract to..."_i18n, [this](){
|
||||
std::string out;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, "Enter the path to the folder to extract into", fs::AppendPath(m_path, ""))) && !out.empty()) {
|
||||
UnzipFiles(out);
|
||||
}
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
if (!check_all_ext(ZIP_EXTENSIONS) || m_selected_count) {
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Compress to zip"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Compress Options"_i18n, Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Compress"_i18n, [this](){
|
||||
ZipFiles("");
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Compress to..."_i18n, [this](){
|
||||
std::string out;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, "Enter the path to the folder to extract into", m_path)) && !out.empty()) {
|
||||
ZipFiles(out);
|
||||
}
|
||||
}));
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
@@ -529,7 +612,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Create Folder"_i18n, [this](){
|
||||
std::string out;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, "Set Folder Name"_i18n.c_str())) && !out.empty()) {
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, "Set Folder Name"_i18n.c_str(), fs::AppendPath(m_path, ""))) && !out.empty()) {
|
||||
App::PopToMenu();
|
||||
|
||||
fs::FsPath full_path;
|
||||
@@ -555,28 +638,15 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
||||
}
|
||||
|
||||
if (m_fs_type == FsType::Sd && m_entries_current.size()) {
|
||||
if (App::GetInstallEnable() && HasTypeInSelectedEntries(FsDirEntryType_File) && !m_selected_count && (GetEntry().GetExtension() == "nro" || !FindFileAssocFor().empty())) {
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Install Forwarder"_i18n, [this](){;
|
||||
if (App::GetInstallPrompt()) {
|
||||
App::Push(std::make_shared<OptionBox>(
|
||||
"WARNING: Installing forwarders will lead to a ban!"_i18n,
|
||||
"Back"_i18n, "Install"_i18n, 0, [this](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
InstallForwarder();
|
||||
}
|
||||
}
|
||||
));
|
||||
} else {
|
||||
InstallForwarder();
|
||||
}
|
||||
}));
|
||||
}
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Upload"_i18n, [this](){
|
||||
UploadFiles();
|
||||
}));
|
||||
}
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Ignore read only"_i18n, m_ignore_read_only.Get(), [this](bool& v_out){
|
||||
m_ignore_read_only.Set(v_out);
|
||||
m_fs->SetIgnoreReadOnly(v_out);
|
||||
}, "Yes"_i18n, "No"_i18n));
|
||||
}));
|
||||
|
||||
SidebarEntryArray::Items mount_items;
|
||||
mount_items.push_back("Sd"_i18n);
|
||||
@@ -650,7 +720,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
}
|
||||
|
||||
auto text_id = ThemeEntryID_TEXT;
|
||||
if (m_index == i) {
|
||||
const auto selected = m_index == i;
|
||||
if (selected) {
|
||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||
} else {
|
||||
@@ -673,7 +744,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
} else if (IsExtension(ext, INSTALL_EXTENSIONS)) {
|
||||
// todo: maybe replace this icon with something else?
|
||||
icon = ThemeEntryID_ICON_NRO;
|
||||
} else if (IsExtension(ext, "zip")) {
|
||||
} else if (IsExtension(ext, ZIP_EXTENSIONS)) {
|
||||
icon = ThemeEntryID_ICON_ZIP;
|
||||
} else if (IsExtension(ext, "nro")) {
|
||||
icon = ThemeEntryID_ICON_NRO;
|
||||
@@ -820,12 +891,9 @@ void Menu::InstallForwarder() {
|
||||
));
|
||||
}
|
||||
|
||||
void Menu::InstallFile(const FileEntry& target) {
|
||||
std::vector<FileEntry> targets{target};
|
||||
InstallFiles(targets);
|
||||
}
|
||||
void Menu::InstallFiles() {
|
||||
const auto targets = GetSelectedEntries();
|
||||
|
||||
void Menu::InstallFiles(const std::vector<FileEntry>& targets) {
|
||||
App::Push(std::make_shared<OptionBox>("Install Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 0, [this, targets](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
App::PopToMenu();
|
||||
@@ -848,6 +916,343 @@ void Menu::InstallFiles(const std::vector<FileEntry>& targets) {
|
||||
}));
|
||||
}
|
||||
|
||||
void Menu::UnzipFiles(fs::FsPath dir_path) {
|
||||
const auto targets = GetSelectedEntries();
|
||||
|
||||
// set to current path.
|
||||
if (dir_path.empty()) {
|
||||
dir_path = m_path;
|
||||
}
|
||||
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Extracting "_i18n, "", [this, dir_path, targets](auto pbox) mutable -> bool {
|
||||
constexpr auto chunk_size = 1024 * 512; // 512KiB
|
||||
auto& fs = *m_fs.get();
|
||||
|
||||
for (auto& e : targets) {
|
||||
pbox->SetTitle(e.GetName());
|
||||
|
||||
const auto zip_out = GetNewPath(e);
|
||||
auto zfile = unzOpen64(zip_out);
|
||||
if (!zfile) {
|
||||
log_write("failed to open zip: %s\n", zip_out.s);
|
||||
return false;
|
||||
}
|
||||
ON_SCOPE_EXIT(unzClose(zfile));
|
||||
|
||||
unz_global_info64 pglobal_info;
|
||||
if (UNZ_OK != unzGetGlobalInfo64(zfile, &pglobal_info)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < pglobal_info.number_entry; i++) {
|
||||
if (i > 0) {
|
||||
if (UNZ_OK != unzGoToNextFile(zfile)) {
|
||||
log_write("failed to unzGoToNextFile\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
|
||||
log_write("failed to open current file\n");
|
||||
return false;
|
||||
}
|
||||
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
|
||||
|
||||
unz_file_info64 info;
|
||||
char name[512];
|
||||
if (UNZ_OK != unzGetCurrentFileInfo64(zfile, &info, name, sizeof(name), 0, 0, 0, 0)) {
|
||||
log_write("failed to get current info\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto file_path = fs::AppendPath(dir_path, name);
|
||||
pbox->NewTransfer(name);
|
||||
|
||||
// create directories
|
||||
fs.CreateDirectoryRecursivelyWithPath(file_path);
|
||||
|
||||
Result rc;
|
||||
if (R_FAILED(rc = fs.CreateFile(file_path, info.uncompressed_size, 0)) && rc != FsError_PathAlreadyExists) {
|
||||
log_write("failed to create file: %s 0x%04X\n", file_path.s, rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile f;
|
||||
if (R_FAILED(rc = fs.OpenFile(file_path, FsOpenMode_Write, &f))) {
|
||||
log_write("failed to open file: %s 0x%04X\n", file_path.s, rc);
|
||||
return false;
|
||||
}
|
||||
ON_SCOPE_EXIT(fsFileClose(&f));
|
||||
|
||||
if (R_FAILED(rc = fsFileSetSize(&f, info.uncompressed_size))) {
|
||||
log_write("failed to set file size: %s 0x%04X\n", file_path.s, rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<char> buf(chunk_size);
|
||||
s64 offset{};
|
||||
while (offset < info.uncompressed_size) {
|
||||
if (pbox->ShouldExit()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
|
||||
if (bytes_read <= 0) {
|
||||
log_write("failed to read zip file: %s\n", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (R_FAILED(rc = fsFileWrite(&f, offset, buf.data(), bytes_read, FsWriteOption_None))) {
|
||||
log_write("failed to write file: %s 0x%04X\n", file_path.s, rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
pbox->UpdateTransfer(offset, info.uncompressed_size);
|
||||
offset += bytes_read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [this](bool success){
|
||||
if (success) {
|
||||
App::Notify("Extract success!");
|
||||
} else {
|
||||
App::Notify("Extract failed!");
|
||||
}
|
||||
Scan(m_path);
|
||||
log_write("did extract\n");
|
||||
}));
|
||||
}
|
||||
|
||||
void Menu::ZipFiles(fs::FsPath zip_out) {
|
||||
const auto targets = GetSelectedEntries();
|
||||
|
||||
// set to current path.
|
||||
if (zip_out.empty()) {
|
||||
if (std::size(targets) == 1) {
|
||||
const auto name = targets[0].name;
|
||||
const auto ext = std::strrchr(targets[0].name, '.');
|
||||
fs::FsPath file_path;
|
||||
if (!ext) {
|
||||
std::snprintf(file_path, sizeof(file_path), "%s.zip", name);
|
||||
} else {
|
||||
std::snprintf(file_path, sizeof(file_path), "%.*s.zip", (int)(ext - name), name);
|
||||
}
|
||||
zip_out = fs::AppendPath(m_path, file_path);
|
||||
log_write("zip out: %s name: %s file_path: %s\n", zip_out.s, name, file_path.s);
|
||||
} else {
|
||||
// loop until we find an unused file name.
|
||||
for (u64 i = 0; ; i++) {
|
||||
fs::FsPath file_path = "Archive.zip";
|
||||
if (i) {
|
||||
std::snprintf(file_path, sizeof(file_path), "Archive (%zu).zip", i);
|
||||
}
|
||||
|
||||
zip_out = fs::AppendPath(m_path, file_path);
|
||||
if (!fs::FileExists(&m_fs->m_fs, zip_out)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!std::string_view(zip_out).ends_with(".zip")) {
|
||||
zip_out += ".zip";
|
||||
}
|
||||
}
|
||||
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Compressing "_i18n, "", [this, zip_out, targets](auto pbox) mutable -> bool {
|
||||
constexpr auto chunk_size = 1024 * 512; // 512KiB
|
||||
auto& fs = *m_fs.get();
|
||||
|
||||
const auto t = std::time(NULL);
|
||||
const auto tm = std::localtime(&t);
|
||||
|
||||
// pre-calculate the time rather than calculate it in the loop.
|
||||
zip_fileinfo zip_info{};
|
||||
zip_info.tmz_date.tm_sec = tm->tm_sec;
|
||||
zip_info.tmz_date.tm_min = tm->tm_min;
|
||||
zip_info.tmz_date.tm_hour = tm->tm_hour;
|
||||
zip_info.tmz_date.tm_mday = tm->tm_mday;
|
||||
zip_info.tmz_date.tm_mon = tm->tm_mon;
|
||||
zip_info.tmz_date.tm_year = tm->tm_year;
|
||||
|
||||
auto zfile = zipOpen(zip_out, APPEND_STATUS_CREATE);
|
||||
if (!zfile) {
|
||||
log_write("failed to open zip: %s\n", zip_out.s);
|
||||
return false;
|
||||
}
|
||||
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_VERSION_HASH));
|
||||
|
||||
const auto zip_add = [&](const fs::FsPath& file_path){
|
||||
// the file name needs to be relative to the current directory.
|
||||
const char* file_name_in_zip = file_path.s + std::strlen(m_path);
|
||||
|
||||
// root paths are banned in zips, they will warn when extracting otherwise.
|
||||
if (file_name_in_zip[0] == '/') {
|
||||
file_name_in_zip++;
|
||||
}
|
||||
|
||||
pbox->NewTransfer(file_name_in_zip);
|
||||
|
||||
const auto ext = std::strrchr(file_name_in_zip, '.');
|
||||
const auto raw = ext && IsExtension(ext + 1, COMPRESSED_EXTENSIONS);
|
||||
|
||||
if (ZIP_OK != zipOpenNewFileInZip2(zfile, file_name_in_zip, &zip_info, NULL, 0, NULL, 0, NULL, Z_DEFLATED, Z_DEFAULT_COMPRESSION, raw)) {
|
||||
return false;
|
||||
}
|
||||
ON_SCOPE_EXIT(zipCloseFileInZip(zfile));
|
||||
|
||||
FsFile f;
|
||||
Result rc;
|
||||
if (R_FAILED(rc = fs.OpenFile(file_path, FsOpenMode_Read, &f))) {
|
||||
log_write("failed to open file: %s 0x%04X\n", file_path.s, rc);
|
||||
return false;
|
||||
}
|
||||
ON_SCOPE_EXIT(fsFileClose(&f));
|
||||
|
||||
s64 file_size;
|
||||
if (R_FAILED(rc = fsFileGetSize(&f, &file_size))) {
|
||||
log_write("failed to get file size: %s 0x%04X\n", file_path.s, rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<char> buf(chunk_size);
|
||||
s64 offset{};
|
||||
while (offset < file_size) {
|
||||
if (pbox->ShouldExit()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
u64 bytes_read;
|
||||
if (R_FAILED(rc = fsFileRead(&f, offset, buf.data(), buf.size(), FsReadOption_None, &bytes_read))) {
|
||||
log_write("failed to write file: %s 0x%04X\n", file_path.s, rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ZIP_OK != zipWriteInFileInZip(zfile, buf.data(), bytes_read)) {
|
||||
log_write("failed to write zip file: %s\n", file_path.s);
|
||||
return false;
|
||||
}
|
||||
|
||||
pbox->UpdateTransfer(offset, file_size);
|
||||
offset += bytes_read;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
for (auto& e : targets) {
|
||||
pbox->SetTitle(e.GetName());
|
||||
if (e.IsFile()) {
|
||||
const auto file_path = GetNewPath(e);
|
||||
if (!zip_add(file_path)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
FsDirCollections collections;
|
||||
get_collections(GetNewPath(e), e.name, collections);
|
||||
|
||||
for (const auto& collection : collections) {
|
||||
for (const auto& file : collection.files) {
|
||||
const auto file_path = fs::AppendPath(collection.path, file.name);
|
||||
if (!zip_add(file_path)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [this](bool success){
|
||||
if (success) {
|
||||
App::Notify("Compress success!");
|
||||
} else {
|
||||
App::Notify("Compress failed!");
|
||||
}
|
||||
Scan(m_path);
|
||||
log_write("did compress\n");
|
||||
}));
|
||||
}
|
||||
|
||||
void Menu::UploadFiles() {
|
||||
const auto targets = GetSelectedEntries();
|
||||
|
||||
const auto network_locations = location::Load();
|
||||
if (network_locations.empty()) {
|
||||
App::Notify("No upload locations set!");
|
||||
return;
|
||||
}
|
||||
|
||||
PopupList::Items items;
|
||||
for (const auto&p : network_locations) {
|
||||
items.emplace_back(p.name);
|
||||
}
|
||||
|
||||
App::Push(std::make_shared<PopupList>(
|
||||
"Select upload location"_i18n, items, [this, network_locations](auto op_index){
|
||||
if (!op_index) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto loc = network_locations[*op_index];
|
||||
App::Push(std::make_shared<ProgressBox>(0, "Uploading"_i18n, "", [this, loc](auto pbox) -> bool {
|
||||
auto targets = GetSelectedEntries();
|
||||
|
||||
const auto file_add = [&](const fs::FsPath& file_path, const char* name){
|
||||
// the file name needs to be relative to the current directory.
|
||||
const auto relative_file_name = file_path.s + std::strlen(m_path);
|
||||
pbox->SetTitle(name);
|
||||
pbox->NewTransfer(relative_file_name);
|
||||
|
||||
const auto result = curl::Api().FromFile(
|
||||
CURL_LOCATION_TO_API(loc),
|
||||
curl::Path{file_path},
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()},
|
||||
curl::UploadInfo{relative_file_name}
|
||||
);
|
||||
|
||||
return result.success;
|
||||
};
|
||||
|
||||
for (auto& e : targets) {
|
||||
if (e.IsFile()) {
|
||||
const auto file_path = GetNewPath(e);
|
||||
if (!file_add(file_path, e.GetName().c_str())) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
FsDirCollections collections;
|
||||
get_collections(GetNewPath(e), e.name, collections);
|
||||
|
||||
for (const auto& collection : collections) {
|
||||
for (const auto& file : collection.files) {
|
||||
const auto file_path = fs::AppendPath(collection.path, file.name);
|
||||
if (!file_add(file_path, file.name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [this](bool success){
|
||||
ResetSelection();
|
||||
|
||||
if (success) {
|
||||
App::Notify("Upload successfull!");
|
||||
log_write("Upload successfull!!!\n");
|
||||
} else {
|
||||
App::Notify("Upload failed!");
|
||||
log_write("Upload failed!!!\n");
|
||||
}
|
||||
}));
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
|
||||
log_write("new scan path: %s\n", new_path.s);
|
||||
if (!is_walk_up && !m_path.empty() && !m_entries_current.empty()) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "i18n.hpp"
|
||||
#include "ftpsrv_helper.hpp"
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
|
||||
namespace sphaira::ui::menu::ftp {
|
||||
namespace {
|
||||
@@ -151,6 +152,12 @@ Menu::Menu() : MenuBase{"FTP Install (EXPERIMENTAL)"_i18n} {
|
||||
}});
|
||||
|
||||
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();
|
||||
@@ -170,6 +177,11 @@ Menu::~Menu() {
|
||||
m_source->Disable();
|
||||
}
|
||||
|
||||
if (!m_was_ftp_enabled) {
|
||||
log_write("[FTP] disabling on exit\n");
|
||||
App::SetFtpEnable(false);
|
||||
}
|
||||
|
||||
log_write("closing data!!!!\n");
|
||||
}
|
||||
|
||||
|
||||
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
@@ -7,6 +7,7 @@
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
|
||||
namespace sphaira::ui::menu::gc {
|
||||
namespace {
|
||||
@@ -44,6 +45,13 @@ auto BuildGcPath(const char* name, const FsGameCardHandle* handle, FsGameCardPar
|
||||
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;
|
||||
}
|
||||
@@ -174,30 +182,24 @@ Menu::Menu() : MenuBase{"GameCard"_i18n} {
|
||||
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));
|
||||
UpdateStorageSize();
|
||||
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.
|
||||
bool inserted{};
|
||||
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());
|
||||
} else {
|
||||
log_write("trying to unmount\n");
|
||||
GcUnmount();
|
||||
}
|
||||
if (R_SUCCEEDED(eventWait(std::addressof(m_event), 0))) {
|
||||
GcOnEvent();
|
||||
}
|
||||
|
||||
MenuBase::Update(controller, touch);
|
||||
@@ -267,6 +269,13 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
});
|
||||
}
|
||||
|
||||
void Menu::OnFocusGained() {
|
||||
MenuBase::OnFocusGained();
|
||||
|
||||
GcOnEvent();
|
||||
UpdateStorageSize();
|
||||
}
|
||||
|
||||
Result Menu::GcMount() {
|
||||
GcUnmount();
|
||||
|
||||
@@ -312,7 +321,7 @@ Result Menu::GcMount() {
|
||||
std::vector<u8> extended_header;
|
||||
std::vector<NcmPackagedContentInfo> infos;
|
||||
const auto path = BuildGcPath(e.name, &m_handle);
|
||||
R_TRY(yati::ParseCnmtNca(path, header, extended_header, infos));
|
||||
R_TRY(nca::ParseCnmt(path, 0, header, extended_header, infos));
|
||||
|
||||
u8 key_gen;
|
||||
FsRightsId rights_id;
|
||||
@@ -321,23 +330,24 @@ Result Menu::GcMount() {
|
||||
// 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);
|
||||
collections.emplace_back(e.name, e.file_size, NcmContentType_Meta, 0);
|
||||
|
||||
for (const auto& info : infos) {
|
||||
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.info.content_type == NcmContentType_DeltaFragment) {
|
||||
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.info.content_id);
|
||||
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.info.content_type);
|
||||
collections.emplace_back(it->name, it->file_size, info.content_type, info.id_offset);
|
||||
}
|
||||
|
||||
const auto app_id = ncm::GetAppId(header);
|
||||
@@ -388,20 +398,18 @@ Result Menu::GcMount() {
|
||||
} else {
|
||||
App::Notify("Gc install failed!"_i18n);
|
||||
}
|
||||
|
||||
UpdateStorageSize();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
if (m_entries.size() > 1) {
|
||||
SetAction(Button::L, Action{"Prev"_i18n, [this](){
|
||||
SetAction(Button::L2, Action{"Prev"_i18n, [this](){
|
||||
if (m_entry_index != 0) {
|
||||
OnChangeIndex(m_entry_index - 1);
|
||||
}
|
||||
}});
|
||||
SetAction(Button::R, Action{"Next"_i18n, [this](){
|
||||
SetAction(Button::R2, Action{"Next"_i18n, [this](){
|
||||
if (m_entry_index < m_entries.size()) {
|
||||
OnChangeIndex(m_entry_index + 1);
|
||||
}
|
||||
@@ -409,6 +417,7 @@ Result Menu::GcMount() {
|
||||
}
|
||||
|
||||
OnChangeIndex(0);
|
||||
m_mounted = true;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
@@ -420,8 +429,8 @@ void Menu::GcUnmount() {
|
||||
m_lang_entry = {};
|
||||
FreeImage();
|
||||
|
||||
RemoveAction(Button::L);
|
||||
RemoveAction(Button::R);
|
||||
RemoveAction(Button::L2);
|
||||
RemoveAction(Button::R2);
|
||||
}
|
||||
|
||||
Result Menu::GcPoll(bool* inserted) {
|
||||
@@ -439,6 +448,28 @@ Result Menu::GcPoll(bool* inserted) {
|
||||
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};
|
||||
@@ -464,6 +495,31 @@ void Menu::OnChangeIndex(s64 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.
|
||||
@@ -475,7 +531,16 @@ void Menu::OnChangeIndex(s64 new_index) {
|
||||
NacpStruct nacp;
|
||||
std::vector<u8> icon;
|
||||
const auto path = BuildGcPath(collection.name.c_str(), &m_handle);
|
||||
if (R_SUCCEEDED(yati::ParseControlNca(path, m_entries[m_entry_index].app_id, &nacp, sizeof(nacp), &icon))) {
|
||||
|
||||
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);
|
||||
|
||||
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,9 +25,14 @@ 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::A, Action{"Launch"_i18n, [this](){
|
||||
nro_launch(m_entries[m_index].path);
|
||||
@@ -52,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();
|
||||
@@ -62,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
|
||||
@@ -80,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));
|
||||
|
||||
@@ -97,7 +114,7 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
||||
if (op_index && *op_index) {
|
||||
InstallHomebrew();
|
||||
}
|
||||
}
|
||||
}, m_entries[m_index].image
|
||||
));
|
||||
} else {
|
||||
InstallHomebrew();
|
||||
@@ -108,17 +125,11 @@ 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) {
|
||||
@@ -141,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
|
||||
@@ -159,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::drawImage(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage(), 15);
|
||||
|
||||
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());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -238,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());
|
||||
|
||||
@@ -384,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);
|
||||
}
|
||||
@@ -394,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
|
||||
|
||||
@@ -6,6 +6,15 @@
|
||||
#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"
|
||||
@@ -22,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
|
||||
@@ -139,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},
|
||||
@@ -171,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;
|
||||
}
|
||||
@@ -208,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));
|
||||
@@ -227,6 +269,7 @@ 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, [](){
|
||||
App::DisplayThemeOptions();
|
||||
@@ -238,15 +281,15 @@ MainMenu::MainMenu() {
|
||||
|
||||
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(), [](bool& enable){
|
||||
App::SetMtpEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
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](){
|
||||
@@ -285,7 +328,7 @@ MainMenu::MainMenu() {
|
||||
|
||||
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();
|
||||
@@ -336,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);
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -414,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) {
|
||||
@@ -452,7 +453,7 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
}
|
||||
}));
|
||||
}}),
|
||||
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;
|
||||
@@ -460,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();
|
||||
@@ -536,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 {
|
||||
@@ -603,16 +605,14 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
}
|
||||
}
|
||||
|
||||
gfx::drawImage(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage(), 15);
|
||||
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());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
namespace sphaira::ui::menu::usb {
|
||||
namespace {
|
||||
|
||||
constexpr u64 CONNECTION_TIMEOUT = 1e+9 * 3;
|
||||
constexpr u64 TRANSFER_TIMEOUT = 1e+9 * 5;
|
||||
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);
|
||||
@@ -21,17 +22,29 @@ void thread_func(void* user) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto rc = app->m_usb_source->WaitForConnection(CONNECTION_TIMEOUT, app->m_usb_speed, app->m_usb_count);
|
||||
const auto rc = app->m_usb_source->IsUsbConnected(CONNECTION_TIMEOUT);
|
||||
if (rc == ::sphaira::usb::UsbDs::Result_Cancelled) {
|
||||
break;
|
||||
}
|
||||
|
||||
// set connected status
|
||||
mutexLock(&app->m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&app->m_mutex));
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
app->m_state = State::Connected_WaitForFileList;
|
||||
} else {
|
||||
app->m_state = State::None;
|
||||
}
|
||||
mutexUnlock(&app->m_mutex);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
app->m_state = State::Connected;
|
||||
break;
|
||||
} else if (R_FAILED(rc) && R_VALUE(rc) != 0xEA01) {
|
||||
log_write("got: 0x%X value: 0x%X\n", rc, R_VALUE(rc));
|
||||
app->m_state = State::Failed;
|
||||
break;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,14 +72,10 @@ Menu::Menu() : MenuBase{"USB"_i18n} {
|
||||
if (R_FAILED(m_usb_source->GetOpenResult())) {
|
||||
log_write("usb init open\n");
|
||||
m_state = State::Failed;
|
||||
} else {
|
||||
if (R_FAILED(m_usb_source->Init())) {
|
||||
log_write("usb init failed\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);
|
||||
@@ -76,6 +85,7 @@ Menu::Menu() : MenuBase{"USB"_i18n} {
|
||||
Menu::~Menu() {
|
||||
// signal for thread to exit and wait.
|
||||
m_stop_source.request_stop();
|
||||
m_usb_source->SignalCancel();
|
||||
threadWaitForExit(&m_thread);
|
||||
threadClose(&m_thread);
|
||||
|
||||
@@ -95,54 +105,38 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
switch (m_state) {
|
||||
case State::None:
|
||||
break;
|
||||
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));
|
||||
|
||||
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");
|
||||
for (u32 i = 0; i < m_usb_count; i++) {
|
||||
std::string file_name;
|
||||
u64 file_size;
|
||||
if (R_FAILED(m_usb_source->GetFileInfo(file_name, file_size))) {
|
||||
return false;
|
||||
}
|
||||
log_write("inside progress box\n");
|
||||
for (const auto& file_name : m_names) {
|
||||
m_usb_source->SetFileNameForTranfser(file_name);
|
||||
|
||||
log_write("got file name: %s size: %lX\n", file_name.c_str(), file_size);
|
||||
|
||||
const auto rc = yati::InstallFromSource(pbox, m_usb_source, file_name);
|
||||
if (R_FAILED(rc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
App::Notify("Installed via usb"_i18n);
|
||||
m_usb_source->Finished();
|
||||
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;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [this](bool result){
|
||||
if (result) {
|
||||
App::Notify("Usb install success!"_i18n);
|
||||
m_state = State::Done;
|
||||
} else {
|
||||
App::Notify("Usb install failed!"_i18n);
|
||||
m_state = State::Failed;
|
||||
}
|
||||
}));
|
||||
break;
|
||||
App::Notify("Installed via usb"_i18n);
|
||||
}
|
||||
|
||||
case State::Progress:
|
||||
break;
|
||||
|
||||
case State::Done:
|
||||
break;
|
||||
|
||||
case State::Failed:
|
||||
break;
|
||||
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;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +151,12 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Connected:
|
||||
case State::Connected_WaitForFileList:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Connected, waiting for file list..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Connected_StartingTransfer:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Connected, starting transfer..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Progress:
|
||||
|
||||
@@ -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,8 +35,29 @@ constexpr std::array buttons = {
|
||||
std::pair{Button::R3, "\uE105"},
|
||||
};
|
||||
|
||||
// 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);
|
||||
@@ -42,6 +65,10 @@ void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rou
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -120,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);
|
||||
@@ -129,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);
|
||||
@@ -136,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) {
|
||||
@@ -166,6 +219,10 @@ void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture,
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -270,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;
|
||||
|
||||
@@ -23,7 +23,7 @@ auto OptionBoxEntry::Selected(bool enable) -> void {
|
||||
m_selected = enable;
|
||||
}
|
||||
|
||||
OptionBox::OptionBox(const std::string& message, const Option& a, Callback cb)
|
||||
OptionBox::OptionBox(const std::string& message, const Option& a, Callback cb, int image)
|
||||
: m_message{message}
|
||||
, m_callback{cb} {
|
||||
|
||||
@@ -40,14 +40,15 @@ OptionBox::OptionBox(const std::string& message, const Option& a, Callback cb)
|
||||
Setup(0);
|
||||
}
|
||||
|
||||
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb)
|
||||
: OptionBox{message, a, b, 0, cb} {
|
||||
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb, int image)
|
||||
: OptionBox{message, a, b, 0, cb, image} {
|
||||
|
||||
}
|
||||
|
||||
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, s64 index, Callback cb)
|
||||
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, s64 index, Callback cb, int image)
|
||||
: m_message{message}
|
||||
, m_callback{cb} {
|
||||
, m_callback{cb}
|
||||
, m_image{image} {
|
||||
|
||||
m_pos.w = 770.f;
|
||||
m_pos.h = 295.f;
|
||||
@@ -65,17 +66,6 @@ OptionBox::OptionBox(const std::string& message, const Option& a, const Option&
|
||||
Setup(index);
|
||||
}
|
||||
|
||||
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, Callback cb)
|
||||
: OptionBox{message, a, b, c, 0, cb} {
|
||||
|
||||
}
|
||||
|
||||
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, s64 index, Callback cb)
|
||||
: m_message{message}
|
||||
, m_callback{cb} {
|
||||
|
||||
}
|
||||
|
||||
auto OptionBox::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||
Widget::Update(controller, touch);
|
||||
|
||||
@@ -92,13 +82,25 @@ auto OptionBox::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||
}
|
||||
|
||||
auto OptionBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
const float padding = 15;
|
||||
gfx::dimBackground(vg);
|
||||
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP));
|
||||
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP), 5);
|
||||
|
||||
nvgSave(vg);
|
||||
nvgTextLineHeight(vg, 1.5);
|
||||
gfx::drawTextBox(vg, m_pos.x + padding, m_pos.y + 110.f, 26.f, m_pos.w - padding*2, theme->GetColour(ThemeEntryID_TEXT), m_message.c_str(), NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
|
||||
if (m_image) {
|
||||
Vec4 image{m_pos};
|
||||
image.x += 40;
|
||||
image.y += 40;
|
||||
image.w = 150;
|
||||
image.h = 150;
|
||||
|
||||
const float padding = 40;
|
||||
gfx::drawImage(vg, image, m_image, 5);
|
||||
gfx::drawTextBox(vg, image.x + image.w + padding, m_pos.y + 110.f, 22.f, m_pos.w - (image.x - m_pos.x) - image.w - padding*2, theme->GetColour(ThemeEntryID_TEXT), m_message.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_BASELINE);
|
||||
} else {
|
||||
const float padding = 30;
|
||||
gfx::drawTextBox(vg, m_pos.x + padding, m_pos.y + 110.f, 24.f, m_pos.w - padding*2, theme->GetColour(ThemeEntryID_TEXT), m_message.c_str(), NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
|
||||
}
|
||||
nvgRestore(vg);
|
||||
|
||||
gfx::drawRect(vg, m_spacer_line, theme->GetColour(ThemeEntryID_LINE_SEPARATOR));
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "app.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
@@ -73,6 +74,8 @@ PopupList::PopupList(std::string title, Items items, Callback cb, s64 index)
|
||||
}})
|
||||
);
|
||||
|
||||
m_starting_index = m_index;
|
||||
|
||||
m_pos.w = 1280.f;
|
||||
const float a = std::min(370.f, (60.f * static_cast<float>(m_items.size())));
|
||||
m_pos.h = 80.f + 140.f + a;
|
||||
@@ -110,15 +113,24 @@ auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
|
||||
m_list->Draw(vg, theme, m_items.size(), [this](auto* vg, auto* theme, auto v, auto i) {
|
||||
const auto& [x, y, w, h] = v;
|
||||
if (m_index == i) {
|
||||
auto colour = ThemeEntryID_TEXT;
|
||||
const auto selected = m_index == i;
|
||||
if (selected) {
|
||||
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||
gfx::drawText(vg, x + m_text_xoffset, y + (h / 2.f), 20.f, m_items[i].c_str(), NULL, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED));
|
||||
} else {
|
||||
if (i != m_items.size() - 1) {
|
||||
gfx::drawRect(vg, x, y + h, w, 1.f, theme->GetColour(ThemeEntryID_LINE_SEPARATOR));
|
||||
}
|
||||
gfx::drawText(vg, x + m_text_xoffset, y + (h / 2.f), 20.f, m_items[i].c_str(), NULL, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT));
|
||||
}
|
||||
|
||||
if (m_starting_index == i) {
|
||||
colour = ThemeEntryID_TEXT_SELECTED;
|
||||
gfx::drawText(vg, x + w - m_text_xoffset, y + (h / 2.f), 20.f, "\uE14B", NULL, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(colour));
|
||||
}
|
||||
|
||||
const auto text_x = x + m_text_xoffset;
|
||||
const auto text_clip_w = w - 60.f - m_text_xoffset;
|
||||
m_scroll_text.Draw(vg, selected, text_x, y + (h / 2.f), text_clip_w, 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(colour), m_items[i]);
|
||||
});
|
||||
|
||||
Widget::Draw(vg, theme);
|
||||
|
||||
@@ -95,7 +95,7 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
}
|
||||
|
||||
gfx::dimBackground(vg);
|
||||
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP));
|
||||
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP), 5);
|
||||
|
||||
// The pop up shape.
|
||||
// const Vec4 box = { 255, 145, 770, 430 };
|
||||
@@ -108,7 +108,7 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH());
|
||||
|
||||
if (m_image) {
|
||||
gfx::drawImage(vg, GetX() + 30, GetY() + 30, 128, 128, m_image, 10);
|
||||
gfx::drawImage(vg, GetX() + 30, GetY() + 30, 128, 128, m_image, 5);
|
||||
}
|
||||
|
||||
// shapes.
|
||||
@@ -234,7 +234,7 @@ auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_pat
|
||||
R_TRY(fsFileSetSize(&dst_file, src_size));
|
||||
|
||||
s64 offset{};
|
||||
std::vector<u8> buf(1024*1024*8); // 8MiB
|
||||
std::vector<u8> buf(1024*1024*4); // 4MiB
|
||||
|
||||
while (offset < src_size) {
|
||||
if (ShouldExit()) {
|
||||
@@ -248,6 +248,7 @@ auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_pat
|
||||
R_TRY(fsFileWrite(&dst_file, offset, buf.data(), bytes_read, FsWriteOption_None));
|
||||
Yield();
|
||||
|
||||
UpdateTransfer(offset, src_size);
|
||||
offset += bytes_read;
|
||||
}
|
||||
|
||||
|
||||
85
sphaira/source/ui/scrolling_text.cpp
Normal file
85
sphaira/source/ui/scrolling_text.cpp
Normal file
@@ -0,0 +1,85 @@
|
||||
#include "ui/scrolling_text.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "app.hpp"
|
||||
#include <cstdarg>
|
||||
|
||||
namespace sphaira::ui {
|
||||
namespace {
|
||||
|
||||
auto GetTextScrollSpeed() -> float {
|
||||
switch (App::GetTextScrollSpeed()) {
|
||||
case 0: return 0.5;
|
||||
default: case 1: return 1.0;
|
||||
case 2: return 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
void DrawClipped(NVGcontext* vg, const Vec4& clip, float x, float y, float size, int align, const NVGcolor& colour, const std::string& str) {
|
||||
nvgSave(vg);
|
||||
nvgIntersectScissor(vg, clip.x, clip.y, clip.w, clip.h); // clip
|
||||
gfx::drawText(vg, x, y, size, colour, str.c_str(), align);
|
||||
nvgRestore(vg);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ScrollingText::Draw(NVGcontext* vg, bool focus, float x, float y, float w, float size, int align, const NVGcolor& colour, const std::string& text_entry) {
|
||||
const Vec4 clip{x, 0, w, 720};
|
||||
|
||||
if (!focus) {
|
||||
DrawClipped(vg, clip, x, y, size, align, colour, text_entry);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_str != text_entry) {
|
||||
Reset(text_entry);
|
||||
}
|
||||
|
||||
float bounds[4];
|
||||
auto value_str = text_entry;
|
||||
nvgFontSize(vg, size);
|
||||
nvgTextAlign(vg, align);
|
||||
nvgTextBounds(vg, 0, 0, value_str.c_str(), nullptr, bounds);
|
||||
|
||||
if (focus) {
|
||||
const auto scroll_amount = GetTextScrollSpeed();
|
||||
if (bounds[2] > w) {
|
||||
value_str += " ";
|
||||
nvgTextBounds(vg, 0, 0, value_str.c_str(), nullptr, bounds);
|
||||
|
||||
if (!m_text_xoff) {
|
||||
m_tick++;
|
||||
if (m_tick >= 60) {
|
||||
m_tick = 0;
|
||||
m_text_xoff += scroll_amount;
|
||||
}
|
||||
} else if (bounds[2] > m_text_xoff) {
|
||||
m_text_xoff += std::min(scroll_amount, bounds[2] - m_text_xoff);
|
||||
} else {
|
||||
m_text_xoff = 0;
|
||||
}
|
||||
|
||||
value_str += text_entry;
|
||||
}
|
||||
}
|
||||
|
||||
x -= m_text_xoff;
|
||||
DrawClipped(vg, clip, x, y, size, align, colour, value_str);
|
||||
}
|
||||
|
||||
void ScrollingText::DrawArgs(NVGcontext* vg, bool focus, float x, float y, float w, float size, int align, const NVGcolor& colour, const char* s, ...) {
|
||||
std::va_list v{};
|
||||
va_start(v, s);
|
||||
char buffer[0x100];
|
||||
std::vsnprintf(buffer, sizeof(buffer), s, v);
|
||||
va_end(v);
|
||||
Draw(vg, focus, x, y, w, size, align, colour, buffer);
|
||||
}
|
||||
|
||||
void ScrollingText::Reset(const std::string& text_entry) {
|
||||
m_str = text_entry;
|
||||
m_tick = 0;
|
||||
m_text_xoff = 0;
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "ui/popup_list.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
namespace sphaira::ui {
|
||||
namespace {
|
||||
@@ -43,6 +44,13 @@ SidebarEntryBool::SidebarEntryBool(std::string title, bool option, Callback cb,
|
||||
, m_true_str{std::move(true_str)}
|
||||
, m_false_str{std::move(false_str)} {
|
||||
|
||||
if (m_true_str == "On") {
|
||||
m_true_str = i18n::get(m_true_str);
|
||||
}
|
||||
if (m_false_str == "Off") {
|
||||
m_false_str = i18n::get(m_false_str);
|
||||
}
|
||||
|
||||
SetAction(Button::A, Action{"OK"_i18n, [this](){
|
||||
m_option ^= 1;
|
||||
m_callback(m_option);
|
||||
|
||||
@@ -86,6 +86,11 @@ auto Widget::GetUiButtons() const -> uiButtons {
|
||||
uiButtons draw_actions;
|
||||
draw_actions.reserve(m_actions.size());
|
||||
|
||||
const std::pair<Button, Button> swap_buttons[] = {
|
||||
{Button::L, Button::R},
|
||||
{Button::L2, Button::R2},
|
||||
};
|
||||
|
||||
// build array
|
||||
for (const auto& [button, action] : m_actions) {
|
||||
if (action.IsHidden() || action.m_hint.empty()) {
|
||||
@@ -94,13 +99,19 @@ auto Widget::GetUiButtons() const -> uiButtons {
|
||||
|
||||
uiButton ui_button{button, action};
|
||||
|
||||
// swap
|
||||
if (button == Button::R && draw_actions.size() && draw_actions.back().m_button == Button::L) {
|
||||
const auto s = draw_actions.back();
|
||||
draw_actions.back().m_button = button;
|
||||
draw_actions.back().m_action = action;
|
||||
draw_actions.emplace_back(s);
|
||||
} else {
|
||||
bool should_swap = false;
|
||||
for (auto [left, right] : swap_buttons) {
|
||||
if (button == right && draw_actions.size() && draw_actions.back().m_button == left) {
|
||||
const auto s = draw_actions.back();
|
||||
draw_actions.back().m_button = button;
|
||||
draw_actions.back().m_action = action;
|
||||
draw_actions.emplace_back(s);
|
||||
should_swap = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!should_swap) {
|
||||
draw_actions.emplace_back(ui_button);
|
||||
}
|
||||
}
|
||||
|
||||
98
sphaira/source/usb/base.cpp
Normal file
98
sphaira/source/usb/base.cpp
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (c) Atmosphère-NX
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms and conditions of the GNU General Public License,
|
||||
* version 2, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// The USB transfer code was taken from Haze (part of Atmosphere).
|
||||
|
||||
#include "usb/base.hpp"
|
||||
#include "log.hpp"
|
||||
#include "defines.hpp"
|
||||
#include <ranges>
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::usb {
|
||||
|
||||
Base::Base(u64 transfer_timeout) {
|
||||
m_transfer_timeout = transfer_timeout;
|
||||
ueventCreate(GetCancelEvent(), true);
|
||||
// this avoids allocations during transfers.
|
||||
m_aligned.reserve(1024 * 1024 * 16);
|
||||
}
|
||||
|
||||
Result Base::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) {
|
||||
u32 xfer_id;
|
||||
|
||||
/* If we're not configured yet, wait to become configured first. */
|
||||
R_TRY(IsUsbConnected(timeout));
|
||||
|
||||
/* Select the appropriate endpoint and begin a transfer. */
|
||||
const auto ep = read ? UsbSessionEndpoint_Out : UsbSessionEndpoint_In;
|
||||
R_TRY(TransferAsync(ep, page, size, std::addressof(xfer_id)));
|
||||
|
||||
/* Try to wait for the event. */
|
||||
R_TRY(WaitTransferCompletion(ep, timeout));
|
||||
|
||||
/* Return what we transferred. */
|
||||
return GetTransferResult(ep, xfer_id, nullptr, out_size_transferred);
|
||||
}
|
||||
|
||||
// while it may seem like a bad idea to transfer data to a buffer and copy it
|
||||
// in practice, this has no impact on performance.
|
||||
// the switch is *massively* bottlenecked by slow io (nand and sd).
|
||||
// so making usb transfers zero-copy provides no benefit other than increased
|
||||
// code complexity and the increase of future bugs if/when sphaira is forked
|
||||
// an changes are made.
|
||||
// yati already goes to great lengths to be zero-copy during installing
|
||||
// by swapping buffers and inflating in-place.
|
||||
|
||||
// NOTE: it is now possible to request the transfer buffer using GetTransferBuffer(),
|
||||
// which will always be aligned and have the size aligned.
|
||||
// this allows for zero-copy transferrs to take place.
|
||||
// this is used in usb_upload.cpp.
|
||||
// do note that this relies of the host sending / receiving buffers of an aligned size.
|
||||
Result Base::TransferAll(bool read, void *data, u32 size, u64 timeout) {
|
||||
auto buf = static_cast<u8*>(data);
|
||||
auto transfer_buf = m_aligned.data();
|
||||
const auto alias = buf == transfer_buf;
|
||||
|
||||
if (!alias) {
|
||||
m_aligned.resize(size);
|
||||
}
|
||||
|
||||
while (size) {
|
||||
if (!alias && !read) {
|
||||
std::memcpy(transfer_buf, buf, size);
|
||||
}
|
||||
|
||||
u32 out_size_transferred;
|
||||
R_TRY(TransferPacketImpl(read, transfer_buf, size, &out_size_transferred, timeout));
|
||||
|
||||
if (!alias && read) {
|
||||
std::memcpy(buf, transfer_buf, out_size_transferred);
|
||||
}
|
||||
|
||||
if (alias) {
|
||||
transfer_buf += out_size_transferred;
|
||||
} else {
|
||||
buf += out_size_transferred;
|
||||
}
|
||||
|
||||
size -= out_size_transferred;
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::usb
|
||||
112
sphaira/source/usb/usb_uploader.cpp
Normal file
112
sphaira/source/usb/usb_uploader.cpp
Normal file
@@ -0,0 +1,112 @@
|
||||
// The USB protocol was taken from Tinfoil, by Adubbz.
|
||||
|
||||
#include "usb/usb_uploader.hpp"
|
||||
#include "usb/tinfoil.hpp"
|
||||
#include "log.hpp"
|
||||
#include "defines.hpp"
|
||||
|
||||
namespace sphaira::usb::upload {
|
||||
namespace {
|
||||
|
||||
namespace tinfoil = usb::tinfoil;
|
||||
|
||||
const UsbHsInterfaceFilter FILTER{
|
||||
.Flags = UsbHsInterfaceFilterFlags_idVendor |
|
||||
UsbHsInterfaceFilterFlags_idProduct |
|
||||
UsbHsInterfaceFilterFlags_bcdDevice_Min |
|
||||
UsbHsInterfaceFilterFlags_bcdDevice_Max |
|
||||
UsbHsInterfaceFilterFlags_bDeviceClass |
|
||||
UsbHsInterfaceFilterFlags_bDeviceSubClass |
|
||||
UsbHsInterfaceFilterFlags_bDeviceProtocol |
|
||||
UsbHsInterfaceFilterFlags_bInterfaceClass |
|
||||
UsbHsInterfaceFilterFlags_bInterfaceSubClass |
|
||||
UsbHsInterfaceFilterFlags_bInterfaceProtocol,
|
||||
.idVendor = 0x057e,
|
||||
.idProduct = 0x3000,
|
||||
.bcdDevice_Min = 0x0100,
|
||||
.bcdDevice_Max = 0x0100,
|
||||
.bDeviceClass = 0x00,
|
||||
.bDeviceSubClass = 0x00,
|
||||
.bDeviceProtocol = 0x00,
|
||||
.bInterfaceClass = USB_CLASS_VENDOR_SPEC,
|
||||
.bInterfaceSubClass = USB_CLASS_VENDOR_SPEC,
|
||||
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
|
||||
};
|
||||
|
||||
constexpr u8 INDEX = 0;
|
||||
|
||||
} // namespace
|
||||
|
||||
Usb::Usb(u64 transfer_timeout) {
|
||||
m_usb = std::make_unique<usb::UsbHs>(INDEX, FILTER, transfer_timeout);
|
||||
m_usb->Init();
|
||||
}
|
||||
|
||||
Usb::~Usb() {
|
||||
}
|
||||
|
||||
Result Usb::WaitForConnection(u64 timeout, std::span<const std::string> names) {
|
||||
R_TRY(m_usb->IsUsbConnected(timeout));
|
||||
|
||||
std::string names_list;
|
||||
for (auto& name : names) {
|
||||
names_list += name + '\n';
|
||||
}
|
||||
|
||||
tinfoil::TUSHeader header{};
|
||||
header.magic = tinfoil::Magic_List0;
|
||||
header.nspListSize = names_list.length();
|
||||
|
||||
R_TRY(m_usb->TransferAll(false, &header, sizeof(header), timeout));
|
||||
R_TRY(m_usb->TransferAll(false, names_list.data(), names_list.length(), timeout));
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::PollCommands() {
|
||||
tinfoil::USBCmdHeader header;
|
||||
R_TRY(m_usb->TransferAll(true, &header, sizeof(header)));
|
||||
R_UNLESS(header.magic == tinfoil::Magic_Command0, Result_BadMagic);
|
||||
|
||||
if (header.cmdId == tinfoil::USBCmdId::EXIT) {
|
||||
return Result_Exit;
|
||||
} else if (header.cmdId == tinfoil::USBCmdId::FILE_RANGE) {
|
||||
return FileRangeCmd(header.dataSize);
|
||||
} else {
|
||||
return Result_BadCommand;
|
||||
}
|
||||
}
|
||||
|
||||
Result Usb::FileRangeCmd(u64 data_size) {
|
||||
tinfoil::FileRangeCmdHeader header;
|
||||
R_TRY(m_usb->TransferAll(true, &header, sizeof(header)));
|
||||
|
||||
std::string path(header.nspNameLen, '\0');
|
||||
R_TRY(m_usb->TransferAll(true, path.data(), header.nspNameLen));
|
||||
|
||||
// send response header.
|
||||
R_TRY(m_usb->TransferAll(false, &header, sizeof(header)));
|
||||
|
||||
s64 curr_off = 0x0;
|
||||
s64 end_off = header.size;
|
||||
s64 read_size = header.size;
|
||||
|
||||
// use transfer buffer directly to avoid copy overhead.
|
||||
auto& buf = m_usb->GetTransferBuffer();
|
||||
buf.resize(header.size);
|
||||
|
||||
while (curr_off < end_off) {
|
||||
if (curr_off + read_size >= end_off) {
|
||||
read_size = end_off - curr_off;
|
||||
}
|
||||
|
||||
u64 bytes_read;
|
||||
R_TRY(Read(path, buf.data(), header.offset + curr_off, read_size, &bytes_read));
|
||||
R_TRY(m_usb->TransferAll(false, buf.data(), bytes_read));
|
||||
curr_off += bytes_read;
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::usb::upload
|
||||
272
sphaira/source/usb/usbds.cpp
Normal file
272
sphaira/source/usb/usbds.cpp
Normal file
@@ -0,0 +1,272 @@
|
||||
#include "usb/usbds.hpp"
|
||||
#include "log.hpp"
|
||||
#include "defines.hpp"
|
||||
#include <ranges>
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::usb {
|
||||
namespace {
|
||||
|
||||
// untested, should work tho.
|
||||
// TODO: pr missing speed fields to libnx.
|
||||
Result usbDsGetSpeed(u32 *out) {
|
||||
if (hosversionBefore(8,0,0)) {
|
||||
return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
|
||||
}
|
||||
|
||||
serviceAssumeDomain(usbDsGetServiceSession());
|
||||
return serviceDispatchOut(usbDsGetServiceSession(), hosversionAtLeast(11,0,0) ? 11 : 12, *out);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UsbDs::~UsbDs() {
|
||||
usbDsExit();
|
||||
}
|
||||
|
||||
Result UsbDs::Init() {
|
||||
log_write("doing USB init\n");
|
||||
R_TRY(usbDsInitialize());
|
||||
|
||||
static SetSysSerialNumber serial_number{};
|
||||
R_TRY(setsysInitialize());
|
||||
ON_SCOPE_EXIT(setsysExit());
|
||||
R_TRY(setsysGetSerialNumber(&serial_number));
|
||||
|
||||
u8 iManufacturer, iProduct, iSerialNumber;
|
||||
static constexpr u16 supported_langs[1] = {0x0409};
|
||||
// Send language descriptor
|
||||
R_TRY(usbDsAddUsbLanguageStringDescriptor(nullptr, supported_langs, std::size(supported_langs)));
|
||||
// Send manufacturer
|
||||
R_TRY(usbDsAddUsbStringDescriptor(&iManufacturer, "Nintendo"));
|
||||
// Send product
|
||||
R_TRY(usbDsAddUsbStringDescriptor(&iProduct, "Nintendo Switch"));
|
||||
// Send serial number
|
||||
R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, serial_number.number));
|
||||
|
||||
// Send device descriptors
|
||||
struct usb_device_descriptor device_descriptor = {
|
||||
.bLength = USB_DT_DEVICE_SIZE,
|
||||
.bDescriptorType = USB_DT_DEVICE,
|
||||
.bcdUSB = 0x0110,
|
||||
.bDeviceClass = 0x00,
|
||||
.bDeviceSubClass = 0x00,
|
||||
.bDeviceProtocol = 0x00,
|
||||
.bMaxPacketSize0 = 0x40,
|
||||
.idVendor = 0x057e,
|
||||
.idProduct = 0x3000,
|
||||
.bcdDevice = 0x0100,
|
||||
.iManufacturer = iManufacturer,
|
||||
.iProduct = iProduct,
|
||||
.iSerialNumber = iSerialNumber,
|
||||
.bNumConfigurations = 0x01
|
||||
};
|
||||
|
||||
// Full Speed is USB 1.1
|
||||
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Full, &device_descriptor));
|
||||
|
||||
// High Speed is USB 2.0
|
||||
device_descriptor.bcdUSB = 0x0200;
|
||||
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_High, &device_descriptor));
|
||||
|
||||
// Super Speed is USB 3.0
|
||||
device_descriptor.bcdUSB = 0x0300;
|
||||
// Upgrade packet size to 512
|
||||
device_descriptor.bMaxPacketSize0 = 0x09;
|
||||
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Super, &device_descriptor));
|
||||
|
||||
// Define Binary Object Store
|
||||
const u8 bos[0x16] = {
|
||||
0x05, // .bLength
|
||||
USB_DT_BOS, // .bDescriptorType
|
||||
0x16, 0x00, // .wTotalLength
|
||||
0x02, // .bNumDeviceCaps
|
||||
|
||||
// USB 2.0
|
||||
0x07, // .bLength
|
||||
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
|
||||
0x02, // .bDevCapabilityType
|
||||
0x02, 0x00, 0x00, 0x00, // dev_capability_data
|
||||
|
||||
// USB 3.0
|
||||
0x0A, // .bLength
|
||||
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
|
||||
0x03, /* .bDevCapabilityType */
|
||||
0x00, /* .bmAttributes */
|
||||
0x0E, 0x00, /* .wSpeedSupported */
|
||||
0x03, /* .bFunctionalitySupport */
|
||||
0x00, /* .bU1DevExitLat */
|
||||
0x00, 0x00 /* .bU2DevExitLat */
|
||||
};
|
||||
|
||||
R_TRY(usbDsSetBinaryObjectStore(bos, sizeof(bos)));
|
||||
|
||||
struct usb_interface_descriptor interface_descriptor = {
|
||||
.bLength = USB_DT_INTERFACE_SIZE,
|
||||
.bDescriptorType = USB_DT_INTERFACE,
|
||||
.bInterfaceNumber = USBDS_DEFAULT_InterfaceNumber, // set below
|
||||
.bNumEndpoints = static_cast<u8>(std::size(m_endpoints)),
|
||||
.bInterfaceClass = USB_CLASS_VENDOR_SPEC,
|
||||
.bInterfaceSubClass = USB_CLASS_VENDOR_SPEC,
|
||||
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
|
||||
};
|
||||
|
||||
struct usb_endpoint_descriptor endpoint_descriptor_in = {
|
||||
.bLength = USB_DT_ENDPOINT_SIZE,
|
||||
.bDescriptorType = USB_DT_ENDPOINT,
|
||||
.bEndpointAddress = USB_ENDPOINT_IN,
|
||||
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
||||
};
|
||||
|
||||
struct usb_endpoint_descriptor endpoint_descriptor_out = {
|
||||
.bLength = USB_DT_ENDPOINT_SIZE,
|
||||
.bDescriptorType = USB_DT_ENDPOINT,
|
||||
.bEndpointAddress = USB_ENDPOINT_OUT,
|
||||
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
||||
};
|
||||
|
||||
const struct usb_ss_endpoint_companion_descriptor endpoint_companion = {
|
||||
.bLength = sizeof(struct usb_ss_endpoint_companion_descriptor),
|
||||
.bDescriptorType = USB_DT_SS_ENDPOINT_COMPANION,
|
||||
.bMaxBurst = 0x0F,
|
||||
.bmAttributes = 0x00,
|
||||
.wBytesPerInterval = 0x00,
|
||||
};
|
||||
|
||||
R_TRY(usbDsRegisterInterface(&m_interface));
|
||||
|
||||
interface_descriptor.bInterfaceNumber = m_interface->interface_index;
|
||||
endpoint_descriptor_in.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
|
||||
endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
|
||||
|
||||
// Full Speed Config
|
||||
endpoint_descriptor_in.wMaxPacketSize = 0x40;
|
||||
endpoint_descriptor_out.wMaxPacketSize = 0x40;
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
|
||||
// High Speed Config
|
||||
endpoint_descriptor_in.wMaxPacketSize = 0x200;
|
||||
endpoint_descriptor_out.wMaxPacketSize = 0x200;
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
|
||||
// Super Speed Config
|
||||
endpoint_descriptor_in.wMaxPacketSize = 0x400;
|
||||
endpoint_descriptor_out.wMaxPacketSize = 0x400;
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
|
||||
|
||||
//Setup endpoints.
|
||||
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_In], endpoint_descriptor_in.bEndpointAddress));
|
||||
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_Out], endpoint_descriptor_out.bEndpointAddress));
|
||||
|
||||
R_TRY(usbDsInterface_EnableInterface(m_interface));
|
||||
R_TRY(usbDsEnable());
|
||||
|
||||
log_write("success USB init\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
// the below code is taken from libnx, with the addition of a uevent to cancel.
|
||||
Result UsbDs::IsUsbConnected(u64 timeout) {
|
||||
Result rc;
|
||||
UsbState state = UsbState_Detached;
|
||||
|
||||
rc = usbDsGetState(&state);
|
||||
if (R_FAILED(rc)) return rc;
|
||||
if (state == UsbState_Configured) return 0;
|
||||
|
||||
bool has_timeout = timeout != UINT64_MAX;
|
||||
u64 deadline = 0;
|
||||
|
||||
const std::array waiters{
|
||||
waiterForEvent(usbDsGetStateChangeEvent()),
|
||||
waiterForUEvent(GetCancelEvent()),
|
||||
};
|
||||
|
||||
if (has_timeout)
|
||||
deadline = armGetSystemTick() + armNsToTicks(timeout);
|
||||
|
||||
do {
|
||||
if (has_timeout) {
|
||||
s64 remaining = deadline - armGetSystemTick();
|
||||
timeout = remaining > 0 ? armTicksToNs(remaining) : 0;
|
||||
}
|
||||
|
||||
s32 idx;
|
||||
rc = waitObjects(&idx, waiters.data(), waiters.size(), timeout);
|
||||
eventClear(usbDsGetStateChangeEvent());
|
||||
|
||||
// check if we got one of the cancel events.
|
||||
if (R_SUCCEEDED(rc) && idx == waiters.size() - 1) {
|
||||
rc = Result_Cancelled;
|
||||
break;
|
||||
}
|
||||
|
||||
rc = usbDsGetState(&state);
|
||||
} while (R_SUCCEEDED(rc) && state != UsbState_Configured && timeout > 0);
|
||||
|
||||
if (R_SUCCEEDED(rc) && state != UsbState_Configured && timeout == 0)
|
||||
return KERNELRESULT(TimedOut);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
Result UsbDs::GetSpeed(UsbDeviceSpeed* out) {
|
||||
return usbDsGetSpeed((u32*)out);
|
||||
}
|
||||
|
||||
Event *UsbDs::GetCompletionEvent(UsbSessionEndpoint ep) {
|
||||
return std::addressof(m_endpoints[ep]->CompletionEvent);
|
||||
}
|
||||
|
||||
Result UsbDs::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) {
|
||||
const std::array waiters{
|
||||
waiterForEvent(GetCompletionEvent(ep)),
|
||||
waiterForEvent(usbDsGetStateChangeEvent()),
|
||||
waiterForUEvent(GetCancelEvent()),
|
||||
};
|
||||
|
||||
s32 idx;
|
||||
auto rc = waitObjects(&idx, waiters.data(), waiters.size(), timeout);
|
||||
|
||||
// check if we got one of the cancel events.
|
||||
if (R_SUCCEEDED(rc) && idx == waiters.size() - 1) {
|
||||
log_write("got usb cancel event\n");
|
||||
rc = Result_Cancelled;
|
||||
} else if (R_SUCCEEDED(rc) && idx == waiters.size() - 2) {
|
||||
log_write("got usbDsGetStateChangeEvent() event\n");
|
||||
rc = KERNELRESULT(TimedOut);
|
||||
}
|
||||
|
||||
|
||||
if (R_FAILED(rc)) {
|
||||
R_TRY(usbDsEndpoint_Cancel(m_endpoints[ep]));
|
||||
eventClear(GetCompletionEvent(ep));
|
||||
eventClear(usbDsGetStateChangeEvent());
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
Result UsbDs::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) {
|
||||
return usbDsEndpoint_PostBufferAsync(m_endpoints[ep], buffer, size, out_urb_id);
|
||||
}
|
||||
|
||||
Result UsbDs::GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) {
|
||||
UsbDsReportData report_data;
|
||||
|
||||
R_TRY(eventClear(GetCompletionEvent(ep)));
|
||||
R_TRY(usbDsEndpoint_GetReportData(m_endpoints[ep], std::addressof(report_data)));
|
||||
R_TRY(usbDsParseReportData(std::addressof(report_data), urb_id, out_requested_size, out_transferred_size));
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::usb
|
||||
202
sphaira/source/usb/usbhs.cpp
Normal file
202
sphaira/source/usb/usbhs.cpp
Normal file
@@ -0,0 +1,202 @@
|
||||
#include "usb/usbhs.hpp"
|
||||
#include "log.hpp"
|
||||
#include "defines.hpp"
|
||||
#include <ranges>
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::usb {
|
||||
namespace {
|
||||
|
||||
struct Bcd {
|
||||
constexpr Bcd(u16 v) : value{v} {}
|
||||
|
||||
u8 major() const { return (value >> 8) & 0xFF; }
|
||||
u8 minor() const { return (value >> 4) & 0xF; }
|
||||
u8 macro() const { return (value >> 0) & 0xF; }
|
||||
|
||||
const u16 value;
|
||||
};
|
||||
|
||||
Result usbHsParseReportData(UsbHsXferReport* reports, u32 count, u32 xferId, u32 *requestedSize, u32 *transferredSize) {
|
||||
Result rc = 0;
|
||||
u32 pos;
|
||||
UsbHsXferReport *entry = NULL;
|
||||
if (count>8) count = 8;
|
||||
|
||||
for(pos=0; pos<count; pos++) {
|
||||
entry = &reports[pos];
|
||||
if (entry->xferId == xferId) break;
|
||||
}
|
||||
|
||||
if (pos == count) return MAKERESULT(Module_Libnx, LibnxError_NotFound);
|
||||
rc = entry->res;
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
if (requestedSize) *requestedSize = entry->requestedSize;
|
||||
if (transferredSize) *transferredSize = entry->transferredSize;
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UsbHs::UsbHs(u8 index, const UsbHsInterfaceFilter& filter, u64 transfer_timeout)
|
||||
: Base{transfer_timeout}
|
||||
, m_index{index}
|
||||
, m_filter{filter} {
|
||||
|
||||
}
|
||||
|
||||
UsbHs::~UsbHs() {
|
||||
Close();
|
||||
usbHsDestroyInterfaceAvailableEvent(std::addressof(m_event), m_index);
|
||||
usbHsExit();
|
||||
}
|
||||
|
||||
Result UsbHs::Init() {
|
||||
log_write("doing USB init\n");
|
||||
R_TRY(usbHsInitialize());
|
||||
R_TRY(usbHsCreateInterfaceAvailableEvent(&m_event, true, m_index, &m_filter));
|
||||
log_write("success USB init\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result UsbHs::IsUsbConnected(u64 timeout) {
|
||||
if (m_connected) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
const std::array waiters{
|
||||
waiterForEvent(&m_event),
|
||||
waiterForUEvent(GetCancelEvent()),
|
||||
};
|
||||
|
||||
s32 idx;
|
||||
R_TRY(waitObjects(&idx, waiters.data(), waiters.size(), timeout));
|
||||
|
||||
if (idx == waiters.size() - 1) {
|
||||
return Result_Cancelled;
|
||||
}
|
||||
|
||||
return Connect();
|
||||
}
|
||||
|
||||
Result UsbHs::Connect() {
|
||||
Close();
|
||||
|
||||
s32 total;
|
||||
R_TRY(usbHsQueryAvailableInterfaces(&m_filter, &m_interface, sizeof(m_interface), &total));
|
||||
R_TRY(usbHsAcquireUsbIf(&m_s, &m_interface));
|
||||
|
||||
const auto bcdUSB = Bcd{m_interface.device_desc.bcdUSB};
|
||||
const auto bcdDevice = Bcd{m_interface.device_desc.bcdDevice};
|
||||
|
||||
// log lsusb style.
|
||||
log_write("[USBHS] pathstr: %s\n", m_interface.pathstr);
|
||||
log_write("Bus: %03u Device: %03u ID: %04x:%04x\n\n", m_interface.busID, m_interface.deviceID, m_interface.device_desc.idVendor, m_interface.device_desc.idProduct);
|
||||
|
||||
log_write("Device Descriptor:\n");
|
||||
log_write("\tbLength: %u\n", m_interface.device_desc.bLength);
|
||||
log_write("\tbDescriptorType: %u\n", m_interface.device_desc.bDescriptorType);
|
||||
log_write("\tbcdUSB: %u:%u%u\n", bcdUSB.major(), bcdUSB.minor(), bcdUSB.macro());
|
||||
log_write("\tbDeviceClass: %u\n", m_interface.device_desc.bDeviceClass);
|
||||
log_write("\tbDeviceSubClass: %u\n", m_interface.device_desc.bDeviceSubClass);
|
||||
log_write("\tbDeviceProtocol: %u\n", m_interface.device_desc.bDeviceProtocol);
|
||||
log_write("\tbMaxPacketSize0: %u\n", m_interface.device_desc.bMaxPacketSize0);
|
||||
log_write("\tidVendor: 0x%x\n", m_interface.device_desc.idVendor);
|
||||
log_write("\tidProduct: 0x%x\n", m_interface.device_desc.idProduct);
|
||||
log_write("\tbcdDevice: %u:%u%u\n", bcdDevice.major(), bcdDevice.minor(), bcdDevice.macro());
|
||||
log_write("\tiManufacturer: %u\n", m_interface.device_desc.iManufacturer);
|
||||
log_write("\tiProduct: %u\n", m_interface.device_desc.iProduct);
|
||||
log_write("\tiSerialNumber: %u\n", m_interface.device_desc.iSerialNumber);
|
||||
log_write("\tbNumConfigurations: %u\n", m_interface.device_desc.bNumConfigurations);
|
||||
|
||||
log_write("\tConfiguration Descriptor:\n");
|
||||
log_write("\t\tbLength: %u\n", m_interface.config_desc.bLength);
|
||||
log_write("\t\tbDescriptorType: %u\n", m_interface.config_desc.bDescriptorType);
|
||||
log_write("\t\twTotalLength: %u\n", m_interface.config_desc.wTotalLength);
|
||||
log_write("\t\tbNumInterfaces: %u\n", m_interface.config_desc.bNumInterfaces);
|
||||
log_write("\t\tbConfigurationValue: %u\n", m_interface.config_desc.bConfigurationValue);
|
||||
log_write("\t\tiConfiguration: %u\n", m_interface.config_desc.iConfiguration);
|
||||
log_write("\t\tbmAttributes: 0x%x\n", m_interface.config_desc.bmAttributes);
|
||||
log_write("\t\tMaxPower: %u (%u mA)\n", m_interface.config_desc.MaxPower, m_interface.config_desc.MaxPower * 2);
|
||||
|
||||
struct usb_endpoint_descriptor invalid_desc{};
|
||||
for (u8 i = 0; i < std::size(m_s.inf.inf.input_endpoint_descs); i++) {
|
||||
const auto& desc = m_s.inf.inf.input_endpoint_descs[i];
|
||||
if (std::memcmp(&desc, &invalid_desc, sizeof(desc))) {
|
||||
log_write("\t[USBHS] desc[%u] wMaxPacketSize: 0x%X\n", i, desc.wMaxPacketSize);
|
||||
}
|
||||
}
|
||||
|
||||
auto& input_descs = m_s.inf.inf.input_endpoint_descs[0];
|
||||
R_TRY(usbHsIfOpenUsbEp(&m_s, &m_endpoints[UsbSessionEndpoint_Out], 1, input_descs.wMaxPacketSize, &input_descs));
|
||||
|
||||
auto& output_descs = m_s.inf.inf.output_endpoint_descs[0];
|
||||
R_TRY(usbHsIfOpenUsbEp(&m_s, &m_endpoints[UsbSessionEndpoint_In], 1, output_descs.wMaxPacketSize, &output_descs));
|
||||
|
||||
m_connected = true;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void UsbHs::Close() {
|
||||
usbHsEpClose(std::addressof(m_endpoints[UsbSessionEndpoint_In]));
|
||||
usbHsEpClose(std::addressof(m_endpoints[UsbSessionEndpoint_Out]));
|
||||
usbHsIfClose(std::addressof(m_s));
|
||||
|
||||
m_endpoints[UsbSessionEndpoint_In] = {};
|
||||
m_endpoints[UsbSessionEndpoint_Out] = {};
|
||||
m_s = {};
|
||||
m_connected = false;
|
||||
}
|
||||
|
||||
Event *UsbHs::GetCompletionEvent(UsbSessionEndpoint ep) {
|
||||
return usbHsEpGetXferEvent(&m_endpoints[ep]);
|
||||
}
|
||||
|
||||
Result UsbHs::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) {
|
||||
const std::array waiters{
|
||||
waiterForEvent(GetCompletionEvent(ep)),
|
||||
waiterForEvent(usbHsGetInterfaceStateChangeEvent()),
|
||||
waiterForUEvent(GetCancelEvent()),
|
||||
};
|
||||
|
||||
s32 idx;
|
||||
auto rc = waitObjects(&idx, waiters.data(), waiters.size(), timeout);
|
||||
|
||||
// check if we got one of the cancel events.
|
||||
if (R_SUCCEEDED(rc) && idx == waiters.size() - 1) {
|
||||
log_write("got usb cancel event\n");
|
||||
rc = Result_Cancelled;
|
||||
} else if (R_SUCCEEDED(rc) && idx == waiters.size() - 2) {
|
||||
log_write("got usb timeout event\n");
|
||||
rc = KERNELRESULT(TimedOut);
|
||||
Close();
|
||||
}
|
||||
|
||||
if (R_FAILED(rc)) {
|
||||
log_write("failed to wait for event\n");
|
||||
eventClear(GetCompletionEvent(ep));
|
||||
eventClear(usbHsGetInterfaceStateChangeEvent());
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
Result UsbHs::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_xfer_id) {
|
||||
return usbHsEpPostBufferAsync(&m_endpoints[ep], buffer, size, 0, out_xfer_id);
|
||||
}
|
||||
|
||||
Result UsbHs::GetTransferResult(UsbSessionEndpoint ep, u32 xfer_id, u32 *out_requested_size, u32 *out_transferred_size) {
|
||||
u32 count;
|
||||
UsbHsXferReport report_data[8];
|
||||
|
||||
R_TRY(eventClear(GetCompletionEvent(ep)));
|
||||
R_TRY(usbHsEpGetXferReport(&m_endpoints[ep], report_data, std::size(report_data), std::addressof(count)));
|
||||
R_TRY(usbHsParseReportData(report_data, count, xfer_id, out_requested_size, out_transferred_size));
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::usb
|
||||
@@ -1,54 +0,0 @@
|
||||
#include "web.hpp"
|
||||
#include "log.hpp"
|
||||
#include "defines.hpp"
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira {
|
||||
|
||||
auto WebShow(const std::string& url, bool show_error) -> Result {
|
||||
// showError("Running in applet mode\nPlease launch hbmenu by holding R on an APP (e.g. a game) NOT an applet (e.g. Gallery)", "", 0);
|
||||
// showError("Error: Nag active, check more details", "Browser won't launch if supernag is active\n\nUse gagorder or switch-sys-tweak (the latter is bundled with BrowseNX) to disable supernag.", 0);
|
||||
// log_write("web show with url: %s\n", url.c_str());
|
||||
// return 0;
|
||||
WebCommonConfig config{};
|
||||
WebCommonReply reply{};
|
||||
WebExitReason reason{};
|
||||
AccountUid account_uid{};
|
||||
char last_url[FS_MAX_PATH]{};
|
||||
size_t last_url_len{};
|
||||
|
||||
// WebBackgroundKind_Unknown1 = shows background
|
||||
// WebBackgroundKind_Unknown2 = shows background faded
|
||||
if (R_FAILED(accountTrySelectUserWithoutInteraction(&account_uid, false))) { log_write("failed: accountTrySelectUserWithoutInteraction\n"); }
|
||||
if (R_FAILED(webPageCreate(&config, url.c_str()))) { log_write("failed: webPageCreate\n"); }
|
||||
if (R_FAILED(webConfigSetWhitelist(&config, "^http"))) { log_write("failed: webConfigSetWhitelist\n"); }
|
||||
if (R_FAILED(webConfigSetEcClientCert(&config, true))) { log_write("failed: webConfigSetEcClientCert\n"); }
|
||||
if (R_FAILED(webConfigSetScreenShot(&config, true))) { log_write("failed: webConfigSetScreenShot\n"); }
|
||||
if (R_FAILED(webConfigSetBootDisplayKind(&config, WebBootDisplayKind_Black))) { log_write("failed: webConfigSetBootDisplayKind\n"); }
|
||||
if (R_FAILED(webConfigSetBackgroundKind(&config, WebBackgroundKind_Default))) { log_write("failed: webConfigSetBackgroundKind\n"); }
|
||||
if (R_FAILED(webConfigSetPointer(&config, true))) { log_write("failed: webConfigSetPointer\n"); }
|
||||
if (R_FAILED(webConfigSetLeftStickMode(&config, WebLeftStickMode_Pointer))) { log_write("failed: webConfigSetLeftStickMode\n"); }
|
||||
// if (R_FAILED(webConfigSetBootAsMediaPlayer(&config, true))) { log_write("failed: webConfigSetBootAsMediaPlayer\n"); }
|
||||
if (R_FAILED(webConfigSetJsExtension(&config, true))) { log_write("failed: webConfigSetJsExtension\n"); }
|
||||
if (R_FAILED(webConfigSetMediaPlayerAutoClose(&config, true))) { log_write("failed: webConfigSetMediaPlayerAutoClose\n"); }
|
||||
if (R_FAILED(webConfigSetPageCache(&config, true))) { log_write("failed: webConfigSetPageCache\n"); }
|
||||
if (R_FAILED(webConfigSetFooterFixedKind(&config, WebFooterFixedKind_Hidden))) { log_write("failed: webConfigSetFooterFixedKind\n"); }
|
||||
if (R_FAILED(webConfigSetPageFade(&config, true))) { log_write("failed: webConfigSetPageFade\n"); }
|
||||
if (R_FAILED(webConfigSetPageScrollIndicator(&config, true))) { log_write("failed: webConfigSetPageScrollIndicator\n"); }
|
||||
// if (R_FAILED(webConfigSetMediaPlayerSpeedControl(&config, true))) { log_write("failed: webConfigSetMediaPlayerSpeedControl\n"); }
|
||||
if (R_FAILED(webConfigSetBootMode(&config, WebSessionBootMode_AllForeground))) { log_write("failed: webConfigSetBootMode\n"); }
|
||||
if (R_FAILED(webConfigSetTransferMemory(&config, true))) { log_write("failed: webConfigSetTransferMemory\n"); }
|
||||
if (R_FAILED(webConfigSetTouchEnabledOnContents(&config, true))) { log_write("failed: webConfigSetTouchEnabledOnContents\n"); }
|
||||
// if (R_FAILED(webConfigSetMediaPlayerUi(&config, true))) { log_write("failed: webConfigSetMediaPlayerUi\n"); }
|
||||
// if (R_FAILED(webConfigSetWebAudio(&config, true))) { log_write("failed: webConfigSetWebAudio\n"); }
|
||||
if (R_FAILED(webConfigSetPageCache(&config, true))) { log_write("failed: webConfigSetPageCache\n"); }
|
||||
if (R_FAILED(webConfigSetBootLoadingIcon(&config, true))) { log_write("failed: webConfigSetBootLoadingIcon\n"); }
|
||||
if (R_FAILED(webConfigSetUid(&config, account_uid))) { log_write("failed: webConfigSetUid\n"); }
|
||||
if (R_FAILED(webConfigShow(&config, &reply))) { log_write("failed: webConfigShow\n"); }
|
||||
if (R_FAILED(webReplyGetExitReason(&reply, &reason))) { log_write("failed: webReplyGetExitReason\n"); }
|
||||
if (R_FAILED(webReplyGetLastUrl(&reply, last_url, sizeof(last_url), &last_url_len))) { log_write("failed: webReplyGetLastUrl\n"); }
|
||||
log_write("last url: %s\n", last_url);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include <memory>
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::yati::container {
|
||||
namespace {
|
||||
@@ -22,6 +23,38 @@ struct Pfs0FileTableEntry {
|
||||
u32 padding;
|
||||
};
|
||||
|
||||
// stdio-like wrapper for std::vector
|
||||
struct BufHelper {
|
||||
BufHelper() = default;
|
||||
BufHelper(std::span<const u8> data) {
|
||||
write(data);
|
||||
}
|
||||
|
||||
void write(const void* data, u64 size) {
|
||||
if (offset + size >= buf.size()) {
|
||||
buf.resize(offset + size);
|
||||
}
|
||||
std::memcpy(buf.data() + offset, data, size);
|
||||
offset += size;
|
||||
}
|
||||
|
||||
void write(std::span<const u8> data) {
|
||||
write(data.data(), data.size());
|
||||
}
|
||||
|
||||
void seek(u64 where_to) {
|
||||
offset = where_to;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
auto tell() const {
|
||||
return offset;
|
||||
}
|
||||
|
||||
std::vector<u8> buf;
|
||||
u64 offset{};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Result Nsp::GetCollections(Collections& out) {
|
||||
@@ -56,4 +89,48 @@ Result Nsp::GetCollections(Collections& out) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
auto Nsp::Build(std::span<CollectionEntry> entries, s64& size) -> std::vector<u8> {
|
||||
BufHelper buf;
|
||||
|
||||
Pfs0Header header{};
|
||||
std::vector<Pfs0FileTableEntry> file_table(entries.size());
|
||||
std::vector<char> string_table;
|
||||
|
||||
u64 string_offset{};
|
||||
u64 data_offset{};
|
||||
|
||||
for (u32 i = 0; i < entries.size(); i++) {
|
||||
file_table[i].data_offset = data_offset;
|
||||
file_table[i].data_size = entries[i].size;
|
||||
file_table[i].name_offset = string_offset;
|
||||
file_table[i].padding = 0;
|
||||
|
||||
string_table.resize(string_offset + entries[i].name.length() + 1);
|
||||
std::memcpy(string_table.data() + string_offset, entries[i].name.c_str(), entries[i].name.length() + 1);
|
||||
|
||||
data_offset += file_table[i].data_size;
|
||||
string_offset += entries[i].name.length() + 1;
|
||||
}
|
||||
|
||||
// align table
|
||||
string_table.resize((string_table.size() + 0x1F) & ~0x1F);
|
||||
|
||||
header.magic = PFS0_MAGIC;
|
||||
header.total_files = entries.size();
|
||||
header.string_table_size = string_table.size();
|
||||
header.padding = 0;
|
||||
|
||||
buf.write(&header, sizeof(header));
|
||||
buf.write(file_table.data(), sizeof(Pfs0FileTableEntry) * file_table.size());
|
||||
buf.write(string_table.data(), string_table.size());
|
||||
|
||||
// calculate nsp size.
|
||||
size = buf.tell();
|
||||
for (const auto& e : file_table) {
|
||||
size += e.data_size;
|
||||
}
|
||||
|
||||
return buf.buf;
|
||||
}
|
||||
|
||||
} // namespace sphaira::yati::container
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "yati/nx/es.hpp"
|
||||
#include "yati/nx/crypto.hpp"
|
||||
#include "yati/nx/nxdumptool_rsa.h"
|
||||
#include "yati/nx/service_guard.h"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include <memory>
|
||||
@@ -9,12 +10,124 @@
|
||||
namespace sphaira::es {
|
||||
namespace {
|
||||
|
||||
Service g_esSrv;
|
||||
|
||||
NX_GENERATE_SERVICE_GUARD(es);
|
||||
|
||||
Result _esInitialize() {
|
||||
return smGetService(&g_esSrv, "es");
|
||||
}
|
||||
|
||||
void _esCleanup() {
|
||||
serviceClose(&g_esSrv);
|
||||
}
|
||||
|
||||
Result ListTicket(u32 cmd_id, s32 *out_entries_written, FsRightsId* out_ids, s32 count) {
|
||||
struct {
|
||||
u32 num_rights_ids_written;
|
||||
} out;
|
||||
|
||||
const Result rc = serviceDispatchInOut(&g_esSrv, cmd_id, *out_entries_written, out,
|
||||
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
|
||||
.buffers = { { out_ids, count * sizeof(*out_ids) } },
|
||||
);
|
||||
|
||||
if (R_SUCCEEDED(rc) && out_entries_written) *out_entries_written = out.num_rights_ids_written;
|
||||
return rc;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result ImportTicket(Service* srv, const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size) {
|
||||
return serviceDispatch(srv, 1,
|
||||
Result Initialize() {
|
||||
return esInitialize();
|
||||
}
|
||||
|
||||
void Exit() {
|
||||
esExit();
|
||||
}
|
||||
|
||||
Service* GetServiceSession() {
|
||||
return &g_esSrv;
|
||||
}
|
||||
|
||||
Result ImportTicket(const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size) {
|
||||
return serviceDispatch(&g_esSrv, 1,
|
||||
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In, SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
|
||||
.buffers = { { tik_buf, tik_size }, { cert_buf, cert_size } });
|
||||
.buffers = { { tik_buf, tik_size }, { cert_buf, cert_size } }
|
||||
);
|
||||
}
|
||||
|
||||
Result CountCommonTicket(s32* count) {
|
||||
return serviceDispatchOut(&g_esSrv, 9, *count);
|
||||
}
|
||||
|
||||
Result CountPersonalizedTicket(s32* count) {
|
||||
return serviceDispatchOut(&g_esSrv, 10, *count);
|
||||
}
|
||||
|
||||
Result ListCommonTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count) {
|
||||
return ListTicket(11, out_entries_written, out_ids, count);
|
||||
}
|
||||
|
||||
Result ListPersonalizedTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count) {
|
||||
return ListTicket(12, out_entries_written, out_ids, count);
|
||||
}
|
||||
|
||||
Result ListMissingPersonalizedTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count) {
|
||||
return ListTicket(13, out_entries_written, out_ids, count);
|
||||
}
|
||||
|
||||
Result GetCommonTicketSize(u64 *size_out, const FsRightsId* rightsId) {
|
||||
return serviceDispatchInOut(&g_esSrv, 14, *rightsId, *size_out);
|
||||
}
|
||||
|
||||
Result GetCommonTicketData(u64 *size_out, void *tik_data, u64 tik_size, const FsRightsId* rightsId) {
|
||||
return serviceDispatchInOut(&g_esSrv, 16, *rightsId, *size_out,
|
||||
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
|
||||
.buffers = { { tik_data, tik_size } },
|
||||
);
|
||||
}
|
||||
|
||||
Result GetCommonTicketAndCertificateSize(u64 *tik_size_out, u64 *cert_size_out, const FsRightsId* rightsId) {
|
||||
if (hosversionBefore(4,0,0)) {
|
||||
return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
|
||||
}
|
||||
|
||||
struct {
|
||||
u64 ticket_size;
|
||||
u64 cert_size;
|
||||
} out;
|
||||
|
||||
const Result rc = serviceDispatchInOut(&g_esSrv, 22, *rightsId, out);
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
*tik_size_out = out.ticket_size;
|
||||
*cert_size_out = out.cert_size;
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (hosversionBefore(4,0,0)) {
|
||||
return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
|
||||
}
|
||||
|
||||
struct {
|
||||
u64 ticket_size;
|
||||
u64 cert_size;
|
||||
} out;
|
||||
|
||||
const Result rc = serviceDispatchInOut(&g_esSrv, 23, *rightsId, out,
|
||||
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out, SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
|
||||
.buffers = { { tik_buf, tik_size }, { cert_buf, cert_size } }
|
||||
);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
*tik_size_out = out.ticket_size;
|
||||
*cert_size_out = out.cert_size;
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
|
||||
@@ -101,25 +101,26 @@ Result parse_keys(Keys& out, bool read_from_file) {
|
||||
ON_SCOPE_EXIT(setcalExit());
|
||||
R_TRY(setcalGetEticketDeviceKey(std::addressof(out.eticket_device_key)));
|
||||
|
||||
R_UNLESS(ini_browse(cb, std::addressof(out), "/switch/prod.keys"), 0x1);
|
||||
// it doesn't matter if this fails, its just that title decryption will also fail.
|
||||
if (ini_browse(cb, std::addressof(out), "/switch/prod.keys")) {
|
||||
// decrypt eticket device key.
|
||||
if (out.eticket_rsa_kek.IsValid()) {
|
||||
auto rsa_key = (es::EticketRsaDeviceKey*)out.eticket_device_key.key;
|
||||
|
||||
// decrypt eticket device key.
|
||||
if (out.eticket_rsa_kek.IsValid()) {
|
||||
auto rsa_key = (es::EticketRsaDeviceKey*)out.eticket_device_key.key;
|
||||
Aes128CtrContext eticket_aes_ctx{};
|
||||
aes128CtrContextCreate(&eticket_aes_ctx, &out.eticket_rsa_kek, rsa_key->ctr);
|
||||
aes128CtrCrypt(&eticket_aes_ctx, &(rsa_key->private_exponent), &(rsa_key->private_exponent), sizeof(es::EticketRsaDeviceKey) - sizeof(rsa_key->ctr));
|
||||
|
||||
Aes128CtrContext eticket_aes_ctx{};
|
||||
aes128CtrContextCreate(&eticket_aes_ctx, &out.eticket_rsa_kek, rsa_key->ctr);
|
||||
aes128CtrCrypt(&eticket_aes_ctx, &(rsa_key->private_exponent), &(rsa_key->private_exponent), sizeof(es::EticketRsaDeviceKey) - sizeof(rsa_key->ctr));
|
||||
|
||||
const auto public_exponent = std::byteswap(rsa_key->public_exponent);
|
||||
if (public_exponent != 0x10001) {
|
||||
log_write("etick decryption fail: 0x%X\n", public_exponent);
|
||||
if (public_exponent == 0) {
|
||||
log_write("eticket device id is NULL\n");
|
||||
const auto public_exponent = std::byteswap(rsa_key->public_exponent);
|
||||
if (public_exponent != 0x10001) {
|
||||
log_write("etick decryption fail: 0x%X\n", public_exponent);
|
||||
if (public_exponent == 0) {
|
||||
log_write("eticket device id is NULL\n");
|
||||
}
|
||||
R_THROW(0x1);
|
||||
} else {
|
||||
log_write("eticket match\n");
|
||||
}
|
||||
R_THROW(0x1);
|
||||
} else {
|
||||
log_write("eticket match\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,74 @@ Result VerifyFixedKey(const Header& header) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result ParseCnmt(const fs::FsPath& path, u64 program_id, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos) {
|
||||
FsFileSystem fs;
|
||||
R_TRY(fsOpenFileSystemWithId(std::addressof(fs), program_id, FsFileSystemType_ContentMeta, path, FsContentAttributes_All));
|
||||
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
|
||||
|
||||
FsDir dir;
|
||||
R_TRY(fsFsOpenDirectory(std::addressof(fs), fs::FsPath{"/"}, FsDirOpenMode_ReadFiles, std::addressof(dir)));
|
||||
ON_SCOPE_EXIT(fsDirClose(std::addressof(dir)));
|
||||
|
||||
s64 total_entries;
|
||||
FsDirectoryEntry buf;
|
||||
R_TRY(fsDirRead(std::addressof(dir), std::addressof(total_entries), 1, std::addressof(buf)));
|
||||
|
||||
FsFile file;
|
||||
R_TRY(fsFsOpenFile(std::addressof(fs), fs::AppendPath("/", buf.name), FsOpenMode_Read, std::addressof(file)));
|
||||
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
|
||||
|
||||
s64 offset{};
|
||||
u64 bytes_read;
|
||||
R_TRY(fsFileRead(std::addressof(file), offset, std::addressof(header), sizeof(header), 0, std::addressof(bytes_read)));
|
||||
offset += bytes_read;
|
||||
|
||||
// read extended header
|
||||
extended_header.resize(header.meta_header.extended_header_size);
|
||||
R_TRY(fsFileRead(std::addressof(file), offset, extended_header.data(), extended_header.size(), 0, std::addressof(bytes_read)));
|
||||
offset += bytes_read;
|
||||
|
||||
// read infos.
|
||||
infos.resize(header.meta_header.content_count);
|
||||
R_TRY(fsFileRead(std::addressof(file), offset, infos.data(), infos.size() * sizeof(NcmPackagedContentInfo), 0, std::addressof(bytes_read)));
|
||||
offset += bytes_read;
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result ParseControl(const fs::FsPath& path, u64 program_id, void* nacp_out, s64 nacp_size, std::vector<u8>* icon_out) {
|
||||
FsFileSystem fs;
|
||||
R_TRY(fsOpenFileSystemWithId(std::addressof(fs), program_id, FsFileSystemType_ContentControl, path, FsContentAttributes_All));
|
||||
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
|
||||
|
||||
// read nacp.
|
||||
if (nacp_out) {
|
||||
FsFile file;
|
||||
R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/control.nacp"}, FsOpenMode_Read, std::addressof(file)));
|
||||
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
|
||||
|
||||
u64 bytes_read;
|
||||
R_TRY(fsFileRead(&file, 0, nacp_out, nacp_size, 0, &bytes_read));
|
||||
}
|
||||
|
||||
// read icon.
|
||||
if (icon_out) {
|
||||
// todo: use matching icon based on the language version.
|
||||
FsFile file;
|
||||
R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/icon_AmericanEnglish.dat"}, FsOpenMode_Read, std::addressof(file)));
|
||||
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
|
||||
|
||||
s64 size;
|
||||
R_TRY(fsFileGetSize(std::addressof(file), std::addressof(size)));
|
||||
icon_out->resize(size);
|
||||
|
||||
u64 bytes_read;
|
||||
R_TRY(fsFileRead(&file, 0, icon_out->data(), icon_out->size(), 0, &bytes_read));
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
auto GetKeyGenStr(u8 key_gen) -> const char* {
|
||||
switch (key_gen) {
|
||||
case KeyGenerationOld_100: return "1.0.0";
|
||||
@@ -172,6 +240,7 @@ auto GetKeyGenStr(u8 key_gen) -> const char* {
|
||||
case KeyGeneration_1700: return "17.0.0";
|
||||
case KeyGeneration_1800: return "18.0.0";
|
||||
case KeyGeneration_1900: return "19.0.0";
|
||||
case KeyGeneration_2000: return "20.0.0";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
|
||||
@@ -1,12 +1,66 @@
|
||||
#include "yati/nx/ncm.hpp"
|
||||
#include "defines.hpp"
|
||||
#include <memory>
|
||||
#include <bit>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace sphaira::ncm {
|
||||
namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
auto GetMetaTypeStr(u8 meta_type) -> const char* {
|
||||
switch (meta_type) {
|
||||
case NcmContentMetaType_Unknown: return "Unknown";
|
||||
case NcmContentMetaType_SystemProgram: return "SystemProgram";
|
||||
case NcmContentMetaType_SystemData: return "SystemData";
|
||||
case NcmContentMetaType_SystemUpdate: return "SystemUpdate";
|
||||
case NcmContentMetaType_BootImagePackage: return "BootImagePackage";
|
||||
case NcmContentMetaType_BootImagePackageSafe: return "BootImagePackageSafe";
|
||||
case NcmContentMetaType_Application: return "Application";
|
||||
case NcmContentMetaType_Patch: return "Patch";
|
||||
case NcmContentMetaType_AddOnContent: return "AddOnContent";
|
||||
case NcmContentMetaType_Delta: return "Delta";
|
||||
case NcmContentMetaType_DataPatch: return "DataPatch";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
// taken from nxdumptool
|
||||
auto GetMetaTypeShortStr(u8 meta_type) -> const char* {
|
||||
switch (meta_type) {
|
||||
case NcmContentMetaType_Unknown: return "UNK";
|
||||
case NcmContentMetaType_SystemProgram: return "SYSPRG";
|
||||
case NcmContentMetaType_SystemData: return "SYSDAT";
|
||||
case NcmContentMetaType_SystemUpdate: return "SYSUPD";
|
||||
case NcmContentMetaType_BootImagePackage: return "BIP";
|
||||
case NcmContentMetaType_BootImagePackageSafe: return "BIPS";
|
||||
case NcmContentMetaType_Application: return "BASE";
|
||||
case NcmContentMetaType_Patch: return "UPD";
|
||||
case NcmContentMetaType_AddOnContent: return "DLC";
|
||||
case NcmContentMetaType_Delta: return "DELTA";
|
||||
case NcmContentMetaType_DataPatch: return "DLCUPD";
|
||||
}
|
||||
|
||||
return "UNK";
|
||||
}
|
||||
|
||||
auto GetStorageIdStr(u8 storage_id) -> const char* {
|
||||
switch (storage_id) {
|
||||
case NcmStorageId_None: return "None";
|
||||
case NcmStorageId_Host: return "Host";
|
||||
case NcmStorageId_GameCard: return "GameCard";
|
||||
case NcmStorageId_BuiltInSystem: return "BuiltInSystem";
|
||||
case NcmStorageId_BuiltInUser: return "BuiltInUser";
|
||||
case NcmStorageId_SdCard: return "SdCard";
|
||||
case NcmStorageId_Any: return "Any";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
auto GetAppId(u8 meta_type, u64 id) -> u64 {
|
||||
if (meta_type == NcmContentMetaType_Patch) {
|
||||
return id ^ 0x800;
|
||||
@@ -25,6 +79,18 @@ auto GetAppId(const PackagedContentMeta& meta) -> u64 {
|
||||
return GetAppId(meta.meta_type, meta.title_id);
|
||||
}
|
||||
|
||||
auto GetContentIdFromStr(const char* str) -> NcmContentId {
|
||||
char lowerU64[0x11]{};
|
||||
char upperU64[0x11]{};
|
||||
std::memcpy(lowerU64, str, 0x10);
|
||||
std::memcpy(upperU64, str + 0x10, 0x10);
|
||||
|
||||
NcmContentId nca_id{};
|
||||
*(u64*)nca_id.c = std::byteswap(std::strtoul(lowerU64, nullptr, 0x10));
|
||||
*(u64*)(nca_id.c + 8) = std::byteswap(std::strtoul(upperU64, nullptr, 0x10));
|
||||
return nca_id;
|
||||
}
|
||||
|
||||
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id) {
|
||||
bool has;
|
||||
R_TRY(ncmContentStorageHas(cs, std::addressof(has), content_id));
|
||||
|
||||
@@ -14,368 +14,95 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Most of the usb transfer code was taken from Haze.
|
||||
// The USB transfer code was taken from Haze (part of Atmosphere).
|
||||
// The USB protocol was taken from Tinfoil, by Adubbz.
|
||||
|
||||
#include "yati/source/usb.hpp"
|
||||
#include "usb/tinfoil.hpp"
|
||||
#include "log.hpp"
|
||||
#include <ranges>
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
namespace {
|
||||
|
||||
constexpr u32 MAGIC = 0x53504841;
|
||||
constexpr u32 VERSION = 2;
|
||||
|
||||
struct SendHeader {
|
||||
u32 magic;
|
||||
u32 version;
|
||||
};
|
||||
|
||||
struct RecvHeader {
|
||||
u32 magic;
|
||||
u32 version;
|
||||
u32 bcdUSB;
|
||||
u32 count;
|
||||
};
|
||||
namespace tinfoil = usb::tinfoil;
|
||||
|
||||
} // namespace
|
||||
|
||||
Usb::Usb(u64 transfer_timeout) {
|
||||
m_open_result = usbDsInitialize();
|
||||
m_transfer_timeout = transfer_timeout;
|
||||
m_usb = std::make_unique<usb::UsbDs>(transfer_timeout);
|
||||
m_open_result = m_usb->Init();
|
||||
}
|
||||
|
||||
Usb::~Usb() {
|
||||
if (R_SUCCEEDED(GetOpenResult())) {
|
||||
usbDsExit();
|
||||
}
|
||||
}
|
||||
|
||||
Result Usb::Init() {
|
||||
log_write("doing USB init\n");
|
||||
R_TRY(m_open_result);
|
||||
Result Usb::WaitForConnection(u64 timeout, std::vector<std::string>& out_names) {
|
||||
tinfoil::TUSHeader header;
|
||||
R_TRY(m_usb->TransferAll(true, &header, sizeof(header), timeout));
|
||||
R_UNLESS(header.magic == tinfoil::Magic_List0, Result_BadMagic);
|
||||
R_UNLESS(header.nspListSize > 0, Result_BadCount);
|
||||
log_write("USB got header\n");
|
||||
|
||||
u8 iManufacturer, iProduct, iSerialNumber;
|
||||
static const u16 supported_langs[1] = {0x0409};
|
||||
// Send language descriptor
|
||||
R_TRY(usbDsAddUsbLanguageStringDescriptor(NULL, supported_langs, sizeof(supported_langs)/sizeof(u16)));
|
||||
// Send manufacturer
|
||||
R_TRY(usbDsAddUsbStringDescriptor(&iManufacturer, "Nintendo"));
|
||||
// Send product
|
||||
R_TRY(usbDsAddUsbStringDescriptor(&iProduct, "Nintendo Switch"));
|
||||
// Send serial number
|
||||
R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, "SerialNumber"));
|
||||
std::vector<char> names(header.nspListSize);
|
||||
R_TRY(m_usb->TransferAll(true, names.data(), names.size(), timeout));
|
||||
|
||||
// Send device descriptors
|
||||
struct usb_device_descriptor device_descriptor = {
|
||||
.bLength = USB_DT_DEVICE_SIZE,
|
||||
.bDescriptorType = USB_DT_DEVICE,
|
||||
.bcdUSB = 0x0110,
|
||||
.bDeviceClass = 0x00,
|
||||
.bDeviceSubClass = 0x00,
|
||||
.bDeviceProtocol = 0x00,
|
||||
.bMaxPacketSize0 = 0x40,
|
||||
.idVendor = 0x057e,
|
||||
.idProduct = 0x3000,
|
||||
.bcdDevice = 0x0100,
|
||||
.iManufacturer = iManufacturer,
|
||||
.iProduct = iProduct,
|
||||
.iSerialNumber = iSerialNumber,
|
||||
.bNumConfigurations = 0x01
|
||||
};
|
||||
|
||||
// Full Speed is USB 1.1
|
||||
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Full, &device_descriptor));
|
||||
|
||||
// High Speed is USB 2.0
|
||||
device_descriptor.bcdUSB = 0x0200;
|
||||
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_High, &device_descriptor));
|
||||
|
||||
// Super Speed is USB 3.0
|
||||
device_descriptor.bcdUSB = 0x0300;
|
||||
// Upgrade packet size to 512
|
||||
device_descriptor.bMaxPacketSize0 = 0x09;
|
||||
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Super, &device_descriptor));
|
||||
|
||||
// Define Binary Object Store
|
||||
const u8 bos[0x16] = {
|
||||
0x05, // .bLength
|
||||
USB_DT_BOS, // .bDescriptorType
|
||||
0x16, 0x00, // .wTotalLength
|
||||
0x02, // .bNumDeviceCaps
|
||||
|
||||
// USB 2.0
|
||||
0x07, // .bLength
|
||||
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
|
||||
0x02, // .bDevCapabilityType
|
||||
0x02, 0x00, 0x00, 0x00, // dev_capability_data
|
||||
|
||||
// USB 3.0
|
||||
0x0A, // .bLength
|
||||
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
|
||||
0x03, /* .bDevCapabilityType */
|
||||
0x00, /* .bmAttributes */
|
||||
0x0E, 0x00, /* .wSpeedSupported */
|
||||
0x03, /* .bFunctionalitySupport */
|
||||
0x00, /* .bU1DevExitLat */
|
||||
0x00, 0x00 /* .bU2DevExitLat */
|
||||
};
|
||||
|
||||
R_TRY(usbDsSetBinaryObjectStore(bos, sizeof(bos)));
|
||||
|
||||
struct usb_interface_descriptor interface_descriptor = {
|
||||
.bLength = USB_DT_INTERFACE_SIZE,
|
||||
.bDescriptorType = USB_DT_INTERFACE,
|
||||
.bInterfaceNumber = USBDS_DEFAULT_InterfaceNumber, // set below
|
||||
.bNumEndpoints = static_cast<u8>(std::size(m_endpoints)),
|
||||
.bInterfaceClass = USB_CLASS_VENDOR_SPEC,
|
||||
.bInterfaceSubClass = USB_CLASS_VENDOR_SPEC,
|
||||
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
|
||||
};
|
||||
|
||||
|
||||
struct usb_endpoint_descriptor endpoint_descriptor_in = {
|
||||
.bLength = USB_DT_ENDPOINT_SIZE,
|
||||
.bDescriptorType = USB_DT_ENDPOINT,
|
||||
.bEndpointAddress = USB_ENDPOINT_IN,
|
||||
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
||||
.wMaxPacketSize = 0x40,
|
||||
};
|
||||
|
||||
struct usb_endpoint_descriptor endpoint_descriptor_out = {
|
||||
.bLength = USB_DT_ENDPOINT_SIZE,
|
||||
.bDescriptorType = USB_DT_ENDPOINT,
|
||||
.bEndpointAddress = USB_ENDPOINT_OUT,
|
||||
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
||||
.wMaxPacketSize = 0x40,
|
||||
};
|
||||
|
||||
const struct usb_ss_endpoint_companion_descriptor endpoint_companion = {
|
||||
.bLength = sizeof(struct usb_ss_endpoint_companion_descriptor),
|
||||
.bDescriptorType = USB_DT_SS_ENDPOINT_COMPANION,
|
||||
.bMaxBurst = 0x0F,
|
||||
.bmAttributes = 0x00,
|
||||
.wBytesPerInterval = 0x00,
|
||||
};
|
||||
|
||||
R_TRY(usbDsRegisterInterface(&m_interface));
|
||||
|
||||
interface_descriptor.bInterfaceNumber = m_interface->interface_index;
|
||||
endpoint_descriptor_in.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
|
||||
endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
|
||||
|
||||
// Full Speed Config
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
|
||||
// High Speed Config
|
||||
endpoint_descriptor_in.wMaxPacketSize = 0x200;
|
||||
endpoint_descriptor_out.wMaxPacketSize = 0x200;
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
|
||||
// Super Speed Config
|
||||
endpoint_descriptor_in.wMaxPacketSize = 0x400;
|
||||
endpoint_descriptor_out.wMaxPacketSize = 0x400;
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
|
||||
|
||||
//Setup endpoints.
|
||||
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_In], endpoint_descriptor_in.bEndpointAddress));
|
||||
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_Out], endpoint_descriptor_out.bEndpointAddress));
|
||||
|
||||
R_TRY(usbDsInterface_EnableInterface(m_interface));
|
||||
R_TRY(usbDsEnable());
|
||||
|
||||
log_write("success USB init\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::WaitForConnection(u64 timeout, u32& speed, u32& count) {
|
||||
const SendHeader send_header{
|
||||
.magic = MAGIC,
|
||||
.version = VERSION,
|
||||
};
|
||||
|
||||
alignas(0x1000) u8 aligned[0x1000]{};
|
||||
std::memcpy(aligned, std::addressof(send_header), sizeof(send_header));
|
||||
|
||||
// send header.
|
||||
u32 transferredSize;
|
||||
R_TRY(TransferPacketImpl(false, aligned, sizeof(send_header), &transferredSize, timeout));
|
||||
|
||||
// receive header.
|
||||
struct RecvHeader recv_header{};
|
||||
R_TRY(TransferPacketImpl(true, aligned, sizeof(recv_header), &transferredSize, timeout));
|
||||
|
||||
// copy data into header struct.
|
||||
std::memcpy(&recv_header, aligned, sizeof(recv_header));
|
||||
|
||||
// validate received header.
|
||||
R_UNLESS(recv_header.magic == MAGIC, Result_BadMagic);
|
||||
R_UNLESS(recv_header.version == VERSION, Result_BadVersion);
|
||||
R_UNLESS(recv_header.count > 0, Result_BadCount);
|
||||
|
||||
count = recv_header.count;
|
||||
speed = recv_header.bcdUSB;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::GetFileInfo(std::string& name_out, u64& size_out) {
|
||||
struct {
|
||||
u64 size;
|
||||
u64 name_length;
|
||||
} file_info_meta;
|
||||
|
||||
alignas(0x1000) u8 aligned[0x1000]{};
|
||||
|
||||
// receive meta.
|
||||
u32 transferredSize;
|
||||
R_TRY(TransferPacketImpl(true, aligned, sizeof(file_info_meta), &transferredSize, m_transfer_timeout));
|
||||
std::memcpy(&file_info_meta, aligned, sizeof(file_info_meta));
|
||||
R_UNLESS(file_info_meta.name_length < sizeof(aligned), 0x1);
|
||||
|
||||
R_TRY(TransferPacketImpl(true, aligned, file_info_meta.name_length, &transferredSize, m_transfer_timeout));
|
||||
name_out.resize(file_info_meta.name_length);
|
||||
std::memcpy(name_out.data(), aligned, name_out.size());
|
||||
|
||||
size_out = file_info_meta.size;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
bool Usb::GetConfigured() const {
|
||||
UsbState usb_state;
|
||||
usbDsGetState(std::addressof(usb_state));
|
||||
return usb_state == UsbState_Configured;
|
||||
}
|
||||
|
||||
Event *Usb::GetCompletionEvent(UsbSessionEndpoint ep) const {
|
||||
return std::addressof(m_endpoints[ep]->CompletionEvent);
|
||||
}
|
||||
|
||||
Result Usb::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) const {
|
||||
auto event = GetCompletionEvent(ep);
|
||||
const auto rc = eventWait(event, timeout);
|
||||
|
||||
if (R_FAILED(rc)) {
|
||||
R_TRY(usbDsEndpoint_Cancel(m_endpoints[ep]));
|
||||
eventClear(event);
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
Result Usb::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) const {
|
||||
return usbDsEndpoint_PostBufferAsync(m_endpoints[ep], buffer, size, out_urb_id);
|
||||
}
|
||||
|
||||
Result Usb::GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) const {
|
||||
UsbDsReportData report_data;
|
||||
|
||||
R_TRY(eventClear(std::addressof(m_endpoints[ep]->CompletionEvent)));
|
||||
R_TRY(usbDsEndpoint_GetReportData(m_endpoints[ep], std::addressof(report_data)));
|
||||
R_TRY(usbDsParseReportData(std::addressof(report_data), urb_id, out_requested_size, out_transferred_size));
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) const {
|
||||
u32 urb_id;
|
||||
|
||||
/* If we're not configured yet, wait to become configured first. */
|
||||
// R_TRY(usbDsWaitReady(timeout));
|
||||
if (!GetConfigured()) {
|
||||
R_TRY(eventWait(usbDsGetStateChangeEvent(), timeout));
|
||||
R_TRY(eventClear(usbDsGetStateChangeEvent()));
|
||||
R_THROW(0xEA01);
|
||||
}
|
||||
|
||||
/* Select the appropriate endpoint and begin a transfer. */
|
||||
const auto ep = read ? UsbSessionEndpoint_Out : UsbSessionEndpoint_In;
|
||||
R_TRY(TransferAsync(ep, page, size, std::addressof(urb_id)));
|
||||
|
||||
/* Try to wait for the event. */
|
||||
R_TRY(WaitTransferCompletion(ep, timeout));
|
||||
|
||||
/* Return what we transferred. */
|
||||
return GetTransferResult(ep, urb_id, nullptr, out_size_transferred);
|
||||
}
|
||||
|
||||
Result Usb::SendCommand(s64 off, s64 size) const {
|
||||
struct {
|
||||
u32 hash;
|
||||
u32 magic;
|
||||
s64 off;
|
||||
s64 size;
|
||||
} meta{0, 0, off, size};
|
||||
|
||||
alignas(0x1000) static u8 aligned[0x1000]{};
|
||||
std::memcpy(aligned, std::addressof(meta), sizeof(meta));
|
||||
|
||||
u32 transferredSize;
|
||||
return TransferPacketImpl(false, aligned, sizeof(meta), &transferredSize, m_transfer_timeout);
|
||||
}
|
||||
|
||||
Result Usb::Finished() const {
|
||||
return SendCommand(0, 0);
|
||||
}
|
||||
|
||||
Result Usb::InternalRead(void* _buf, s64 off, s64 size) const {
|
||||
u8* buf = (u8*)_buf;
|
||||
alignas(0x1000) u8 aligned[0x1000]{};
|
||||
const auto stored_size = size;
|
||||
s64 total = 0;
|
||||
|
||||
while (size) {
|
||||
auto read_size = size;
|
||||
auto read_buf = buf;
|
||||
|
||||
if (u64(buf) & 0xFFF) {
|
||||
read_size = std::min<u64>(size, sizeof(aligned) - (u64(buf) & 0xFFF));
|
||||
read_buf = aligned;
|
||||
log_write("unaligned read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
|
||||
} else if (read_size & 0xFFF) {
|
||||
if (read_size <= 0xFFF) {
|
||||
log_write("unaligned small read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
|
||||
read_buf = aligned;
|
||||
} else {
|
||||
log_write("unaligned big read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
|
||||
// read as much as possible into buffer, the rest will
|
||||
// be handled in a second read which will be aligned size aligned.
|
||||
read_size = read_size & ~0xFFF;
|
||||
}
|
||||
out_names.clear();
|
||||
for (const auto& name : std::views::split(names, '\n')) {
|
||||
if (!name.empty()) {
|
||||
out_names.emplace_back(name.data(), name.size());
|
||||
}
|
||||
|
||||
R_TRY(SendCommand(off, read_size));
|
||||
|
||||
u32 transferredSize{};
|
||||
R_TRY(TransferPacketImpl(true, read_buf, read_size, &transferredSize, m_transfer_timeout));
|
||||
R_UNLESS(transferredSize <= read_size, Result_BadTransferSize);
|
||||
|
||||
if (read_buf == aligned) {
|
||||
std::memcpy(buf, aligned, transferredSize);
|
||||
}
|
||||
|
||||
if (transferredSize < read_size) {
|
||||
log_write("reading less than expected! %u vs %zd stored: %zd\n", transferredSize, read_size, stored_size);
|
||||
}
|
||||
|
||||
off += transferredSize;
|
||||
buf += transferredSize;
|
||||
size -= transferredSize;
|
||||
total += transferredSize;
|
||||
}
|
||||
|
||||
R_UNLESS(total == stored_size, Result_BadTotalSize);
|
||||
for (auto& name : out_names) {
|
||||
log_write("got name: %s\n", name.c_str());
|
||||
}
|
||||
|
||||
R_UNLESS(!out_names.empty(), Result_BadCount);
|
||||
log_write("USB SUCCESS\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void Usb::SetFileNameForTranfser(const std::string& name) {
|
||||
m_transfer_file_name = name;
|
||||
}
|
||||
|
||||
Result Usb::SendCmdHeader(u32 cmdId, size_t dataSize, u64 timeout) {
|
||||
tinfoil::USBCmdHeader header{
|
||||
.magic = tinfoil::Magic_Command0,
|
||||
.type = tinfoil::USBCmdType::REQUEST,
|
||||
.cmdId = cmdId,
|
||||
.dataSize = dataSize,
|
||||
};
|
||||
|
||||
return m_usb->TransferAll(false, &header, sizeof(header), timeout);
|
||||
}
|
||||
|
||||
Result Usb::SendFileRangeCmd(u64 off, u64 size, u64 timeout) {
|
||||
tinfoil::FileRangeCmdHeader fRangeHeader;
|
||||
fRangeHeader.size = size;
|
||||
fRangeHeader.offset = off;
|
||||
fRangeHeader.nspNameLen = m_transfer_file_name.size();
|
||||
fRangeHeader.padding = 0;
|
||||
|
||||
R_TRY(SendCmdHeader(tinfoil::USBCmdId::FILE_RANGE, sizeof(fRangeHeader) + fRangeHeader.nspNameLen, timeout));
|
||||
R_TRY(m_usb->TransferAll(false, &fRangeHeader, sizeof(fRangeHeader), timeout));
|
||||
R_TRY(m_usb->TransferAll(false, m_transfer_file_name.data(), m_transfer_file_name.size(), timeout));
|
||||
|
||||
tinfoil::USBCmdHeader responseHeader;
|
||||
R_TRY(m_usb->TransferAll(true, &responseHeader, sizeof(responseHeader), timeout));
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::Finished(u64 timeout) {
|
||||
return SendCmdHeader(tinfoil::USBCmdId::EXIT, 0, timeout);
|
||||
}
|
||||
|
||||
Result Usb::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||
R_TRY(GetOpenResult());
|
||||
R_TRY(InternalRead(buf, off, size));
|
||||
R_TRY(SendFileRangeCmd(off, size, m_usb->GetTransferTimeout()));
|
||||
R_TRY(m_usb->TransferAll(true, buf, size));
|
||||
*bytes_read = size;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
#include "i18n.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
#include <new>
|
||||
#include <zstd.h>
|
||||
#include <minIni.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace sphaira::yati {
|
||||
namespace {
|
||||
@@ -30,40 +30,10 @@ constexpr NcmStorageId NCM_STORAGE_IDS[]{
|
||||
NcmStorageId_SdCard,
|
||||
};
|
||||
|
||||
// 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* {
|
||||
// log_write("allocating ptr size: %zu\n", n);
|
||||
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 {
|
||||
// log_write("deleting ptr size: %zu\n", n);
|
||||
::operator delete[] (p, n, align);
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr inline std::align_val_t align{Align};
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct PageAllocator : CustomVectorAllocator<T, 0x1000> {
|
||||
using value_type = T; // used by std::vector
|
||||
};
|
||||
|
||||
template<class T, class U>
|
||||
bool operator==(const PageAllocator <T>&, const PageAllocator <U>&) { return true; }
|
||||
|
||||
using PageAlignedVector = std::vector<u8, PageAllocator<u8>>;
|
||||
|
||||
constexpr u32 KEYGEN_LIMIT = 0x20;
|
||||
|
||||
struct NcaCollection : container::CollectionEntry {
|
||||
nca::Header header{};
|
||||
// NcmContentType
|
||||
u8 type{};
|
||||
NcmContentId content_id{};
|
||||
@@ -72,6 +42,8 @@ struct NcaCollection : container::CollectionEntry {
|
||||
u8 hash[SHA256_HASH_SIZE]{};
|
||||
// set true if nca has been modified.
|
||||
bool modified{};
|
||||
// set if the nca was not installed.
|
||||
bool skipped{};
|
||||
};
|
||||
|
||||
struct CnmtCollection : NcaCollection {
|
||||
@@ -81,7 +53,7 @@ struct CnmtCollection : NcaCollection {
|
||||
// if set, the ticket / cert will be installed once all nca's have installed.
|
||||
std::vector<FsRightsId> rights_id{};
|
||||
|
||||
NcmContentMetaHeader header{};
|
||||
NcmContentMetaHeader meta_header{};
|
||||
NcmContentMetaKey key{};
|
||||
NcmContentInfo content_info{};
|
||||
std::vector<u8> extended_header{};
|
||||
@@ -107,7 +79,7 @@ struct ThreadBuffer {
|
||||
buf.reserve(INFLATE_BUFFER_MAX);
|
||||
}
|
||||
|
||||
PageAlignedVector buf;
|
||||
std::vector<u8> buf;
|
||||
s64 off;
|
||||
};
|
||||
|
||||
@@ -137,7 +109,7 @@ public:
|
||||
return ringbuf_capacity() - ringbuf_size();
|
||||
}
|
||||
|
||||
void ringbuf_push(PageAlignedVector& buf_in, s64 off_in) {
|
||||
void ringbuf_push(std::vector<u8>& buf_in, s64 off_in) {
|
||||
auto& value = this->buf[this->w_index % ringbuf_capacity()];
|
||||
value.off = off_in;
|
||||
std::swap(value.buf, buf_in);
|
||||
@@ -145,7 +117,7 @@ public:
|
||||
this->w_index = (this->w_index + 1U) % (ringbuf_capacity() * 2U);
|
||||
}
|
||||
|
||||
void ringbuf_pop(PageAlignedVector& buf_out, s64& off_out) {
|
||||
void ringbuf_pop(std::vector<u8>& buf_out, s64& off_out) {
|
||||
auto& value = this->buf[this->r_index % ringbuf_capacity()];
|
||||
off_out = value.off;
|
||||
std::swap(value.buf, buf_out);
|
||||
@@ -178,7 +150,7 @@ struct ThreadData {
|
||||
|
||||
Result Read(void* buf, s64 size, u64* bytes_read);
|
||||
|
||||
Result SetDecompressBuf(PageAlignedVector& buf, s64 off, s64 size) {
|
||||
Result SetDecompressBuf(std::vector<u8>& buf, s64 off, s64 size) {
|
||||
buf.resize(size);
|
||||
|
||||
mutexLock(std::addressof(read_mutex));
|
||||
@@ -192,7 +164,7 @@ struct ThreadData {
|
||||
return condvarWakeOne(std::addressof(can_decompress));
|
||||
}
|
||||
|
||||
Result GetDecompressBuf(PageAlignedVector& buf_out, s64& off_out) {
|
||||
Result GetDecompressBuf(std::vector<u8>& buf_out, s64& off_out) {
|
||||
mutexLock(std::addressof(read_mutex));
|
||||
if (!read_buffers.ringbuf_size()) {
|
||||
R_TRY(condvarWait(std::addressof(can_decompress), std::addressof(read_mutex)));
|
||||
@@ -204,7 +176,7 @@ struct ThreadData {
|
||||
return condvarWakeOne(std::addressof(can_read));
|
||||
}
|
||||
|
||||
Result SetWriteBuf(PageAlignedVector& buf, s64 size, bool skip_verify) {
|
||||
Result SetWriteBuf(std::vector<u8>& buf, s64 size, bool skip_verify) {
|
||||
buf.resize(size);
|
||||
if (!skip_verify) {
|
||||
sha256ContextUpdate(std::addressof(sha256), buf.data(), buf.size());
|
||||
@@ -221,7 +193,7 @@ struct ThreadData {
|
||||
return condvarWakeOne(std::addressof(can_write));
|
||||
}
|
||||
|
||||
Result GetWriteBuf(PageAlignedVector& buf_out, s64& off_out) {
|
||||
Result GetWriteBuf(std::vector<u8>& buf_out, s64& off_out) {
|
||||
mutexLock(std::addressof(write_mutex));
|
||||
if (!write_buffers.ringbuf_size()) {
|
||||
R_TRY(condvarWait(std::addressof(can_write), std::addressof(write_mutex)));
|
||||
@@ -276,8 +248,8 @@ struct Yati {
|
||||
|
||||
Result Setup(const ConfigOverride& override);
|
||||
Result InstallNca(std::span<TikCollection> tickets, NcaCollection& nca);
|
||||
Result InstallNcaInternal(std::span<TikCollection> tickets, NcaCollection& nca);
|
||||
Result InstallCnmtNca(std::span<TikCollection> tickets, CnmtCollection& cnmt, const container::Collections& collections);
|
||||
Result InstallControlNca(std::span<TikCollection> tickets, const CnmtCollection& cnmt, NcaCollection& nca);
|
||||
|
||||
Result readFuncInternal(ThreadData* t);
|
||||
Result decompressFuncInternal(ThreadData* t);
|
||||
@@ -303,7 +275,6 @@ struct Yati {
|
||||
NcmContentMetaDatabase db{};
|
||||
NcmStorageId storage_id{};
|
||||
|
||||
Service es{};
|
||||
Service ns_app{};
|
||||
std::unique_ptr<container::Base> container{};
|
||||
Config config{};
|
||||
@@ -361,11 +332,11 @@ HashStr hexIdToStr(auto id) {
|
||||
// parsing ncz headers, sections and reading ncz blocks
|
||||
Result Yati::readFuncInternal(ThreadData* t) {
|
||||
// the main buffer which data is read into.
|
||||
PageAlignedVector buf;
|
||||
std::vector<u8> buf;
|
||||
// workaround ncz block reading ahead. if block isn't found, we usually
|
||||
// would seek back to the offset, however this is not possible in stream
|
||||
// mode, so we instead store the data to the temp buffer and pre-pend it.
|
||||
PageAlignedVector temp_buf;
|
||||
std::vector<u8> temp_buf;
|
||||
buf.reserve(t->max_buffer_size);
|
||||
temp_buf.reserve(t->max_buffer_size);
|
||||
|
||||
@@ -453,12 +424,12 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
|
||||
|
||||
s64 inflate_offset{};
|
||||
Aes128CtrContext ctx{};
|
||||
PageAlignedVector inflate_buf{};
|
||||
std::vector<u8> inflate_buf{};
|
||||
inflate_buf.reserve(t->max_buffer_size);
|
||||
|
||||
s64 written{};
|
||||
s64 decompress_buf_off{};
|
||||
PageAlignedVector buf{};
|
||||
std::vector<u8> buf{};
|
||||
buf.reserve(t->max_buffer_size);
|
||||
|
||||
// encrypts the nca and passes the buffer to the write thread.
|
||||
@@ -471,7 +442,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
|
||||
// the remaining data.
|
||||
// rather that copying the entire vector to the write thread,
|
||||
// only copy (store) the remaining amount.
|
||||
PageAlignedVector temp_vector{};
|
||||
std::vector<u8> temp_vector{};
|
||||
if (size < inflate_offset) {
|
||||
temp_vector.resize(inflate_offset - size);
|
||||
std::memcpy(temp_vector.data(), inflate_buf.data() + size, temp_vector.size());
|
||||
@@ -480,7 +451,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
|
||||
for (s64 off = 0; off < size;) {
|
||||
// log_write("looking for section\n");
|
||||
if (!ncz_section || !ncz_section->InRange(written)) {
|
||||
auto it = std::find_if(t->ncz_sections.cbegin(), t->ncz_sections.cend(), [written](auto& e){
|
||||
auto it = std::ranges::find_if(t->ncz_sections, [written](auto& e){
|
||||
return e.InRange(written);
|
||||
});
|
||||
|
||||
@@ -532,12 +503,17 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
|
||||
if (!is_ncz || !decompress_buf_off) {
|
||||
// check nca header
|
||||
if (!decompress_buf_off) {
|
||||
log_write("reading nca header\n");
|
||||
|
||||
nca::Header header{};
|
||||
crypto::cryptoAes128Xts(buf.data(), std::addressof(header), keys.header_key, 0, 0x200, sizeof(header), false);
|
||||
log_write("verifying nca header magic\n");
|
||||
R_UNLESS(header.magic == 0x3341434E, Result_InvalidNcaMagic);
|
||||
log_write("nca magic is ok! type: %u\n", header.content_type);
|
||||
|
||||
// store the unmodified header.
|
||||
t->nca->header = header;
|
||||
|
||||
if (!config.skip_rsa_header_fixed_key_verify) {
|
||||
log_write("verifying nca fixed key\n");
|
||||
R_TRY(nca::VerifyFixedKey(header));
|
||||
@@ -547,6 +523,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
|
||||
}
|
||||
|
||||
t->write_size = header.size;
|
||||
log_write("setting placeholder size: %zu\n", header.size);
|
||||
R_TRY(ncmContentStorageSetPlaceHolderSize(std::addressof(cs), std::addressof(t->nca->placeholder_id), header.size));
|
||||
|
||||
if (!config.ignore_distribution_bit && header.distribution_type == nca::DistributionType_GameCard) {
|
||||
@@ -556,11 +533,13 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
|
||||
|
||||
TikCollection* ticket = nullptr;
|
||||
if (isRightsIdValid(header.rights_id)) {
|
||||
auto it = std::find_if(t->tik.begin(), t->tik.end(), [header](auto& e){
|
||||
auto it = std::ranges::find_if(t->tik, [&header](auto& e){
|
||||
return !std::memcmp(&header.rights_id, &e.rights_id, sizeof(e.rights_id));
|
||||
});
|
||||
|
||||
log_write("looking for ticket %s\n", hexIdToStr(header.rights_id).str);
|
||||
R_UNLESS(it != t->tik.end(), Result_TicketNotFound);
|
||||
log_write("ticket found\n");
|
||||
it->required = true;
|
||||
ticket = &(*it);
|
||||
}
|
||||
@@ -632,7 +611,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
|
||||
// todo: blocks need to use read offset, as the offset + size is compressed range.
|
||||
if (t->ncz_blocks.size()) {
|
||||
if (!ncz_block || !ncz_block->InRange(decompress_buf_off)) {
|
||||
auto it = std::find_if(t->ncz_blocks.cbegin(), t->ncz_blocks.cend(), [decompress_buf_off](auto& e){
|
||||
auto it = std::ranges::find_if(t->ncz_blocks, [decompress_buf_off](auto& e){
|
||||
return e.InRange(decompress_buf_off);
|
||||
});
|
||||
|
||||
@@ -709,7 +688,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
|
||||
|
||||
// write thread writes data to the nca placeholder.
|
||||
Result Yati::writeFuncInternal(ThreadData* t) {
|
||||
PageAlignedVector buf;
|
||||
std::vector<u8> buf;
|
||||
buf.reserve(t->max_buffer_size);
|
||||
|
||||
while (t->write_offset < t->write_size && R_SUCCEEDED(t->GetResults())) {
|
||||
@@ -782,18 +761,23 @@ Yati::~Yati() {
|
||||
splCryptoExit();
|
||||
serviceClose(std::addressof(ns_app));
|
||||
nsExit();
|
||||
es::Exit();
|
||||
|
||||
for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) {
|
||||
ncmContentMetaDatabaseClose(std::addressof(ncm_db[i]));
|
||||
ncmContentStorageClose(std::addressof(ncm_cs[i]));
|
||||
}
|
||||
|
||||
serviceClose(std::addressof(es));
|
||||
appletSetMediaPlaybackState(false);
|
||||
|
||||
if (config.boost_mode) {
|
||||
appletSetCpuBoostMode(ApmCpuBoostMode_Normal);
|
||||
}
|
||||
}
|
||||
|
||||
Result Yati::Setup(const ConfigOverride& override) {
|
||||
config.sd_card_install = override.sd_card_install.value_or(App::GetApp()->m_install_sd.Get());
|
||||
config.boost_mode = App::GetApp()->m_boost_mode.Get();
|
||||
config.allow_downgrade = App::GetApp()->m_allow_downgrade.Get();
|
||||
config.skip_if_already_installed = App::GetApp()->m_skip_if_already_installed.Get();
|
||||
config.ticket_only = App::GetApp()->m_ticket_only.Get();
|
||||
@@ -811,11 +795,15 @@ Result Yati::Setup(const ConfigOverride& override) {
|
||||
config.lower_system_version = override.lower_system_version.value_or(App::GetApp()->m_lower_system_version.Get());
|
||||
storage_id = config.sd_card_install ? NcmStorageId_SdCard : NcmStorageId_BuiltInUser;
|
||||
|
||||
if (config.boost_mode) {
|
||||
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
|
||||
}
|
||||
|
||||
R_TRY(source->GetOpenResult());
|
||||
R_TRY(splCryptoInitialize());
|
||||
R_TRY(nsInitialize());
|
||||
R_TRY(nsGetApplicationManagerInterface(std::addressof(ns_app)));
|
||||
R_TRY(smGetService(std::addressof(es), "es"));
|
||||
R_TRY(es::Initialize());
|
||||
|
||||
for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) {
|
||||
R_TRY(ncmOpenContentMetaDatabase(std::addressof(ncm_db[i]), NCM_STORAGE_IDS[i]));
|
||||
@@ -829,10 +817,17 @@ Result Yati::Setup(const ConfigOverride& override) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Yati::InstallNca(std::span<TikCollection> tickets, NcaCollection& nca) {
|
||||
log_write("in install nca\n");
|
||||
pbox->NewTransfer(nca.name);
|
||||
keys::parse_hex_key(std::addressof(nca.content_id), nca.name.c_str());
|
||||
Result Yati::InstallNcaInternal(std::span<TikCollection> tickets, NcaCollection& nca) {
|
||||
if (config.skip_if_already_installed) {
|
||||
R_TRY(ncmContentStorageHas(std::addressof(cs), std::addressof(nca.skipped), std::addressof(nca.content_id)));
|
||||
if (nca.skipped) {
|
||||
log_write("\tskipped nca as it's already installed ncmContentStorageHas()\n");
|
||||
R_TRY(ncmContentStorageReadContentIdFile(std::addressof(cs), std::addressof(nca.header), sizeof(nca.header), std::addressof(nca.content_id), 0));
|
||||
crypto::cryptoAes128Xts(std::addressof(nca.header), std::addressof(nca.header), keys.header_key, 0, 0x200, sizeof(nca.header), false);
|
||||
R_SUCCEED();
|
||||
}
|
||||
}
|
||||
|
||||
log_write("generateing placeholder\n");
|
||||
R_TRY(ncmContentStorageGeneratePlaceHolderId(std::addressof(cs), std::addressof(nca.placeholder_id)));
|
||||
log_write("creating placeholder\n");
|
||||
@@ -918,39 +913,73 @@ Result Yati::InstallNca(std::span<TikCollection> tickets, NcaCollection& nca) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Yati::InstallNca(std::span<TikCollection> tickets, NcaCollection& nca) {
|
||||
log_write("in install nca\n");
|
||||
pbox->NewTransfer(nca.name);
|
||||
keys::parse_hex_key(std::addressof(nca.content_id), nca.name.c_str());
|
||||
|
||||
R_TRY(InstallNcaInternal(tickets, nca));
|
||||
|
||||
fs::FsPath path;
|
||||
if (nca.skipped) {
|
||||
R_TRY(ncmContentStorageGetPath(std::addressof(cs), path, sizeof(path), std::addressof(nca.content_id)));
|
||||
} else {
|
||||
R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs)));
|
||||
R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(nca.placeholder_id)));
|
||||
}
|
||||
|
||||
if (nca.header.content_type == nca::ContentType_Program) {
|
||||
// todo: verify npdm key.
|
||||
} else if (nca.header.content_type == nca::ContentType_Control) {
|
||||
NacpLanguageEntry entry;
|
||||
std::vector<u8> icon;
|
||||
// this may fail if tickets aren't installed and the nca uses title key crypto.
|
||||
if (R_SUCCEEDED(nca::ParseControl(path, nca.header.program_id, &entry, sizeof(entry), &icon))) {
|
||||
pbox->SetTitle(entry.name).SetImageData(icon);
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Yati::InstallCnmtNca(std::span<TikCollection> tickets, CnmtCollection& cnmt, const container::Collections& collections) {
|
||||
R_TRY(InstallNca(tickets, cnmt));
|
||||
|
||||
fs::FsPath path;
|
||||
R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs)));
|
||||
R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(cnmt.placeholder_id)));
|
||||
if (cnmt.skipped) {
|
||||
R_TRY(ncmContentStorageGetPath(std::addressof(cs), path, sizeof(path), std::addressof(cnmt.content_id)));
|
||||
} else {
|
||||
R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs)));
|
||||
R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(cnmt.placeholder_id)));
|
||||
}
|
||||
|
||||
ncm::PackagedContentMeta header;
|
||||
std::vector<NcmPackagedContentInfo> infos;
|
||||
R_TRY(ParseCnmtNca(path, header, cnmt.extended_header, infos));
|
||||
R_TRY(nca::ParseCnmt(path, cnmt.header.program_id, header, cnmt.extended_header, infos));
|
||||
|
||||
for (const auto& info : infos) {
|
||||
if (info.info.content_type == NcmContentType_DeltaFragment) {
|
||||
for (const auto& packed_info : infos) {
|
||||
const auto& info = packed_info.info;
|
||||
if (info.content_type == NcmContentType_DeltaFragment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto str = hexIdToStr(info.info.content_id);
|
||||
const auto it = std::find_if(collections.cbegin(), collections.cend(), [str](auto& e){
|
||||
const auto str = hexIdToStr(info.content_id);
|
||||
const auto it = std::ranges::find_if(collections, [&str](auto& e){
|
||||
return e.name.find(str.str) != e.name.npos;
|
||||
});
|
||||
|
||||
R_UNLESS(it != collections.cend(), Result_NcaNotFound);
|
||||
|
||||
log_write("found: %s\n", str.str);
|
||||
cnmt.infos.emplace_back(info);
|
||||
cnmt.infos.emplace_back(packed_info);
|
||||
auto& nca = cnmt.ncas.emplace_back(*it);
|
||||
nca.type = info.info.content_type;
|
||||
nca.type = info.content_type;
|
||||
}
|
||||
|
||||
// update header
|
||||
cnmt.header = header.meta_header;
|
||||
cnmt.header.content_count = cnmt.infos.size() + 1;
|
||||
cnmt.header.storage_id = 0;
|
||||
cnmt.meta_header = header.meta_header;
|
||||
cnmt.meta_header.content_count = cnmt.infos.size() + 1;
|
||||
cnmt.meta_header.storage_id = 0;
|
||||
|
||||
cnmt.key.id = header.title_id;
|
||||
cnmt.key.version = header.title_version;
|
||||
@@ -979,32 +1008,12 @@ Result Yati::InstallCnmtNca(std::span<TikCollection> tickets, CnmtCollection& cn
|
||||
return lhs.type > rhs.type;
|
||||
};
|
||||
|
||||
std::sort(cnmt.ncas.begin(), cnmt.ncas.end(), sorter);
|
||||
std::ranges::sort(cnmt.ncas, sorter);
|
||||
|
||||
log_write("found all cnmts\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Yati::InstallControlNca(std::span<TikCollection> tickets, const CnmtCollection& cnmt, NcaCollection& nca) {
|
||||
R_TRY(InstallNca(tickets, nca));
|
||||
|
||||
fs::FsPath path;
|
||||
R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs)));
|
||||
R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(nca.placeholder_id)));
|
||||
|
||||
// this can fail if it's not a valid control nca, examples are mario 3d all stars.
|
||||
// there are 4 control ncas, only 1 is valid (InvalidNcaId 0x235E02).
|
||||
NacpLanguageEntry entry;
|
||||
std::vector<u8> icon;
|
||||
if (R_SUCCEEDED(yati::ParseControlNca(path, ncm::GetAppId(cnmt.key), &entry, sizeof(entry), &icon))) {
|
||||
pbox->SetTitle(entry.name).SetImageData(icon);
|
||||
} else {
|
||||
log_write("\tWARNING: failed to parse control nca!\n");
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Yati::ParseTicketsIntoCollection(std::vector<TikCollection>& tickets, const container::Collections& collections, bool read_data) {
|
||||
for (const auto& collection : collections) {
|
||||
if (collection.name.ends_with(".tik")) {
|
||||
@@ -1012,7 +1021,7 @@ Result Yati::ParseTicketsIntoCollection(std::vector<TikCollection>& tickets, con
|
||||
keys::parse_hex_key(entry.rights_id.c, collection.name.c_str());
|
||||
const auto str = collection.name.substr(0, collection.name.length() - 4) + ".cert";
|
||||
|
||||
const auto cert = std::find_if(collections.cbegin(), collections.cend(), [str](auto& e){
|
||||
const auto cert = std::ranges::find_if(collections, [&str](auto& e){
|
||||
return e.name.find(str) != e.name.npos;
|
||||
});
|
||||
|
||||
@@ -1091,6 +1100,13 @@ Result Yati::ShouldSkip(const CnmtCollection& cnmt, bool& skip) {
|
||||
} else if (config.skip_data_patch && cnmt.key.type == NcmContentMetaType_DataPatch) {
|
||||
log_write("\tskipping: [NcmContentMetaType_DataPatch]\n");
|
||||
skip = true;
|
||||
} else if (config.skip_if_already_installed) {
|
||||
bool has;
|
||||
R_TRY(ncmContentMetaDatabaseHas(std::addressof(db), std::addressof(has), std::addressof(cnmt.key)));
|
||||
if (has) {
|
||||
log_write("\tskipping: [ncmContentMetaDatabaseHas()]\n");
|
||||
skip = true;
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
@@ -1105,7 +1121,7 @@ Result Yati::ImportTickets(std::span<TikCollection> tickets) {
|
||||
log_write("patching ticket\n");
|
||||
R_TRY(es::PatchTicket(ticket.ticket, keys));
|
||||
log_write("installing ticket\n");
|
||||
R_TRY(es::ImportTicket(std::addressof(es), ticket.ticket.data(), ticket.ticket.size(), ticket.cert.data(), ticket.cert.size()));
|
||||
R_TRY(es::ImportTicket(ticket.ticket.data(), ticket.ticket.size(), ticket.cert.data(), ticket.cert.size()));
|
||||
ticket.required = false;
|
||||
}
|
||||
}
|
||||
@@ -1183,7 +1199,7 @@ Result Yati::RegisterNcasAndPushRecord(const CnmtCollection& cnmt, u32 latest_ve
|
||||
log_write("registered cnmt nca\n");
|
||||
|
||||
for (auto& nca : cnmt.ncas) {
|
||||
if (nca.type != NcmContentType_DeltaFragment) {
|
||||
if (!nca.skipped && nca.type != NcmContentType_DeltaFragment) {
|
||||
log_write("registering nca: %s\n", nca.name.c_str());
|
||||
R_TRY(ncm::Register(std::addressof(cs), std::addressof(nca.content_id), std::addressof(nca.placeholder_id)));
|
||||
log_write("registered nca: %s\n", nca.name.c_str());
|
||||
@@ -1194,7 +1210,7 @@ Result Yati::RegisterNcasAndPushRecord(const CnmtCollection& cnmt, u32 latest_ve
|
||||
|
||||
// build ncm meta and push to the database.
|
||||
BufHelper buf{};
|
||||
buf.write(std::addressof(cnmt.header), sizeof(cnmt.header));
|
||||
buf.write(std::addressof(cnmt.meta_header), sizeof(cnmt.meta_header));
|
||||
buf.write(cnmt.extended_header.data(), cnmt.extended_header.size());
|
||||
buf.write(std::addressof(cnmt.content_info), sizeof(cnmt.content_info));
|
||||
|
||||
@@ -1262,12 +1278,7 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr<source::Base> sour
|
||||
|
||||
log_write("installing nca's\n");
|
||||
for (auto& nca : cnmt.ncas) {
|
||||
if (nca.type == NcmContentType_Control) {
|
||||
log_write("installing control nca\n");
|
||||
R_TRY(yati->InstallControlNca(tickets, cnmt, nca));
|
||||
} else {
|
||||
R_TRY(yati->InstallNca(tickets, nca));
|
||||
}
|
||||
R_TRY(yati->InstallNca(tickets, nca));
|
||||
}
|
||||
|
||||
R_TRY(yati->ImportTickets(tickets));
|
||||
@@ -1284,6 +1295,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
|
||||
R_TRY(yati->Setup(override));
|
||||
|
||||
// not supported with stream installs (yet).
|
||||
yati->config.skip_if_already_installed = false;
|
||||
yati->config.convert_to_standard_crypto = false;
|
||||
yati->config.lower_master_key = false;
|
||||
|
||||
@@ -1309,7 +1321,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
|
||||
return lhs.offset < rhs.offset;
|
||||
};
|
||||
|
||||
std::sort(collections.begin(), collections.end(), sorter);
|
||||
std::ranges::sort(collections, sorter);
|
||||
|
||||
for (const auto& collection : collections) {
|
||||
if (collection.name.ends_with(".nca") || collection.name.ends_with(".ncz")) {
|
||||
@@ -1326,7 +1338,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
|
||||
keys::parse_hex_key(rights_id.c, collection.name.c_str());
|
||||
const auto str = collection.name.substr(0, collection.name.length() - 4) + ".cert";
|
||||
|
||||
auto entry = std::find_if(tickets.begin(), tickets.end(), [rights_id](auto& e){
|
||||
auto entry = std::ranges::find_if(tickets, [&rights_id](auto& e){
|
||||
return !std::memcmp(&rights_id, &e.rights_id, sizeof(rights_id));
|
||||
});
|
||||
|
||||
@@ -1345,7 +1357,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
|
||||
for (auto& cnmt : cnmts) {
|
||||
// copy nca structs into cnmt.
|
||||
for (auto& cnmt_nca : cnmt.ncas) {
|
||||
auto it = std::find_if(ncas.cbegin(), ncas.cend(), [cnmt_nca](auto& e){
|
||||
auto it = std::ranges::find_if(ncas, [&cnmt_nca](auto& e){
|
||||
return e.name == cnmt_nca.name;
|
||||
});
|
||||
|
||||
@@ -1412,71 +1424,4 @@ Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr<source::Bas
|
||||
}
|
||||
}
|
||||
|
||||
Result ParseCnmtNca(const fs::FsPath& path, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos) {
|
||||
FsFileSystem fs;
|
||||
R_TRY(fsOpenFileSystem(std::addressof(fs), FsFileSystemType_ContentMeta, path));
|
||||
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
|
||||
|
||||
FsDir dir;
|
||||
R_TRY(fsFsOpenDirectory(std::addressof(fs), fs::FsPath{"/"}, FsDirOpenMode_ReadFiles, std::addressof(dir)));
|
||||
ON_SCOPE_EXIT(fsDirClose(std::addressof(dir)));
|
||||
|
||||
s64 total_entries;
|
||||
FsDirectoryEntry buf;
|
||||
R_TRY(fsDirRead(std::addressof(dir), std::addressof(total_entries), 1, std::addressof(buf)));
|
||||
|
||||
FsFile file;
|
||||
R_TRY(fsFsOpenFile(std::addressof(fs), fs::AppendPath("/", buf.name), FsOpenMode_Read, std::addressof(file)));
|
||||
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
|
||||
|
||||
s64 offset{};
|
||||
u64 bytes_read;
|
||||
R_TRY(fsFileRead(std::addressof(file), offset, std::addressof(header), sizeof(header), 0, std::addressof(bytes_read)));
|
||||
offset += bytes_read;
|
||||
|
||||
// read extended header
|
||||
extended_header.resize(header.meta_header.extended_header_size);
|
||||
R_TRY(fsFileRead(std::addressof(file), offset, extended_header.data(), extended_header.size(), 0, std::addressof(bytes_read)));
|
||||
offset += bytes_read;
|
||||
|
||||
// read infos.
|
||||
infos.resize(header.meta_header.content_count);
|
||||
R_TRY(fsFileRead(std::addressof(file), offset, infos.data(), infos.size() * sizeof(NcmPackagedContentInfo), 0, std::addressof(bytes_read)));
|
||||
offset += bytes_read;
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result ParseControlNca(const fs::FsPath& path, u64 id, void* nacp_out, s64 nacp_size, std::vector<u8>* icon_out) {
|
||||
FsFileSystem fs;
|
||||
R_TRY(fsOpenFileSystemWithId(std::addressof(fs), id, FsFileSystemType_ContentControl, path, FsContentAttributes_All));
|
||||
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
|
||||
|
||||
// read nacp.
|
||||
if (nacp_out) {
|
||||
FsFile file;
|
||||
R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/control.nacp"}, FsOpenMode_Read, std::addressof(file)));
|
||||
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
|
||||
|
||||
u64 bytes_read;
|
||||
R_TRY(fsFileRead(&file, 0, nacp_out, nacp_size, 0, &bytes_read));
|
||||
}
|
||||
|
||||
// read icon.
|
||||
if (icon_out) {
|
||||
FsFile file;
|
||||
R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/icon_AmericanEnglish.dat"}, FsOpenMode_Read, std::addressof(file)));
|
||||
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
|
||||
|
||||
s64 size;
|
||||
R_TRY(fsFileGetSize(std::addressof(file), std::addressof(size)));
|
||||
icon_out->resize(size);
|
||||
|
||||
u64 bytes_read;
|
||||
R_TRY(fsFileRead(&file, 0, icon_out->data(), icon_out->size(), 0, &bytes_read));
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::yati
|
||||
|
||||
171
tools/usb_install_pc.py
Normal file
171
tools/usb_install_pc.py
Normal file
@@ -0,0 +1,171 @@
|
||||
# This script depends on PyUSB. You can get it with pip install pyusb.
|
||||
# You will also need libusb installed
|
||||
|
||||
# My sincere apologies for this process being overly complicated. Apparently Python and Windows
|
||||
# aren't very friendly :(
|
||||
# Windows Instructions:
|
||||
# 1. Download Zadig from https://zadig.akeo.ie/.
|
||||
# 2. With your switch plugged in and on the Tinfoil USB install menu,
|
||||
# choose "List All Devices" under the options menu in Zadig, and select libnx USB comms.
|
||||
# 3. Choose libusbK from the driver list and click the "Replace Driver" button.
|
||||
# 4. Run this script
|
||||
|
||||
# macOS Instructions:
|
||||
# 1. Install Homebrew https://brew.sh
|
||||
# 2. Install Python 3
|
||||
# sudo mkdir /usr/local/Frameworks
|
||||
# sudo chown $(whoami) /usr/local/Frameworks
|
||||
# brew install python
|
||||
# 3. Install PyUSB
|
||||
# pip3 install pyusb
|
||||
# 4. Install libusb
|
||||
# brew install libusb
|
||||
# 5. Plug in your Switch and go to Tinfoil > Title Management > USB Install NSP
|
||||
# 6. Run this script
|
||||
# python3 usb_install_pc.py <path/to/nsp_folder>
|
||||
|
||||
import usb.core
|
||||
import usb.util
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
CMD_ID_EXIT = 0
|
||||
CMD_ID_FILE_RANGE = 1
|
||||
|
||||
CMD_TYPE_RESPONSE = 1
|
||||
|
||||
# list of supported extensions.
|
||||
EXTS = (".nsp", ".xci", ".nsz", ".xcz")
|
||||
|
||||
def send_response_header(out_ep, cmd_id, data_size):
|
||||
out_ep.write(b'TUC0') # Tinfoil USB Command 0
|
||||
out_ep.write(struct.pack('<B', CMD_TYPE_RESPONSE))
|
||||
out_ep.write(b'\x00' * 3)
|
||||
out_ep.write(struct.pack('<I', cmd_id))
|
||||
out_ep.write(struct.pack('<Q', data_size))
|
||||
out_ep.write(b'\x00' * 0xC)
|
||||
|
||||
def file_range_cmd(nsp_dir, in_ep, out_ep, data_size):
|
||||
file_range_header = in_ep.read(0x20)
|
||||
|
||||
range_size = struct.unpack('<Q', file_range_header[:8])[0]
|
||||
range_offset = struct.unpack('<Q', file_range_header[8:16])[0]
|
||||
nsp_name_len = struct.unpack('<Q', file_range_header[16:24])[0]
|
||||
#in_ep.read(0x8) # Reserved
|
||||
nsp_name = bytes(in_ep.read(nsp_name_len)).decode('utf-8')
|
||||
|
||||
print('Range Size: {}, Range Offset: {}, Name len: {}, Name: {}'.format(range_size, range_offset, nsp_name_len, nsp_name))
|
||||
send_response_header(out_ep, CMD_ID_FILE_RANGE, range_size)
|
||||
|
||||
with open(nsp_name, 'rb') as f:
|
||||
f.seek(range_offset)
|
||||
|
||||
curr_off = 0x0
|
||||
end_off = range_size
|
||||
read_size = 0x800000
|
||||
|
||||
while curr_off < end_off:
|
||||
if curr_off + read_size >= end_off:
|
||||
read_size = end_off - curr_off
|
||||
|
||||
buf = f.read(read_size)
|
||||
out_ep.write(data=buf, timeout=0)
|
||||
curr_off += read_size
|
||||
|
||||
def poll_commands(nsp_dir, in_ep, out_ep):
|
||||
while True:
|
||||
cmd_header = bytes(in_ep.read(0x20, timeout=0))
|
||||
magic = cmd_header[:4]
|
||||
print('Magic: {}'.format(magic), flush=True)
|
||||
|
||||
if magic != b'TUC0': # Tinfoil USB Command 0
|
||||
continue
|
||||
|
||||
cmd_type = struct.unpack('<B', cmd_header[4:5])[0]
|
||||
cmd_id = struct.unpack('<I', cmd_header[8:12])[0]
|
||||
data_size = struct.unpack('<Q', cmd_header[12:20])[0]
|
||||
|
||||
print('Cmd Type: {}, Command id: {}, Data size: {}'.format(cmd_type, cmd_id, data_size), flush=True)
|
||||
|
||||
if cmd_id == CMD_ID_EXIT:
|
||||
print('Exiting...')
|
||||
break
|
||||
elif cmd_id == CMD_ID_FILE_RANGE:
|
||||
file_range_cmd(nsp_dir, in_ep, out_ep, data_size)
|
||||
|
||||
def send_nsp_list(nsp_dir, out_ep):
|
||||
nsp_path_list = list()
|
||||
nsp_path_list_len = 0
|
||||
|
||||
# Add all files with the extension .nsp in the provided dir
|
||||
for nsp_path in [f for f in nsp_dir.iterdir() if f.is_file() and (f.suffix in EXTS)]:
|
||||
nsp_path_list.append(nsp_path.__str__() + '\n')
|
||||
nsp_path_list_len += len(nsp_path.__str__()) + 1
|
||||
|
||||
print('Sending header...')
|
||||
|
||||
out_ep.write(b'TUL0') # Tinfoil USB List 0
|
||||
out_ep.write(struct.pack('<I', nsp_path_list_len))
|
||||
out_ep.write(b'\x00' * 0x8) # Padding
|
||||
|
||||
print('Sending NSP list: {}'.format(nsp_path_list))
|
||||
|
||||
for nsp_path in nsp_path_list:
|
||||
out_ep.write(nsp_path)
|
||||
|
||||
def print_usage():
|
||||
print("""\
|
||||
usb_install_pc.py
|
||||
|
||||
Used for the installation of NSPs over USB.
|
||||
|
||||
Usage: usb_install_pc.py <nsp folder>""")
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 2:
|
||||
print_usage()
|
||||
sys.exit(1)
|
||||
|
||||
nsp_dir = Path(sys.argv[1])
|
||||
|
||||
if not nsp_dir.is_dir():
|
||||
raise ValueError('1st argument must be a directory')
|
||||
|
||||
print("waiting for switch...\n")
|
||||
dev = None
|
||||
|
||||
while (dev is None):
|
||||
dev = usb.core.find(idVendor=0x057E, idProduct=0x3000)
|
||||
time.sleep(0.5)
|
||||
|
||||
print("found the switch!\n")
|
||||
|
||||
cfg = None
|
||||
|
||||
try:
|
||||
cfg = dev.get_active_configuration()
|
||||
print("found active config")
|
||||
except usb.core.USBError:
|
||||
print("no currently active config")
|
||||
cfg = None
|
||||
|
||||
if cfg is None:
|
||||
dev.reset()
|
||||
dev.set_configuration()
|
||||
cfg = dev.get_active_configuration()
|
||||
|
||||
is_out_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_OUT
|
||||
is_in_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_IN
|
||||
out_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_out_ep)
|
||||
in_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_in_ep)
|
||||
|
||||
assert out_ep is not None
|
||||
assert in_ep is not None
|
||||
|
||||
print("iManufacturer: {} iProduct: {} iSerialNumber: {}".format(dev.manufacturer, dev.product, dev.serial_number))
|
||||
print("bcdUSB: {} bMaxPacketSize0: {}".format(hex(dev.bcdUSB), dev.bMaxPacketSize0))
|
||||
|
||||
send_nsp_list(nsp_dir, out_ep)
|
||||
poll_commands(nsp_dir, in_ep, out_ep)
|
||||
@@ -1,141 +0,0 @@
|
||||
# based on usb.py from Tinfoil, by Adubbz.
|
||||
import struct
|
||||
import sys
|
||||
import os
|
||||
import usb.core
|
||||
import usb.util
|
||||
import time
|
||||
import glob
|
||||
from pathlib import Path
|
||||
|
||||
# magic number (SPHA) for the script and switch.
|
||||
MAGIC = 0x53504841
|
||||
# version of the usb script.
|
||||
VERSION = 2
|
||||
# list of supported extensions.
|
||||
EXTS = (".nsp", ".xci", ".nsz", ".xcz")
|
||||
|
||||
def verify_switch(bcdUSB, count, in_ep, out_ep):
|
||||
header = in_ep.read(8, timeout=0)
|
||||
switch_magic = struct.unpack('<I', header[0:4])[0]
|
||||
switch_version = struct.unpack('<I', header[4:8])[0]
|
||||
|
||||
if switch_magic != MAGIC:
|
||||
raise Exception("Unexpected magic {}".format(switch_magic))
|
||||
|
||||
if switch_version != VERSION:
|
||||
raise Exception("Unexpected version {}".format(switch_version))
|
||||
|
||||
send_data = struct.pack('<IIII', MAGIC, VERSION, bcdUSB, count)
|
||||
out_ep.write(data=send_data, timeout=0)
|
||||
|
||||
def send_file_info(path, in_ep, out_ep):
|
||||
file_name = Path(path).name
|
||||
file_size = Path(path).stat().st_size
|
||||
file_name_len = len(file_name)
|
||||
|
||||
send_data = struct.pack('<QQ', file_size, file_name_len)
|
||||
out_ep.write(data=send_data, timeout=0)
|
||||
out_ep.write(data=file_name, timeout=0)
|
||||
|
||||
def wait_for_input(path, in_ep, out_ep):
|
||||
buf = None
|
||||
predicted_off = 0
|
||||
print("now waiting for intput\n")
|
||||
|
||||
with open(path, "rb") as file:
|
||||
while True:
|
||||
header = in_ep.read(24, timeout=0)
|
||||
|
||||
range_offset = struct.unpack('<Q', header[8:16])[0]
|
||||
range_size = struct.unpack('<Q', header[16:24])[0]
|
||||
|
||||
if (range_offset == 0 and range_size == 0):
|
||||
break
|
||||
|
||||
if (buf != None and range_offset == predicted_off and range_size == len(buf)):
|
||||
# print("predicted the read off {} size {}".format(predicted_off, len(buf)))
|
||||
pass
|
||||
else:
|
||||
file.seek(range_offset)
|
||||
buf = file.read(range_size)
|
||||
|
||||
if (len(buf) != range_size):
|
||||
# print("off: {} size: {}".format(range_offset, range_size))
|
||||
raise ValueError('bad buf size!!!!!')
|
||||
|
||||
result = out_ep.write(data=buf, timeout=0)
|
||||
if (len(buf) != result):
|
||||
print("off: {} size: {}".format(range_offset, range_size))
|
||||
raise ValueError('bad result!!!!!')
|
||||
|
||||
predicted_off = range_offset + range_size
|
||||
buf = file.read(range_size)
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("hello world")
|
||||
|
||||
# check which mode the user has selected.
|
||||
args = len(sys.argv)
|
||||
if (args != 2):
|
||||
print("either run python usb_total.py game.nsp OR drag and drop the game onto the python file (if python is in your path)")
|
||||
sys.exit(1)
|
||||
|
||||
path = sys.argv[1]
|
||||
files = []
|
||||
|
||||
if os.path.isfile(path) and path.endswith(EXTS):
|
||||
files.append(path)
|
||||
elif os.path.isdir(path):
|
||||
for f in glob.glob(path + "/**/*.*", recursive=True):
|
||||
if os.path.isfile(f) and f.endswith(EXTS):
|
||||
files.append(f)
|
||||
else:
|
||||
raise ValueError('must be a file!')
|
||||
|
||||
# for file in files:
|
||||
# print("found file: {}".format(file))
|
||||
|
||||
# Find the switch
|
||||
print("waiting for switch...\n")
|
||||
dev = None
|
||||
|
||||
while (dev is None):
|
||||
dev = usb.core.find(idVendor=0x057E, idProduct=0x3000)
|
||||
time.sleep(0.5)
|
||||
|
||||
print("found the switch!\n")
|
||||
|
||||
cfg = None
|
||||
|
||||
try:
|
||||
cfg = dev.get_active_configuration()
|
||||
print("found active config")
|
||||
except usb.core.USBError:
|
||||
print("no currently active config")
|
||||
cfg = None
|
||||
|
||||
if cfg is None:
|
||||
dev.set_configuration()
|
||||
cfg = dev.get_active_configuration()
|
||||
|
||||
is_out_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_OUT
|
||||
is_in_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_IN
|
||||
out_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_out_ep)
|
||||
in_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_in_ep)
|
||||
assert out_ep is not None
|
||||
assert in_ep is not None
|
||||
|
||||
print("iManufacturer: {} iProduct: {} iSerialNumber: {}".format(dev.manufacturer, dev.product, dev.serial_number))
|
||||
print("bcdUSB: {} bMaxPacketSize0: {}".format(hex(dev.bcdUSB), dev.bMaxPacketSize0))
|
||||
|
||||
try:
|
||||
verify_switch(dev.bcdUSB, len(files), in_ep, out_ep)
|
||||
|
||||
for file in files:
|
||||
print("installing file: {}".format(file))
|
||||
send_file_info(file, in_ep, out_ep)
|
||||
wait_for_input(file, in_ep, out_ep)
|
||||
dev.reset()
|
||||
except Exception as inst:
|
||||
print("An exception occurred " + str(inst))
|
||||
Reference in New Issue
Block a user