Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d27bf5492 | ||
|
|
6b85d2cef1 | ||
|
|
aae9930f5e | ||
|
|
eca19aa4bf | ||
|
|
8e02538405 | ||
|
|
928da0cbda | ||
|
|
267693c6ab | ||
|
|
3f99afaa38 | ||
|
|
8e67e5f0fc | ||
|
|
cb1508e6d5 | ||
|
|
070be1ff94 | ||
|
|
7730eacea8 | ||
|
|
c5e3373fe1 | ||
|
|
d7ec620173 | ||
|
|
1c72350d4a | ||
|
|
4ef15f8b81 | ||
|
|
8fc7b614a0 | ||
|
|
0789a69975 | ||
|
|
b405a816c9 | ||
|
|
99c1db3655 | ||
|
|
6b099de63c | ||
|
|
275707fe27 | ||
|
|
c535b96b12 | ||
|
|
6b77cbb0c0 | ||
|
|
a33d8e1061 | ||
|
|
aaf11211dc | ||
|
|
83b2aca942 | ||
|
|
fbae286dff | ||
|
|
ba9b6b54bf | ||
|
|
1677514355 | ||
|
|
ec1042efa3 | ||
|
|
b03ad4ade3 | ||
|
|
04f6e5d2a8 | ||
|
|
16c074db1a | ||
|
|
8d958a2d1d | ||
|
|
74c1cd3be0 | ||
|
|
0fd5f348e2 | ||
|
|
0c9433d0d3 | ||
|
|
8fb34d42dc | ||
|
|
be831eb04a | ||
|
|
43ebab52d4 | ||
|
|
a5f9eaa392 | ||
|
|
cc2064f296 | ||
|
|
f2462cff81 | ||
|
|
b2e25abf08 | ||
|
|
cd0817bd11 | ||
|
|
e88ca8ede1 | ||
|
|
7a83269d98 | ||
|
|
4be1d48215 | ||
|
|
8485ff1e99 | ||
|
|
be66b10f49 | ||
|
|
1f22971493 | ||
|
|
ea943088e5 | ||
|
|
298be4a344 | ||
|
|
f37fc13b7c | ||
|
|
506b74868e | ||
|
|
4a59d1cfda | ||
|
|
7201c8347f | ||
|
|
c8a3df3cfc | ||
|
|
2ef7742903 | ||
|
|
f98135325a | ||
|
|
fd765aa8c8 | ||
|
|
ec93dd5a7d | ||
|
|
0e885ff2d5 | ||
|
|
5893cb575e | ||
|
|
b46136b959 | ||
|
|
390c1e870d | ||
|
|
17b341d83a | ||
|
|
391234ea7b | ||
|
|
92eb3a9ecd | ||
|
|
491445a12f |
40
README.md
@@ -4,16 +4,16 @@ A homebrew menu for the Nintendo Switch.
|
||||
|
||||
[See the GBATemp thread for more details / discussion](https://gbatemp.net/threads/sphaira-hbmenu-replacement.664523/).
|
||||
|
||||
[We have now have a Discord server!](https://discord.gg/8vZBsrprEc). Please use the issues tab to report bugs, as it is much easier for me to track.
|
||||
[We have now have a Discord server!](https://discord.gg/8vZBsrprEc) Please use the issues tab to report bugs, as it is much easier for me to track.
|
||||
|
||||
## Showcase
|
||||
|
||||
| | |
|
||||
:-------------------------:|:-------------------------:
|
||||
 | 
|
||||
 | 
|
||||
 | 
|
||||
 | 
|
||||
 | 
|
||||
 | 
|
||||
 | 
|
||||
 | 
|
||||
|
||||
## Bug reports
|
||||
|
||||
@@ -86,16 +86,22 @@ The output will be found in `build/MinSizeRel/sphaira.nro`
|
||||
|
||||
## Credits
|
||||
|
||||
- borealis
|
||||
- stb
|
||||
- yyjson
|
||||
- nx-hbmenu
|
||||
- nx-hbloader
|
||||
- deko3d-nanovg
|
||||
- libpulsar
|
||||
- minIni
|
||||
- GBATemp
|
||||
- hb-appstore
|
||||
- haze
|
||||
- nxdumptool (for gamecard bin dumping and rsa verify code)
|
||||
- [borealis](https://github.com/natinusala/borealis)
|
||||
- [stb](https://github.com/nothings/stb)
|
||||
- [yyjson](https://github.com/ibireme/yyjson)
|
||||
- [nx-hbmenu](https://github.com/switchbrew/nx-hbmenu)
|
||||
- [nx-hbloader](https://github.com/switchbrew/nx-hbloader)
|
||||
- [deko3d-nanovg](https://github.com/Adubbz/nanovg-deko3d)
|
||||
- [libpulsar](https://github.com/p-sam/switch-libpulsar)
|
||||
- [minIni](https://github.com/compuphase/minIni)
|
||||
- [GBATemp](https://gbatemp.net/threads/sphaira-hbmenu-replacement.664523/)
|
||||
- [hb-appstore](https://github.com/fortheusers/hb-appstore)
|
||||
- [haze](https://github.com/Atmosphere-NX/Atmosphere/tree/master/troposphere/haze)
|
||||
- [nxdumptool](https://github.com/DarkMatterCore/nxdumptool) (for gamecard bin dumping and rsa verify code)
|
||||
- [Liam0](switch-010editor-templates) (for ticket / cert structs)
|
||||
- [libusbhsfs](https://github.com/DarkMatterCore/libusbhsfs)
|
||||
- [libnxtc](https://github.com/DarkMatterCore/libnxtc)
|
||||
- [oss-nvjpg](https://github.com/averne/oss-nvjpg)
|
||||
- [nsz](https://github.com/nicoboss/nsz)
|
||||
- [themezer](https://themezer.net/)
|
||||
- Everyone who has contributed to this project!
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
{
|
||||
"[Applet Mode]": " | Applet Modus |",
|
||||
"[Applet Mode]": "[Applet Modus]",
|
||||
"No Internet": "Kein Internet",
|
||||
"Switch-Handheld!": "Handheld!",
|
||||
"Switch-Docked!": "Angedockt!",
|
||||
"Switch-Handheld!": "Handheld-Modus!",
|
||||
"Switch-Docked!": "TV-Modus!",
|
||||
"Warning! Logs are enabled, Sphaira will run slowly!": "Warnung! Protokollierung ist aktiviert, Sphaira läuft langsam!",
|
||||
"Audio disabled due to suspended game": "Audio deaktivert wegen Spielabbruch",
|
||||
"Are you sure you wish to cancel?": "Bist du sicher dass du abbrechen willst?",
|
||||
"An error occurred": "",
|
||||
"An error occurred": "Ein Fehler ist aufgetreten.",
|
||||
"If this message appears repeatedly, please open an issue.": "Bei wiederholtem Auftreten bitte Issue erstellen.",
|
||||
|
||||
"Menu Options": " Menü | Optionen",
|
||||
"Menu Options": "Menü | Optionen",
|
||||
"Menu": "Menü",
|
||||
"Theme": "Themes",
|
||||
"Theme Options": " Themes | Optionen",
|
||||
"Select Theme": "Theme wählen",
|
||||
"Theme": "Designs",
|
||||
"Theme Options": "Designs | Optionen",
|
||||
"Select Theme": "Design wählen",
|
||||
"Music": "Musik",
|
||||
"12 Hour Time": "12-Std Zeitformat",
|
||||
"Download Default Music": "",
|
||||
"Failed to download default_music.bfstm, please try again": "",
|
||||
"Overwrite current default music?": "",
|
||||
"Download Default Music": "Standardmusik herunterladen",
|
||||
"Failed to download default_music.bfstm, please try again": "Fehler beim Herunterladen von default_music.bfstm. Bitte versuchen Sie es erneut.",
|
||||
"Overwrite current default music?": "Aktuelle Standardmusik überschreiben?",
|
||||
|
||||
"Network": "Konnektivität",
|
||||
"Network Options": "Konnektivität | Optionen",
|
||||
@@ -27,6 +28,8 @@
|
||||
"Nxlink Connected": "NXLink | Verbunden",
|
||||
"Nxlink Upload": "NXLink | wird hochgeladen...",
|
||||
"Nxlink Finished": "NXLink | Hochladen beendet",
|
||||
"Hdd": "HDD",
|
||||
"Hdd write protect": "HDD Schreibgeschützt",
|
||||
|
||||
"Language": "Sprache",
|
||||
"Auto": "Systemsprache",
|
||||
@@ -46,33 +49,46 @@
|
||||
"Ukrainian": "Українська",
|
||||
|
||||
"Misc": "Extras",
|
||||
"Misc Options": " Extras | Optionen",
|
||||
"Misc Options": "Extras | Optionen",
|
||||
"Games": "Spiele",
|
||||
"Game Options": "",
|
||||
"Hide forwarders": "",
|
||||
"Launch random game": "",
|
||||
"Game Options": "Spiel | Optionen",
|
||||
"Hide forwarders": "Forwarders ausblenden",
|
||||
"Launch random game": "Zufälliges Spiel starten",
|
||||
"List meta records": "",
|
||||
"Entries": "",
|
||||
"Failed to list application meta entries": "",
|
||||
"No meta entries found...\n": "",
|
||||
"Updating application record list": "",
|
||||
"Dump": "",
|
||||
"Dump options": "",
|
||||
"Dump Options": "",
|
||||
"Select content to dump": "",
|
||||
"Dump All": "",
|
||||
"Dump Application": "",
|
||||
"Dump Patch": "",
|
||||
"Dump AddOnContent": "",
|
||||
"Dump DataPatch": "",
|
||||
"Created nested folder": "",
|
||||
"Append folder with .xci": "",
|
||||
"Trim XCI": "",
|
||||
"Label trimmed XCI": "",
|
||||
"Multi-threaded USB transfer": "",
|
||||
"Dump All Bins": "",
|
||||
"Dump XCI": "",
|
||||
"Dump Card ID Set": "",
|
||||
"Dump Card UID": "",
|
||||
"Dump Certificate": "",
|
||||
"Dump Initial Data": "",
|
||||
"Select dump location": "",
|
||||
"microSD card (/dumps/NSP/)": "",
|
||||
"microSD card (/dumps/)": "",
|
||||
"USB transfer (Switch 2 Switch)": "",
|
||||
"/dev/null (Speed Test)": "",
|
||||
"Dumping": "",
|
||||
"Dump successfull!": "",
|
||||
"Dump failed!": "",
|
||||
"Success": "",
|
||||
"Delete successfull!": "",
|
||||
"Delete failed!": "",
|
||||
"Success": "",
|
||||
|
||||
"Themezer": "Themezer | NX Themes",
|
||||
"Themezer Options": " Themezer | Optionen",
|
||||
@@ -86,9 +102,10 @@
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Lade JSON-File",
|
||||
"Select asset to download for ": "Wähle Asset für den Download von ",
|
||||
"Failed to download json": "Download von JSON-Datei fehlgeschlagen",
|
||||
"Failed to download app!": "Download der App fehlgeschlagen",
|
||||
|
||||
"FTP Install": "",
|
||||
"FTP Install (EXPERIMENTAL)": "",
|
||||
"Connection Type: WiFi | Strength: ": "",
|
||||
"Connection Type: Ethernet": "",
|
||||
"Connection Type: None": "",
|
||||
@@ -98,19 +115,19 @@
|
||||
"Password:": "",
|
||||
"SSID:": "",
|
||||
"Passphrase:": "",
|
||||
"Failed to install via FTP, press B to exit...": "",
|
||||
"Ftp install success!": "",
|
||||
"Ftp install failed!": "",
|
||||
"Failed to install, press B to exit...": "",
|
||||
"Install success!": "",
|
||||
"Install failed!": "",
|
||||
"USB Install": "",
|
||||
"USB": "",
|
||||
"Connected, waiting for file list...": "",
|
||||
"Connected, starting transfer...": "",
|
||||
"Connected, waiting for file list...": "Verbunden, warte auf die Dateiliste...",
|
||||
"Connected, starting transfer...": "Verbunden, Übertragung startet...",
|
||||
"Failed to init usb, press B to exit...": "",
|
||||
"Waiting for connection...": "",
|
||||
"Transferring data...": "",
|
||||
"Waiting for connection...": "Warten auf Verbindung...",
|
||||
"Transferring data...": "Datenübertragung...",
|
||||
"USB connected, sending file list": "",
|
||||
"Sent file list, waiting for command...": "",
|
||||
"waiting for usb connection...": "",
|
||||
"waiting for usb connection...": "Warten auf USB Verbindung...",
|
||||
"Disable MTP for usb install": "",
|
||||
"Re-enabled MTP": "",
|
||||
"Installed via usb": "",
|
||||
@@ -122,9 +139,12 @@
|
||||
"GC": "",
|
||||
"System memory %.1f GB": "",
|
||||
"microSD card %.1f GB": "",
|
||||
"Nand Install": "",
|
||||
"SD Card Install": "",
|
||||
"Exit": "",
|
||||
"Install disabled...\nPlease enable installing via the install options.": "",
|
||||
"No GameCard inserted": "Keine Spielkarte eingelegt",
|
||||
"GameCard is already trimmed!": "",
|
||||
"WARNING: GameCard is already trimmed!": "",
|
||||
"Continue": "",
|
||||
"Gc install success!": "",
|
||||
"Gc install failed!": "",
|
||||
|
||||
@@ -160,15 +180,15 @@
|
||||
"Negative image": "Negativ-Bild",
|
||||
"Format": "Format",
|
||||
"Trimming Format": "Beschnitt-Format",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"External Light Filter": "Externes Lichtfilter",
|
||||
"Load Default": "Standard laden",
|
||||
|
||||
"Advanced": "Erweitert...",
|
||||
"Web": "",
|
||||
"Select URL": "",
|
||||
"Enter custom URL": "",
|
||||
"Enter URL": "",
|
||||
|
||||
"Advanced": "Erweitert",
|
||||
"Advanced Options": " Erweitert | Optionen",
|
||||
"Logging": "Protokollieren",
|
||||
"Replace hbmenu on exit": "hbmenu durch sphaira ersetzen",
|
||||
@@ -181,10 +201,12 @@
|
||||
"Restored hbmenu": "hbmenu wurde wiederhergestellt",
|
||||
"Restart Sphaira?": "sphaira erneut starten?",
|
||||
"Press OK to restart Sphaira": "Drücke OK um sphaira erneut zustarten",
|
||||
"Boost CPU during transfer": "",
|
||||
"Text scroll speed": "Laufschrift Tempo",
|
||||
"Slow": "Niedrig",
|
||||
"Normal": "Mittel",
|
||||
"Fast": "Hoch",
|
||||
"Set left-side menu": "",
|
||||
"Set right-side menu": "",
|
||||
"Install options": "",
|
||||
"Install Options": "",
|
||||
@@ -194,7 +216,6 @@
|
||||
"Install location": "Einhängepunkt",
|
||||
"System memory": "NAND Systemspeicher",
|
||||
"microSD card": "SD-Karte",
|
||||
"Boost CPU clock": "",
|
||||
"Allow downgrade": "",
|
||||
"Skip if already installed": "",
|
||||
"Ticket only": "",
|
||||
@@ -222,15 +243,17 @@
|
||||
"Creating Control": "Erstelle Control",
|
||||
"Creating Meta": "Erstelle Meta",
|
||||
"Writing Nca": "Schreibe NCA",
|
||||
"Updating ncm databse": "Aktualisiere NCM-Datenbank",
|
||||
"Updating ncm database": "Aktualisiere NCM-Datenbank",
|
||||
"Pushing application record": "Übertrage Anwendungsdaten",
|
||||
"Failed to install forwarder": "Fehler beim installieren des Forwarders",
|
||||
"Unstar": "Kein Favorit",
|
||||
"Star": "Favorit",
|
||||
"Unstarred ": "Favorit entfernt ",
|
||||
"Starred ": "Favorit ",
|
||||
"Failed to remove old forwarder, please manually remove it!": "",
|
||||
|
||||
"AppStore": "hb-AppStore",
|
||||
"Appstore": "",
|
||||
"Appstore": "hb-AppStore",
|
||||
"Store": "hb-Store",
|
||||
"Filter: %s | Sort: %s | Order: %s": "Rubrik: %s | Sort.nach.: %s | Ordnung: %s",
|
||||
"AppStore Options": " hb-AppStore | Optionen",
|
||||
@@ -238,7 +261,7 @@
|
||||
"Changelog": "Neuerungen",
|
||||
"Details": "Details",
|
||||
"version: %s": "Version: %s",
|
||||
"updated: %s": "Letztes Update am: %s",
|
||||
"updated: %s": "Letztes Update: %s",
|
||||
"category: %s": "Rubrik: %s",
|
||||
"extracted: %.2f MiB": "Größe: %.2f MiB",
|
||||
"app_dls: %s": "Anzahl Downloads: %s",
|
||||
@@ -257,12 +280,13 @@
|
||||
"Copy": "Kopieren",
|
||||
"Copying ": "Kopiert wird: ",
|
||||
"Paste": "Einfügen",
|
||||
"Paste ": "Einfügen von: ",
|
||||
" file(s)?": " Datei/en?",
|
||||
"Paste file(s)?": "",
|
||||
"Pasting ": "Eingefügt wird: ",
|
||||
"Pasting": "Eingefügt wurde:",
|
||||
"Rename": "Umbenennen",
|
||||
"Set New File Name": "Neuen Dateinamen festlegen",
|
||||
"Failed to delete directory": "",
|
||||
"Failed to delete file": "",
|
||||
"Extract zip": "",
|
||||
"Extract Options": "",
|
||||
"Extract here": "",
|
||||
@@ -285,13 +309,17 @@
|
||||
"Create Folder": "Neuer Ordner",
|
||||
"Set Folder Name": "Ordner umbenennen",
|
||||
"Creating ": "Erstellt wird: ",
|
||||
"View as text (unfinished)": "Als Text anzeigen",
|
||||
"Upload": "",
|
||||
"Select upload location": "",
|
||||
"No upload locations set!": "",
|
||||
"Uploading": "",
|
||||
"Upload successfull!": "",
|
||||
"Upload failed!": "",
|
||||
"View as text (unfinished)": "Als Text anzeigen",
|
||||
"Hash": "",
|
||||
"Hash Options": "",
|
||||
"Hashing": "",
|
||||
"Failed to hash file...": "",
|
||||
"Ignore read only": "Schreibschutz umgehen?",
|
||||
"Mount": "Einhängen",
|
||||
"Sd": "SD-Karte | Root-Verzeichnis",
|
||||
@@ -302,9 +330,10 @@
|
||||
"Launch ": "Starte ",
|
||||
"Launch option for: ": "Start Option für: ",
|
||||
"Select launcher for: ": "Wähle Launcher für: ",
|
||||
"Close FileBrowser?": "Datei-Manager schließen?",
|
||||
|
||||
"Sort By": "Sortierung",
|
||||
"Sort Options": " Sortierung | Optionen",
|
||||
"Sort Options": "Sortierung | Optionen",
|
||||
"Filter": "Rubrik",
|
||||
"All": "Alles anzeigen",
|
||||
"Emulators": "Emulatoren",
|
||||
@@ -335,25 +364,25 @@
|
||||
"Search": "Suchen",
|
||||
|
||||
"Options": "Optionen",
|
||||
"Split": "",
|
||||
"OK": "OK",
|
||||
"Back": "Zurück",
|
||||
"Select": "Auswählen",
|
||||
"Open": "Öffne",
|
||||
"Close": "Schließe",
|
||||
"Launch": "Starte",
|
||||
"Restart": "Neustart",
|
||||
"Next": "",
|
||||
"Prev": "",
|
||||
"Unstar": "Kein Favorit",
|
||||
"Star": "Favorit",
|
||||
"Yes": "Ja",
|
||||
"No": "Nein",
|
||||
"On": "",
|
||||
"Off": "",
|
||||
"On": "An",
|
||||
"Off": "Aus",
|
||||
|
||||
"Install": "Installieren",
|
||||
"Install Selected files?": "",
|
||||
"Installing ": "Installiert wird: ",
|
||||
"Installed ": "",
|
||||
"Installed ": "Installiert ",
|
||||
"Installed!": "Installiert!",
|
||||
"Trying to load ": "Versucht zu laden wird: ",
|
||||
"Checking MD5": "Checke MD5 Prüfsumme",
|
||||
@@ -367,12 +396,13 @@
|
||||
"Remove": "Entfernen",
|
||||
"Completely remove ": "Komplett gelöscht wird: ",
|
||||
"Removing ": "Entfernt wird: ",
|
||||
"Removed ": "Entfernt wurde: ",
|
||||
"Uninstalling ": "Deinstalliert wird: ",
|
||||
"Removed ": "Entfernt wurde: ",
|
||||
|
||||
"Download": "Download",
|
||||
"Downloading ": "Heruntergeladen wird: ",
|
||||
"Downloaded ": "Heruntergeladen wurde: ",
|
||||
"Download via the Network options!": "",
|
||||
|
||||
"Update": "Update",
|
||||
"Update avaliable: ": "Update verfügbar: ",
|
||||
@@ -380,13 +410,13 @@
|
||||
"Updated to ": "Aktualisiert auf: ",
|
||||
"Failed to download update": "Herunterladen des Updates fehlgeschlagen!",
|
||||
|
||||
"%zu hours %zu minutes remaining": "",
|
||||
"%zu minutes %zu seconds remaining": "",
|
||||
"%zu seconds remaining": "",
|
||||
"%zu hours %zu minutes remaining": "%zu Stunden und %zu Minuten verbleibend",
|
||||
"%zu minutes %zu seconds remaining": "%zu Minuten und %zu Sekunde verbleibend",
|
||||
"%zu seconds remaining": "%zu Sekunde verbleibend",
|
||||
|
||||
"Loading...": "Wird geladen...",
|
||||
"Loading": "Wird geladen",
|
||||
"Empty!": "Keine Daten!",
|
||||
"Not Ready...": "Nicht bereit...",
|
||||
"Error loading page!": "Ladefehler!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"No Internet": "No Internet",
|
||||
"Switch-Handheld!": "Switch-Handheld!",
|
||||
"Switch-Docked!": "Switch-Docked!",
|
||||
"Warning! Logs are enabled, Sphaira will run slowly!": "Warning! Logs are enabled, Sphaira will run slowly!",
|
||||
"Audio disabled due to suspended game": "Audio disabled due to suspended game",
|
||||
"Are you sure you wish to cancel?": "Are you sure you wish to cancel?",
|
||||
"An error occurred": "An error occurred",
|
||||
@@ -27,6 +28,8 @@
|
||||
"Nxlink Connected": "Nxlink Connected",
|
||||
"Nxlink Upload": "Nxlink Upload",
|
||||
"Nxlink Finished": "Nxlink Finished",
|
||||
"Hdd": "Hdd",
|
||||
"Hdd write protect": "Hdd write protect",
|
||||
|
||||
"Language": "Language",
|
||||
"Auto": "Auto",
|
||||
@@ -57,22 +60,35 @@
|
||||
"No meta entries found...\n": "No meta entries found...\n",
|
||||
"Updating application record list": "Updating application record list",
|
||||
"Dump": "Dump",
|
||||
"Dump options": "Dump options",
|
||||
"Dump Options": "Dump Options",
|
||||
"Select content to dump": "Select content to dump",
|
||||
"Dump All": "Dump All",
|
||||
"Dump Application": "Dump Application",
|
||||
"Dump Patch": "Dump Patch",
|
||||
"Dump AddOnContent": "Dump AddOnContent",
|
||||
"Dump DataPatch": "Dump DataPatch",
|
||||
"Created nested folder": "Created nested folder",
|
||||
"Append folder with .xci": "Append folder with .xci",
|
||||
"Trim XCI": "Trim XCI",
|
||||
"Label trimmed XCI": "Label trimmed XCI",
|
||||
"Multi-threaded USB transfer": "Multi-threaded USB transfer",
|
||||
"Dump All Bins": "Dump All Bins",
|
||||
"Dump XCI": "Dump XCI",
|
||||
"Dump Card ID Set": "Dump Card ID Set",
|
||||
"Dump Card UID": "Dump Card UID",
|
||||
"Dump Certificate": "Dump Certificate",
|
||||
"Dump Initial Data": "Dump Initial Data",
|
||||
"Select dump location": "Select dump location",
|
||||
"microSD card (/dumps/NSP/)": "microSD card (/dumps/NSP/)",
|
||||
"microSD card (/dumps/)": "microSD card (/dumps/)",
|
||||
"USB transfer (Switch 2 Switch)": "USB transfer (Switch 2 Switch)",
|
||||
"/dev/null (Speed Test)": "/dev/null (Speed Test)",
|
||||
"Dumping": "Dumping",
|
||||
"Dump successfull!": "Dump successfull!",
|
||||
"Dump failed!": "Dump failed!",
|
||||
"Success": "Success",
|
||||
"Delete successfull!": "Delete successfull!",
|
||||
"Delete failed!": "Delete failed!",
|
||||
"Success": "Success",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Themezer Options",
|
||||
@@ -86,9 +102,10 @@
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Downloading json",
|
||||
"Select asset to download for ": "Select asset to download for ",
|
||||
"Failed to download json": "Failed to download json",
|
||||
"Failed to download app!": "Failed to download app!",
|
||||
|
||||
"FTP Install": "FTP Install",
|
||||
"FTP Install (EXPERIMENTAL)": "FTP Install (EXPERIMENTAL)",
|
||||
"Connection Type: WiFi | Strength: ": "Connection Type: WiFi | Strength: ",
|
||||
"Connection Type: Ethernet": "Connection Type: Ethernet",
|
||||
"Connection Type: None": "Connection Type: None",
|
||||
@@ -98,9 +115,9 @@
|
||||
"Password:": "Password:",
|
||||
"SSID:": "SSID:",
|
||||
"Passphrase:": "Passphrase",
|
||||
"Failed to install via FTP, press B to exit...": "Failed to install via FTP, press to exit...",
|
||||
"Ftp install success!": "Ftp install success!",
|
||||
"Ftp install failed!": "Ftp install failed!",
|
||||
"Failed to install, press B to exit...": "Failed to install, press to exit...",
|
||||
"Install success!": "Install success!",
|
||||
"Install failed!": "Install failed!",
|
||||
"USB Install": "USB Install",
|
||||
"USB": "USB",
|
||||
"Connected, waiting for file list...": "Connected, waiting for file list...",
|
||||
@@ -122,9 +139,12 @@
|
||||
"GC": "GC",
|
||||
"System memory %.1f GB": "System memory %.1f GB",
|
||||
"microSD card %.1f GB": "microSD card %.1f GB",
|
||||
"Nand Install": "Nand Install",
|
||||
"SD Card Install": "SD Card Install",
|
||||
"Exit": "Exit",
|
||||
"Install disabled...\nPlease enable installing via the install options.": "Install disabled...\nPlease enable installing via the install options.",
|
||||
"No GameCard inserted": "No GameCard inserted",
|
||||
"GameCard is already trimmed!": "GameCard is already trimmed!",
|
||||
"WARNING: GameCard is already trimmed!": "WARNING: GameCard is already trimmed!",
|
||||
"Continue": "Continue",
|
||||
"Gc install success!": "Gc install success!",
|
||||
"Gc install failed!": "Gc install failed!",
|
||||
|
||||
@@ -160,14 +180,14 @@
|
||||
"Negative image": "Negative image",
|
||||
"Format": "Format",
|
||||
"Trimming Format": "Trimming Format",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"External Light Filter": "External Light Filter",
|
||||
"Load Default": "Load Default",
|
||||
|
||||
"Web": "Web",
|
||||
"Select URL": "Select URL",
|
||||
"Enter custom URL": "Enter custom URL",
|
||||
"Enter URL": "Enter URL",
|
||||
|
||||
"Advanced": "Advanced",
|
||||
"Advanced Options": "Advanced Options",
|
||||
"Logging": "Logging",
|
||||
@@ -181,10 +201,12 @@
|
||||
"Restored hbmenu": "Restored hbmenu",
|
||||
"Restart Sphaira?": "Restart Sphaira?",
|
||||
"Press OK to restart Sphaira": "Press OK to restart Sphaira",
|
||||
"Boost CPU during transfer": "Boost CPU during transfer",
|
||||
"Text scroll speed": "Text scroll speed",
|
||||
"Slow": "Slow",
|
||||
"Normal": "Normal",
|
||||
"Fast": "Fast",
|
||||
"Set left-side menu": "Set left-side menu",
|
||||
"Set right-side menu": "Set right-side menu",
|
||||
"Install options": "Install options",
|
||||
"Install Options": "Install Options",
|
||||
@@ -194,7 +216,6 @@
|
||||
"Install location": "Install location",
|
||||
"System memory": "System memory",
|
||||
"microSD card": "microSD card",
|
||||
"Boost CPU clock": "Boost CPU clock",
|
||||
"Allow downgrade": "Allow downgrade",
|
||||
"Skip if already installed": "Skip if already installed",
|
||||
"Ticket only": "Ticket only",
|
||||
@@ -222,9 +243,11 @@
|
||||
"Creating Control": "Creating Control",
|
||||
"Creating Meta": "Creating Meta",
|
||||
"Writing Nca": "Writing Nca",
|
||||
"Updating ncm databse": "Updating ncm databse",
|
||||
"Updating ncm database": "Updating ncm database",
|
||||
"Pushing application record": "Pushing application record",
|
||||
"Failed to install forwarder": "Failed to install forwarder",
|
||||
"Unstar": "Unstar",
|
||||
"Star": "Star",
|
||||
"Unstarred ": "Unstarred ",
|
||||
"Starred ": "Starred ",
|
||||
"Failed to remove old forwarder, please manually remove it!": "Failed to remove old forwarder, please manually remove it!",
|
||||
@@ -257,12 +280,13 @@
|
||||
"Copy": "Copy",
|
||||
"Copying ": "Copying ",
|
||||
"Paste": "Paste",
|
||||
"Paste ": "Paste ",
|
||||
" file(s)?": " file(s)?",
|
||||
"Paste file(s)?": "Paste file(s)?",
|
||||
"Pasting ": "Pasting ",
|
||||
"Pasting": "Pasting",
|
||||
"Rename": "Rename",
|
||||
"Set New File Name": "Set New File Name",
|
||||
"Failed to delete directory": "Failed to delete directory",
|
||||
"Failed to delete file": "Failed to delete file",
|
||||
"Extract zip": "Extract zip",
|
||||
"Extract Options": "Extract Options",
|
||||
"Extract here": "Extract here",
|
||||
@@ -285,13 +309,17 @@
|
||||
"Create Folder": "Create Folder",
|
||||
"Set Folder Name": "Set Folder Name",
|
||||
"Creating ": "Creating ",
|
||||
"View as text (unfinished)": "View as text (unfinished)",
|
||||
"Upload": "Upload",
|
||||
"Select upload location": "Select upload location",
|
||||
"No upload locations set!": "No upload locations set!",
|
||||
"Uploading": "Uploading",
|
||||
"Upload successfull!": "Upload successfull!",
|
||||
"Upload failed!": "Upload failed!",
|
||||
"View as text (unfinished)": "View as text (unfinished)",
|
||||
"Hash": "Hash",
|
||||
"Hash Options": "Hash Options",
|
||||
"Hashing": "Hashing",
|
||||
"Failed to hash file...": "Failed to hash file...",
|
||||
"Ignore read only": "Ignore read only",
|
||||
"Mount": "Mount",
|
||||
"Sd": "Sd",
|
||||
@@ -302,6 +330,7 @@
|
||||
"Launch ": "Launch ",
|
||||
"Launch option for: ": "Launch option for: ",
|
||||
"Select launcher for: ": "Select launcher for: ",
|
||||
"Close FileBrowser?": "Close FileBrowser?",
|
||||
|
||||
"Sort By": "Sort By",
|
||||
"Sort Options": "Sort Options",
|
||||
@@ -335,16 +364,16 @@
|
||||
"Search": "Search",
|
||||
|
||||
"Options": "Options",
|
||||
"Split": "Split",
|
||||
"OK": "OK",
|
||||
"Back": "Back",
|
||||
"Select": "Select",
|
||||
"Open": "Open",
|
||||
"Close": "Close",
|
||||
"Launch": "Launch",
|
||||
"Restart": "Restart",
|
||||
"Next": "Next",
|
||||
"Prev": "Prev",
|
||||
"Unstar": "Unstar",
|
||||
"Star": "Star",
|
||||
"Yes": "Yes",
|
||||
"No": "No",
|
||||
"On": "On",
|
||||
@@ -367,12 +396,13 @@
|
||||
"Remove": "Remove",
|
||||
"Completely remove ": "Completely remove ",
|
||||
"Removing ": "Removing ",
|
||||
"Removed ": "Removed ",
|
||||
"Uninstalling ": "Uninstalling ",
|
||||
"Removed ": "Removed ",
|
||||
|
||||
"Download": "Download",
|
||||
"Downloading ": "Downloading ",
|
||||
"Downloaded ": "Downloaded ",
|
||||
"Download via the Network options!": "Download via the Network options!",
|
||||
|
||||
"Update": "Update",
|
||||
"Update avaliable: ": "Update avaliable: ",
|
||||
@@ -389,4 +419,4 @@
|
||||
"Empty!": "Empty!",
|
||||
"Not Ready...": "Not Ready...",
|
||||
"Error loading page!": "Error loading page!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
"No Internet": "Sin Internet",
|
||||
"Switch-Handheld!": "¡Switch-Modo-Portátil!",
|
||||
"Switch-Docked!": "¡Switch-Modo-TV!",
|
||||
"Audio disabled due to suspended game": "",
|
||||
"Warning! Logs are enabled, Sphaira will run slowly!": "ADVERTENCIA, ¡El registro de eventos está activado, Sphaira correrá más lento!",
|
||||
"Audio disabled due to suspended game": "Audio deshabilitado debido al juego suspendido",
|
||||
"Are you sure you wish to cancel?": "¿Estás seguro que deseas cancelar?",
|
||||
"An error occurred": "",
|
||||
"An error occurred": "Ha ocurrido un error",
|
||||
"If this message appears repeatedly, please open an issue.": "Si este mensaje aparece repetidamente, por favor abrir un 'issue'.",
|
||||
|
||||
"Menu Options": "Opciones de menú",
|
||||
@@ -14,10 +15,10 @@
|
||||
"Theme Options": "Opciones de tema",
|
||||
"Select Theme": "Seleccionar tema",
|
||||
"Music": "Música",
|
||||
"12 Hour Time": "",
|
||||
"Download Default Music": "",
|
||||
"Failed to download default_music.bfstm, please try again": "",
|
||||
"Overwrite current default music?": "",
|
||||
"12 Hour Time": "Reloj de 12 horas",
|
||||
"Download Default Music": "Descargar música por defecto",
|
||||
"Failed to download default_music.bfstm, please try again": "Fallo al descargar archivo default_music.bfstm, intentar nuevamente",
|
||||
"Overwrite current default music?": "¿Sobreescribir la música por defecto actual?",
|
||||
|
||||
"Network": "Red",
|
||||
"Network Options": "Opciones de red",
|
||||
@@ -27,6 +28,8 @@
|
||||
"Nxlink Connected": "NXlink conectado",
|
||||
"Nxlink Upload": "NXlink subida",
|
||||
"Nxlink Finished": "NXlink finalizado",
|
||||
"Hdd": "Disco Duro",
|
||||
"Hdd write protect": "Disco duro solo lectura",
|
||||
|
||||
"Language": "Idioma",
|
||||
"Auto": "Automático",
|
||||
@@ -48,31 +51,44 @@
|
||||
"Misc": "Varios",
|
||||
"Misc Options": "Opciones varias",
|
||||
"Games": "Juegos",
|
||||
"Game Options": "",
|
||||
"Hide forwarders": "",
|
||||
"Launch random game": "",
|
||||
"List meta records": "",
|
||||
"Entries": "",
|
||||
"Failed to list application meta entries": "",
|
||||
"No meta entries found...\n": "",
|
||||
"Updating application record list": "",
|
||||
"Dump": "",
|
||||
"Select content to dump": "",
|
||||
"Dump All": "",
|
||||
"Dump Application": "",
|
||||
"Dump Patch": "",
|
||||
"Dump AddOnContent": "",
|
||||
"Dump DataPatch": "",
|
||||
"Select dump location": "",
|
||||
"microSD card (/dumps/NSP/)": "",
|
||||
"USB transfer (Switch 2 Switch)": "",
|
||||
"/dev/null (Speed Test)": "",
|
||||
"Dumping": "",
|
||||
"Dump successfull!": "",
|
||||
"Dump failed!": "",
|
||||
"Success": "",
|
||||
"Delete successfull!": "",
|
||||
"Delete failed!": "",
|
||||
"Game Options": "Opciones de Juegos",
|
||||
"Hide forwarders": "Ocultar forwarders",
|
||||
"Launch random game": "Ejecutar un juego aleatorio",
|
||||
"List meta records": "Listar registros meta",
|
||||
"Entries": "Entradas",
|
||||
"Failed to list application meta entries": "Error al listar las entradas meta del aplicativo",
|
||||
"No meta entries found...\n": "No se encontró entradas meta...\n",
|
||||
"Updating application record list": "Actualizando la lista de registros del aplicativo",
|
||||
"Dump": "Volcar",
|
||||
"Dump options": "Opciones de volcado",
|
||||
"Dump Options": "Opciones de volcado",
|
||||
"Select content to dump": "Seleccione contenido a volcar",
|
||||
"Dump All": "Volcar todo",
|
||||
"Dump Application": "Volcar aplicativo",
|
||||
"Dump Patch": "Volcar parche",
|
||||
"Dump AddOnContent": "Volcar Contenido adicional",
|
||||
"Dump DataPatch": "Volcar Datos de parche",
|
||||
"Created nested folder": "Se creó una carpeta anidadada",
|
||||
"Append folder with .xci": "Adjuntar carpeta con archivo .XCI",
|
||||
"Trim XCI": "Archivo XCI recortado",
|
||||
"Label trimmed XCI": "Archivo XCI con etiqueta removida",
|
||||
"Multi-threaded USB transfer": "Transferencia USB multi-hilos",
|
||||
"Dump All Bins": "Volcar todos los binarios",
|
||||
"Dump XCI": "Volcar archivo XCI",
|
||||
"Dump Card ID Set": "Volcar conjunto de ID de tarjeta",
|
||||
"Dump Card UID": "Volcar UID de tarjeta",
|
||||
"Dump Certificate": "Volcar Certificado",
|
||||
"Dump Initial Data": "Volcar datos iniciales",
|
||||
"Select dump location": "Escoja la ubicación para el volcado",
|
||||
"microSD card (/dumps/)": "Tarjeta microSD (/dumps/)",
|
||||
"USB transfer (Switch 2 Switch)": "Transferencia USB (de Switch a Switch)",
|
||||
"/dev/null (Speed Test)": "Medir la velocidad usando dispositivo /dev/null",
|
||||
"Dumping": "Volcando",
|
||||
"Dump successfull!": "Volcado satisfactorio",
|
||||
"Dump failed!": "Error en el volcado",
|
||||
"Delete successfull!": "Borrado satisfactorio",
|
||||
"Delete failed!": "Error al borrar",
|
||||
"Success": "Proceso Exitoso",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Opciones de Themezer",
|
||||
@@ -86,51 +102,55 @@
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Descargando json",
|
||||
"Select asset to download for ": "Seleccionar recurso a descargar para ",
|
||||
"Failed to download json": "Error al descargar JSON",
|
||||
"Failed to download app!": "¡Error al descargar aplicativo!",
|
||||
|
||||
"FTP Install": "",
|
||||
"FTP Install (EXPERIMENTAL)": "",
|
||||
"Connection Type: WiFi | Strength: ": "",
|
||||
"Connection Type: Ethernet": "",
|
||||
"Connection Type: None": "",
|
||||
"Host:": "",
|
||||
"Port:": "",
|
||||
"Username:": "",
|
||||
"Password:": "",
|
||||
"SSID:": "",
|
||||
"Passphrase:": "",
|
||||
"Failed to install via FTP, press B to exit...": "",
|
||||
"Ftp install success!": "",
|
||||
"Ftp install failed!": "",
|
||||
"USB Install": "",
|
||||
"USB": "",
|
||||
"Connected, waiting for file list...": "",
|
||||
"Connected, starting transfer...": "",
|
||||
"Failed to init usb, press B to exit...": "",
|
||||
"Waiting for connection...": "",
|
||||
"Transferring data...": "",
|
||||
"USB connected, sending file list": "",
|
||||
"Sent file list, waiting for command...": "",
|
||||
"waiting for usb connection...": "",
|
||||
"Disable MTP for usb install": "",
|
||||
"Re-enabled MTP": "",
|
||||
"Installed via usb": "",
|
||||
"Usb install success!": "",
|
||||
"Usb install failed!": "",
|
||||
"Press B to exit...": "",
|
||||
"GameCard Install": "",
|
||||
"GameCard": "",
|
||||
"GC": "",
|
||||
"System memory %.1f GB": "",
|
||||
"microSD card %.1f GB": "",
|
||||
"Nand Install": "",
|
||||
"SD Card Install": "",
|
||||
"Exit": "",
|
||||
"Gc install success!": "",
|
||||
"Gc install failed!": "",
|
||||
"FTP Install": "Instalación vía FTP",
|
||||
"Connection Type: WiFi | Strength: ": "Tipo de conexión: WiFi | Fuerza de la señal",
|
||||
"Connection Type: Ethernet": "Tipo de Conexión: Ethernet",
|
||||
"Connection Type: None": "Tipo de Conexión: Ninguna",
|
||||
"Host:": "Host",
|
||||
"Port:": "Puerto",
|
||||
"Username:": "Nombre de Usuario",
|
||||
"Password:": "Contraseña",
|
||||
"SSID:": "SSID",
|
||||
"Passphrase:": "Parafrase",
|
||||
"Failed to install, press B to exit...": "Error al instalar vía FTP, presione B para salir…",
|
||||
"Install success!": "¡Instalación vía FTP satisfactoria!",
|
||||
"Install failed!": "¡Error en instalación vía FTP!",
|
||||
"USB Install": "Instalación vía USB",
|
||||
"USB": "USB",
|
||||
"Connected, waiting for file list...": "Conectado, esperando lista de archivos…",
|
||||
"Connected, starting transfer...": "Conectado, inicando la transferencia…",
|
||||
"Failed to init usb, press B to exit...": "Error al iniciar USB, presione B para salir…",
|
||||
"Waiting for connection...": "Esperando por conexión…",
|
||||
"Transferring data...": "Tranfiriendo datos…",
|
||||
"USB connected, sending file list": "USB conectado, enviando listado de archivos",
|
||||
"Sent file list, waiting for command...": "Listado de archivos enviado, esprendo comando…",
|
||||
"waiting for usb connection...": "Esperando por conexión USB…",
|
||||
"Disable MTP for usb install": "Instalación vía MTP Deshabilitada",
|
||||
"Re-enabled MTP": "Instalación vía MTP rehabilitado",
|
||||
"Installed via usb": "Instalado vía USB",
|
||||
"Usb install success!": "Instalación USB satisfactoria",
|
||||
"Usb install failed!": "Error en instalación USB",
|
||||
"Press B to exit...": "Presione B para salir…",
|
||||
"GameCard Install": "Instalación de Tarjeta de Juego",
|
||||
"GameCard": "Tarjeta de Juego",
|
||||
"GC": "TJ",
|
||||
"System memory %.1f GB": "Memoria del sistema %.1f GB",
|
||||
"microSD card %.1f GB": "Tarjeta microSD %.1f GB",
|
||||
"Exit": "Salir",
|
||||
"Install disabled...\nPlease enable installing via the install options.": "Instalación deshabilitada…\nPor favor habilite instalar aplicativos en las opciones de instalación",
|
||||
"No GameCard inserted": "No hay una Tarjeta de Juego insertada",
|
||||
"GameCard is already trimmed!": "¡La Tarjeta de Juego ya se encuentra recortada!",
|
||||
"WARNING: GameCard is already trimmed!": "ADVERTENCIA: ¡La Tarjeta de Juego ya se encuentra recortada!",
|
||||
"Continue": "Continuar",
|
||||
"Gc install success!": "Instalación de TJ satisfactoria",
|
||||
"Gc install failed!": "Fallo al instalar TJ",
|
||||
|
||||
"IRS (Infrared Joycon Camera)": "",
|
||||
"IRS": "",
|
||||
"Irs": "IRS",
|
||||
"IRS (Infrared Joycon Camera)": "IRS (Cámara infraroja del JoyCon)",
|
||||
"IRS": "IRS",
|
||||
"Irs": "Irs",
|
||||
"Ambient Noise Level: ": "Nivel de Ruido Ambiente",
|
||||
"Controller": "Control",
|
||||
"Pad ": "GamePad ",
|
||||
@@ -160,17 +180,17 @@
|
||||
"Negative image": "Imagen negativa",
|
||||
"Format": "Formato",
|
||||
"Trimming Format": "Formato de recorte",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"External Light Filter": "Filtro de luz externa",
|
||||
"Load Default": "Cargar predeterminado",
|
||||
|
||||
"Web": "Web",
|
||||
"Select URL": "Seleccione el URL",
|
||||
"Enter custom URL": "Ingrese un URL personalizado",
|
||||
"Enter URL": "Escriba el URL",
|
||||
|
||||
"Advanced": "Avanzado",
|
||||
"Advanced Options": "Opciones avanzadas",
|
||||
"Logging": "Registro",
|
||||
"Logging": "Bitácora",
|
||||
"Replace hbmenu on exit": "Reemplazar hbmenu",
|
||||
"Restore hbmenu?": "¿Restaurar hbmenu?",
|
||||
"Restore": "Restaurar",
|
||||
@@ -181,35 +201,36 @@
|
||||
"Restored hbmenu": "hbmenu restaurado",
|
||||
"Restart Sphaira?": "¿Reiniciar sphaira?",
|
||||
"Press OK to restart Sphaira": "Presiona OK para reiniciar sphaira",
|
||||
"Boost CPU during transfer": "Tranferir con Overclock",
|
||||
"Text scroll speed": "Velocidad de scroll",
|
||||
"Slow": "Lento",
|
||||
"Normal": "Normal",
|
||||
"Fast": "Rápido",
|
||||
"Set right-side menu": "",
|
||||
"Install options": "",
|
||||
"Install Options": "",
|
||||
"Enable sysmmc": "",
|
||||
"Enable emummc": "",
|
||||
"Set left-side menu": "Menú izquierdo",
|
||||
"Set right-side menu": "Menú Drecho",
|
||||
"Install options": "Opciones de Instalación",
|
||||
"Install Options": "Opciones de Instalación",
|
||||
"Enable sysmmc": "Habilitar SysMMC",
|
||||
"Enable emummc": "Habilitar EmuMMC",
|
||||
"Show install warning": "Precaución de instalación",
|
||||
"Install location": "Dispositivo de instalación",
|
||||
"System memory": "Memoria de sistema",
|
||||
"microSD card": "microSD",
|
||||
"Boost CPU clock": "",
|
||||
"Allow downgrade": "",
|
||||
"Skip if already installed": "",
|
||||
"Ticket only": "",
|
||||
"Skip base": "",
|
||||
"Skip patch": "",
|
||||
"Skip dlc": "",
|
||||
"Skip data patch": "",
|
||||
"Skip ticket": "",
|
||||
"Skip NCA hash verify": "",
|
||||
"Skip RSA header verify": "",
|
||||
"Skip RSA NPDM verify": "",
|
||||
"Ignore distribution bit": "",
|
||||
"Convert to standard crypto": "",
|
||||
"Lower master key": "",
|
||||
"Lower system version": "",
|
||||
"Allow downgrade": "Permitir instalar versiones anteriores",
|
||||
"Skip if already installed": "Saltar si ya está instalado",
|
||||
"Ticket only": "Únicamente tiquetes",
|
||||
"Skip base": "Saltar base",
|
||||
"Skip patch": "Saltar parche",
|
||||
"Skip dlc": "Saltar DLC",
|
||||
"Skip data patch": "Saltar datos de parche",
|
||||
"Skip ticket": "Saltar Tiquete",
|
||||
"Skip NCA hash verify": "Saltar verificación de Hash NCA",
|
||||
"Skip RSA header verify": "Saltar verificación de encabezado RSA",
|
||||
"Skip RSA NPDM verify": "Saltar verificación de NPDM RSA",
|
||||
"Ignore distribution bit": "Ignorar bit de distribución",
|
||||
"Convert to standard crypto": "Convertir a crypto estándar",
|
||||
"Lower master key": "Bajar versión de llave maestra",
|
||||
"Lower system version": "Bajar versión de sistema",
|
||||
|
||||
"Homebrew": "Homebrew",
|
||||
"Apps": "Apps",
|
||||
@@ -222,15 +243,17 @@
|
||||
"Creating Control": "Creando Control",
|
||||
"Creating Meta": "Creando Meta",
|
||||
"Writing Nca": "Creando NCA",
|
||||
"Updating ncm databse": "Actualizando base de datos ncm",
|
||||
"Updating ncm database": "Actualizando base de datos ncm",
|
||||
"Pushing application record": "Registro de aplicación",
|
||||
"Failed to install forwarder": "Fallo al instalar forwarder",
|
||||
"Unstar": "Quitar favorito",
|
||||
"Star": "Favorito",
|
||||
"Unstarred ": "Quitar Favorito",
|
||||
"Starred ": "Favorito",
|
||||
"Failed to remove old forwarder, please manually remove it!": "",
|
||||
"Failed to remove old forwarder, please manually remove it!": "Error al remover forwarder anterior, por favor remuévalo manualmente",
|
||||
|
||||
"AppStore": "Tienda",
|
||||
"Appstore": "",
|
||||
"Appstore": "Tienda",
|
||||
"Store": "Tienda",
|
||||
"Filter: %s | Sort: %s | Order: %s": "Filtrar: %s | Clasificar: %s | Orden: %s",
|
||||
"AppStore Options": "Opciones de la Tienda",
|
||||
@@ -257,51 +280,57 @@
|
||||
"Copy": "Copiar",
|
||||
"Copying ": "Copiando ",
|
||||
"Paste": "Pegar",
|
||||
"Paste ": "Pegar ",
|
||||
" file(s)?": " ¿archivo(s)?",
|
||||
"Paste file(s)?": "Pegar archivo(s)",
|
||||
"Pasting ": "Pegando ",
|
||||
"Pasting": "Pegando",
|
||||
"Rename": "Renombrar",
|
||||
"Set New File Name": "Establecer nuevo nombre de archivo",
|
||||
"Extract zip": "",
|
||||
"Extract Options": "",
|
||||
"Extract here": "",
|
||||
"Extract to root": "",
|
||||
"Are you sure you want to extract to root?": "",
|
||||
"Extract to...": "",
|
||||
"Enter the path to the folder to extract into": "",
|
||||
"Extracting ": "",
|
||||
"Extract success!": "",
|
||||
"Extract failed!": "",
|
||||
"Compress to zip": "",
|
||||
"Compress Options": "",
|
||||
"Compress": "",
|
||||
"Compress to...": "",
|
||||
"Compressing ": "",
|
||||
"Compress success!": "",
|
||||
"Compress failed!": "",
|
||||
"Failed to delete directory": "Error al borrar carpeta",
|
||||
"Failed to delete file": "Error al borrar archivo",
|
||||
"Extract zip": "Extraer carpeta comprimida ZIP",
|
||||
"Extract Options": "Opciones de extracción",
|
||||
"Extract here": "Extraer aquí",
|
||||
"Extract to root": "Extraer en la raíz",
|
||||
"Are you sure you want to extract to root?": "¿Seguro que desea extraerlo en la carpeta raíz?",
|
||||
"Extract to...": "Extraer en…",
|
||||
"Enter the path to the folder to extract into": "Ingrese la ruta en la cual extraer",
|
||||
"Extracting ": "Extrayendo",
|
||||
"Extract success!": "¡Extracción satisfactoria!",
|
||||
"Extract failed!": "¡Extracción fallida!",
|
||||
"Compress to zip": "Comprimir en archivo ZIP",
|
||||
"Compress Options": "Opciones de compresión",
|
||||
"Compress": "Comprimir",
|
||||
"Compress to...": "Comprimir en…",
|
||||
"Compressing ": "Comprimiendo",
|
||||
"Compress success!": "¡Compresión satisfactoria!",
|
||||
"Compress failed!": "¡Error en la descompresión!",
|
||||
"Create File": "Crear archivo",
|
||||
"Set File Name": "Establecer nombre de archivo",
|
||||
"Create Folder": "Crear carpeta",
|
||||
"Set Folder Name": "Establecer nombre de carpeta",
|
||||
"Creating ": "Creando ",
|
||||
"Upload": "",
|
||||
"Select upload location": "",
|
||||
"No upload locations set!": "",
|
||||
"Uploading": "",
|
||||
"Upload successfull!": "",
|
||||
"Upload failed!": "",
|
||||
"View as text (unfinished)": "Ver como texto (sin terminar)",
|
||||
"Upload": "Subir",
|
||||
"Select upload location": "Seleccione la ubicación a la cual subir",
|
||||
"No upload locations set!": "No se ha definido la ubicación para subir",
|
||||
"Uploading": "Subiendo",
|
||||
"Upload successfull!": "¡Subida satisfactoria!",
|
||||
"Upload failed!": "¡Error en la subida!",
|
||||
"Hash": "Hash",
|
||||
"Hash Options": "Opciones de Hash",
|
||||
"Hashing": "Creando Hash",
|
||||
"Failed to hash file...": "Error al crear Hash del archivo…",
|
||||
"Ignore read only": "Ignorar sólo lectura",
|
||||
"Mount": "Montar",
|
||||
"Sd": "SD",
|
||||
"Image System memory": "Imagen memoria interna",
|
||||
"Image microSD card": "Imagen tarjeta microSD",
|
||||
"Empty...": "Vacío...",
|
||||
"Empty...": "Vacío…",
|
||||
"Open with DayBreak?": "¿Abrir con DayBreak?",
|
||||
"Launch ": "Abrir ",
|
||||
"Launch option for: ": "Opción de abrir con: ",
|
||||
"Select launcher for: ": "Seleccionar abrir con: ",
|
||||
"Close FileBrowser?": "¿Cerrar explorador de archivos?",
|
||||
|
||||
"Sort By": "Ordenar por",
|
||||
"Sort Options": "Opciones de clasificación",
|
||||
@@ -328,32 +357,32 @@
|
||||
"Ascending": "Ascendente",
|
||||
"Ascending (Up)": "Ascendente (arriba)",
|
||||
"Asc": "Ascendente",
|
||||
"Layout": "",
|
||||
"List": "",
|
||||
"Icon": "",
|
||||
"Grid": "",
|
||||
"Layout": "Diseño",
|
||||
"List": "Lista",
|
||||
"Icon": "Íconos",
|
||||
"Grid": "Malla",
|
||||
"Search": "Buscar",
|
||||
|
||||
"Options": "Opciones",
|
||||
"Split": "Dividir",
|
||||
"OK": "OK",
|
||||
"Back": "Atrás",
|
||||
"Select": "Seleccionar",
|
||||
"Open": "Abrir",
|
||||
"Close": "Cerrar",
|
||||
"Launch": "Ejecutar",
|
||||
"Restart": "Reiniciar",
|
||||
"Next": "",
|
||||
"Prev": "",
|
||||
"Unstar": "Quitar favorito",
|
||||
"Star": "Favorito",
|
||||
"Next": "Siguiente",
|
||||
"Prev": "Anterior",
|
||||
"Yes": "Sí",
|
||||
"No": "No",
|
||||
"On": "",
|
||||
"Off": "",
|
||||
"On": "Activo",
|
||||
"Off": "Apagado",
|
||||
|
||||
"Install": "Instalar",
|
||||
"Install Selected files?": "",
|
||||
"Install Selected files?": "¡Instalar los archivos seleccionados?",
|
||||
"Installing ": "Instalando ",
|
||||
"Installed ": "",
|
||||
"Installed ": "Instalado",
|
||||
"Installed!": "¡Instalado!",
|
||||
"Trying to load ": "Intentando cargar ",
|
||||
"Checking MD5": "Chequeando MD5",
|
||||
@@ -367,12 +396,13 @@
|
||||
"Remove": "Borrar",
|
||||
"Completely remove ": "Eliminar completamente",
|
||||
"Removing ": "Removiendo ",
|
||||
"Removed ": "Removido ",
|
||||
"Uninstalling ": "Desinstalando ",
|
||||
"Removed ": "Removido ",
|
||||
|
||||
"Download": "Descargar",
|
||||
"Downloading ": "Descargando ",
|
||||
"Downloaded ": "Descargado ",
|
||||
"Download via the Network options!": "¡Descargar vía las opciones de red!",
|
||||
|
||||
"Update": "Actualizar",
|
||||
"Update avaliable: ": "Actualización disponible: ",
|
||||
@@ -380,13 +410,13 @@
|
||||
"Updated to ": "Actualizado a ",
|
||||
"Failed to download update": "Fallo al descargar actualización",
|
||||
|
||||
"%zu hours %zu minutes remaining": "",
|
||||
"%zu minutes %zu seconds remaining": "",
|
||||
"%zu seconds remaining": "",
|
||||
"%zu hours %zu minutes remaining": "Faltan %zu horas %zu minutos",
|
||||
"%zu minutes %zu seconds remaining": "Faltan %zu minutos %zu segundos",
|
||||
"%zu seconds remaining": "Faltan %zu segundos",
|
||||
|
||||
"Loading...": "Cargando...",
|
||||
"Loading...": "Cargando…",
|
||||
"Loading": "Cargando",
|
||||
"Empty!": "¡Vacío!",
|
||||
"Not Ready...": "No listo aún...",
|
||||
"Not Ready...": "No listo aún…",
|
||||
"Error loading page!": "¡Error cargando la página!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
{
|
||||
"[Applet Mode]": "[Mode Applet]",
|
||||
"No Internet": "Pas d'Internet",
|
||||
"Switch-Handheld!": "Switch-Portable",
|
||||
"Switch-Docked!": "Switch-Dockée",
|
||||
"Audio disabled due to suspended game": "Audio désactivé à cause d'un jeu suspendu",
|
||||
"Are you sure you wish to cancel?": "Souhaitez-vous vraiment annuler?",
|
||||
"Switch-Handheld!": "Mode Portable",
|
||||
"Switch-Docked!": "Mode Dockée",
|
||||
"Warning! Logs are enabled, Sphaira will run slowly!": "Attention ! Les journaux sont activés, Sphaira fonctionnera lentement !",
|
||||
"Audio disabled due to suspended game": "Audio désactivé à cause d'un jeu en pause",
|
||||
"Are you sure you wish to cancel?": "Souhaitez-vous vraiment annuler ?",
|
||||
"An error occurred": "Une erreur s'est produite",
|
||||
"If this message appears repeatedly, please open an issue.": "Si ce message apparait en boucle veuillez ouvrir une issue.",
|
||||
"If this message appears repeatedly, please open an issue.": "Si ce message apparaît en boucle, veuillez ouvrir une issue sur :",
|
||||
|
||||
"Menu Options": "Options des Menus",
|
||||
"Menu": "Menu",
|
||||
"Theme": "Thème",
|
||||
"Theme Options": "Options de Thème",
|
||||
"Select Theme": "Choisir un Thème",
|
||||
"Menu Options": "Paramètres du menu",
|
||||
"Menu": "Paramètres",
|
||||
"Theme": "Thèmes",
|
||||
"Theme Options": "Paramètres des Thèmes",
|
||||
"Select Theme": "Choisir un thème",
|
||||
"Music": "Musique",
|
||||
"12 Hour Time": "Temps sur 12 heures",
|
||||
"12 Hour Time": "Format horloge 12 heures",
|
||||
"Download Default Music": "Télécharger la musique par défaut",
|
||||
"Failed to download default_music.bfstm, please try again": "Echec du téléchargement de default_music.bfstm, veuillez réessayer",
|
||||
"Overwrite current default music?": "Remplacer la musique actuelle par défaut?",
|
||||
"Failed to download default_music.bfstm, please try again": "Échec du téléchargement de default_music.bfstm, veuillez réessayer",
|
||||
"Overwrite current default music?": "Remplacer la musique actuelle par défaut ?",
|
||||
|
||||
"Network": "Réseau",
|
||||
"Network Options": "Options Réseau",
|
||||
"Network Options": "Paramètres Réseau",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink Connecté",
|
||||
"Nxlink Upload": "Nxlink téléversement",
|
||||
"Nxlink Upload": "Téléversement Nxlink",
|
||||
"Nxlink Finished": "Nxlink terminé",
|
||||
"Hdd": "Disque Dur",
|
||||
"Hdd write protect": "Protection en écriture du disque dur",
|
||||
|
||||
"Language": "Langue",
|
||||
"Auto": "Auto",
|
||||
@@ -47,65 +50,79 @@
|
||||
|
||||
"Misc": "Divers",
|
||||
"Misc Options": "Options Diverses",
|
||||
"Games": "Jeux",
|
||||
"Game Options": "Options de jeu",
|
||||
"Games": "Jeux installés",
|
||||
"Game Options": "Options des jeux",
|
||||
"Hide forwarders": "Masquer les forwarders",
|
||||
"Launch random game": "Lancer un jeu au hasard",
|
||||
"List meta records": "Lister les enregistrements meta",
|
||||
"Entries": "Entrées",
|
||||
"List meta records": "Lister les méta-enregistrements",
|
||||
"Entries": "Meta disponible",
|
||||
"Failed to list application meta entries": "Le listage des entrées meta de l'application a échoué",
|
||||
"No meta entries found...\n": "Aucune entrée meta trouvée...\n",
|
||||
"Updating application record list": "Mise à jour de la liste d'enregistrement de l'application",
|
||||
"Dump": "Dumper",
|
||||
"Select content to dump": "Sélectionner un contenu à dumper",
|
||||
"Dump options": "Option du Dumper",
|
||||
"Dump options": "Paramètres",
|
||||
"Select content to dump": "Contenu à dumper",
|
||||
"Dump All": "Tout dumper",
|
||||
"Dump Application": "Dumper Application",
|
||||
"Dump Patch": "Dumper mise à jour",
|
||||
"Dump AddOnContent": "Dumper DLCs",
|
||||
"Dump DataPatch": "Dumper patch de données",
|
||||
"Select dump location": "Sélectionner l'emplacement du dump",
|
||||
"microSD card (/dumps/NSP/)": "carte microSD (/dumps/NSP/)",
|
||||
"Dump Application": "Dumper uniquement l'application (jeu)",
|
||||
"Dump Patch": "Dumper uniquement la mise à jour",
|
||||
"Dump AddOnContent": "Dumper uniquement le(s) DLCs",
|
||||
"Dump DataPatch": "Dumper le patch de données",
|
||||
"Created nested folder": "Créer un dossier imbriqué",
|
||||
"Append folder with .xci": "Ajouter un dossier avec .xci",
|
||||
"Trim XCI": "Trimmer le XCI",
|
||||
"Label trimmed XCI": "Étiquette sur le XCI trimmer",
|
||||
"Multi-threaded USB transfer": "Transfert USB multithread",
|
||||
"Dump All Bins": "Dumper tous les éléments",
|
||||
"Dump XCI": "Dumper le XCI",
|
||||
"Dump Card ID Set": "Dumper l'ID de la cartouche",
|
||||
"Dump Card UID": "Dumper l'UID de la cartouche",
|
||||
"Dump Certificate": "Dumper le certificat",
|
||||
"Dump Initial Data": "Dumper l'Initial Data",
|
||||
"Select dump location": "Sélectionner l'emplacement du dump :",
|
||||
"microSD card (/dumps/)": "carte microSD (/dumps/)",
|
||||
"USB transfer (Switch 2 Switch)": "Transfert USB (Switch à Switch",
|
||||
"/dev/null (Speed Test)": "/dev/null (test de vitesse)",
|
||||
"Dumping": "Dump en cours",
|
||||
"Dump successfull!": "Dump réussi!",
|
||||
"Dump failed!": "Dump échoué!",
|
||||
"Dumping": "Dump en cours...",
|
||||
"Dump successfull!": "Dump réussi !",
|
||||
"Dump failed!": "Échec du dump !",
|
||||
"Delete successful!": "Fichier supprimé !",
|
||||
"Delete failed!": "Échec de la suppression !",
|
||||
"Success": "Succès",
|
||||
"Delete successfull!": "Suprression réussie!",
|
||||
"Delete failed!": "Suprression échouée!",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Options Themezer",
|
||||
"Nsfw": "Nsfw",
|
||||
"Themezer": "Themezer (thèmes Switch)",
|
||||
"Themezer Options": "Paramètres",
|
||||
"Nsfw": "Contenu NSFW",
|
||||
"Page": "Page",
|
||||
"Page %zu / %zu": "Page %zu / %zu",
|
||||
"Enter Page Number": "Entrez Un Numéro De Page",
|
||||
"Enter Page Number": "Numéro de la page :",
|
||||
"Bad Page": "Page inexistante",
|
||||
"Download theme?": "Télécharger le thème?",
|
||||
"Download theme?": "Voulez-vous télécharger ce thème ?",
|
||||
|
||||
"GitHub": "GitHub",
|
||||
"GitHub": "Github",
|
||||
"Downloading json": "Téléchargement du json",
|
||||
"Select asset to download for ": "Sélectionner l'asset à télécharger pour ",
|
||||
"Select asset to download for ": "Sélectionnez l'actif à télécharger pour :",
|
||||
"Failed to download json": "Échec du téléchargement du json",
|
||||
"Failed to download app!": "Échec du téléchargement de l'application",
|
||||
|
||||
"FTP Install": "Installation via FTP",
|
||||
"FTP Install (EXPERIMENTAL)": "Installation via FTP (EXPERIMENTAL)",
|
||||
"Connection Type: WiFi | Strength: ": "Type de connexion: WiFi, force du signal: ",
|
||||
"Connection Type: Ethernet": "Type de connexion: Ethernet",
|
||||
"Connection Type: None": "Type de connexion: Aucune",
|
||||
"Host:": "Hôte:",
|
||||
"Port:": "Port:",
|
||||
"Username:": "Nom d'utilisateur:",
|
||||
"Password:": "Mot de passe:",
|
||||
"SSID:": "SSID:",
|
||||
"Passphrase:": "Mot de passe:",
|
||||
"Failed to install via FTP, press B to exit...": "Installation via FTP échouée, appuyer sur B pour quitter...",
|
||||
"Ftp install success!": "Installation via FTP réussie!",
|
||||
"Ftp install failed!": "Installation via FTP échouée!",
|
||||
"USB Install": "Installation via USB",
|
||||
"USB": "USB",
|
||||
"FTP Install": "Installer contenu via FTP",
|
||||
"Connection Type: WiFi | Strength: ": "Type de connexion : WiFi / Force du signal :",
|
||||
"Connection Type: Ethernet": "Type de connexion : Ethernet",
|
||||
"Connection Type: None": "Type de connexion : Aucune",
|
||||
"Host:": "Hôte :",
|
||||
"Port:": "Port :",
|
||||
"Username:": "Nom d'utilisateur :",
|
||||
"Password:": "Mot de passe :",
|
||||
"SSID:": "SSID :",
|
||||
"Passphrase:": "Mot de passe :",
|
||||
"Failed to install, press B to exit...": "Installation échouée, appuyer sur B pour quitter...",
|
||||
"Install success!": "Installation réussie !",
|
||||
"Install failed!": "Installation échouée !",
|
||||
"USB Install": "Installer contenu via USB",
|
||||
"USB": "Installation via USB",
|
||||
"Connected, waiting for file list...": "Connecté, en attente de la liste des fichiers...",
|
||||
"Connected, starting transfer...": "Connecté, début du transfère...",
|
||||
"Failed to init usb, press B to exit...": "Echec de l'initialisation de l'USB, appuyer sur B pour quitter...",
|
||||
"Failed to init usb, press B to exit...": "Échec de l'initialisation de l'USB, appuyer sur B pour quitter...",
|
||||
"Waiting for connection...": "En attente de la connexion...",
|
||||
"Transferring data...": "Transfère de données...",
|
||||
"USB connected, sending file list": "USB connecté, envoi de la liste des fichiers",
|
||||
@@ -114,29 +131,32 @@
|
||||
"Disable MTP for usb install": "Désactivation du MTP pour l'installation via USB",
|
||||
"Re-enabled MTP": "Réactivation du MTP",
|
||||
"Installed via usb": "Installé via USB",
|
||||
"Usb install success!": "Installation via USB réussie!",
|
||||
"Usb install failed!": "Installation via USB échouée!",
|
||||
"Usb install success!": "Installation via USB réussie !",
|
||||
"Usb install failed!": "Installation via USB échouée !",
|
||||
"Press B to exit...": "Appuyer sur B pour quitter...",
|
||||
"GameCard Install": "Installation de la cartouche",
|
||||
"GameCard": "Cartouche",
|
||||
"GameCard Install": "Installation de la cartouche du jeu",
|
||||
"GameCard": "Cartouche du jeu",
|
||||
"GC": "GC",
|
||||
"System memory %.1f GB": "Mémoire système %.1f GB",
|
||||
"microSD card %.1f GB": "Carte microSD %.1f GB",
|
||||
"Nand Install": "Installer sur la Nand",
|
||||
"SD Card Install": "Installer sur la carte SD",
|
||||
"Exit": "Quitter",
|
||||
"Gc install success!": "Installation de la cartouche réussie!",
|
||||
"Gc install failed!": "Installation de la cartouche échouée!",
|
||||
"Install disabled...\nPlease enable installing via the install options.": "Installation désactivée...\nVeuillez activer l'installation via les options d'installation.",
|
||||
"No GameCard inserted": "Aucune cartouche de jeu insérée !",
|
||||
"GameCard is already trimmed!": "La carte du jeu est déjà trimmer !",
|
||||
"WARNING: GameCard is already trimmed!": "AVERTISSEMENT : la carte du jeu est déjà trimmer ! ",
|
||||
"Continue": "Continuer",
|
||||
"Gc install success!": "Installation de la cartouche réussie !",
|
||||
"Gc install failed!": "Installation de la cartouche échouée !",
|
||||
|
||||
"IRS (Infrared Joycon Camera)": "IRS (infrarouge de la caméra du Joycon",
|
||||
"IRS": "IRS",
|
||||
"IRS (Infrared Joycon Camera)": "IRS (infrarouge de la cam du Joycon",
|
||||
"IRS": "Caméra Infrarouge",
|
||||
"Irs": "Irs",
|
||||
"Ambient Noise Level: ": "Niveau De Bruit Ambiant: ",
|
||||
"Ambient Noise Level: ": "Niveau de bruit ambiant: ",
|
||||
"Controller": "Contrôleur",
|
||||
"Pad ": "Manette ",
|
||||
"HandHeld": "Portable",
|
||||
" (Available)": " (Disponible)",
|
||||
" (Unsupported)": "Non supporté",
|
||||
"Pad ": "Manette",
|
||||
"HandHeld": "Joycon",
|
||||
" (Available)": " (Connectée)",
|
||||
" (Unsupported)": "Non supportée",
|
||||
" (Unconnected)": " (Non connectée)",
|
||||
"Rotation": "Rotation",
|
||||
"0 (Sideways)": "0 (Paysage)",
|
||||
@@ -155,59 +175,60 @@
|
||||
"Dim group": "Groupe sombre",
|
||||
"None": "Aucun",
|
||||
"Gain": "Gain",
|
||||
"Negative Image": "Image Négative",
|
||||
"Normal image": "Image normale",
|
||||
"Negative image": "Image négative",
|
||||
"Format": "Format",
|
||||
"Trimming Format": "Format de Découpe",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"External Light Filter": "Filtre de Lumière Externe",
|
||||
"Load Default": "Charger par Défaut",
|
||||
"Negative Image": "Format d'image",
|
||||
"Normal image": "Normale",
|
||||
"Negative image": "Négative",
|
||||
"Format": "Format d'image",
|
||||
"Trimming Format": "Format du découpage",
|
||||
"External Light Filter": "Filtre de lumière externe",
|
||||
"Load Default": "Rétablir par Défaut",
|
||||
|
||||
"Advanced": "Avancé",
|
||||
"Advanced Options": "Options Avancées",
|
||||
"Logging": "Journalisation",
|
||||
"Replace hbmenu on exit": "Remplacer hbmenu quand quitté",
|
||||
"Restore hbmenu?": "Restaurer hbmenu?",
|
||||
"Restore": "Restaurer",
|
||||
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "/switch/hbmenu.nro n'a pas été trouvé\nUtiliser l'Appstore pour réinstaller le hbmenu",
|
||||
"Failed to restore hbmenu, please re-download hbmenu": "Echec de la restauration de hbmenu, veuillez le réinstaller",
|
||||
"Failed to restore hbmenu, using sphaira instead": "Echec de la restauration de hbmenu, sphaira sera utilisé à la place",
|
||||
"Restored hbmenu, closing sphaira": "Hbmenu restauré, fermeture de sphaira",
|
||||
"Restored hbmenu": "Hbmenu restauré",
|
||||
"Restart Sphaira?": "Redémarrer Sphaira?",
|
||||
"Web": "Web",
|
||||
"Select URL": "Sélectionnez une adresse",
|
||||
"Enter custom URL": "Entrez une adresse personnalisée",
|
||||
"Enter URL": "Entrer l'adresse :",
|
||||
|
||||
"Advanced": "Menu avancé",
|
||||
"Advanced Options": "Menu avancé",
|
||||
"Logging": "Fichier journal",
|
||||
"Replace hbmenu on exit": "Remplacer HBmenu par Sphaira",
|
||||
"Restore hbmenu?": "Restaurer HBmenu ?",
|
||||
"Restore": "Restaurer !",
|
||||
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "/switch/hbmenu.nro non trouvé\nRéinstaller le HBmenu via Appstore",
|
||||
"Failed to restore hbmenu, please re-download hbmenu": "Échec de la restauration du HBmenu, veuillez le réinstaller",
|
||||
"Failed to restore hbmenu, using sphaira instead": "Échec de la restauration de HBmenu, Sphaira sera utilisé à la place",
|
||||
"Restored hbmenu, closing sphaira": "Hbmenu restauré, fermeture de Sphaira",
|
||||
"Restored hbmenu": "HBmenu restauré !",
|
||||
"Restart Sphaira?": "Redémarrer Sphaira ?",
|
||||
"Press OK to restart Sphaira": "Appuyez sur OK pour redémarrer Sphaira",
|
||||
"Boost CPU during transfer": "Augmenter le CPU pendant le transfert",
|
||||
"Text scroll speed": "Vitesse de défilement du texte",
|
||||
"Slow": "Lent",
|
||||
"Normal": "Normal",
|
||||
"Fast": "Rapide",
|
||||
"Set right-side menu": "Configurer le menu de droite",
|
||||
"Install options": "Options d'installation",
|
||||
"Set left-side menu": "Menu de gauche par défaut",
|
||||
"Set right-side menu": "Menu de droite par défaut",
|
||||
"Install options": "Options d'installation des jeux",
|
||||
"Install Options": "Options d'Installation",
|
||||
"Enable sysmmc": "Activer sur la sysmmc",
|
||||
"Enable emummc": "Activer sur l'emummc",
|
||||
"Show install warning": "Afficher l'avertissement d'installation",
|
||||
"Enable sysmmc": "Afficher la SYSmmc",
|
||||
"Enable emummc": "Afficher l'EMUmmc",
|
||||
"Show install warning": "Avertissement lors d'installation",
|
||||
"Install location": "Emplacement d'installation",
|
||||
"System memory": "Mémoire système",
|
||||
"microSD card": "Carte microSD",
|
||||
"Boost CPU clock": "Augmenter la vitesse de l'horloge CPU",
|
||||
"Allow downgrade": "Autoriser le downgrade",
|
||||
"Skip if already installed": "Ignorer si déjà installé",
|
||||
"Ticket only": "Seulement le ticket",
|
||||
"Skip base": "Ignorer base",
|
||||
"Skip patch": "Ignorer mise à jour",
|
||||
"Skip dlc": "Ignorer DLC",
|
||||
"Skip data patch": "Ignorer patch de données",
|
||||
"Skip base": "Ignorer la base du jeu",
|
||||
"Skip patch": "Ignorer les mises à jour",
|
||||
"Skip dlc": "Ignorer le(s) DLC",
|
||||
"Skip data patch": "Ignorer le patch de données",
|
||||
"Skip ticket": "Ignorer ticket",
|
||||
"Skip NCA hash verify": "Ignorer la vérification du hash NCA",
|
||||
"Skip RSA header verify": "Ignorer la vérification de l'entête RSA",
|
||||
"Skip RSA NPDM verify": "Ignorer la vérification RSA NPDM",
|
||||
"Skip NCA hash verify": "Vérification du hash NCA",
|
||||
"Skip RSA header verify": "Vérification de l'entête RSA",
|
||||
"Skip RSA NPDM verify": "Vérification RSA NPDM",
|
||||
"Ignore distribution bit": "Ignorer le bit de distribution",
|
||||
"Convert to standard crypto": "Convertir vers la crypto standard",
|
||||
"Convert to standard crypto": "Convertir en crypto standard",
|
||||
"Lower master key": "Abaisser la master key",
|
||||
"Lower system version": "Abaisser la version du système",
|
||||
|
||||
@@ -216,33 +237,35 @@
|
||||
"Homebrew Options": "Options Homebrew",
|
||||
"Hide Sphaira": "Masquer Sphaira",
|
||||
"Install Forwarder": "Installer le Forwarder",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "ATTENTION: L'installation de forwarders entraînera un ban!",
|
||||
"Installing Forwarder": "Installation Du Forwarder",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "ATTENTION : L'installation de forwarders entraînera un ban !",
|
||||
"Installing Forwarder": "Installation du Forwarder...",
|
||||
"Creating Program": "Création de Program",
|
||||
"Creating Control": "Création de Control",
|
||||
"Creating Meta": "Création de Meta",
|
||||
"Writing Nca": "Ecriture NCA",
|
||||
"Updating ncm databse": "Mise à jour de ncm databse",
|
||||
"Writing Nca": "Écriture NCA",
|
||||
"Updating ncm database": "Mise à jour de la base de données ncm",
|
||||
"Pushing application record": "Ajout de l'enregistrement de l'application",
|
||||
"Failed to install forwarder": "Echec de l'installation du forwarder",
|
||||
"Unstarred ": "Retiré des favories ",
|
||||
"Starred ": "Ajouté aux favories ",
|
||||
"Failed to remove old forwarder, please manually remove it!": "Supression de l'ancien forwarder échouée, supprimez-le manuellement!",
|
||||
"Failed to install forwarder": "Échec de l'installation du forwarder",
|
||||
"Unstar": "Retirer des favories",
|
||||
"Star": "Ajouter aux favories",
|
||||
"Unstarred ": "Retiré des favories : ",
|
||||
"Starred ": "Ajouté aux favories : ",
|
||||
"Failed to remove old forwarder, please manually remove it!": "Suppression de l'ancien forwarder échouée, supprimez-le manuellement !",
|
||||
|
||||
"AppStore": "AppStore",
|
||||
"Appstore": "Magasin d'applications",
|
||||
"Store": "Magasin",
|
||||
"Filter: %s | Sort: %s | Order: %s": "Filtre: %s | Tri: %s | Ordre: %s",
|
||||
"Appstore": "Boutique d'applications",
|
||||
"Store": "AppStore",
|
||||
"Filter: %s | Sort : %s | Order: %s" : "Filtre : %s | Tri : %s | Ordre : %s",
|
||||
"AppStore Options": "Options de l'AppStore",
|
||||
"Info": "Info.",
|
||||
"Changelog": "Journal des modifications",
|
||||
"Details": "Détails",
|
||||
"version: %s": "version: %s",
|
||||
"updated: %s": "Mis à jour: %s",
|
||||
"category: %s": "catégorie: %s",
|
||||
"version: %s": "version : %s",
|
||||
"updated: %s": "Mis à jour : %s",
|
||||
"category: %s": "catégorie : %s",
|
||||
"extracted: %.2f MiB": "Extrait: %.2f MiB",
|
||||
"app_dls: %s": "app_dls: %s",
|
||||
"More by Author": "Plus de cet Auteur",
|
||||
"More by Author": "Plus de cet auteur",
|
||||
"Leave Feedback": "Laisser un avis",
|
||||
|
||||
"FileBrowser": "Explorateur de Fichiers",
|
||||
@@ -250,58 +273,64 @@
|
||||
"%zd files": "%zd fichiers",
|
||||
"%zd dirs": "%zd dossiers",
|
||||
"File Options": "Options de Fichier",
|
||||
"Show Hidden": "Afficher Masqués",
|
||||
"Show Hidden": "Afficher fichier masqué",
|
||||
"Folders First": "Dossiers en Premier",
|
||||
"Hidden Last": "Masqués en Dernier",
|
||||
"Cut": "Couper",
|
||||
"Copy": "Copier",
|
||||
"Copying ": "Copie en cours ",
|
||||
"Copying ": "Copie en cours...",
|
||||
"Paste": "Coller",
|
||||
"Paste ": "Coller ",
|
||||
" file(s)?": " fichier(s)?",
|
||||
"Pasting ": "Collage en cours ",
|
||||
"Pasting": "Collage en cours",
|
||||
"Paste file(s)?": "Copier le(s) fichier(s) ?",
|
||||
"Pasting": "Collage en cours...",
|
||||
"Pasting": "Collage en cours...",
|
||||
"Rename": "Renommer",
|
||||
"Set New File Name": "Nouveau Nom Du Fichier",
|
||||
"Set New File Name": "Nouveau nom du fichier",
|
||||
"Failed to delete directory": "Échec de la suppression du répertoire",
|
||||
"Failed to delete file": "Échec de la suppression du fichier",
|
||||
"Extract zip": "Extraire le zip",
|
||||
"Extract Options": "Options d'extraction",
|
||||
"Extract here": "Extraire ici",
|
||||
"Extract to root": "Extraire à la racine",
|
||||
"Are you sure you want to extract to root?": "Souhaitez-vous vraiment extraire à la racine?",
|
||||
"Are you sure you want to extract to root?": "Souhaitez-vous vraiment extraire à la racine ?",
|
||||
"Extract to...": "Extraire vers...",
|
||||
"Enter the path to the folder to extract into": "Entrer le chemin du répertoire vers lequel extraire",
|
||||
"Extracting ": "Extraction en cours ",
|
||||
"Extract success!": "Extraction réussie!",
|
||||
"Extract failed!": "Extraction échouée!",
|
||||
"Extracting ": "Extraction en cours...",
|
||||
"Extract success!": "Extraction réussie !",
|
||||
"Extract failed!": "Extraction échouée !",
|
||||
"Compress to zip": "Compresser en zip",
|
||||
"Compress Options": "Options de compression",
|
||||
"Compress": "Compresser",
|
||||
"Compress to...": "Compresser vers...",
|
||||
"Compressing ": "Compression en cours ",
|
||||
"Compress success!": "Compression réussie!",
|
||||
"Compress failed!": "Compression échouée!",
|
||||
"Create File": "Créer un Fichier",
|
||||
"Set File Name": "Nommer Le Fichier",
|
||||
"Create Folder": "Créer un Dossier",
|
||||
"Set Folder Name": "Nommer Le Dossier",
|
||||
"Compressing ": "Compression en cours...",
|
||||
"Compress success!": "Compression réussie !",
|
||||
"Compress failed!": "Compression échouée !",
|
||||
"Create File": "Créer un fichier",
|
||||
"Set File Name": "Nommer Le fichier",
|
||||
"Create Folder": "Créer un dossier",
|
||||
"Set Folder Name": "Nommer le dossier",
|
||||
"Creating ": "Création ",
|
||||
"Upload": "Upload",
|
||||
"Select upload location": "Sélectionner l'emplacement d'upload",
|
||||
"No upload locations set!": "Aucun emplacement d'upload configuré!",
|
||||
"Uploading": "Upload en cours",
|
||||
"Upload successfull!": "Upload réussi!",
|
||||
"Upload failed!": "Upload échoué!",
|
||||
"View as text (unfinished)": "Afficher sous forme de texte (inachevé)",
|
||||
"Upload": "Téléverser",
|
||||
"Select upload location": "Emplacement du téléversement :",
|
||||
"No upload locations set!": "Aucun emplacement de téléversement configuré !",
|
||||
"Uploading": "Téléversement en cours...",
|
||||
"Upload successfull!": "Téléversement réussi !",
|
||||
"Upload failed!": "Téléversement échoué !",
|
||||
"Hash": "Hash",
|
||||
"Hash Options": "Options du Hash",
|
||||
"Hashing": "Hachage",
|
||||
"Failed to hash file...": "Échec du hachage du fichier...",
|
||||
"Ignore read only": "Ignorer lecture seule",
|
||||
"Mount": "Monter",
|
||||
"Sd": "Sd",
|
||||
"Image System memory": "Image de la mémoire System",
|
||||
"Sd": "SD",
|
||||
"Image System memory": "Image de la mémoire Système",
|
||||
"Image microSD card": "Image de la Carte microSD",
|
||||
"Empty...": "Vide...",
|
||||
"Open with DayBreak?": "Ouvrir avec DayBreak?",
|
||||
"Open with DayBreak?": "Ouvrir avec DayBreak ?",
|
||||
"Launch ": "Lancer ",
|
||||
"Launch option for: ": "Option de lancement pour: ",
|
||||
"Select launcher for: ": "Sélectionner le lanceur pour: ",
|
||||
"Launch option for: ": "Option de lancement pour : ",
|
||||
"Select launcher for: ": "Sélectionner le lanceur pour : ",
|
||||
"Close FileBrowser?": "Fermer le navigateur de fichiers ?",
|
||||
|
||||
"Sort By": "Tri Par",
|
||||
"Sort Options": "Options de Tri",
|
||||
@@ -311,15 +340,15 @@
|
||||
"Tools": "Outils",
|
||||
"Themes": "Thèmes",
|
||||
"Legacy": "Legacy",
|
||||
"Sort": "Tri",
|
||||
"Sort": "Trier",
|
||||
"Size": "Taille",
|
||||
"Size (Star)": "Taille (Favories)",
|
||||
"Size (Star)": "Taille (Favoris)",
|
||||
"Alphabetical": "Alphabétique",
|
||||
"Alphabetical (Star)": "Alphabétique (Favories)",
|
||||
"Alphabetical (Star)": "Alphabétique (Favoris)",
|
||||
"Updated": "Mis à jour",
|
||||
"Updated (Star)": "Mis à jour (Favories)",
|
||||
"Updated (Star)": "Mis à jour (Favoris)",
|
||||
"Downloads": "Téléchargements",
|
||||
"Likes": "Likes",
|
||||
"Likes": "Aimer",
|
||||
"ID": "ID",
|
||||
"Order": "Ordre",
|
||||
"Descending": "Décroissant",
|
||||
@@ -332,61 +361,62 @@
|
||||
"List": "Liste",
|
||||
"Icon": "Icône",
|
||||
"Grid": "Grille",
|
||||
"Search": "Recherche",
|
||||
"Search": "Lancer la recherche",
|
||||
|
||||
"Options": "Options",
|
||||
"Split": "Découper",
|
||||
"OK": "OK",
|
||||
"Back": "Retour",
|
||||
"Select": "Sélectionner",
|
||||
"Open": "Ouvrir",
|
||||
"Close": "Fermer",
|
||||
"Launch": "Exécuter",
|
||||
"Restart": "Redémarrer",
|
||||
"Next": "Suivant",
|
||||
"Prev": "Précédent",
|
||||
"Unstar": "Retirer des favories",
|
||||
"Star": "Ajouter aux favories",
|
||||
"Yes": "Oui",
|
||||
"No": "Non",
|
||||
"On": "On",
|
||||
"Off": "Off",
|
||||
|
||||
"Install": "Installer",
|
||||
"Install Selected files?": "Installer les fichiers sélectionnés?",
|
||||
"Installing ": "Installation en cours ",
|
||||
"Install Selected files?": "Installer les fichiers sélectionnés ?",
|
||||
"Installing ": "Installation en cours...",
|
||||
"Installed ": "Installé ",
|
||||
"Installed!": "Installé!",
|
||||
"Trying to load ": "Tente de charger ",
|
||||
"Installed!": "Installé !",
|
||||
"Trying to load ": "Essayer de charger ",
|
||||
"Checking MD5": "Vérification MD5",
|
||||
|
||||
"Delete": "Supprimer",
|
||||
"Delete Selected files?": "Supprimer les fichiers sélectionnés?",
|
||||
"Delete Selected files?": "Supprimer les fichiers sélectionnés ?",
|
||||
"Are you sure you want to delete ": "Êtes-vous sûr de vouloir supprimer ",
|
||||
"Scanning ": "Scan en cours ",
|
||||
"Deleting ": "Suppression en cours ",
|
||||
"Deleting": "Suppression en cours",
|
||||
"Scanning ": "Scan en cours...",
|
||||
"Deleting ": "Suppression en cours...",
|
||||
"Deleting": "Suppression en cours...",
|
||||
"Remove": "Supprimer",
|
||||
"Completely remove ": "Supprimer totalement ",
|
||||
"Removing ": "Suppression en cours ",
|
||||
"Removing ": "Suppression en cours...",
|
||||
"Uninstalling ": "Désinstallation en cours...",
|
||||
"Removed ": "Supprimé ",
|
||||
"Uninstalling ": "Désinstallation en cours ",
|
||||
|
||||
"Download": "Télécharger",
|
||||
"Downloading ": "Téléchargement en cours ",
|
||||
"Downloaded ": "Téléchargé",
|
||||
"Downloading ": "Téléchargement en cours...",
|
||||
"Downloaded ": "Téléchargé ",
|
||||
"Download via the Network options!": "Téléchargez via les options réseau !",
|
||||
|
||||
"Update": "Mise à jour",
|
||||
"Update avaliable: ": "Mise à jour disponible: ",
|
||||
"Download update: ": "Télécharger la mise à jour: ",
|
||||
"Update avaliable: ": "Mise à jour disponible : ",
|
||||
"Download update: ": "Télécharger la mise à jour : ",
|
||||
"Updated to ": "Mis à jour vers ",
|
||||
"Failed to download update": "Echec du téléchargement de la mise à jour",
|
||||
"Failed to download update": "Échec du téléchargement de la mise à jour !",
|
||||
|
||||
"%zu hours %zu minutes remaining": "%zu heures %zu minutes restantes",
|
||||
"%zu minutes %zu seconds remaining": "%zu minutes %zu secondes restantes",
|
||||
"%zu seconds remaining": "%zu secondes restantes",
|
||||
|
||||
"Loading...": "Chargement...",
|
||||
"Loading": "Chargement en cours",
|
||||
"Empty!": "Vide!",
|
||||
"Not Ready...": "Pas prêt",
|
||||
"Loading": "Chargement en cours...",
|
||||
"Empty!": "Vide !",
|
||||
"Not Ready...": "Pas prêt !",
|
||||
"Error loading page!": "Erreur du chargement de la page!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"No Internet": "Niente Internet",
|
||||
"Switch-Handheld!": "Switch Portatile",
|
||||
"Switch-Docked!": "Switch Dock",
|
||||
"Warning! Logs are enabled, Sphaira will run slowly!": "",
|
||||
"Audio disabled due to suspended game": "Audio disabilitato poichè un app è in pausa",
|
||||
"Are you sure you wish to cancel?": "Sei sicuro di voler annullare?",
|
||||
"An error occurred": "",
|
||||
@@ -27,6 +28,8 @@
|
||||
"Nxlink Connected": "Nxlink connesso",
|
||||
"Nxlink Upload": "Nxlink upload",
|
||||
"Nxlink Finished": "Nxlink finito",
|
||||
"Hdd": "",
|
||||
"Hdd write protect": "",
|
||||
|
||||
"Language": "Lingua",
|
||||
"Auto": "Auto",
|
||||
@@ -57,22 +60,35 @@
|
||||
"No meta entries found...\n": "",
|
||||
"Updating application record list": "",
|
||||
"Dump": "",
|
||||
"Dump options": "",
|
||||
"Dump Options": "",
|
||||
"Select content to dump": "",
|
||||
"Dump All": "",
|
||||
"Dump Application": "",
|
||||
"Dump Patch": "",
|
||||
"Dump AddOnContent": "",
|
||||
"Dump DataPatch": "",
|
||||
"Created nested folder": "",
|
||||
"Append folder with .xci": "",
|
||||
"Trim XCI": "",
|
||||
"Label trimmed XCI": "",
|
||||
"Multi-threaded USB transfer": "",
|
||||
"Dump All Bins": "",
|
||||
"Dump XCI": "",
|
||||
"Dump Card ID Set": "",
|
||||
"Dump Card UID": "",
|
||||
"Dump Certificate": "",
|
||||
"Dump Initial Data": "",
|
||||
"Select dump location": "",
|
||||
"microSD card (/dumps/NSP/)": "",
|
||||
"microSD card (/dumps/)": "",
|
||||
"USB transfer (Switch 2 Switch)": "",
|
||||
"/dev/null (Speed Test)": "",
|
||||
"Dumping": "",
|
||||
"Dump successfull!": "",
|
||||
"Dump failed!": "",
|
||||
"Success": "",
|
||||
"Delete successfull!": "",
|
||||
"Delete failed!": "",
|
||||
"Success": "",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Impostazioni Themezer",
|
||||
@@ -86,9 +102,10 @@
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Scaricamento json",
|
||||
"Select asset to download for ": "",
|
||||
"Failed to download json": "",
|
||||
"Failed to download app!": "",
|
||||
|
||||
"FTP Install": "",
|
||||
"FTP Install (EXPERIMENTAL)": "",
|
||||
"Connection Type: WiFi | Strength: ": "",
|
||||
"Connection Type: Ethernet": "",
|
||||
"Connection Type: None": "",
|
||||
@@ -98,9 +115,9 @@
|
||||
"Password:": "",
|
||||
"SSID:": "",
|
||||
"Passphrase:": "",
|
||||
"Failed to install via FTP, press B to exit...": "",
|
||||
"Ftp install success!": "",
|
||||
"Ftp install failed!": "",
|
||||
"Failed to install, press B to exit...": "",
|
||||
"Install success!": "",
|
||||
"Install failed!": "",
|
||||
"USB Install": "",
|
||||
"USB": "",
|
||||
"Connected, waiting for file list...": "",
|
||||
@@ -122,9 +139,12 @@
|
||||
"GC": "",
|
||||
"System memory %.1f GB": "",
|
||||
"microSD card %.1f GB": "",
|
||||
"Nand Install": "",
|
||||
"SD Card Install": "",
|
||||
"Exit": "",
|
||||
"Install disabled...\nPlease enable installing via the install options.": "",
|
||||
"No GameCard inserted": "",
|
||||
"GameCard is already trimmed!": "",
|
||||
"WARNING: GameCard is already trimmed!": "",
|
||||
"Continue": "",
|
||||
"Gc install success!": "",
|
||||
"Gc install failed!": "",
|
||||
|
||||
@@ -160,14 +180,14 @@
|
||||
"Negative image": "Immagine negativa",
|
||||
"Format": "Formato",
|
||||
"Trimming Format": "Formato di ritaglio",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"External Light Filter": "Filtro luce esterno",
|
||||
"Load Default": "Carica predefinito",
|
||||
|
||||
"Web": "",
|
||||
"Select URL": "",
|
||||
"Enter custom URL": "",
|
||||
"Enter URL": "",
|
||||
|
||||
"Advanced": "Avanzato",
|
||||
"Advanced Options": "Opzioni avanzate",
|
||||
"Logging": "Logging",
|
||||
@@ -181,10 +201,12 @@
|
||||
"Restored hbmenu": "hbmenu ripristinato",
|
||||
"Restart Sphaira?": "Vuoi riavviare Sphaira?",
|
||||
"Press OK to restart Sphaira": "Premi OK per riavviare Sphaira",
|
||||
"Boost CPU during transfer": "",
|
||||
"Text scroll speed": "",
|
||||
"Slow": "",
|
||||
"Normal": "",
|
||||
"Fast": "",
|
||||
"Set left-side menu": "",
|
||||
"Set right-side menu": "",
|
||||
"Install options": "",
|
||||
"Install Options": "",
|
||||
@@ -194,7 +216,6 @@
|
||||
"Install location": "Installa posizione",
|
||||
"System memory": "Memoria di sistema",
|
||||
"microSD card": "Scheda microSD",
|
||||
"Boost CPU clock": "",
|
||||
"Allow downgrade": "",
|
||||
"Skip if already installed": "",
|
||||
"Ticket only": "",
|
||||
@@ -222,9 +243,11 @@
|
||||
"Creating Control": "",
|
||||
"Creating Meta": "",
|
||||
"Writing Nca": "",
|
||||
"Updating ncm databse": "",
|
||||
"Updating ncm database": "",
|
||||
"Pushing application record": "",
|
||||
"Failed to install forwarder": "",
|
||||
"Unstar": "Rimuovi dai preferiti",
|
||||
"Star": "Aggiungi ai preferiti",
|
||||
"Unstarred ": "",
|
||||
"Starred ": "",
|
||||
"Failed to remove old forwarder, please manually remove it!": "",
|
||||
@@ -257,12 +280,13 @@
|
||||
"Copy": "Copia",
|
||||
"Copying ": "Copio",
|
||||
"Paste": "Incolla",
|
||||
"Paste ": "Incolla ",
|
||||
" file(s)?": "(i)file?",
|
||||
"Paste file(s)?": "",
|
||||
"Pasting ": "Incollo",
|
||||
"Pasting": "Incollo",
|
||||
"Rename": "Rinomina",
|
||||
"Set New File Name": "Imposta nuovo nome",
|
||||
"Failed to delete directory": "",
|
||||
"Failed to delete file": "",
|
||||
"Extract zip": "",
|
||||
"Extract Options": "",
|
||||
"Extract here": "",
|
||||
@@ -285,13 +309,17 @@
|
||||
"Create Folder": "Crea cartella",
|
||||
"Set Folder Name": "Imposta nome",
|
||||
"Creating ": "Creazione",
|
||||
"View as text (unfinished)": "Visualizza come testo (non finito)",
|
||||
"Upload": "",
|
||||
"Select upload location": "",
|
||||
"No upload locations set!": "",
|
||||
"Uploading": "",
|
||||
"Upload successfull!": "",
|
||||
"Upload failed!": "",
|
||||
"View as text (unfinished)": "Visualizza come testo (non finito)",
|
||||
"Hash": "",
|
||||
"Hash Options": "",
|
||||
"Hashing": "",
|
||||
"Failed to hash file...": "",
|
||||
"Ignore read only": "Ignora read only",
|
||||
"Mount": "Monta",
|
||||
"Sd": "SD",
|
||||
@@ -302,6 +330,7 @@
|
||||
"Launch ": "Lancia",
|
||||
"Launch option for: ": "Lancia opzione per",
|
||||
"Select launcher for: ": "Scegli launcher per",
|
||||
"Close FileBrowser?": "",
|
||||
|
||||
"Sort By": "Ordina per",
|
||||
"Sort Options": "Opzioni filtro",
|
||||
@@ -335,16 +364,16 @@
|
||||
"Search": "Ricerca",
|
||||
|
||||
"Options": "Opzioni",
|
||||
"Split": "",
|
||||
"OK": "OK",
|
||||
"Back": "Indietro",
|
||||
"Select": "Seleziona",
|
||||
"Open": "Apri",
|
||||
"Close": "",
|
||||
"Launch": "Lancia",
|
||||
"Restart": "Riavvia",
|
||||
"Next": "",
|
||||
"Prev": "",
|
||||
"Unstar": "Rimuovi dai preferiti",
|
||||
"Star": "Aggiungi ai preferiti",
|
||||
"Yes": "Sì",
|
||||
"No": "No",
|
||||
"On": "",
|
||||
@@ -367,12 +396,13 @@
|
||||
"Remove": "Rimuovi",
|
||||
"Completely remove ": "Elimina definitivamente",
|
||||
"Removing ": "Rimozione",
|
||||
"Removed ": "Rimosso",
|
||||
"Uninstalling ": "Disinstallazione",
|
||||
"Removed ": "Rimosso",
|
||||
|
||||
"Download": "Download",
|
||||
"Downloading ": "Scaricando",
|
||||
"Downloaded ": "Scaricato",
|
||||
"Download via the Network options!": "",
|
||||
|
||||
"Update": "Aggiorna",
|
||||
"Update avaliable: ": "Aggiornamento disponibile",
|
||||
@@ -389,4 +419,4 @@
|
||||
"Empty!": "Vuoto!",
|
||||
"Not Ready...": "Non pronto...",
|
||||
"Error loading page!": "Errore nel caricare la pagina!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"[Applet Mode]": "[Appletモード]",
|
||||
"No Internet": "インターネットなし",
|
||||
"Switch-Handheld!": "ハンドヘルド!",
|
||||
"Switch-Docked!": "ドック接続!",
|
||||
"Audio disabled due to suspended game": "ゲームが一時停止状態の場合、オーディオは無効になります",
|
||||
"Switch-Handheld!": "ハンドヘルドになりました!",
|
||||
"Switch-Docked!": "ドックに接続しました!",
|
||||
"Warning! Logs are enabled, Sphaira will run slowly!": "警告: ログが有効になったため、Sphairaの速度が遅くなります",
|
||||
"Audio disabled due to suspended game": "ゲームが一時停止状態の場合、BGMは無効になります",
|
||||
"Are you sure you wish to cancel?": "本当に取り消しますか?",
|
||||
"An error occurred": "不具合のお知らせ",
|
||||
"If this message appears repeatedly, please open an issue.": "このメッセージが繰り返し表示される場合は、問題を開いてください",
|
||||
"If this message appears repeatedly, please open an issue.": "このメッセージが繰り返し表示される場合、問題を開いてください",
|
||||
|
||||
"Menu Options": "メニュー設定",
|
||||
"Menu": "メニュー",
|
||||
@@ -24,9 +25,11 @@
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink 接続",
|
||||
"Nxlink Upload": "Nxlink アップロード",
|
||||
"Nxlink Finished": "Nxlink 終了",
|
||||
"Nxlink Connected": "Nxlinkに接続しました",
|
||||
"Nxlink Upload": "Nxlinkにアップロードされました",
|
||||
"Nxlink Finished": "Nxlinkを終了します",
|
||||
"Hdd": "HDD",
|
||||
"Hdd write protect": "HDD書き込み保護",
|
||||
|
||||
"Language": "言語",
|
||||
"Auto": "自動",
|
||||
@@ -57,22 +60,35 @@
|
||||
"No meta entries found...\n": "メタエントリが見つかりませんでした\n",
|
||||
"Updating application record list": "ゲームのレコードを更新しています",
|
||||
"Dump": "吸出し",
|
||||
"Dump options": "吸出し設定",
|
||||
"Dump Options": "吸出し設定",
|
||||
"Select content to dump": "吸出すコンテンツを選択",
|
||||
"Dump All": "全て",
|
||||
"Dump Application": "ゲームのみ",
|
||||
"Dump Patch": "ゲームパッチのみ",
|
||||
"Dump AddOnContent": "DLCのみ",
|
||||
"Dump DataPatch": "DLCパッチのみ",
|
||||
"Created nested folder": "ネストされたフォルダを作成",
|
||||
"Append folder with .xci": ".xci付きフォルダを追加",
|
||||
"Trim XCI": "XCIをトリム",
|
||||
"Label trimmed XCI": "トリム済みXCIのラベルを指定",
|
||||
"Multi-threaded USB transfer": "マルチスレッドのUSB転送",
|
||||
"Dump All Bins": "すべてのBINを吸出す",
|
||||
"Dump XCI": "XCIを吸出す",
|
||||
"Dump Card ID Set": "ゲームカードIDを吸出す",
|
||||
"Dump Card UID": "ゲームカードUIDを吸出す",
|
||||
"Dump Certificate": "証明書を吸出す",
|
||||
"Dump Initial Data": "初期データを吸出す",
|
||||
"Select dump location": "吸出し位置を選択",
|
||||
"microSD card (/dumps/NSP/)": "SDカード (/dumps/NSP/)",
|
||||
"USB transfer (Switch 2 Switch)": "USB転送 (Switch 2 Switch)",
|
||||
"microSD card (/dumps/)": "SDカード (/dumps/)",
|
||||
"USB transfer (Switch 2 Switch)": "USBで転送 (Switch 2 Switch)",
|
||||
"/dev/null (Speed Test)": "/dev/null (Speed Test)",
|
||||
"Dumping": "吸出し中",
|
||||
"Dump successfull!": "吸出し完了!",
|
||||
"Dump failed!": "吸出し失敗!",
|
||||
"Success": "完了",
|
||||
"Delete successfull!": "削除完了!",
|
||||
"Delete failed!": "削除失敗!",
|
||||
"Success": "完了",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Themezer設定",
|
||||
@@ -86,31 +102,32 @@
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "JSONからダウンロード",
|
||||
"Select asset to download for ": "ダウンロードアイテムを選択 ",
|
||||
"Failed to download json": "JSONからのダウンロードに失敗しました!",
|
||||
"Failed to download app!": "アプリのダウンロードに失敗しました!",
|
||||
|
||||
"FTP Install": "FTPでインストール",
|
||||
"FTP Install (EXPERIMENTAL)": "FTPでインストール(実験機能)",
|
||||
"Connection Type: WiFi | Strength: ": "接続: WiFi | 強度: ",
|
||||
"Connection Type: Ethernet": "接続: イーサネット",
|
||||
"Connection Type: None": "接続: なし",
|
||||
"Host:": "ホースと:",
|
||||
"Port:": "Port:",
|
||||
"Host:": "ホスト:",
|
||||
"Port:": "ポート:",
|
||||
"Username:": "ユーザー名:",
|
||||
"Password:": "暗証番号:",
|
||||
"SSID:": "SSID:",
|
||||
"Passphrase:": "WiFi暗証番号:",
|
||||
"Failed to install via FTP, press B to exit...": "FTP経由でインストールできませんでした、を押して終了します",
|
||||
"Ftp install success!": "FTPインストール完了!",
|
||||
"Ftp install failed!": "FTPインストール失敗!",
|
||||
"Failed to install, press B to exit...": "FTP経由でインストールできませんでした、を押して終了します",
|
||||
"Install success!": "FTPインストール完了!",
|
||||
"Install failed!": "FTPインストール失敗!",
|
||||
"USB Install": "USBインストール",
|
||||
"USB": "USBインストール",
|
||||
"Connected, waiting for file list...": "接続されました、ファイル リスト待機中",
|
||||
"Connected, starting transfer...": "接続されました、転送開始",
|
||||
"Connected, waiting for file list...": "接続されました、ファイルリストを待っています",
|
||||
"Connected, starting transfer...": "接続されました、転送を開始します",
|
||||
"Failed to init usb, press B to exit...": "USB接続できませんでした、を押して終了します",
|
||||
"Waiting for connection...": "接続待機中",
|
||||
"Transferring data...": "データ転送中",
|
||||
"USB connected, sending file list": "接続されました、ファイルリスト送信中",
|
||||
"Sent file list, waiting for command...": "ファイルリストを送信しました、入力待機中",
|
||||
"waiting for usb connection...": "USB接続待機中",
|
||||
"Transferring data...": "データを転送しています",
|
||||
"USB connected, sending file list": "接続されました、ファイルリスト送信します",
|
||||
"Sent file list, waiting for command...": "ファイルリストを送信しました、入力を待っています",
|
||||
"waiting for usb connection...": "USBの接続を待っています",
|
||||
"Disable MTP for usb install": "USBインストールのため、MTPを無効にします",
|
||||
"Re-enabled MTP": "MTPに再接続します",
|
||||
"Installed via usb": "USBインストールに成功しました",
|
||||
@@ -122,9 +139,12 @@
|
||||
"GC": "ゲームカード",
|
||||
"System memory %.1f GB": "本体保存メモリー %.1f GB",
|
||||
"microSD card %.1f GB": "SDカード %.1f GB",
|
||||
"Nand Install": "本体保存メモリーにインストール",
|
||||
"SD Card Install": "SDカードにインストール",
|
||||
"Exit": "もどる",
|
||||
"Install disabled...\nPlease enable installing via the install options.": "インストールが無効化されています\nインストール設定で有効にしてください",
|
||||
"No GameCard inserted": "ゲームカードが挿入されていません",
|
||||
"GameCard is already trimmed!": "ゲームカードがすでにトリムされています!",
|
||||
"WARNING: GameCard is already trimmed!": "警告: ゲームカードがすでにトリムされています!",
|
||||
"Continue": "つづく",
|
||||
"Gc install success!": "ゲームカードインストール完了!",
|
||||
"Gc install failed!": "ゲームカードインストール失敗!",
|
||||
|
||||
@@ -160,14 +180,14 @@
|
||||
"Negative image": "ネガティブなイメージ",
|
||||
"Format": "解像度",
|
||||
"Trimming Format": "トリミングされた解像度",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"External Light Filter": "外光フィルター",
|
||||
"Load Default": "基本設定に戻す",
|
||||
|
||||
"Web": "ウェブサイト",
|
||||
"Select URL": "URLを選択してください",
|
||||
"Enter custom URL": "URLを直接入力",
|
||||
"Enter URL": "URLを記入してください",
|
||||
|
||||
"Advanced": "高度な",
|
||||
"Advanced Options": "高度設定",
|
||||
"Logging": "ログの取得",
|
||||
@@ -181,10 +201,12 @@
|
||||
"Restored hbmenu": "hbmenuに復元されました",
|
||||
"Restart Sphaira?": "Sphairaを再起動しますか?",
|
||||
"Press OK to restart Sphaira": "確認ボタンを押してSphairaを再起動",
|
||||
"Boost CPU during transfer": "転送する間CPUを加速",
|
||||
"Text scroll speed": "流れる文字の速さ",
|
||||
"Slow": "遅い",
|
||||
"Slow": "遅く",
|
||||
"Normal": "普通",
|
||||
"Fast": "速い",
|
||||
"Fast": "速く",
|
||||
"Set left-side menu": "左側メニュー設定",
|
||||
"Set right-side menu": "右側メニュー設定",
|
||||
"Install options": "インストール設定",
|
||||
"Install Options": "インストール設定",
|
||||
@@ -194,7 +216,6 @@
|
||||
"Install location": "インストール経路",
|
||||
"System memory": "本体保存メモリー",
|
||||
"microSD card": "SDカード",
|
||||
"Boost CPU clock": "CPUクロックをブースト",
|
||||
"Allow downgrade": "ダウングレード許可",
|
||||
"Skip if already installed": "既にインストールされている場合はスキップします",
|
||||
"Ticket only": "チケットのみ設置",
|
||||
@@ -222,9 +243,11 @@
|
||||
"Creating Control": "コントロール作成中",
|
||||
"Creating Meta": "メター作成中",
|
||||
"Writing Nca": "Nca書き取り中",
|
||||
"Updating ncm databse": "ncmのDBをアップデート中",
|
||||
"Updating ncm database": "ncmのDBをアップデート中",
|
||||
"Pushing application record": "アプリの記録をプッシュ中",
|
||||
"Failed to install forwarder": "Forwarderのインストール失敗",
|
||||
"Unstar": "お気に入り解除",
|
||||
"Star": "お気に入り",
|
||||
"Unstarred ": "お気に入り解除: ",
|
||||
"Starred ": "お気に入りに登録: ",
|
||||
"Failed to remove old forwarder, please manually remove it!": "古いForwarderを削除できませんでした、手動で削除してください!",
|
||||
@@ -247,8 +270,8 @@
|
||||
|
||||
"FileBrowser": "ファイルブラウザ",
|
||||
"Files": "ファイル",
|
||||
"%zd files": "%zd個のファイル",
|
||||
"%zd dirs": "%zd個のフォルダー",
|
||||
"%zd files": "%zd ファイル",
|
||||
"%zd dirs": "%zd フォルダー",
|
||||
"File Options": "ファイル設定",
|
||||
"Show Hidden": "非表示ファイルを表示",
|
||||
"Folders First": "フォルダーを優先",
|
||||
@@ -257,12 +280,13 @@
|
||||
"Copy": "コピー",
|
||||
"Copying ": "コピー中 ",
|
||||
"Paste": "ペースト",
|
||||
"Paste ": " ",
|
||||
" file(s)?": "個のファイルをペーストしますか?",
|
||||
"Paste file(s)?": "",
|
||||
"Pasting ": "ペースト中 ",
|
||||
"Pasting": "ペースト中",
|
||||
"Rename": "名前の変更",
|
||||
"Set New File Name": "新しい名前を入力",
|
||||
"Failed to delete directory": "フォルダーを削除できませんでした",
|
||||
"Failed to delete file": "ファイルを削除できませんでした",
|
||||
"Extract zip": "ZIPファイルを解凍",
|
||||
"Extract Options": "解凍設定",
|
||||
"Extract here": "ここに解凍",
|
||||
@@ -285,13 +309,17 @@
|
||||
"Create Folder": "フォルダーの作成",
|
||||
"Set Folder Name": "名前を入力",
|
||||
"Creating ": "作成中 ",
|
||||
"View as text (unfinished)": "テキストとして表示 (未完成)",
|
||||
"Upload": "アップロード",
|
||||
"Select upload location": "アップロードの位置を設定",
|
||||
"No upload locations set!": "アップロードの位置が設定されていません",
|
||||
"Uploading": "アップロード中",
|
||||
"Upload successfull!": "アップロード完了!",
|
||||
"Upload failed!": "アップロード失敗!",
|
||||
"View as text (unfinished)": "テキストとして表示 (未完成)",
|
||||
"Hash": "ハッシュ",
|
||||
"Hash Options": "ハッシュ設定",
|
||||
"Hashing": "ハッシュ化中",
|
||||
"Failed to hash file...": "ハッシュ化できませんでした",
|
||||
"Ignore read only": "読み取り専用を無視する",
|
||||
"Mount": "マウント",
|
||||
"Sd": "SDメモリーカード",
|
||||
@@ -302,6 +330,7 @@
|
||||
"Launch ": "起動しますか",
|
||||
"Launch option for: ": "起動設定: ",
|
||||
"Select launcher for: ": "起動ランチャーを選ぶ: ",
|
||||
"Close FileBrowser?": "ファイルブラウザを閉じますか?",
|
||||
|
||||
"Sort By": "並べ替え",
|
||||
"Sort Options": "並べ替え設定",
|
||||
@@ -335,16 +364,16 @@
|
||||
"Search": "検索",
|
||||
|
||||
"Options": "設定",
|
||||
"Split": "スクリーン分割",
|
||||
"OK": "確認",
|
||||
"Back": "戻る",
|
||||
"Select": "選択",
|
||||
"Open": "開く",
|
||||
"Close": "閉じる",
|
||||
"Launch": "起動",
|
||||
"Restart": "再起動",
|
||||
"Next": "次へ",
|
||||
"Prev": "前へ",
|
||||
"Unstar": "お気に入り解除",
|
||||
"Star": "お気に入り",
|
||||
"Yes": "はい",
|
||||
"No": "いいえ",
|
||||
"On": "オン",
|
||||
@@ -367,18 +396,19 @@
|
||||
"Remove": "除去",
|
||||
"Completely remove ": "除去しますか ",
|
||||
"Removing ": "除去中 ",
|
||||
"Removed ": "除去完了 ",
|
||||
"Uninstalling ": "アンインストール中 ",
|
||||
"Removed ": "除去完了 ",
|
||||
|
||||
"Download": "ダウンロード",
|
||||
"Downloading ": "ダウンロード中 ",
|
||||
"Downloaded ": "ダウンロード完了 ",
|
||||
"Download via the Network options!": "ネットワーク設定からダウンロードしました!",
|
||||
|
||||
"Update": "アップデート",
|
||||
"Update avaliable: ": "アップデート可能: ",
|
||||
"Download update: ": "アップデートをダウンロード: ",
|
||||
"Updated to ": "アップデート: ",
|
||||
"Failed to download update": "アップデートのダウンロード失敗",
|
||||
"Failed to download update": "アップデートのダウンロードに失敗しました",
|
||||
|
||||
"%zu hours %zu minutes remaining": "残り %zu 時間 %zu 分",
|
||||
"%zu minutes %zu seconds remaining": "残り %zu 分",
|
||||
@@ -389,4 +419,4 @@
|
||||
"Empty!": "何も見つかりません",
|
||||
"Not Ready...": "準備ができていません",
|
||||
"Error loading page!": "ページのロードエラー"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"No Internet": "인터넷 연결 없음",
|
||||
"Switch-Handheld!": "휴대모드로 전환됨!",
|
||||
"Switch-Docked!": "독 모드로 전환됨!",
|
||||
"Warning! Logs are enabled, Sphaira will run slowly!": "경고: 로깅 활성화, 앱이 느려집니다!",
|
||||
"Audio disabled due to suspended game": "게임 실행 중에는 BGM이 비활성화 됩니다",
|
||||
"Are you sure you wish to cancel?": "정말 취소할까요?",
|
||||
"An error occurred": "오류가 발생했습니다!",
|
||||
@@ -27,6 +28,8 @@
|
||||
"Nxlink Connected": "Nxlink 연결됨",
|
||||
"Nxlink Upload": "Nxlink 업로드",
|
||||
"Nxlink Finished": "Nxlink 종료됨",
|
||||
"Hdd": "HDD",
|
||||
"Hdd write protect": "HDD 쓰기 방지",
|
||||
|
||||
"Language": "언어",
|
||||
"Auto": "자동",
|
||||
@@ -57,22 +60,35 @@
|
||||
"No meta entries found...\n": "메타 항목을 찾을 수 없습니다...\n",
|
||||
"Updating application record list": "앱 기록 업데이트 중",
|
||||
"Dump": "덤프",
|
||||
"Select content to dump": "덤프 옵션",
|
||||
"Dump options": "덤프 옵션",
|
||||
"Dump Options": "덤프 옵션",
|
||||
"Select content to dump": "덤프 콘텐츠 선택",
|
||||
"Dump All": "모든 콘텐츠",
|
||||
"Dump Application": "게임",
|
||||
"Dump Patch": "게임 패치",
|
||||
"Dump AddOnContent": "DLC",
|
||||
"Dump DataPatch": "DLC 패치",
|
||||
"Created nested folder": "중첩 폴더 생성",
|
||||
"Append folder with .xci": ".xci 파일이 포함된 폴더 추가",
|
||||
"Trim XCI": "XCI 트림",
|
||||
"Label trimmed XCI": "트림된 XCI의 라벨 지정",
|
||||
"Multi-threaded USB transfer": "멀티스레드 USB 전송",
|
||||
"Dump All Bins": "모든 바이너리 덤프",
|
||||
"Dump XCI": "XCI 덤프",
|
||||
"Dump Card ID Set": "카트리지 ID 덤프",
|
||||
"Dump Card UID": "카트리지 UID 덤프",
|
||||
"Dump Certificate": "서명 덤프",
|
||||
"Dump Initial Data": "초기화 데이터 덤프",
|
||||
"Select dump location": "덤프 위치",
|
||||
"microSD card (/dumps/NSP/)": "SD 카드 (sdmc:/dumps/NSP/)",
|
||||
"microSD card (/dumps/)": "SD 카드 (sdmc:/dumps/)",
|
||||
"USB transfer (Switch 2 Switch)": "USB 전송 (Switch 2 Switch)",
|
||||
"/dev/null (Speed Test)": "/dev/null (Speed Test)",
|
||||
"/dev/null (Speed Test)": "/dev/null (벤치마크)",
|
||||
"Dumping": "덤프 중",
|
||||
"Dump successfull!": "덤프 완료!",
|
||||
"Dump failed!": "덤프 실패!",
|
||||
"Success": "완료!",
|
||||
"Delete successfull!": "삭제 완료!",
|
||||
"Delete failed!": "삭제 실패!",
|
||||
"Success": "완료!",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Themezer 옵션",
|
||||
@@ -86,9 +102,10 @@
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "JSON에서 다운로드",
|
||||
"Select asset to download for ": "다운로드 아이템: ",
|
||||
"Failed to download json": "JSON에서 다운로드 실패!",
|
||||
"Failed to download app!": "앱 다운로드 실패!",
|
||||
|
||||
"FTP Install": "FTP 설치",
|
||||
"FTP Install (EXPERIMENTAL)": "FTP 설치 (실험실 기능)",
|
||||
"Connection Type: WiFi | Strength: ": "상태: WiFi | 신호 세기: ",
|
||||
"Connection Type: Ethernet": "상태: 이더넷",
|
||||
"Connection Type: None": "상태: 연결 없음",
|
||||
@@ -98,9 +115,9 @@
|
||||
"Password:": "비밀번호:",
|
||||
"SSID:": "SSID:",
|
||||
"Passphrase:": "WiFi 암호:",
|
||||
"Failed to install via FTP, press B to exit...": "FTP 설치 실패함, 종료하려면 버튼을 입력하세요...",
|
||||
"Ftp install success!": "FTP 설치 완료!",
|
||||
"Ftp install failed!": "FTP 설치 실패!",
|
||||
"Failed to install, press B to exit...": "FTP 설치 실패함, 종료하려면 버튼을 입력하세요...",
|
||||
"Install success!": "FTP 설치 완료!",
|
||||
"Install failed!": "FTP 설치 실패!",
|
||||
"USB Install": "USB 설치",
|
||||
"USB": "USB 설치",
|
||||
"Connected, waiting for file list...": "연결됨, 파일 목록 대기 중...",
|
||||
@@ -122,9 +139,12 @@
|
||||
"GC": "카트리지",
|
||||
"System memory %.1f GB": "본체 저장 메모리 %.1f GB",
|
||||
"microSD card %.1f GB": "SD 카드 %.1f GB",
|
||||
"Nand Install": "본체 저장 메모리에 설치",
|
||||
"SD Card Install": "SD 카드에 설치",
|
||||
"Exit": "나가기",
|
||||
"Install disabled...\nPlease enable installing via the install options.": "설치가 비활성화 되었습니다\n설치 옵션에서 활성화해주세요",
|
||||
"No GameCard inserted": "카트리지가 없습니다",
|
||||
"GameCard is already trimmed!": "카트리지를 이미 트림했습니다",
|
||||
"WARNING: GameCard is already trimmed!": "경고: 카트리지를 이미 트림했습니다",
|
||||
"Continue": "계속",
|
||||
"Gc install success!": "카트리지 설치 완료!",
|
||||
"Gc install failed!": "카트리지 설치 실패!",
|
||||
|
||||
@@ -160,14 +180,14 @@
|
||||
"Negative image": "반전",
|
||||
"Format": "해상도",
|
||||
"Trimming Format": "트리밍 해상도",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"External Light Filter": "외부 조명 필터",
|
||||
"Load Default": "기본값으로 설정",
|
||||
|
||||
"Web": "웹 브라우저",
|
||||
"Select URL": "URL 주소 선택",
|
||||
"Enter custom URL": "직접 지정",
|
||||
"Enter URL": "URL을 기입하세요",
|
||||
|
||||
"Advanced": "고급",
|
||||
"Advanced Options": "고급 옵션",
|
||||
"Logging": "로깅",
|
||||
@@ -181,10 +201,12 @@
|
||||
"Restored hbmenu": "hbmenu 복원됨",
|
||||
"Restart Sphaira?": "Sphaira를 재시작할까요?",
|
||||
"Press OK to restart Sphaira": "확인 버튼 입력하여 Sphaira 재시작",
|
||||
"Boost CPU during transfer": "전송시 CPU 부스트",
|
||||
"Text scroll speed": "긴 텍스트 표시 속도",
|
||||
"Slow": "천천히",
|
||||
"Normal": "보통",
|
||||
"Fast": "빠르게",
|
||||
"Set left-side menu": "좌측 메뉴 설정",
|
||||
"Set right-side menu": "우측 메뉴 설정",
|
||||
"Install options": "설치 옵션",
|
||||
"Install Options": "설치 옵션",
|
||||
@@ -194,7 +216,6 @@
|
||||
"Install location": "설치 위치",
|
||||
"System memory": "본체 저장 메모리",
|
||||
"microSD card": "SD 카드",
|
||||
"Boost CPU clock": "CPU 클럭 향상",
|
||||
"Allow downgrade": "다운그레이드 허용",
|
||||
"Skip if already installed": "설치된 항목 건너뛰기",
|
||||
"Ticket only": "티켓만 설치",
|
||||
@@ -217,14 +238,16 @@
|
||||
"Hide Sphaira": "Sphaira 숨기기",
|
||||
"Install Forwarder": "바로가기 설치",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "경고: 시스낸드에 설치 시, 밴 위험이 있습니다!",
|
||||
"Installing Forwarder": "바로가기 설치",
|
||||
"Installing Forwarder": "바로가기 설치 중",
|
||||
"Creating Program": "프로그램 생성",
|
||||
"Creating Control": "컨트롤 생성",
|
||||
"Creating Meta": "메타 생성",
|
||||
"Writing Nca": "Nca 쓰기",
|
||||
"Updating ncm databse": "Ncm 데이터베이스 업데이트",
|
||||
"Updating ncm database": "Ncm 데이터베이스 업데이트",
|
||||
"Pushing application record": "응용 프로그램 기록 푸싱",
|
||||
"Failed to install forwarder": "바로가기 설치 실패함",
|
||||
"Unstar": "즐겨찾기 해제",
|
||||
"Star": "즐겨찾기",
|
||||
"Unstarred ": "즐겨찾기 해제: ",
|
||||
"Starred ": "즐겨찾기 등록: ",
|
||||
"Failed to remove old forwarder, please manually remove it!": "바로가기 제거 실패함, 직접 제거해주세요!",
|
||||
@@ -257,12 +280,13 @@
|
||||
"Copy": "복사",
|
||||
"Copying ": "복사 중 ",
|
||||
"Paste": "붙여넣기",
|
||||
"Paste ": " ",
|
||||
" file(s)?": "개 항목을 붙여넣을까요?",
|
||||
"Paste file(s)?": "붙여넣을까요?",
|
||||
"Pasting ": "붙여넣는 중 ",
|
||||
"Pasting": "붙여넣기",
|
||||
"Pasting": "붙여넣는 중",
|
||||
"Rename": "이름 바꾸기",
|
||||
"Set New File Name": "새 파일명 입력",
|
||||
"Failed to delete directory": "폴더 삭제 실패",
|
||||
"Failed to delete file": "파일 삭제 실패",
|
||||
"Extract zip": "압축 해제",
|
||||
"Extract Options": "압축 해제 옵션",
|
||||
"Extract here": "여기에 풀기",
|
||||
@@ -285,13 +309,17 @@
|
||||
"Create Folder": "새 폴더",
|
||||
"Set Folder Name": "폴더명 입력",
|
||||
"Creating ": "생성 중 ",
|
||||
"View as text (unfinished)": "텍스트로 보기 (미완성)",
|
||||
"Upload": "업로드",
|
||||
"Select upload location": "업로드 위치 선택",
|
||||
"No upload locations set!": "업로드 위치가 설정되지 않았습니다!",
|
||||
"Uploading": "업로드 중 ",
|
||||
"Upload successfull!": "업로드 완료!",
|
||||
"Upload failed!": "업로드 실패!",
|
||||
"View as text (unfinished)": "텍스트로 보기 (미완성)",
|
||||
"Hash": "해시",
|
||||
"Hash Options": "해시 옵션",
|
||||
"Hashing": "해시화중",
|
||||
"Failed to hash file...": "해시화에 실패했습니다...",
|
||||
"Ignore read only": "읽기 전용 설정 무시",
|
||||
"Mount": "마운트",
|
||||
"Sd": "SD 카드",
|
||||
@@ -302,6 +330,7 @@
|
||||
"Launch ": "실행할까요 ",
|
||||
"Launch option for: ": "실행 옵션: ",
|
||||
"Select launcher for: ": "실행 런처: ",
|
||||
"Close FileBrowser?": "파일 탐색창을 닫을까요?",
|
||||
|
||||
"Sort By": "정렬",
|
||||
"Sort Options": "정렬 옵션",
|
||||
@@ -335,16 +364,16 @@
|
||||
"Search": "검색",
|
||||
|
||||
"Options": "설정",
|
||||
"Split": "화면 분할",
|
||||
"OK": "확인",
|
||||
"Back": "뒤로",
|
||||
"Select": "선택",
|
||||
"Open": "열기",
|
||||
"Close": "닫기",
|
||||
"Launch": "실행",
|
||||
"Restart": "재시작",
|
||||
"Next": "다음",
|
||||
"Prev": "이전",
|
||||
"Unstar": "즐겨찾기 해제",
|
||||
"Star": "즐겨찾기",
|
||||
"Yes": "예",
|
||||
"No": "아니요",
|
||||
"On": "켬",
|
||||
@@ -367,12 +396,13 @@
|
||||
"Remove": "제거",
|
||||
"Completely remove ": "정말 삭제할까요 ",
|
||||
"Removing ": "제거 중 ",
|
||||
"Uninstalling ": "설치 제거중 ",
|
||||
"Removed ": "제거됨: ",
|
||||
"Uninstalling ": "설치 제거됨: ",
|
||||
|
||||
"Download": "다운로드",
|
||||
"Downloading ": "다운로드 중 ",
|
||||
"Downloaded ": "다운로드 완료: ",
|
||||
"Download via the Network options!": "네트워크 옵션에서 다운로드했습니다!",
|
||||
|
||||
"Update": "업데이트",
|
||||
"Update avaliable: ": "업데이트 가능: ",
|
||||
@@ -389,4 +419,4 @@
|
||||
"Empty!": "찾을 수 없습니다!",
|
||||
"Not Ready...": "준비되지 않음...",
|
||||
"Error loading page!": "페이지 로딩 오류!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"No Internet": "Geen internet",
|
||||
"Switch-Handheld!": "",
|
||||
"Switch-Docked!": "",
|
||||
"Warning! Logs are enabled, Sphaira will run slowly!": "",
|
||||
"Audio disabled due to suspended game": "",
|
||||
"Are you sure you wish to cancel?": "",
|
||||
"An error occurred": "",
|
||||
@@ -27,6 +28,8 @@
|
||||
"Nxlink Connected": "",
|
||||
"Nxlink Upload": "",
|
||||
"Nxlink Finished": "",
|
||||
"Hdd": "",
|
||||
"Hdd write protect": "",
|
||||
|
||||
"Language": "Taal",
|
||||
"Auto": "",
|
||||
@@ -57,22 +60,35 @@
|
||||
"No meta entries found...\n": "",
|
||||
"Updating application record list": "",
|
||||
"Dump": "",
|
||||
"Dump options": "",
|
||||
"Dump Options": "",
|
||||
"Select content to dump": "",
|
||||
"Dump All": "",
|
||||
"Dump Application": "",
|
||||
"Dump Patch": "",
|
||||
"Dump AddOnContent": "",
|
||||
"Dump DataPatch": "",
|
||||
"Created nested folder": "",
|
||||
"Append folder with .xci": "",
|
||||
"Trim XCI": "",
|
||||
"Label trimmed XCI": "",
|
||||
"Multi-threaded USB transfer": "",
|
||||
"Dump All Bins": "",
|
||||
"Dump XCI": "",
|
||||
"Dump Card ID Set": "",
|
||||
"Dump Card UID": "",
|
||||
"Dump Certificate": "",
|
||||
"Dump Initial Data": "",
|
||||
"Select dump location": "",
|
||||
"microSD card (/dumps/NSP/)": "",
|
||||
"microSD card (/dumps/)": "",
|
||||
"USB transfer (Switch 2 Switch)": "",
|
||||
"/dev/null (Speed Test)": "",
|
||||
"Dumping": "",
|
||||
"Dump successfull!": "",
|
||||
"Dump failed!": "",
|
||||
"Success": "",
|
||||
"Delete successfull!": "",
|
||||
"Delete failed!": "",
|
||||
"Success": "",
|
||||
|
||||
"Themezer": "Themamaker",
|
||||
"Themezer Options": "",
|
||||
@@ -86,9 +102,10 @@
|
||||
"GitHub": "",
|
||||
"Downloading json": "",
|
||||
"Select asset to download for ": "",
|
||||
"Failed to download json": "",
|
||||
"Failed to download app!": "",
|
||||
|
||||
"FTP Install": "",
|
||||
"FTP Install (EXPERIMENTAL)": "",
|
||||
"Connection Type: WiFi | Strength: ": "",
|
||||
"Connection Type: Ethernet": "",
|
||||
"Connection Type: None": "",
|
||||
@@ -98,9 +115,9 @@
|
||||
"Password:": "",
|
||||
"SSID:": "",
|
||||
"Passphrase:": "",
|
||||
"Failed to install via FTP, press B to exit...": "",
|
||||
"Ftp install success!": "",
|
||||
"Ftp install failed!": "",
|
||||
"Failed to install, press B to exit...": "",
|
||||
"Install success!": "",
|
||||
"Install failed!": "",
|
||||
"USB Install": "",
|
||||
"USB": "",
|
||||
"Connected, waiting for file list...": "",
|
||||
@@ -122,9 +139,12 @@
|
||||
"GC": "",
|
||||
"System memory %.1f GB": "",
|
||||
"microSD card %.1f GB": "",
|
||||
"Nand Install": "",
|
||||
"SD Card Install": "",
|
||||
"Exit": "",
|
||||
"Install disabled...\nPlease enable installing via the install options.": "",
|
||||
"No GameCard inserted": "",
|
||||
"GameCard is already trimmed!": "",
|
||||
"WARNING: GameCard is already trimmed!": "",
|
||||
"Continue": "",
|
||||
"Gc install success!": "",
|
||||
"Gc install failed!": "",
|
||||
|
||||
@@ -160,14 +180,14 @@
|
||||
"Negative image": "Negatief beeld",
|
||||
"Format": "Formaat",
|
||||
"Trimming Format": "Trimformaat",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"External Light Filter": "Extern lichtfilter",
|
||||
"Load Default": "Standaard laden",
|
||||
|
||||
"Web": "",
|
||||
"Select URL": "",
|
||||
"Enter custom URL": "",
|
||||
"Enter URL": "",
|
||||
|
||||
"Advanced": "Geavanceerd",
|
||||
"Advanced Options": "Bestand maken",
|
||||
"Logging": "Loggen",
|
||||
@@ -181,10 +201,12 @@
|
||||
"Restored hbmenu": "",
|
||||
"Restart Sphaira?": "",
|
||||
"Press OK to restart Sphaira": "",
|
||||
"Boost CPU during transfer": "",
|
||||
"Text scroll speed": "",
|
||||
"Slow": "",
|
||||
"Normal": "",
|
||||
"Fast": "",
|
||||
"Set left-side menu": "",
|
||||
"Set right-side menu": "",
|
||||
"Install options": "",
|
||||
"Install Options": "",
|
||||
@@ -194,7 +216,6 @@
|
||||
"Install location": "",
|
||||
"System memory": "",
|
||||
"microSD card": "",
|
||||
"Boost CPU clock": "",
|
||||
"Allow downgrade": "",
|
||||
"Skip if already installed": "",
|
||||
"Ticket only": "",
|
||||
@@ -222,9 +243,11 @@
|
||||
"Creating Control": "",
|
||||
"Creating Meta": "",
|
||||
"Writing Nca": "",
|
||||
"Updating ncm databse": "",
|
||||
"Updating ncm database": "",
|
||||
"Pushing application record": "",
|
||||
"Failed to install forwarder": "",
|
||||
"Unstar": "",
|
||||
"Star": "",
|
||||
"Unstarred ": "",
|
||||
"Starred ": "",
|
||||
"Failed to remove old forwarder, please manually remove it!": "",
|
||||
@@ -257,12 +280,13 @@
|
||||
"Copy": "Kopiëren",
|
||||
"Copying ": "",
|
||||
"Paste": "",
|
||||
"Paste ": "",
|
||||
" file(s)?": "",
|
||||
"Paste file(s)?": "",
|
||||
"Pasting ": "",
|
||||
"Pasting": "",
|
||||
"Rename": "Hernoemen",
|
||||
"Set New File Name": "",
|
||||
"Failed to delete directory": "",
|
||||
"Failed to delete file": "",
|
||||
"Extract zip": "",
|
||||
"Extract Options": "",
|
||||
"Extract here": "",
|
||||
@@ -285,13 +309,17 @@
|
||||
"Create Folder": "Map maken",
|
||||
"Set Folder Name": "",
|
||||
"Creating ": "",
|
||||
"View as text (unfinished)": "Bekijk als tekst (onvoltooid)",
|
||||
"Upload": "",
|
||||
"Select upload location": "",
|
||||
"No upload locations set!": "",
|
||||
"Uploading": "",
|
||||
"Upload successfull!": "",
|
||||
"Upload failed!": "",
|
||||
"View as text (unfinished)": "Bekijk als tekst (onvoltooid)",
|
||||
"Hash": "",
|
||||
"Hash Options": "",
|
||||
"Hashing": "",
|
||||
"Failed to hash file...": "",
|
||||
"Ignore read only": "",
|
||||
"Mount": "",
|
||||
"Sd": "",
|
||||
@@ -302,6 +330,7 @@
|
||||
"Launch ": "",
|
||||
"Launch option for: ": "",
|
||||
"Select launcher for: ": "",
|
||||
"Close FileBrowser?": "",
|
||||
|
||||
"Sort By": "Sorteer op",
|
||||
"Sort Options": "Sorteeropties",
|
||||
@@ -335,16 +364,16 @@
|
||||
"Search": "Zoekopdracht",
|
||||
|
||||
"Options": "Opties",
|
||||
"Split": "",
|
||||
"OK": "",
|
||||
"Back": "Terug",
|
||||
"Select": "",
|
||||
"Open": "Open",
|
||||
"Close": "",
|
||||
"Launch": "Launch",
|
||||
"Restart": "",
|
||||
"Next": "",
|
||||
"Prev": "",
|
||||
"Unstar": "",
|
||||
"Star": "",
|
||||
"Yes": "Ja",
|
||||
"No": "Nee",
|
||||
"On": "",
|
||||
@@ -367,12 +396,13 @@
|
||||
"Remove": "",
|
||||
"Completely remove ": "",
|
||||
"Removing ": "",
|
||||
"Removed ": "",
|
||||
"Uninstalling ": "",
|
||||
"Removed ": "",
|
||||
|
||||
"Download": "Downloaden",
|
||||
"Downloading ": "",
|
||||
"Downloaded ": "",
|
||||
"Download via the Network options!": "",
|
||||
|
||||
"Update": "",
|
||||
"Update avaliable: ": "",
|
||||
@@ -389,4 +419,4 @@
|
||||
"Empty!": "",
|
||||
"Not Ready...": "",
|
||||
"Error loading page!": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
"No Internet": "Sem internet",
|
||||
"Switch-Handheld!": "Switch-Portátil",
|
||||
"Switch-Docked!": "Switch-Docado",
|
||||
"Warning! Logs are enabled, Sphaira will run slowly!": "AVISO: O registro de depuração está habilitado, o sphaira ficará lento!",
|
||||
"Audio disabled due to suspended game": "Áudio desativado devido ao software suspenso.",
|
||||
"Are you sure you wish to cancel?": "Tem certeza de que quer cancelar?",
|
||||
"An error occurred": "Ocorreu um erro.",
|
||||
"If this message appears repeatedly, please open an issue.": "Se esta mensagem aparecer repetidamente, abra um issue.",
|
||||
|
||||
"Menu Options": "Opções do menu",
|
||||
"Menu Options": "Menu",
|
||||
"Menu": "Menu",
|
||||
"Theme": "Tema",
|
||||
"Theme Options": "Opções de tema",
|
||||
"Theme Options": "Tema",
|
||||
"Select Theme": "Estilo",
|
||||
"Music": "Música",
|
||||
"12 Hour Time": "Relógio de 12 horas",
|
||||
@@ -20,7 +21,7 @@
|
||||
"Overwrite current default music?": "Substituir a música padrão atual?",
|
||||
|
||||
"Network": "Rede",
|
||||
"Network Options": "Opções de rede",
|
||||
"Network Options": "Rede",
|
||||
"Ftp": "Servidor FTP",
|
||||
"Mtp": "Escuta MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
@@ -48,23 +49,37 @@
|
||||
"Ukrainian": "Українська",
|
||||
|
||||
"Misc": "Diversos",
|
||||
"Misc Options": "Opções diversas",
|
||||
"Misc Options": "Diversos",
|
||||
|
||||
"Games": "Softwares",
|
||||
"Game Options": "Opções de software",
|
||||
"Game Options": "Opções do software",
|
||||
"Hide forwarders": "Ocultar atalhos forwarder",
|
||||
"Launch random game": "Iniciar um software aleatório",
|
||||
"List meta records": "Registro de dados",
|
||||
"List meta records": "Lista de registros",
|
||||
"Entries": "Entradas",
|
||||
"Failed to list application meta entries": "Falha ao listar as meta entradas do aplicativo.",
|
||||
"Failed to list application meta entries": "Falha ao listar as meta entradas do software.",
|
||||
"No meta entries found...\n": "Nenhuma entrada de registros encontrada...\n",
|
||||
"Updating application record list": "Atualizando a lista de registros do software...",
|
||||
"Dump": "Exportar dados",
|
||||
"Select content to dump": "Exportação de dados",
|
||||
"Dump": "Exportar software",
|
||||
"Dump options": "Opções de exportação",
|
||||
"Dump Options": "Opções de exportação",
|
||||
"Select content to dump": "Exportação de software",
|
||||
"Dump All": "Exportar tudo",
|
||||
"Dump Application": "Exportar software base",
|
||||
"Dump Patch": "Exportar atualização",
|
||||
"Dump AddOnContent": "Exportar DLCs",
|
||||
"Dump DataPatch": "Exportar atualização de DLCs",
|
||||
"Created nested folder": "Criar pastas aninhadas",
|
||||
"Append folder with .xci": "Acrescentar .xci à pasta",
|
||||
"Trim XCI": "Aparar arquivos .xci",
|
||||
"Label trimmed XCI": "Rotular .xci aparados",
|
||||
"Multi-threaded USB transfer": "Transferências em multithread",
|
||||
"Dump All Bins": "Exportar todos os binários",
|
||||
"Dump XCI": "Exportar .xci",
|
||||
"Dump Card ID Set": "Exportar conjunto de IDs",
|
||||
"Dump Card UID": "Exportar UID",
|
||||
"Dump Certificate": "Exportar certificado",
|
||||
"Dump Initial Data": "Exportar dados iniciais",
|
||||
"Select dump location": "Selecione o local de exportação",
|
||||
"microSD card (/dumps/)": "Cartão microSD (/dumps/)",
|
||||
"USB transfer (Switch 2 Switch)": "Transferência via USB (Switch 2 Switch)",
|
||||
@@ -72,9 +87,36 @@
|
||||
"Dumping": "Exportando...",
|
||||
"Dump successfull!": "Exportação concluída.",
|
||||
"Dump failed!": "Exportação falhou.",
|
||||
"Success": "Concluído.",
|
||||
"Delete successfull!": "Remoção concluída.",
|
||||
"Delete failed!": "Remoção falhou.",
|
||||
"Success": "Concluído.",
|
||||
"Title cache": "Usar cache de títulos",
|
||||
|
||||
"Saves": "Dados salvos",
|
||||
"Save Options": "Opções de dados salvos",
|
||||
"Account": "Usuário",
|
||||
"Data Type": "Tipo de dados",
|
||||
"System": "Sistema",
|
||||
"BCAT": "BCAT",
|
||||
"Device": "Dispositivo",
|
||||
"Temporary": "Temporário",
|
||||
"Cache": "Cache",
|
||||
"System BCAT": "BCAT (Sistema)",
|
||||
"Backup": "Backup",
|
||||
"Auto backup": "Backup automático",
|
||||
"Auto backup on restore": "Fazer backup ao restaurar",
|
||||
"Compress backup": "Comprimir backups",
|
||||
"Are you sure you want to backup save(s)?": "Tem certeza de que quer fazer backup dos dados salvo?",
|
||||
"No saves found in ": "Nenhum dado salvo encontrado em ",
|
||||
"Backed up to ": "Backup salvo em ",
|
||||
"Backup successfull!": "Backup concluído.",
|
||||
"Backup failed!": "Backup falhou.",
|
||||
"Select backup location": "Selecione o local de backup",
|
||||
"Select restore location": "Selecione o local de restauração",
|
||||
"Restore save for: ": "Restaurar backup de dados salvo para: ",
|
||||
"Are you sure you want to restore ": "Tem certeza de que quer restaurar os dados salvo de ",
|
||||
"Restore successfull!": "Restauração concluída.",
|
||||
"Restore failed!": "Restauração falhou.",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Opções do Themezer",
|
||||
@@ -88,11 +130,12 @@
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Baixando JSON",
|
||||
"Select asset to download for ": "Selecione o recurso para baixar de ",
|
||||
"Failed to download json": "Falha ao baixar json.",
|
||||
"Failed to download app!": "falha ao baixar aplicativo.",
|
||||
|
||||
"FTP Install": "Instalação via FTP",
|
||||
"FTP Install (EXPERIMENTAL)": "Instalação via FTP (EXPERIMENTAL)",
|
||||
"Connection Type: WiFi | Strength: ": "Conexão por rede Wi-Fi | Intensidade do sinal: ",
|
||||
"Connection Type: Ethernet": "Conexão por cabo (ethernet)",
|
||||
"Connection Type: Ethernet": "Conexão por cabo Ethernet",
|
||||
"Connection Type: None": "Sem conexão",
|
||||
"Host:": "Host:",
|
||||
"Port:": "Porta:",
|
||||
@@ -100,33 +143,53 @@
|
||||
"Password:": "Senha:",
|
||||
"SSID:": "SSID:",
|
||||
"Passphrase:": "Senha:",
|
||||
"Failed to install via FTP, press B to exit...": "Falha ao instalar via FTP, aperte B para sair.",
|
||||
"Ftp install success!": "Instalação via FTP concluída.",
|
||||
"Ftp install failed!": "Instalação via FTP falhou.",
|
||||
"Failed to install, press B to exit...": "Falha ao instalar, aperte para sair.",
|
||||
"Install success!": "Instalação concluída.",
|
||||
"Install failed!": "Instalação falhou.",
|
||||
"MTP Install": "Instalação via MTP",
|
||||
"State: %s | Speed: %s": "Estado: %s | Velocidade: %s",
|
||||
"Detached": "Desconectado",
|
||||
"Attached": "Conectado",
|
||||
"Powered": "Energia",
|
||||
"Default": "Padrão",
|
||||
"Address": "Endereço",
|
||||
"Configured": "Configurado",
|
||||
"Suspended": "Suspenso",
|
||||
"USB 1.0 Low Speed": "USB 1.0",
|
||||
"USB 1.1 Full Speed": "USB 1.1 (Full Speed)",
|
||||
"USB 2.0 High Speed": "USB 2.0 (High Speed)",
|
||||
"USB 3.0 Super Speed": "USB 3.0 (SuperSpeed)",
|
||||
"Drag'n'Drop (NSP, XCI, NSZ, XCZ) to the install folder": "Arraste o(s) arquivo(s) .nsp/.xci/.nsz/.xcz para a pasta \"Install\".",
|
||||
"Failed to install via MTP, press B to exit...": "Falha ao instalar via MTP, aperte para sair.",
|
||||
"MTP install success!": "Instalação via MTP concluída.",
|
||||
"MTP install failed!": "Instalação via MTP falhou.",
|
||||
"USB Install": "Instalação via USB",
|
||||
"USB": "USB",
|
||||
"Connected, waiting for file list...": "Conectado, aguardando lista de arquivos...",
|
||||
"Connected, starting transfer...": "Conectado, iniciando transferência...",
|
||||
"Failed to init usb, press B to exit...": "Falha ao instalar via USB,\naperte B para sair.",
|
||||
"Failed to init usb, press B to exit...": "Falha ao inicializar USB,\naperte para sair.",
|
||||
"Waiting for connection...": "Aguardando conexão...",
|
||||
"Transferring data...": "Transferindo dados...",
|
||||
"USB connected, sending file list": "USB conectado, enviando lista de arquivos",
|
||||
"Sent file list, waiting for command...": "Lista de arquivos enviada, aguardando comando...",
|
||||
"waiting for usb connection...": "Aguardando conexão USB...",
|
||||
"Disable MTP for usb install": "Escuta MTP desabilitada temporáriamente.",
|
||||
"Disable MTP for usb install": "Escuta MTP desabilitada temporariamente.",
|
||||
"Re-enabled MTP": "Escuta MTP reabilitada.",
|
||||
"Installed via usb": "Instalado via USB",
|
||||
"Usb install success!": "Instalação via USB concluída.",
|
||||
"Usb install failed!": "Instalação via USB falhou.",
|
||||
"Press B to exit...": "Aperte B para sair.",
|
||||
"Press B to exit...": "Aperte para sair.",
|
||||
"GameCard Install": "Instalação de cartão de jogo",
|
||||
"GameCard": "Cartão de jogo",
|
||||
"GC": "Cartão de jogo",
|
||||
"System memory %.1f GB": "Memória do console %.1f GB",
|
||||
"microSD card %.1f GB": "Cartão microSD %.1f GB",
|
||||
"Nand Install": "Instalar na memória do console",
|
||||
"SD Card Install": "Instalar no cartão microSD",
|
||||
"Exit": "Sair",
|
||||
"Install disabled...\nPlease enable installing via the install options.": "Instalação desabilitada.\nHabilite a função através das opções de instalação.",
|
||||
"No GameCard inserted": "Nenhum cartão de jogo inserido.",
|
||||
"GameCard is already trimmed!": "O cartão de jogo já está aparado.",
|
||||
"WARNING: GameCard is already trimmed!": "AVISO: O cartão de jogo já está aparado.",
|
||||
"Continue": "Continuar",
|
||||
"Gc install success!": "Instalação de cartão de jogo concluída.",
|
||||
"Gc install failed!": "Instalação de cartão de jogo falhou.",
|
||||
|
||||
@@ -162,22 +225,22 @@
|
||||
"Negative image": "Imagem negativa",
|
||||
"Format": "Formato",
|
||||
"Trimming Format": "Formato do recorte",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"External Light Filter": "Filtro de luz externa",
|
||||
"Load Default": "Restaurar padrão",
|
||||
|
||||
"Advanced": "Avançados",
|
||||
"Web": "Navegador de internet",
|
||||
"Select URL": "Selecione uma URL",
|
||||
"Enter custom URL": "Digitar URL",
|
||||
"Enter URL": "Digitar URL",
|
||||
|
||||
"Advanced": "Opções avançadas",
|
||||
"Advanced Options": "Opções avançadas",
|
||||
"Logging": "Registro de depuração",
|
||||
"Logging": "Registros de depuração",
|
||||
"Replace hbmenu on exit": "Substituir hbmenu ao sair",
|
||||
"Restore hbmenu?": "Restaurar hbmenu?",
|
||||
"Restore": "Restaurar",
|
||||
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Falha ao buscar /switch/hbmenu.nro\nUse a loja (AppStore) para reinstalar o hbmenu.",
|
||||
"Failed to restore hbmenu, please re-download hbmenu": "Falha ao restaurar o hbmenu, tente baixa-lo novamente.",
|
||||
"Failed to restore hbmenu, please re-download hbmenu": "Falha ao restaurar hbmenu, tente baixa-lo novamente.",
|
||||
"Failed to restore hbmenu, using sphaira instead": "Falha ao restaurar hbmenu, usando sphaira no seu lugar.",
|
||||
"Restored hbmenu, closing sphaira": "hbmenu restaurado, fechando sphaira.",
|
||||
"Restored hbmenu": "hbmenu restaurado.",
|
||||
@@ -192,13 +255,12 @@
|
||||
"Set right-side menu": "Menu do botão R",
|
||||
"Install options": "Opções de instalação",
|
||||
"Install Options": "Opções de instalação",
|
||||
"Enable sysmmc": "Habilitar sysMMC",
|
||||
"Enable emummc": "Habilitar emuMMC",
|
||||
"Enable sysmmc": "Habilitar em sysMMC",
|
||||
"Enable emummc": "Habilitar em emuMMC",
|
||||
"Show install warning": "Mostrar aviso de instalação",
|
||||
"Install location": "Local de instalação",
|
||||
"System memory": "Memória do console",
|
||||
"microSD card": "Cartão microSD",
|
||||
"Boost CPU clock": "Impulsionar CPU",
|
||||
"Allow downgrade": "Permitir downgrade",
|
||||
"Skip if already installed": "Pular se já instalado",
|
||||
"Ticket only": "Instalar apenas tickets",
|
||||
@@ -214,18 +276,11 @@
|
||||
"Convert to standard crypto": "Converter para crypto padrão",
|
||||
"Lower master key": "Reduzir master keys",
|
||||
"Lower system version": "Reduzir versão do sistema",
|
||||
"Dump options": "Opções de exportação",
|
||||
"Dump Options": "Opções de exportação",
|
||||
"Created nested folder": "Criar pastas aninhadas",
|
||||
"Append folder with .xci": "Acrescentar .xci à pasta",
|
||||
"Trim XCI": "Aparar arquivos .xci",
|
||||
"Label trimmed XCI": "Rotular .xci aparados",
|
||||
"Multi-threaded USB transfer": "Trasfêrencia em multithread",
|
||||
|
||||
"Homebrew": "Aplicativos",
|
||||
"Apps": "Aplicativos",
|
||||
"Homebrew Options": "Opções de aplicativo",
|
||||
"Hide Sphaira": "Esconder sphaira",
|
||||
"Homebrew": "Homebrews",
|
||||
"Apps": "Homebrews",
|
||||
"Homebrew Options": "Opções do homebrew",
|
||||
"Hide Sphaira": "Esconder sphaira da lista",
|
||||
"Install Forwarder": "Instalar atalho forwarder",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Instalar atalhos forwarder pode resultar em um banimento!",
|
||||
"Installing Forwarder": "Instalando atalho forwarder...",
|
||||
@@ -233,9 +288,11 @@
|
||||
"Creating Control": "Criando 'Control'",
|
||||
"Creating Meta": "Criando 'Meta'",
|
||||
"Writing Nca": "Gravando NCA",
|
||||
"Updating ncm databse": "Atualizando base de dados NCM",
|
||||
"Updating ncm database": "Atualizando base de dados NCM",
|
||||
"Pushing application record": "Aplicando registro do software",
|
||||
"Failed to install forwarder": "Falha ao instalar atalho forwarder.",
|
||||
"Unstar": "Desfavoritar",
|
||||
"Star": "Favoritar",
|
||||
"Unstarred ": "Desfavoritado ",
|
||||
"Starred ": "Favoritado ",
|
||||
"Failed to remove old forwarder, please manually remove it!": "Falha ao desinstalar atalho forwarder, tente remove-lo manualmente.",
|
||||
@@ -248,11 +305,11 @@
|
||||
"Info": "Informações",
|
||||
"Changelog": "Alterações",
|
||||
"Details": "Detalhes",
|
||||
"version: %s": "versão: %s",
|
||||
"updated: %s": "atualizado: %s",
|
||||
"category: %s": "categoria: %s",
|
||||
"extracted: %.2f MiB": "tam. extraído: %.2f MiB",
|
||||
"app_dls: %s": "downloads: %s",
|
||||
"version: %s": "Versão: %s",
|
||||
"updated: %s": "Atualizado: %s",
|
||||
"category: %s": "Categoria: %s",
|
||||
"extracted: %.2f MiB": "Tamanho: %.2f MiB",
|
||||
"app_dls: %s": "Nº de downloads: %s",
|
||||
"More by Author": "Mais deste autor",
|
||||
"Leave Feedback": "Deixar um feedback",
|
||||
|
||||
@@ -260,23 +317,23 @@
|
||||
"Files": "Arquivos",
|
||||
"%zd files": "%zd arquivo(s)",
|
||||
"%zd dirs": "%zd diretório(s)",
|
||||
"File Options": "Opções de arquivo",
|
||||
"Show Hidden": "Mostrar ocultos",
|
||||
"Folders First": "Pastas primeiro",
|
||||
"Hidden Last": "Ocultos por último",
|
||||
"File Options": "Opções do arquivo",
|
||||
"Show Hidden": "Mostrar arquivos ocultos",
|
||||
"Folders First": "Ordenar pastas primeiro",
|
||||
"Hidden Last": "Ordenar ocultos por último",
|
||||
"Split": "Dividir",
|
||||
"Close": "Fechar",
|
||||
"Cut": "Recortar",
|
||||
"Copy": "Copiar",
|
||||
"Copying ": "Copiando ",
|
||||
"Paste": "Colar",
|
||||
"Paste ": "Colar ",
|
||||
" file(s)?": " arquivo(s)?",
|
||||
"Paste file(s)?": "Colar arquivo(s)?",
|
||||
"Pasting ": "Colando ",
|
||||
"Pasting": "Colando ",
|
||||
"Rename": "Renomear",
|
||||
"Set New File Name": "Defina o nome do novo arquivo",
|
||||
"Extract zip": "Extrair zip",
|
||||
"Failed to delete directory": "Falha ao apagar diretório.",
|
||||
"Failed to delete file": "Falha ao apagar arquivo.",
|
||||
"Extract zip": "Extrair .zip",
|
||||
"Extract Options": "Opções de extração",
|
||||
"Extract here": "Extrair aqui",
|
||||
"Extract to root": "Extrair para a raiz",
|
||||
@@ -286,8 +343,8 @@
|
||||
"Extracting ": "Extraindo ",
|
||||
"Extract success!": "Extração concluída.",
|
||||
"Extract failed!": "Extração falhou.",
|
||||
"Compress to zip": "Comprimir em zip",
|
||||
"Compress Options": "Opções de compressão",
|
||||
"Compress to zip": "Comprimir em .zip",
|
||||
"Compress Options": "Comprimir em .zip",
|
||||
"Compress": "Comprimir",
|
||||
"Compress to...": "Comprimir para...",
|
||||
"Compressing ": "Comprimindo ",
|
||||
@@ -298,29 +355,31 @@
|
||||
"Create Folder": "Criar pasta",
|
||||
"Set Folder Name": "Defina o nome da pasta",
|
||||
"Creating ": "Criando ",
|
||||
"View as text (unfinished)": "Ver como texto (inacabado)",
|
||||
"Upload": "Enviar",
|
||||
"Select upload location": "Selecione o local de envio",
|
||||
"No upload locations set!": "Nenhum local de envio definido.",
|
||||
"Uploading": "Enviando",
|
||||
"Upload successfull!": "Envio concluído.",
|
||||
"Upload failed!": "Envio falhou.",
|
||||
"Hash": "Hash",
|
||||
"Hash Options": "Opções de hash",
|
||||
"View as text (unfinished)": "Ver como texto (inacabado)",
|
||||
"Hash": "Calcular hash",
|
||||
"Hash Options": "Calcular hash",
|
||||
"Hashing": "Calculando hash...",
|
||||
"Failed to hash file...": "Falha ao calcular hash do arquivo.",
|
||||
"Ignore read only": "Ignorar modo somente leitura",
|
||||
"Mount": "Montar",
|
||||
"Sd": "SD",
|
||||
"Image System memory": "Imagem (memória do console)",
|
||||
"Image microSD card": "Imagem (cartão microSD)",
|
||||
"Empty...": "Vazio",
|
||||
"Empty...": "Vazio.",
|
||||
"Open with DayBreak?": "Abrir com DayBreak?",
|
||||
"Launch ": "Iniciar ",
|
||||
"Launch option for: ": "Opções de inicialização para: ",
|
||||
"Select launcher for: ": "Selecionar inicializador para: ",
|
||||
"Close FileBrowser?": "Fechar o gerenciador de arquivos?",
|
||||
"Close FileBrowser?": "Fechar gerenciador de arquivos?",
|
||||
|
||||
"Sort By": "Ordernar",
|
||||
"Sort Options": "Opções de ordenação",
|
||||
"Sort By": "Ordenar",
|
||||
"Sort Options": "Ordenar",
|
||||
"Filter": "Filtro",
|
||||
"All": "Todos",
|
||||
"Emulators": "Emuladores",
|
||||
@@ -351,16 +410,16 @@
|
||||
"Search": "Buscar",
|
||||
|
||||
"Options": "Opções",
|
||||
"Split": "Dividir",
|
||||
"OK": "OK",
|
||||
"Back": "Voltar",
|
||||
"Select": "Selecionar",
|
||||
"Open": "Abrir",
|
||||
"Close": "Fechar",
|
||||
"Launch": "Iniciar",
|
||||
"Restart": "Reiniciar",
|
||||
"Next": "Prómixo",
|
||||
"Prev": "Anterior",
|
||||
"Unstar": "Desfavoritar",
|
||||
"Star": "Favoritar",
|
||||
"Yes": "Sim",
|
||||
"No": "Não",
|
||||
"On": "Sim",
|
||||
@@ -383,12 +442,13 @@
|
||||
"Remove": "Remover",
|
||||
"Completely remove ": "Remover completamente ",
|
||||
"Removing ": "Removendo ",
|
||||
"Removed ": "Removido ",
|
||||
"Uninstalling ": "Desinstalando ",
|
||||
"Removed ": "Removido ",
|
||||
|
||||
"Download": "Baixar",
|
||||
"Downloading ": "Baixando ",
|
||||
"Downloaded ": "Baixado ",
|
||||
"Download via the Network options!": "Baixe através das opções de rede.",
|
||||
|
||||
"Update": "Atualizar",
|
||||
"Update avaliable: ": "Atualização disponível: ",
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
"No Internet": "Нет интернета",
|
||||
"Switch-Handheld!": "Режим Портатива",
|
||||
"Switch-Docked!": "Режим Дока",
|
||||
"Warning! Logs are enabled, Sphaira will run slowly!": "Внимание! Включены логи, Sphaira будет работать медленно!",
|
||||
"Audio disabled due to suspended game": "Звук отключён из-за приостановки игры",
|
||||
"Are you sure you wish to cancel?": "Вы уверены, что хотите отменить?",
|
||||
"An error occurred": "Произошла ошибка",
|
||||
"If this message appears repeatedly, please open an issue.": "Если ошибка появляется неоднократно, пожалуйста, откройте issue",
|
||||
|
||||
"Menu Options": "Параметры меню",
|
||||
"Menu Options": "Опции меню",
|
||||
"Menu": "Меню",
|
||||
"Theme": "Тема",
|
||||
"Theme Options": "Параметры темы",
|
||||
"Theme Options": "Опции темы",
|
||||
"Select Theme": "Выбор темы",
|
||||
"Music": "Музыка",
|
||||
"12 Hour Time": "12-часовое время",
|
||||
@@ -20,13 +21,15 @@
|
||||
"Overwrite current default music?": "Переписать текущую стандартную музыку?",
|
||||
|
||||
"Network": "Сеть",
|
||||
"Network Options": "Параметры сети",
|
||||
"Network Options": "Опции сети",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "NXLink",
|
||||
"Nxlink Connected": "NXLink подключён",
|
||||
"Nxlink Upload": "NXLink отправка",
|
||||
"Nxlink Finished": "NXLink завершено",
|
||||
"Hdd": "HDD",
|
||||
"Hdd write protect": "Защита HDD от записи",
|
||||
|
||||
"Language": "Язык",
|
||||
"Auto": "Авто",
|
||||
@@ -46,9 +49,9 @@
|
||||
"Ukrainian": "Українська",
|
||||
|
||||
"Misc": "Прочее",
|
||||
"Misc Options": "Прочие параметры",
|
||||
"Misc Options": "Прочие опции",
|
||||
"Games": "Игры",
|
||||
"Game Options": "Параметры игр",
|
||||
"Game Options": "Опции игр",
|
||||
"Hide forwarders": "Скрыть форвардеры",
|
||||
"Launch random game": "Запустить случайную игру",
|
||||
"List meta records": "Список мета-записей",
|
||||
@@ -57,25 +60,56 @@
|
||||
"No meta entries found...\n": "Мета-записей не найдено...\n",
|
||||
"Updating application record list": "Обновление списка записей приложений",
|
||||
"Dump": "Дамп",
|
||||
"Dump options": "Опции дампа",
|
||||
"Dump Options": "Опции дампа",
|
||||
"Select content to dump": "Выберите содержимое для дампа",
|
||||
"Dump All": "Дамп всего",
|
||||
"Dump Application": "Дамп приложения",
|
||||
"Dump Patch": "Дамп патча",
|
||||
"Dump AddOnContent": "Дамп дополнительного контента",
|
||||
"Dump DataPatch": "Дамп патча данных",
|
||||
"Created nested folder": "Создать вложенную папку",
|
||||
"Append folder with .xci": "Добавить папку с .xci",
|
||||
"Trim XCI": "Обрезать .xci",
|
||||
"Label trimmed XCI": "Пометить обрезанный .xci",
|
||||
"Multi-threaded USB transfer": "Многоядерная USB передача",
|
||||
"Dump All Bins": "Дамп всех BIN-файлов",
|
||||
"Dump XCI": "Дамп XCI",
|
||||
"Dump Card ID Set": "Дамп набора ID карты",
|
||||
"Dump Card UID": "Дамп UID карты",
|
||||
"Dump Certificate": "Дамп сертификата",
|
||||
"Dump Initial Data": "Дамп начальных данных",
|
||||
"Select dump location": "Выберите место для дампа",
|
||||
"microSD card (/dumps/NSP/)": "microSD карта (/dumps/NSP/)",
|
||||
"microSD card (/dumps/)": "microSD карта (/dumps/)",
|
||||
"USB transfer (Switch 2 Switch)": "Передача по USB (Switch 2 Switch)",
|
||||
"/dev/null (Speed Test)": "/dev/null (тест скорости)",
|
||||
"Dumping": "Снятие дампа",
|
||||
"Dump successfull!": "Дамп выполнен успешно!",
|
||||
"Dump failed!": "Сбой дампа!",
|
||||
"Success": "Успех",
|
||||
"Delete successfull!": "Удаление успешно!",
|
||||
"Delete failed!": "Ошибка удаления!",
|
||||
"Success": "Успех",
|
||||
"Title cache": "Кэш заголовков",
|
||||
|
||||
"Saves": "Сохранения",
|
||||
"Save Options": "Опции сохранений",
|
||||
"Account": "Пользователь",
|
||||
"Backup": "Сделать бэкап",
|
||||
"Auto backup": "Автоматический бэкап",
|
||||
"Auto backup on restore": "Бэкап при восстановлении",
|
||||
"Compress backup": "Сжимать бэкапы",
|
||||
"Are you sure you want to backup save(s)?": "Вы уверены, что хотите сделать бэкап(ы)?",
|
||||
"No saves found in ": "Сохранения не найдены в ",
|
||||
"Backed up to ": "Бэкап сохранен в ",
|
||||
"Backup successfull!": "Бэкап успешно создан!",
|
||||
"Backup failed!": "Ошибка бэкапа!",
|
||||
"Restore save for: ": "Восстановить сохранение для: ",
|
||||
"Are you sure you want to restore ": "Вы уверены, что хотите восстановить ",
|
||||
"Restore successfull!": "Восстановление успешно!",
|
||||
"Restore failed!": "Ошибка восстановления!",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Параметры Themezer",
|
||||
"Themezer Options": "Опции Themezer",
|
||||
"Nsfw": "NSFW",
|
||||
"Page": "Страница",
|
||||
"Page %zu / %zu": "Страница %zu / %zu",
|
||||
@@ -86,9 +120,10 @@
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Загрузка json",
|
||||
"Select asset to download for ": "Выберите ресурс для загрузки: ",
|
||||
"Failed to download json": "Не удалось загрузить json",
|
||||
"Failed to download app!": "Не удалось загрузить приложение!",
|
||||
|
||||
"FTP Install": "Установка по FTP",
|
||||
"FTP Install (EXPERIMENTAL)": "Установка по FTP (ЭКСПЕРИМЕНТАЛЬНО)",
|
||||
"Connection Type: WiFi | Strength: ": "Тип подключения: WiFi | Сигнал: ",
|
||||
"Connection Type: Ethernet": "Тип подключения: Ethernet",
|
||||
"Connection Type: None": "Нет подключения",
|
||||
@@ -98,9 +133,9 @@
|
||||
"Password:": "Пароль:",
|
||||
"SSID:": "SSID:",
|
||||
"Passphrase:": "Пароль:",
|
||||
"Failed to install via FTP, press B to exit...": "Не удалось установить по FTP, нажмите B для выхода...",
|
||||
"Ftp install success!": "Установка по FTP прошла успешно!",
|
||||
"Ftp install failed!": "Сбой установки по FTP!",
|
||||
"Failed to install, press B to exit...": "Не удалось установить по FTP, нажмите B для выхода...",
|
||||
"Install success!": "Установка по FTP прошла успешно!",
|
||||
"Install failed!": "Сбой установки по FTP!",
|
||||
"USB Install": "Установка по USB",
|
||||
"USB": "USB",
|
||||
"Connected, waiting for file list...": "Подключено, ожидание списка файлов...",
|
||||
@@ -122,9 +157,12 @@
|
||||
"GC": "GC",
|
||||
"System memory %.1f GB": "Память системы %.1f ГБ",
|
||||
"microSD card %.1f GB": "Карта microSD %.1f ГБ",
|
||||
"Nand Install": "Установка в NAND",
|
||||
"SD Card Install": "Установка на SD карту",
|
||||
"Exit": "Выход",
|
||||
"Install disabled...\nPlease enable installing via the install options.": "Установка отключена...\nПожалуйста, включите установку в опциях установки.",
|
||||
"No GameCard inserted": "Картридж не вставлен",
|
||||
"GameCard is already trimmed!": "Картридж уже обрезан!",
|
||||
"WARNING: GameCard is already trimmed!": "ВНИМАНИЕ: Картридж уже обрезан!",
|
||||
"Continue": "Продолжить",
|
||||
"Gc install success!": "Установка с картриджа успешна!",
|
||||
"Gc install failed!": "Сбой установки с картриджа!",
|
||||
|
||||
@@ -160,17 +198,17 @@
|
||||
"Negative image": "Включен",
|
||||
"Format": "Разрешение",
|
||||
"Trimming Format": "Обрезка",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"External Light Filter": "Внешний светофильтр",
|
||||
"Load Default": "По умолчанию",
|
||||
|
||||
"Web": "Браузер",
|
||||
"Select URL": "Выберите URL",
|
||||
"Enter custom URL": "Введите свой URL",
|
||||
"Enter URL": "Введите URL",
|
||||
|
||||
"Advanced": "Продвинутые",
|
||||
"Advanced Options": "Расширенные параметры",
|
||||
"Logging": "Журналирование",
|
||||
"Advanced Options": "Расширенные опции",
|
||||
"Logging": "Логи",
|
||||
"Replace hbmenu on exit": "Замена hbmenu при выходе",
|
||||
"Restore hbmenu?": "Восстановить hbmenu?",
|
||||
"Restore": "Восстановить",
|
||||
@@ -181,20 +219,21 @@
|
||||
"Restored hbmenu": "hbmenu восстановлен",
|
||||
"Restart Sphaira?": "Перезапустить Sphaira?",
|
||||
"Press OK to restart Sphaira": "Нажмите OK для перезапуска Sphaira",
|
||||
"Boost CPU during transfer": "Разгон CPU при передаче",
|
||||
"Text scroll speed": "Скорость текста",
|
||||
"Slow": "Медленная",
|
||||
"Normal": "Обычная",
|
||||
"Fast": "Быстрая",
|
||||
"Set left-side menu": "Меню слева",
|
||||
"Set right-side menu": "Меню справа",
|
||||
"Install options": "Параметры установки",
|
||||
"Install Options": "Параметры установки",
|
||||
"Install options": "Опции установки",
|
||||
"Install Options": "Опции установки",
|
||||
"Enable sysmmc": "Включить на сиснанде",
|
||||
"Enable emummc": "Включить на эмунанде",
|
||||
"Show install warning": "Предупрежд. при установке",
|
||||
"Install location": "Место установки",
|
||||
"System memory": "NAND",
|
||||
"microSD card": "microSD",
|
||||
"Boost CPU clock": "Использовать разгон CPU",
|
||||
"Allow downgrade": "Разрешить даунгрейд",
|
||||
"Skip if already installed": "Пропуск установленного",
|
||||
"Ticket only": "Только тикет",
|
||||
@@ -203,7 +242,7 @@
|
||||
"Skip dlc": "Пропустить DLC",
|
||||
"Skip data patch": "Пропустить патч данных",
|
||||
"Skip ticket": "Пропустить тикет",
|
||||
"skip NCA hash verify": "Не проверять NCA hash",
|
||||
"Skip NCA hash verify": "Не проверять NCA hash",
|
||||
"Skip RSA header verify": "Не проверять RSA header",
|
||||
"Skip RSA NPDM verify": "Не проверять RSA NPDM",
|
||||
"Ignore distribution bit": "Игнор. бит распределения",
|
||||
@@ -213,18 +252,20 @@
|
||||
|
||||
"Homebrew": "Homebrew",
|
||||
"Apps": "Приложения",
|
||||
"Homebrew Options": "Параметры Homebrew",
|
||||
"Homebrew Options": "Опции Homebrew",
|
||||
"Hide Sphaira": "Скрыть Sphaira",
|
||||
"Install Forwarder": "Установить форвардер",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "ВНИМАНИЕ: \nУстановка в сиснанд приведет к бану!",
|
||||
"Installing Forwarder": "Установить форвардер",
|
||||
"Installing Forwarder": "Установка форвардера",
|
||||
"Creating Program": "Создание программы",
|
||||
"Creating Control": "Создание элемента управления",
|
||||
"Creating Meta": "Создание меты",
|
||||
"Writing Nca": "Запись NCA",
|
||||
"Updating ncm databse": "Обновление базы данных NCM",
|
||||
"Updating ncm database": "Обновление базы данных NCM",
|
||||
"Pushing application record": "Добавление записи приложения",
|
||||
"Failed to install forwarder": "Не удалось установить форвардер",
|
||||
"Unstar": "Убрать из избранного",
|
||||
"Star": "Добавить в избранное",
|
||||
"Unstarred ": "Удалено из избранного: ",
|
||||
"Starred ": "Добавлено в избранное: ",
|
||||
"Failed to remove old forwarder, please manually remove it!": "Не удалось удалить старый форвардер, удалите его вручную!",
|
||||
@@ -232,8 +273,8 @@
|
||||
"AppStore": "AppStore",
|
||||
"Appstore": "AppStore",
|
||||
"Store": "Магазин",
|
||||
"Filter: %s | Sort: %s | Order: %s": "Показывать: %s | Сорт: %s | Порядок: %s",
|
||||
"AppStore Options": "Параметры AppStore",
|
||||
"Filter: %s | Sort: %s | Order: %s": "Показывать: %s | Сортировка: %s | Порядок: %s",
|
||||
"AppStore Options": "Опции AppStore",
|
||||
"Info": "Информация",
|
||||
"Changelog": "Список изменений",
|
||||
"Details": "Подробности",
|
||||
@@ -242,14 +283,15 @@
|
||||
"category: %s": "категория: %s",
|
||||
"extracted: %.2f MiB": "извлечено: %.2f MiB",
|
||||
"app_dls: %s": "app_dls: %s",
|
||||
"More by Author": "Другие приложения автора",
|
||||
"More by Author": "Другое от автора",
|
||||
"Leave Feedback": "Оставить отзыв",
|
||||
"Visit Website": "Посетить сайт",
|
||||
|
||||
"FileBrowser": "Файловый менеджер",
|
||||
"FileBrowser": "Файлы",
|
||||
"Files": "Файлы",
|
||||
"%zd files": "%zd файлы",
|
||||
"%zd dirs": "%zd разделы",
|
||||
"File Options": "Параметры файла",
|
||||
"File Options": "Опции файла",
|
||||
"Show Hidden": "Показать скрытые",
|
||||
"Folders First": "Папки в начале",
|
||||
"Hidden Last": "Скрытые в конце",
|
||||
@@ -257,14 +299,15 @@
|
||||
"Copy": "Копировать",
|
||||
"Copying ": "Копирование ",
|
||||
"Paste": "Вставить",
|
||||
"Paste ": "Вставить ",
|
||||
" file(s)?": " файл(ов)?",
|
||||
"Paste file(s)?": "Вставить файл(ы)?",
|
||||
"Pasting ": "Вставка ",
|
||||
"Pasting": "Вставка",
|
||||
"Rename": "Переименовать",
|
||||
"Set New File Name": "Задайте новое имя файла",
|
||||
"Failed to delete directory": "Не удалось удалить папку",
|
||||
"Failed to delete file": "Не удалось удалить файл",
|
||||
"Extract zip": "Распаковать .zip",
|
||||
"Extract Options": "Параметры распаковки",
|
||||
"Extract Options": "Опции распаковки",
|
||||
"Extract here": "Распаковать сюда",
|
||||
"Extract to root": "Распаковать в корень",
|
||||
"Are you sure you want to extract to root?": "Вы уверены, что хотите распаковать в корень?",
|
||||
@@ -274,7 +317,7 @@
|
||||
"Extract success!": "Распаковка успешна!",
|
||||
"Extract failed!": "Сбой распаковки!",
|
||||
"Compress to zip": "Сжать в .zip",
|
||||
"Compress Options": "Параметры сжатия",
|
||||
"Compress Options": "Опции сжатия",
|
||||
"Compress": "Сжать",
|
||||
"Compress to...": "Сжать в...",
|
||||
"Compressing ": "Сжатие ",
|
||||
@@ -285,32 +328,37 @@
|
||||
"Create Folder": "Создать папку",
|
||||
"Set Folder Name": "Укажите имя папки",
|
||||
"Creating ": "Создание ",
|
||||
"View as text (unfinished)": "Открыть как текст (не доделано)",
|
||||
"Upload": "Отправить",
|
||||
"Select upload location": "Выберите место загрузки",
|
||||
"No upload locations set!": "Места загрузки не заданы!",
|
||||
"Uploading": "Отправка",
|
||||
"Upload successfull!": "Отправка успешна!",
|
||||
"Upload failed!": "Сбой Отправки!",
|
||||
"View as text (unfinished)": "Открыть как текст (не доделано)",
|
||||
"Hash": "Хэш",
|
||||
"Hash Options": "Опции хэша",
|
||||
"Hashing": "Вычисление хэша",
|
||||
"Failed to hash file...": "Не удалось вычислить хэш файла...",
|
||||
"Ignore read only": "Игнор. только для чтения",
|
||||
"Mount": "Монтирован",
|
||||
"Mount": "Монтировать",
|
||||
"Sd": "Micro SD",
|
||||
"Image System memory": "Альбом Сиснанда",
|
||||
"Image microSD card": "Альбом Эмунанда",
|
||||
"Empty...": "Пусто...",
|
||||
"Open with DayBreak?": "Открыть с помощью DayBreak?",
|
||||
"Launch ": "Запуск ",
|
||||
"Launch option for: ": "Параметры запуска для: ",
|
||||
"Launch option for: ": "Опции запуска для: ",
|
||||
"Select launcher for: ": "Выберите загрузчик для: ",
|
||||
"Close FileBrowser?": "Закрыть Файлы?",
|
||||
|
||||
"Sort By": "Сортировка",
|
||||
"Sort Options": "Параметры сортировки",
|
||||
"Sort Options": "Опции сортировки",
|
||||
"Filter": "Показывать",
|
||||
"All": "Всё",
|
||||
"Emulators": "Эмуляторы",
|
||||
"Tools": "Инструменты",
|
||||
"Themes": "Темы",
|
||||
"Legacy": "Старые",
|
||||
"Legacy": "Легаси",
|
||||
"Sort": "Сортировать",
|
||||
"Size": "По размеру",
|
||||
"Size (Star)": "По размеру (избр.)",
|
||||
@@ -334,17 +382,17 @@
|
||||
"Grid": "Сетка",
|
||||
"Search": "Поиск",
|
||||
|
||||
"Options": "Параметры",
|
||||
"Options": "Опции",
|
||||
"Split": "Разделить",
|
||||
"OK": "ОК",
|
||||
"Back": "Назад",
|
||||
"Select": "Выбрать",
|
||||
"Open": "Открыть",
|
||||
"Close": "Закрыть",
|
||||
"Launch": "Запуск",
|
||||
"Restart": "Перезапустить",
|
||||
"Next": "Далее",
|
||||
"Prev": "Назад",
|
||||
"Unstar": "Убрать из избранного",
|
||||
"Star": "Добавить в избранное",
|
||||
"Yes": "Да",
|
||||
"No": "Нет",
|
||||
"On": "Вкл",
|
||||
@@ -367,12 +415,13 @@
|
||||
"Remove": "Удалить",
|
||||
"Completely remove ": "Полностью удалить ",
|
||||
"Removing ": "Удаляется ",
|
||||
"Removed ": "Удалено ",
|
||||
"Uninstalling ": "Деинсталляция ",
|
||||
"Removed ": "Удалено ",
|
||||
|
||||
"Download": "Скачать",
|
||||
"Downloading ": "Загрузка ",
|
||||
"Downloaded ": "Загружено ",
|
||||
"Download via the Network options!": "Скачайте через: Меню - Сеть",
|
||||
|
||||
"Update": "Обновить",
|
||||
"Update avaliable: ": "Доступно обновление: ",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"No Internet": "Ingen Internetanslutning",
|
||||
"Switch-Handheld!": "Switch Handhållen!",
|
||||
"Switch-Docked!": "Switch Dockad!",
|
||||
"Warning! Logs are enabled, Sphaira will run slowly!": "",
|
||||
"Audio disabled due to suspended game": "Ljud är avstängt på grund av bakgrundsprogram",
|
||||
"Are you sure you wish to cancel?": "Är du säker på att du vill avbryta?",
|
||||
"An error occurred": "",
|
||||
@@ -27,6 +28,8 @@
|
||||
"Nxlink Connected": "Nxlink ansluten",
|
||||
"Nxlink Upload": "Nxlink överför",
|
||||
"Nxlink Finished": "Nxlink klar",
|
||||
"Hdd": "",
|
||||
"Hdd write protect": "",
|
||||
|
||||
"Language": "Språk",
|
||||
"Auto": "Auto",
|
||||
@@ -57,22 +60,35 @@
|
||||
"No meta entries found...\n": "",
|
||||
"Updating application record list": "",
|
||||
"Dump": "",
|
||||
"Dump options": "",
|
||||
"Dump Options": "",
|
||||
"Select content to dump": "",
|
||||
"Dump All": "",
|
||||
"Dump Application": "",
|
||||
"Dump Patch": "",
|
||||
"Dump AddOnContent": "",
|
||||
"Dump DataPatch": "",
|
||||
"Created nested folder": "",
|
||||
"Append folder with .xci": "",
|
||||
"Trim XCI": "",
|
||||
"Label trimmed XCI": "",
|
||||
"Multi-threaded USB transfer": "",
|
||||
"Dump All Bins": "",
|
||||
"Dump XCI": "",
|
||||
"Dump Card ID Set": "",
|
||||
"Dump Card UID": "",
|
||||
"Dump Certificate": "",
|
||||
"Dump Initial Data": "",
|
||||
"Select dump location": "",
|
||||
"microSD card (/dumps/NSP/)": "",
|
||||
"microSD card (/dumps/)": "",
|
||||
"USB transfer (Switch 2 Switch)": "",
|
||||
"/dev/null (Speed Test)": "",
|
||||
"Dumping": "",
|
||||
"Dump successfull!": "",
|
||||
"Dump failed!": "",
|
||||
"Success": "",
|
||||
"Delete successfull!": "",
|
||||
"Delete failed!": "",
|
||||
"Success": "",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Themezer-alternativ",
|
||||
@@ -86,9 +102,10 @@
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Laddar ner JSON",
|
||||
"Select asset to download for ": "Välj tillgång att ladda ner för ",
|
||||
"Failed to download json": "",
|
||||
"Failed to download app!": "",
|
||||
|
||||
"FTP Install": "",
|
||||
"FTP Install (EXPERIMENTAL)": "",
|
||||
"Connection Type: WiFi | Strength: ": "",
|
||||
"Connection Type: Ethernet": "",
|
||||
"Connection Type: None": "",
|
||||
@@ -98,9 +115,9 @@
|
||||
"Password:": "",
|
||||
"SSID:": "",
|
||||
"Passphrase:": "",
|
||||
"Failed to install via FTP, press B to exit...": "",
|
||||
"Ftp install success!": "",
|
||||
"Ftp install failed!": "",
|
||||
"Failed to install, press B to exit...": "",
|
||||
"Install success!": "",
|
||||
"Install failed!": "",
|
||||
"USB Install": "",
|
||||
"USB": "",
|
||||
"Connected, waiting for file list...": "",
|
||||
@@ -122,9 +139,12 @@
|
||||
"GC": "",
|
||||
"System memory %.1f GB": "",
|
||||
"microSD card %.1f GB": "",
|
||||
"Nand Install": "",
|
||||
"SD Card Install": "",
|
||||
"Exit": "",
|
||||
"Install disabled...\nPlease enable installing via the install options.": "",
|
||||
"No GameCard inserted": "",
|
||||
"GameCard is already trimmed!": "",
|
||||
"WARNING: GameCard is already trimmed!": "",
|
||||
"Continue": "",
|
||||
"Gc install success!": "",
|
||||
"Gc install failed!": "",
|
||||
|
||||
@@ -160,14 +180,14 @@
|
||||
"Negative image": "Negativ bild",
|
||||
"Format": "Format",
|
||||
"Trimming Format": "Trimningsformat",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"External Light Filter": "Externt ljusfilter",
|
||||
"Load Default": "Ladda standard",
|
||||
|
||||
"Web": "",
|
||||
"Select URL": "",
|
||||
"Enter custom URL": "",
|
||||
"Enter URL": "",
|
||||
|
||||
"Advanced": "Avancerat",
|
||||
"Advanced Options": "Avancerade alternativ",
|
||||
"Logging": "Loggning",
|
||||
@@ -181,10 +201,12 @@
|
||||
"Restored hbmenu": "Återställde hbmenu.",
|
||||
"Restart Sphaira?": "Starta om Sphaira?",
|
||||
"Press OK to restart Sphaira": "",
|
||||
"Boost CPU during transfer": "",
|
||||
"Text scroll speed": "",
|
||||
"Slow": "",
|
||||
"Normal": "",
|
||||
"Fast": "",
|
||||
"Set left-side menu": "",
|
||||
"Set right-side menu": "",
|
||||
"Install options": "",
|
||||
"Install Options": "",
|
||||
@@ -194,7 +216,6 @@
|
||||
"Install location": "Installationsplats",
|
||||
"System memory": "Systemminne",
|
||||
"microSD card": "microSD-kort",
|
||||
"Boost CPU clock": "",
|
||||
"Allow downgrade": "",
|
||||
"Skip if already installed": "",
|
||||
"Ticket only": "",
|
||||
@@ -222,9 +243,11 @@
|
||||
"Creating Control": "Skapar kontroll",
|
||||
"Creating Meta": "Skapar metadata",
|
||||
"Writing Nca": "Skriver Nca",
|
||||
"Updating ncm databse": "Uppdaterar ncm-databas",
|
||||
"Updating ncm database": "Uppdaterar ncm-databas",
|
||||
"Pushing application record": "Skickar programpost",
|
||||
"Failed to install forwarder": "Misslyckades att installera genväg",
|
||||
"Unstar": "Avmarkera stjärna",
|
||||
"Star": "Markera stjärna",
|
||||
"Unstarred ": "Avmarkerad ",
|
||||
"Starred ": "Markerad ",
|
||||
"Failed to remove old forwarder, please manually remove it!": "",
|
||||
@@ -257,12 +280,13 @@
|
||||
"Copy": "Kopiera",
|
||||
"Copying ": "Kopierar ",
|
||||
"Paste": "Klistra in",
|
||||
"Paste ": "Klistra in ",
|
||||
" file(s)?": " fil(er)?",
|
||||
"Paste file(s)?": "",
|
||||
"Pasting ": "Klistrar in ",
|
||||
"Pasting": "Klistrar in",
|
||||
"Rename": "Byt namn",
|
||||
"Set New File Name": "Ange nytt filnamn",
|
||||
"Failed to delete directory": "",
|
||||
"Failed to delete file": "",
|
||||
"Extract zip": "",
|
||||
"Extract Options": "",
|
||||
"Extract here": "",
|
||||
@@ -285,13 +309,17 @@
|
||||
"Create Folder": "Skapa mapp",
|
||||
"Set Folder Name": "Ange mappnamn",
|
||||
"Creating ": "Skapar ",
|
||||
"View as text (unfinished)": "Visa som text (ofärdig)",
|
||||
"Upload": "",
|
||||
"Select upload location": "",
|
||||
"No upload locations set!": "",
|
||||
"Uploading": "",
|
||||
"Upload successfull!": "",
|
||||
"Upload failed!": "",
|
||||
"View as text (unfinished)": "Visa som text (ofärdig)",
|
||||
"Hash": "",
|
||||
"Hash Options": "",
|
||||
"Hashing": "",
|
||||
"Failed to hash file...": "",
|
||||
"Ignore read only": "Ignorera skrivskydd",
|
||||
"Mount": "Montera",
|
||||
"Sd": "Sd",
|
||||
@@ -302,6 +330,7 @@
|
||||
"Launch ": "Starta ",
|
||||
"Launch option for: ": "Startalternativ för: ",
|
||||
"Select launcher for: ": "Välj startprogram för: ",
|
||||
"Close FileBrowser?": "",
|
||||
|
||||
"Sort By": "Sortera efter",
|
||||
"Sort Options": "Sorteringsalternativ",
|
||||
@@ -335,16 +364,16 @@
|
||||
"Search": "Sök",
|
||||
|
||||
"Options": "Alternativ",
|
||||
"Split": "",
|
||||
"OK": "OK",
|
||||
"Back": "Tillbaka",
|
||||
"Select": "Välj",
|
||||
"Open": "Öppna",
|
||||
"Close": "",
|
||||
"Launch": "Starta",
|
||||
"Restart": "Starta om",
|
||||
"Next": "",
|
||||
"Prev": "",
|
||||
"Unstar": "Avmarkera stjärna",
|
||||
"Star": "Markera stjärna",
|
||||
"Yes": "Ja",
|
||||
"No": "Nej",
|
||||
"On": "",
|
||||
@@ -367,12 +396,13 @@
|
||||
"Remove": "Ta bort",
|
||||
"Completely remove ": "Ta bort helt ",
|
||||
"Removing ": "Tar bort ",
|
||||
"Removed ": "Borttagen ",
|
||||
"Uninstalling ": "Avinstallerar ",
|
||||
"Removed ": "Borttagen ",
|
||||
|
||||
"Download": "Ladda ner",
|
||||
"Downloading ": "Laddar ner ",
|
||||
"Downloaded ": "Nedladdad ",
|
||||
"Download via the Network options!": "",
|
||||
|
||||
"Update": "Uppdatera",
|
||||
"Update avaliable: ": "Uppdatering tillgänglig: ",
|
||||
@@ -389,4 +419,4 @@
|
||||
"Empty!": "Tomt!",
|
||||
"Not Ready...": "Inte redo...",
|
||||
"Error loading page!": "Fel vid laddning av sida!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"[Applet Mode]": "[Режим Аплету]",
|
||||
"[Applet Mode]": "[Аплет]",
|
||||
"No Internet": "Без інтернету",
|
||||
"Switch-Handheld!": "Switch - Портатив!",
|
||||
"Switch-Docked!": "Switch - Докований!",
|
||||
"Switch-Docked!": "Switch - Док!",
|
||||
"Warning! Logs are enabled, Sphaira will run slowly!": "Увага! Логи увімкнені, Sphaira працюватиме повільно!",
|
||||
"Audio disabled due to suspended game": "Аудіо вимкнено через призупинену програму",
|
||||
"Are you sure you wish to cancel?": "Ви впевнені, що хочете скасувати?",
|
||||
"An error occurred": "",
|
||||
"An error occurred": "Сталася помилка",
|
||||
"If this message appears repeatedly, please open an issue.": "Якщо це повідомлення з'являється повторно, будь ласка, повідомте про проблему.",
|
||||
|
||||
"Menu Options": "Опції меню",
|
||||
@@ -16,8 +17,8 @@
|
||||
"Music": "Музика",
|
||||
"12 Hour Time": "12-годинний формат часу",
|
||||
"Download Default Music": "Завантажити музику за замовчуванням",
|
||||
"Failed to download default_music.bfstm, please try again": "",
|
||||
"Overwrite current default music?": "",
|
||||
"Failed to download default_music.bfstm, please try again": "Не вдалося завантажити default_music.bfstm, спробуйте ще раз",
|
||||
"Overwrite current default music?": "Перезаписати поточну музику за замовчуванням?",
|
||||
|
||||
"Network": "Мережа",
|
||||
"Network Options": "Опції мережі",
|
||||
@@ -27,6 +28,8 @@
|
||||
"Nxlink Connected": "Nxlink підключено",
|
||||
"Nxlink Upload": "Nxlink | Завантаження",
|
||||
"Nxlink Finished": "Nxlink | Завершено",
|
||||
"Hdd": "HDD",
|
||||
"Hdd write protect": "Захист HDD від запису",
|
||||
|
||||
"Language": "Мова",
|
||||
"Auto": "Автоматично",
|
||||
@@ -53,26 +56,39 @@
|
||||
"Launch random game": "Запустити випадкову гру",
|
||||
"List meta records": "Список метаданих записів",
|
||||
"Entries": "Записи",
|
||||
"Failed to list application meta entries": "",
|
||||
"No meta entries found...\n": "",
|
||||
"Updating application record list": "",
|
||||
"Dump": "",
|
||||
"Select content to dump": "",
|
||||
"Dump All": "",
|
||||
"Dump Application": "",
|
||||
"Dump Patch": "",
|
||||
"Dump AddOnContent": "",
|
||||
"Dump DataPatch": "",
|
||||
"Select dump location": "",
|
||||
"microSD card (/dumps/NSP/)": "",
|
||||
"USB transfer (Switch 2 Switch)": "",
|
||||
"/dev/null (Speed Test)": "",
|
||||
"Dumping": "",
|
||||
"Dump successfull!": "",
|
||||
"Dump failed!": "",
|
||||
"Success": "",
|
||||
"Delete successfull!": "",
|
||||
"Delete failed!": "",
|
||||
"Failed to list application meta entries": "Не вдалося вивести список метаданих програм",
|
||||
"No meta entries found...\n": "Не знайдено метаданих...\n",
|
||||
"Updating application record list": "Оновлення списку записів програм",
|
||||
"Dump": "Дамп",
|
||||
"Dump options": "Опції дампу",
|
||||
"Dump Options": "Опції дампу",
|
||||
"Select content to dump": "Виберіть вміст для дампу",
|
||||
"Dump All": "Дамп всього",
|
||||
"Dump Application": "Дамп програми",
|
||||
"Dump Patch": "Дамп патчу",
|
||||
"Dump AddOnContent": "Дамп DLC",
|
||||
"Dump DataPatch": "Дамп патчу даних",
|
||||
"Created nested folder": "Створено вкладену теку",
|
||||
"Append folder with .xci": "Додати до теки .xci",
|
||||
"Trim XCI": "Обрізати XCI",
|
||||
"Label trimmed XCI": "Позначити обрізаний XCI",
|
||||
"Multi-threaded USB transfer": "Багатопотокова передача USB",
|
||||
"Dump All Bins": "Дамп всіх BIN-файлів",
|
||||
"Dump XCI": "Дамп XCI",
|
||||
"Dump Card ID Set": "Дамп набору ID картриджа",
|
||||
"Dump Card UID": "Дамп UID картриджа",
|
||||
"Dump Certificate": "Дамп сертифіката",
|
||||
"Dump Initial Data": "Дамп початкових даних",
|
||||
"Select dump location": "Виберіть місце для дампу",
|
||||
"microSD card (/dumps/)": "SD-карта (/dumps/)",
|
||||
"USB transfer (Switch 2 Switch)": "Передача USB (Switch <-> Switch)",
|
||||
"/dev/null (Speed Test)": "Тест швидкості (/dev/null)",
|
||||
"Dumping": "Дампінг",
|
||||
"Dump successfull!": "Дамп успішний!",
|
||||
"Dump failed!": "Помилка дампу!",
|
||||
"Delete successfull!": "Видалення успішне!",
|
||||
"Delete failed!": "Помилка видалення!",
|
||||
"Success": "Успіх",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Опції Themezer",
|
||||
@@ -86,9 +102,10 @@
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Завантаження JSON",
|
||||
"Select asset to download for ": "Виберіть ресурс для завантаження для ",
|
||||
"Failed to download json": "Не вдалося завантажити JSON",
|
||||
"Failed to download app!": "Не вдалося завантажити програму!",
|
||||
|
||||
"FTP Install": "Встановлення через FTP",
|
||||
"FTP Install (EXPERIMENTAL)": "Встановлення через FTP (ЕКСПЕРИМЕНТАЛЬНО)",
|
||||
"Connection Type: WiFi | Strength: ": "Тип підключення: WiFi | Сила сигналу: ",
|
||||
"Connection Type: Ethernet": "Тип підключення: Ethernet",
|
||||
"Connection Type: None": "Тип підключення: Немає",
|
||||
@@ -98,19 +115,19 @@
|
||||
"Password:": "Пароль:",
|
||||
"SSID:": "SSID:",
|
||||
"Passphrase:": "Кодова фраза:",
|
||||
"Failed to install via FTP, press B to exit...": "Не вдалося встановити через FTP, натисніть B для виходу...",
|
||||
"Ftp install success!": "Встановлення через FTP успішно завершено.",
|
||||
"Ftp install failed!": "Встановлення через FTP не вдалося.",
|
||||
"Failed to install, press B to exit...": "Не вдалося встановити через FTP, натисніть B для виходу...",
|
||||
"Install success!": "Успішно встановлено",
|
||||
"Install failed!": "Встановлення не вдалося.",
|
||||
"USB Install": "Встановлення через USB",
|
||||
"USB": "USB",
|
||||
"Connected, waiting for file list...": "",
|
||||
"Connected, starting transfer...": "",
|
||||
"Connected, waiting for file list...": "Підключено, очікування списку файлів...",
|
||||
"Connected, starting transfer...": "Підключено, початок передачі...",
|
||||
"Failed to init usb, press B to exit...": "Не вдалося ініціалізувати USB, натисніть B для виходу...",
|
||||
"Waiting for connection...": "Очікування підключення...",
|
||||
"Transferring data...": "Передача даних...",
|
||||
"USB connected, sending file list": "",
|
||||
"Sent file list, waiting for command...": "",
|
||||
"waiting for usb connection...": "",
|
||||
"USB connected, sending file list": "USB підключено, надсилання списку файлів",
|
||||
"Sent file list, waiting for command...": "Список файлів надіслано, очікування команди...",
|
||||
"waiting for usb connection...": "очікування USB підключення...",
|
||||
"Disable MTP for usb install": "Вимкнути MTP для встановлення через USB",
|
||||
"Re-enabled MTP": "MTP знову увімкнено",
|
||||
"Installed via usb": "Встановлено через USB",
|
||||
@@ -119,17 +136,20 @@
|
||||
"Press B to exit...": "Натисніть B для виходу...",
|
||||
"GameCard Install": "Встановлення з картриджа",
|
||||
"GameCard": "Картридж",
|
||||
"GC": "",
|
||||
"System memory %.1f GB": "",
|
||||
"microSD card %.1f GB": "",
|
||||
"Nand Install": "",
|
||||
"SD Card Install": "",
|
||||
"Exit": "",
|
||||
"GC": "К",
|
||||
"System memory %.1f GB": "Пам'ять консолі %.1f ГБ",
|
||||
"microSD card %.1f GB": "SD-карта %.1f ГБ",
|
||||
"Exit": "Вихід",
|
||||
"Install disabled...\nPlease enable installing via the install options.": "Встановлення вимкнено...\nБудь ласка, увімкніть встановлення в опціях.",
|
||||
"No GameCard inserted": "Картридж не вставлено",
|
||||
"GameCard is already trimmed!": "Картридж вже обрізано!",
|
||||
"WARNING: GameCard is already trimmed!": "УВАГА: Картридж вже обрізано!",
|
||||
"Continue": "Продовжити",
|
||||
"Gc install success!": "Встановлення з картриджа успішно завершено.",
|
||||
"Gc install failed!": "Встановлення з картриджа не вдалося.",
|
||||
|
||||
"IRS (Infrared Joycon Camera)": "ІЧ (Інфрачервона камера Joycon)",
|
||||
"IRS": "",
|
||||
"IRS": "ІЧ",
|
||||
"Irs": "ІЧ-сенсор",
|
||||
"Ambient Noise Level: ": "Рівень навколишнього шуму: ",
|
||||
"Controller": "Контролер",
|
||||
@@ -157,17 +177,17 @@
|
||||
"Gain": "Підсилення",
|
||||
"Negative Image": "Негативне зображення",
|
||||
"Normal image": "Нормальне зображення",
|
||||
"Negative image": "",
|
||||
"Negative image": "Негативне зображення",
|
||||
"Format": "Формат",
|
||||
"Trimming Format": "Формат обрізки",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"External Light Filter": "Фільтр зовнішнього освітлення",
|
||||
"Load Default": "Завантажити типові",
|
||||
|
||||
"Web": "Веб",
|
||||
"Select URL": "Вибрати URL",
|
||||
"Enter custom URL": "Ввести власний URL",
|
||||
"Enter URL": "Ввести URL",
|
||||
|
||||
"Advanced": "Додатково",
|
||||
"Advanced Options": "Додаткові опції",
|
||||
"Logging": "Логування",
|
||||
@@ -181,37 +201,38 @@
|
||||
"Restored hbmenu": "hbmenu відновлено",
|
||||
"Restart Sphaira?": "Перезапустити Sphaira?",
|
||||
"Press OK to restart Sphaira": "Натисніть OK для перезапуску Sphaira",
|
||||
"Boost CPU during transfer": "Розгін при передачі",
|
||||
"Text scroll speed": "Швидк. прокрутки",
|
||||
"Slow": "Повільно",
|
||||
"Normal": "Нормально",
|
||||
"Normal": "Середнє",
|
||||
"Fast": "Швидко",
|
||||
"Set right-side menu": "Праве меню",
|
||||
"Set left-side menu": "Ліве меню (L)",
|
||||
"Set right-side menu": "Праве меню (R)",
|
||||
"Install options": "Опції встановлення",
|
||||
"Install Options": "Опції встановлення",
|
||||
"Enable sysmmc": "",
|
||||
"Enable emummc": "",
|
||||
"Enable sysmmc": "Дозволити в sysMMC",
|
||||
"Enable emummc": "Дозволити в emuMMC",
|
||||
"Show install warning": "Попередж. при встанов.",
|
||||
"Install location": "Місце встановлення",
|
||||
"System memory": "Пам'ять консолі",
|
||||
"microSD card": "SD-карта",
|
||||
"Boost CPU clock": "Розігнати CPU",
|
||||
"Install location": "Ставити в",
|
||||
"System memory": "NAND",
|
||||
"microSD card": "microSD",
|
||||
"Allow downgrade": "Дозволити відкат",
|
||||
"Skip if already installed": "Пропуск, якщо встановл.",
|
||||
"Skip if already installed": "Пропуск встановленого",
|
||||
"Ticket only": "Тільки тікет",
|
||||
"Skip base": "Пропустити базу",
|
||||
"Skip patch": "Пропустити патч",
|
||||
"Skip dlc": "Пропустити DLC",
|
||||
"Skip data patch": "Пропустити патч даних",
|
||||
"Skip ticket": "Пропустити тікет",
|
||||
"Skip NCA hash verify": "",
|
||||
"Skip RSA header verify": "Пропуск перевірку заголовка RSA",
|
||||
"Skip RSA NPDM verify": "Пропуск перевірку NPDM RSA",
|
||||
"Ignore distribution bit": "Ігнорувати біт розподілу",
|
||||
"Convert to standard crypto": "Конвертувати у стандартне шифрування",
|
||||
"Skip NCA hash verify": "Не перевір. NCA hash",
|
||||
"Skip RSA header verify": "Не перевір. RSA header",
|
||||
"Skip RSA NPDM verify": "Не перевір. NPDM RSA",
|
||||
"Ignore distribution bit": "Ігнор. біт розподілу",
|
||||
"Convert to standard crypto": "Конверт. у стандарт. шифр.",
|
||||
"Lower master key": "Знизити майстер-ключ",
|
||||
"Lower system version": "Знизити версію системи",
|
||||
|
||||
"Homebrew": "Домашні програми",
|
||||
"Homebrew": "Homebrew",
|
||||
"Apps": "Програми",
|
||||
"Homebrew Options": "Опції домашніх програм",
|
||||
"Hide Sphaira": "Приховати Sphaira",
|
||||
@@ -222,15 +243,17 @@
|
||||
"Creating Control": "Створення контролера",
|
||||
"Creating Meta": "Створення метаданих",
|
||||
"Writing Nca": "Запис NCA",
|
||||
"Updating ncm databse": "Оновлення бази даних NCM",
|
||||
"Updating ncm database": "Оновлення бази даних NCM",
|
||||
"Pushing application record": "Запис даних програми",
|
||||
"Failed to install forwarder": "Не вдалося встановити форвардер",
|
||||
"Unstar": "Прибрати з обраного",
|
||||
"Star": "Позначити зіркою",
|
||||
"Unstarred ": "Знято зірку з ",
|
||||
"Starred ": "Позначено зіркою ",
|
||||
"Failed to remove old forwarder, please manually remove it!": "",
|
||||
"Failed to remove old forwarder, please manually remove it!": "Не вдалося видалити старий форвардер, видаліть його вручну!",
|
||||
|
||||
"AppStore": "Магазин програм",
|
||||
"Appstore": "",
|
||||
"AppStore": "AppStore",
|
||||
"Appstore": "AppStore",
|
||||
"Store": "Магазин",
|
||||
"Filter: %s | Sort: %s | Order: %s": "Фільтр: %s | Сорт.: %s | Порядок: %s",
|
||||
"AppStore Options": "Опції магазину програм",
|
||||
@@ -257,41 +280,46 @@
|
||||
"Copy": "Копіювати",
|
||||
"Copying ": "Копіювання ",
|
||||
"Paste": "Вставити",
|
||||
"Paste ": "Вставити: ",
|
||||
" file(s)?": " файл(и)?",
|
||||
"Paste file(s)?": "Вставити файл(и)?",
|
||||
"Pasting ": "Вставлення ",
|
||||
"Pasting": "Вставлення",
|
||||
"Rename": "Перейменувати",
|
||||
"Set New File Name": "Введіть нове ім'я файлу",
|
||||
"Extract zip": "",
|
||||
"Extract Options": "",
|
||||
"Extract here": "",
|
||||
"Extract to root": "",
|
||||
"Are you sure you want to extract to root?": "",
|
||||
"Extract to...": "",
|
||||
"Enter the path to the folder to extract into": "",
|
||||
"Extracting ": "",
|
||||
"Extract success!": "",
|
||||
"Extract failed!": "",
|
||||
"Failed to delete directory": "Не вдалося видалити теку",
|
||||
"Failed to delete file": "Не вдалося видалити файл",
|
||||
"Extract zip": "Розпакувати zip",
|
||||
"Extract Options": "Опції розпакування",
|
||||
"Extract here": "Розпакувати тут",
|
||||
"Extract to root": "Розпакувати в корінь",
|
||||
"Are you sure you want to extract to root?": "Ви впевнені, що хочете розпакувати в корінь?",
|
||||
"Extract to...": "Розпакувати в...",
|
||||
"Enter the path to the folder to extract into": "Введіть шлях до теки для розпакування",
|
||||
"Extracting ": "Розпакування ",
|
||||
"Extract success!": "Розпакування успішне!",
|
||||
"Extract failed!": "Помилка розпакування!",
|
||||
"Compress to zip": "Стиснути в zip",
|
||||
"Compress Options": "",
|
||||
"Compress": "",
|
||||
"Compress to...": "",
|
||||
"Compressing ": "",
|
||||
"Compress success!": "",
|
||||
"Compress failed!": "",
|
||||
"Compress Options": "Опції стиснення",
|
||||
"Compress": "Стиснути",
|
||||
"Compress to...": "Стиснути в...",
|
||||
"Compressing ": "Стиснення ",
|
||||
"Compress success!": "Стиснення успішне!",
|
||||
"Compress failed!": "Помилка стиснення!",
|
||||
"Create File": "Створити файл",
|
||||
"Set File Name": "Введіть ім'я файлу",
|
||||
"Create Folder": "Створити теку",
|
||||
"Set Folder Name": "Введіть ім'я теки",
|
||||
"Creating ": "Створення ",
|
||||
"Upload": "",
|
||||
"Select upload location": "",
|
||||
"No upload locations set!": "",
|
||||
"Uploading": "",
|
||||
"Upload successfull!": "",
|
||||
"Upload failed!": "",
|
||||
"View as text (unfinished)": "Переглянути як текст (незавершено)",
|
||||
"Upload": "Завантажити (на сервер)",
|
||||
"Select upload location": "Виберіть місце для завантаження",
|
||||
"No upload locations set!": "Не встановлено місць для завантаження!",
|
||||
"Uploading": "Завантаження (на сервер)",
|
||||
"Upload successfull!": "Завантаження успішне!",
|
||||
"Upload failed!": "Помилка завантаження!",
|
||||
"Hash": "Хеш",
|
||||
"Hash Options": "Опції хешування",
|
||||
"Hashing": "Хешування",
|
||||
"Failed to hash file...": "Не вдалося обчислити хеш файлу...",
|
||||
"Ignore read only": "Ігнорувати лише читання",
|
||||
"Mount": "Монтувати",
|
||||
"Sd": "SD-карта",
|
||||
@@ -302,6 +330,7 @@
|
||||
"Launch ": "Запустити ",
|
||||
"Launch option for: ": "Опція запуску для: ",
|
||||
"Select launcher for: ": "Виберіть лаунчер для: ",
|
||||
"Close FileBrowser?": "Закрити файловий менеджер?",
|
||||
|
||||
"Sort By": "Сортувати за",
|
||||
"Sort Options": "Опції сортування",
|
||||
@@ -328,23 +357,23 @@
|
||||
"Ascending": "За зростанням",
|
||||
"Ascending (Up)": "За зростанням (вгору)",
|
||||
"Asc": "Зрост.",
|
||||
"Layout": "",
|
||||
"List": "",
|
||||
"Icon": "",
|
||||
"Grid": "",
|
||||
"Layout": "Макет",
|
||||
"List": "Список",
|
||||
"Icon": "Іконки",
|
||||
"Grid": "Сітка",
|
||||
"Search": "Пошук",
|
||||
|
||||
"Options": "Налаштування",
|
||||
"Split": "Розділити",
|
||||
"OK": "ОК",
|
||||
"Back": "Назад",
|
||||
"Select": "Вибрати",
|
||||
"Open": "Відкрити",
|
||||
"Close": "Закрити",
|
||||
"Launch": "Запустити",
|
||||
"Restart": "Перезапустити",
|
||||
"Next": "Наступний",
|
||||
"Prev": "Попередній",
|
||||
"Unstar": "Прибрати з обраного",
|
||||
"Star": "Позначити зіркою",
|
||||
"Yes": "Так",
|
||||
"No": "Ні",
|
||||
"On": "Увімк.",
|
||||
@@ -367,12 +396,13 @@
|
||||
"Remove": "Видалити",
|
||||
"Completely remove ": "Повністю видалити ",
|
||||
"Removing ": "Видалення ",
|
||||
"Removed ": "Видалено ",
|
||||
"Uninstalling ": "Видалення ",
|
||||
"Removed ": "Видалено ",
|
||||
|
||||
"Download": "Завантажити",
|
||||
"Downloading ": "Завантаження ",
|
||||
"Downloaded ": "Завантажено ",
|
||||
"Download via the Network options!": "Завантажуйте через опції мережі!",
|
||||
|
||||
"Update": "Оновити",
|
||||
"Update avaliable: ": "Доступне оновлення: ",
|
||||
@@ -380,13 +410,13 @@
|
||||
"Updated to ": "Оновлено до ",
|
||||
"Failed to download update": "Не вдалося завантажити оновлення",
|
||||
|
||||
"%zu hours %zu minutes remaining": "",
|
||||
"%zu minutes %zu seconds remaining": "",
|
||||
"%zu seconds remaining": "",
|
||||
"%zu hours %zu minutes remaining": "залишилось %zu год %zu хв",
|
||||
"%zu minutes %zu seconds remaining": "залишилось %zu хв %zu сек",
|
||||
"%zu seconds remaining": "залишилось %zu сек",
|
||||
|
||||
"Loading...": "Завантаження...",
|
||||
"Loading": "Завантаження",
|
||||
"Empty!": "Пусто!",
|
||||
"Not Ready...": "Не готово...",
|
||||
"Error loading page!": "Помилка завантаження сторінки!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"No Internet": "Không có Internet",
|
||||
"Switch-Handheld!": "Switch-Handheld!",
|
||||
"Switch-Docked!": "Switch-Docked!",
|
||||
"Warning! Logs are enabled, Sphaira will run slowly!": "",
|
||||
"Audio disabled due to suspended game": "",
|
||||
"Are you sure you wish to cancel?": "Bạn có chắn muốn huỷ không?",
|
||||
"An error occurred": "",
|
||||
@@ -27,6 +28,8 @@
|
||||
"Nxlink Connected": "Nxlink Kết Nối",
|
||||
"Nxlink Upload": "Nxlink Đăng Tải",
|
||||
"Nxlink Finished": "Nxlink Hoàn Thành",
|
||||
"Hdd": "",
|
||||
"Hdd write protect": "",
|
||||
|
||||
"Language": "Ngôn ngữ",
|
||||
"Auto": "Tự động",
|
||||
@@ -57,22 +60,35 @@
|
||||
"No meta entries found...\n": "",
|
||||
"Updating application record list": "",
|
||||
"Dump": "",
|
||||
"Dump options": "",
|
||||
"Dump Options": "",
|
||||
"Select content to dump": "",
|
||||
"Dump All": "",
|
||||
"Dump Application": "",
|
||||
"Dump Patch": "",
|
||||
"Dump AddOnContent": "",
|
||||
"Dump DataPatch": "",
|
||||
"Created nested folder": "",
|
||||
"Append folder with .xci": "",
|
||||
"Trim XCI": "",
|
||||
"Label trimmed XCI": "",
|
||||
"Multi-threaded USB transfer": "",
|
||||
"Dump All Bins": "",
|
||||
"Dump XCI": "",
|
||||
"Dump Card ID Set": "",
|
||||
"Dump Card UID": "",
|
||||
"Dump Certificate": "",
|
||||
"Dump Initial Data": "",
|
||||
"Select dump location": "",
|
||||
"microSD card (/dumps/NSP/)": "",
|
||||
"microSD card (/dumps/)": "",
|
||||
"USB transfer (Switch 2 Switch)": "",
|
||||
"/dev/null (Speed Test)": "",
|
||||
"Dumping": "",
|
||||
"Dump successfull!": "",
|
||||
"Dump failed!": "",
|
||||
"Success": "",
|
||||
"Delete successfull!": "",
|
||||
"Delete failed!": "",
|
||||
"Success": "",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Tuỳ chọn Themezer",
|
||||
@@ -86,9 +102,10 @@
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Đang tải json",
|
||||
"Select asset to download for ": "Chọn nội dung để tải xuống cho ",
|
||||
"Failed to download json": "",
|
||||
"Failed to download app!": "",
|
||||
|
||||
"FTP Install": "",
|
||||
"FTP Install (EXPERIMENTAL)": "",
|
||||
"Connection Type: WiFi | Strength: ": "",
|
||||
"Connection Type: Ethernet": "",
|
||||
"Connection Type: None": "",
|
||||
@@ -98,9 +115,9 @@
|
||||
"Password:": "",
|
||||
"SSID:": "",
|
||||
"Passphrase:": "",
|
||||
"Failed to install via FTP, press B to exit...": "",
|
||||
"Ftp install success!": "",
|
||||
"Ftp install failed!": "",
|
||||
"Failed to install, press B to exit...": "",
|
||||
"Install success!": "",
|
||||
"Install failed!": "",
|
||||
"USB Install": "",
|
||||
"USB": "",
|
||||
"Connected, waiting for file list...": "",
|
||||
@@ -122,9 +139,12 @@
|
||||
"GC": "",
|
||||
"System memory %.1f GB": "",
|
||||
"microSD card %.1f GB": "",
|
||||
"Nand Install": "",
|
||||
"SD Card Install": "",
|
||||
"Exit": "",
|
||||
"Install disabled...\nPlease enable installing via the install options.": "",
|
||||
"No GameCard inserted": "",
|
||||
"GameCard is already trimmed!": "",
|
||||
"WARNING: GameCard is already trimmed!": "",
|
||||
"Continue": "",
|
||||
"Gc install success!": "",
|
||||
"Gc install failed!": "",
|
||||
|
||||
@@ -160,14 +180,14 @@
|
||||
"Negative image": "Ảnh âm bản",
|
||||
"Format": "Định dạng",
|
||||
"Trimming Format": "Định dạng cắt tỉa",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"External Light Filter": "Bộ lộc ánh sáng bên ngoài",
|
||||
"Load Default": "Tải mặc định",
|
||||
|
||||
"Web": "",
|
||||
"Select URL": "",
|
||||
"Enter custom URL": "",
|
||||
"Enter URL": "",
|
||||
|
||||
"Advanced": "Mở rộng",
|
||||
"Advanced Options": "Tuỳ chọn mở rộng",
|
||||
"Logging": "Logging",
|
||||
@@ -181,10 +201,12 @@
|
||||
"Restored hbmenu": "Đã khôi phục hbmenu",
|
||||
"Restart Sphaira?": "Khởi động lại Sphaira?",
|
||||
"Press OK to restart Sphaira": "",
|
||||
"Boost CPU during transfer": "",
|
||||
"Text scroll speed": "",
|
||||
"Slow": "",
|
||||
"Normal": "",
|
||||
"Fast": "",
|
||||
"Set left-side menu": "",
|
||||
"Set right-side menu": "",
|
||||
"Install options": "",
|
||||
"Install Options": "",
|
||||
@@ -194,7 +216,6 @@
|
||||
"Install location": "Vị trí cài đặt",
|
||||
"System memory": "Bộ nhớ máy",
|
||||
"microSD card": "Thẻ nhớ",
|
||||
"Boost CPU clock": "",
|
||||
"Allow downgrade": "",
|
||||
"Skip if already installed": "",
|
||||
"Ticket only": "",
|
||||
@@ -222,9 +243,11 @@
|
||||
"Creating Control": "Tạo điều khiển",
|
||||
"Creating Meta": "Tạo Meta",
|
||||
"Writing Nca": "Ghi Nca",
|
||||
"Updating ncm databse": "Cập nhật ncm databse",
|
||||
"Updating ncm database": "Cập nhật ncm database",
|
||||
"Pushing application record": "Đẩy ứng dụng",
|
||||
"Failed to install forwarder": "Cài đặt ra ngoài màn hình thất bại",
|
||||
"Unstar": "Xoá yêu thích",
|
||||
"Star": "Yêu thích",
|
||||
"Unstarred ": "Bỏ yêu thích ",
|
||||
"Starred ": "Đã yêu thích ",
|
||||
"Failed to remove old forwarder, please manually remove it!": "",
|
||||
@@ -257,12 +280,13 @@
|
||||
"Copy": "Sao chép",
|
||||
"Copying ": "Đang sao chép ",
|
||||
"Paste": "Dán",
|
||||
"Paste ": "Paste ",
|
||||
" file(s)?": " tập tin(nhiều)?",
|
||||
"Paste file(s)?": "",
|
||||
"Pasting ": "Đang dán ",
|
||||
"Pasting": "Đang dán",
|
||||
"Rename": "Đổi tên",
|
||||
"Set New File Name": "Đặt tên mới cho tập tin",
|
||||
"Failed to delete directory": "",
|
||||
"Failed to delete file": "",
|
||||
"Extract zip": "",
|
||||
"Extract Options": "",
|
||||
"Extract here": "",
|
||||
@@ -285,13 +309,17 @@
|
||||
"Create Folder": "Tạo thư mục",
|
||||
"Set Folder Name": "Đặt tên thư mục",
|
||||
"Creating ": "Đang tạo ",
|
||||
"View as text (unfinished)": "Xem dạng văn bản (chưa xong)",
|
||||
"Upload": "",
|
||||
"Select upload location": "",
|
||||
"No upload locations set!": "",
|
||||
"Uploading": "",
|
||||
"Upload successfull!": "",
|
||||
"Upload failed!": "",
|
||||
"View as text (unfinished)": "Xem dạng văn bản (chưa xong)",
|
||||
"Hash": "",
|
||||
"Hash Options": "",
|
||||
"Hashing": "",
|
||||
"Failed to hash file...": "",
|
||||
"Ignore read only": "Bỏ qua chỉ đọc",
|
||||
"Mount": "Gắn",
|
||||
"Sd": "Sd",
|
||||
@@ -302,6 +330,7 @@
|
||||
"Launch ": "Chạy ",
|
||||
"Launch option for: ": "Chạy với tuỳ chọn cho: ",
|
||||
"Select launcher for: ": "Chọn trình chạy cho: ",
|
||||
"Close FileBrowser?": "",
|
||||
|
||||
"Sort By": "Sắp xếp bởi",
|
||||
"Sort Options": "Tuỳ chọn sắp xếp",
|
||||
@@ -335,16 +364,16 @@
|
||||
"Search": "Tìm kiếm",
|
||||
|
||||
"Options": "Tuỳ chọn",
|
||||
"Split": "",
|
||||
"OK": "OK",
|
||||
"Back": "Trở về",
|
||||
"Select": "Chọn",
|
||||
"Open": "Mở",
|
||||
"Close": "",
|
||||
"Launch": "Chạy",
|
||||
"Restart": "Khởi động lại",
|
||||
"Next": "",
|
||||
"Prev": "",
|
||||
"Unstar": "Xoá yêu thích",
|
||||
"Star": "Yêu thích",
|
||||
"Yes": "Có",
|
||||
"No": "Không",
|
||||
"On": "",
|
||||
@@ -367,12 +396,13 @@
|
||||
"Remove": "Gỡ",
|
||||
"Completely remove ": "Đã gỡ thành công ",
|
||||
"Removing ": "Đang gỡ ",
|
||||
"Removed ": "Đã gỡ ",
|
||||
"Uninstalling ": "Đang gỡ cài đặt ",
|
||||
"Removed ": "Đã gỡ ",
|
||||
|
||||
"Download": "Tải về",
|
||||
"Downloading ": "Đang tải xuống ",
|
||||
"Downloaded ": "Đã tải xong ",
|
||||
"Download via the Network options!": "",
|
||||
|
||||
"Update": "Cập nhật",
|
||||
"Update avaliable: ": "Cập nhậc có sẵn: ",
|
||||
@@ -389,4 +419,4 @@
|
||||
"Empty!": "Trống!",
|
||||
"Not Ready...": "Chưa sẵn sàng...",
|
||||
"Error loading page!": "Lỗi tải trang!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
"No Internet": "网络未连接",
|
||||
"Switch-Handheld!": "切换至掌机模式!",
|
||||
"Switch-Docked!": "切换至底座模式!",
|
||||
"Warning! Logs are enabled, Sphaira will run slowly!": "警告!日志已启用,将导致Sphaira运行缓慢!",
|
||||
"Audio disabled due to suspended game": "由于游戏暂停,音频已禁用",
|
||||
"Are you sure you wish to cancel?": "您确定要取消吗?",
|
||||
"An error occurred": "",
|
||||
"An error occurred": "发生错误",
|
||||
"If this message appears repeatedly, please open an issue.": "若此消息反复出现,请提交问题报告。",
|
||||
|
||||
"Menu Options": "菜单选项",
|
||||
@@ -16,8 +17,8 @@
|
||||
"Music": "音乐",
|
||||
"12 Hour Time": "12小时制时间",
|
||||
"Download Default Music": "下载默认音乐",
|
||||
"Failed to download default_music.bfstm, please try again": "",
|
||||
"Overwrite current default music?": "",
|
||||
"Failed to download default_music.bfstm, please try again": "下载default_music.bfstm失败,请重试",
|
||||
"Overwrite current default music?": "覆盖当前默认音乐?",
|
||||
|
||||
"Network": "网络",
|
||||
"Network Options": "网络选项",
|
||||
@@ -27,6 +28,8 @@
|
||||
"Nxlink Connected": "Nxlink 已连接",
|
||||
"Nxlink Upload": "Nxlink 上传中",
|
||||
"Nxlink Finished": "Nxlink 已结束",
|
||||
"Hdd": "HDD",
|
||||
"Hdd write protect": "HDD写保护",
|
||||
|
||||
"Language": "语言",
|
||||
"Auto": "自动",
|
||||
@@ -46,33 +49,57 @@
|
||||
"Ukrainian": "Українська",
|
||||
|
||||
"Misc": "拓展",
|
||||
"Misc Options": "拓展设置",
|
||||
"Misc Options": "拓展选项",
|
||||
"Games": "游戏",
|
||||
"Game Options": "游戏选项",
|
||||
"Hide forwarders": "隐藏前端启动",
|
||||
"Launch random game": "开启随机游戏",
|
||||
"Launch random game": "启动随机游戏",
|
||||
"List meta records": "列出元数据记录",
|
||||
"Entries": "条目",
|
||||
"Failed to list application meta entries": "",
|
||||
"No meta entries found...\n": "",
|
||||
"Updating application record list": "",
|
||||
"Dump": "",
|
||||
"Select content to dump": "",
|
||||
"Dump All": "",
|
||||
"Dump Application": "",
|
||||
"Dump Patch": "",
|
||||
"Dump AddOnContent": "",
|
||||
"Dump DataPatch": "",
|
||||
"Select dump location": "",
|
||||
"microSD card (/dumps/NSP/)": "",
|
||||
"USB transfer (Switch 2 Switch)": "",
|
||||
"/dev/null (Speed Test)": "",
|
||||
"Dumping": "",
|
||||
"Dump successfull!": "",
|
||||
"Dump failed!": "",
|
||||
"Success": "",
|
||||
"Delete successfull!": "",
|
||||
"Delete failed!": "",
|
||||
"Failed to list application meta entries": "未能列出应用程序元条目",
|
||||
"No meta entries found...\n": "未找到元条目...\n",
|
||||
"Updating application record list": "更新应用程序记录列表",
|
||||
"Dump": "转储",
|
||||
"Select content to dump": "选择要转储的内容",
|
||||
"Dump All": "转储全部内容",
|
||||
"Dump Application": "转储应用程序(本体)",
|
||||
"Dump Patch": "转储更新补丁(UPD)",
|
||||
"Dump AddOnContent": "转储追加内容(DLC)",
|
||||
"Dump DataPatch": "转储数据补丁",
|
||||
"Dump All Bins": "转储全部文件",
|
||||
"Dump XCI": "转储.xci",
|
||||
"Dump Card ID Set": "转储游戏卡ID",
|
||||
"Dump Card UID": "转储游戏卡UID",
|
||||
"Dump Certificate": "转储游戏证书",
|
||||
"Dump Initial Data": "转储初始数据",
|
||||
"Select dump location": "选择转储位置",
|
||||
"microSD card (/dumps/NSP/)": "microSD卡(/dumps/NSP/)",
|
||||
"USB transfer (Switch 2 Switch)": "USB传输(Switch对Switch)",
|
||||
"/dev/null (Speed Test)": "/dev/null(速度测试)",
|
||||
"Dumping": "正在转储",
|
||||
"Dump successfull!": "转储成功!",
|
||||
"Dump failed!": "转储失败!",
|
||||
"Success": "成功",
|
||||
"Delete successfull!": "删除成功!",
|
||||
"Delete failed!": "删除失败!",
|
||||
"Title cache": "标题缓存",
|
||||
|
||||
"Saves": "存档",
|
||||
"Save Options": "存档选项",
|
||||
"Account": "账户",
|
||||
"Backup": "备份",
|
||||
"Auto backup": "自动备份",
|
||||
"Auto backup on restore": "还原时自动备份",
|
||||
"Compress backup": "压缩备份",
|
||||
"Are you sure you want to backup save(s)?": "您确定要备份存档吗?",
|
||||
"No saves found in ": "未找到存档 ",
|
||||
"Backed up to ": "已备份到 ",
|
||||
"Backup successfull!": "已备份成功!",
|
||||
"Backup failed!": "备份失败!",
|
||||
"Restore save for: ": "还原备份文件: ",
|
||||
"Are you sure you want to restore ": "您确定要还原已备份文件吗 ",
|
||||
"Restore successfull!": "已还原成功!",
|
||||
"Restore failed!": "还原失败!",
|
||||
|
||||
"Themezer": "在线主题",
|
||||
"Themezer Options": "在线主题选项",
|
||||
@@ -86,10 +113,11 @@
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "正在下载 json",
|
||||
"Select asset to download for ": "选择要下载的资源用于 ",
|
||||
"Failed to download json": "下载json失败",
|
||||
"Failed to download app!": "下载应用程序失败!",
|
||||
|
||||
"FTP Install": "通过 FTP 安装",
|
||||
"FTP Install (EXPERIMENTAL)": "通过 FTP 安装(实验性)",
|
||||
"Connection Type: WiFi | Strength: ": "",
|
||||
"Connection Type: WiFi | Strength: ": "连接类型:WiFi |强度:",
|
||||
"Connection Type: Ethernet": "连接类型:以太网",
|
||||
"Connection Type: None": "连接类型:无",
|
||||
"Host:": "主机:",
|
||||
@@ -98,45 +126,48 @@
|
||||
"Password:": "密码:",
|
||||
"SSID:": "网络名称:",
|
||||
"Passphrase:": "密码:",
|
||||
"Failed to install via FTP, press B to exit...": "通过 FTP 安装失败,按 B 键退出...",
|
||||
"Ftp install success!": "通过 FTP 安装成功。",
|
||||
"Ftp install failed!": "通过 FTP 安装失败。",
|
||||
"Failed to install, press B to exit...": "通过 FTP 安装失败,按 B 键退出...",
|
||||
"Install success!": "通过 FTP 安装成功。",
|
||||
"Install failed!": "通过 FTP 安装失败。",
|
||||
"USB Install": "通过 USB 安装",
|
||||
"USB": "USB",
|
||||
"Connected, waiting for file list...": "",
|
||||
"Connected, starting transfer...": "",
|
||||
"Connected, waiting for file list...": "已连接,正在等待文件列表...",
|
||||
"Connected, starting transfer...": "已连接,开始传输...",
|
||||
"Failed to init usb, press B to exit...": "USB 初始化失败,按 B 键退出...",
|
||||
"Waiting for connection...": "等待连接中...",
|
||||
"Transferring data...": "正在传输数据...",
|
||||
"USB connected, sending file list": "",
|
||||
"Sent file list, waiting for command...": "",
|
||||
"waiting for usb connection...": "",
|
||||
"USB connected, sending file list": "USB已连接,正在发送文件列表",
|
||||
"Sent file list, waiting for command...": "已发送文件列表,正在等待命令...",
|
||||
"waiting for usb connection...": "等待USB连接...",
|
||||
"Disable MTP for usb install": "暂时禁用 USB 安装的 MTP 功能",
|
||||
"Re-enabled MTP": "重新启用 MTP",
|
||||
"Installed via usb": "通过 USB 安装",
|
||||
"Usb install success!": "通过 USB 安装成功。",
|
||||
"Usb install failed!": "通过 USB 安装失败。",
|
||||
"Press B to exit...": "按 B 键退出...",
|
||||
"GameCard Install": "卡带安装",
|
||||
"GameCard": "卡带",
|
||||
"GC": "",
|
||||
"System memory %.1f GB": "",
|
||||
"microSD card %.1f GB": "",
|
||||
"Nand Install": "",
|
||||
"SD Card Install": "",
|
||||
"Exit": "",
|
||||
"GameCard Install": "游戏卡安装",
|
||||
"GameCard": "游戏卡",
|
||||
"GC": "GC",
|
||||
"System memory %.1f GB": "主机内存 %.1f GB",
|
||||
"microSD card %.1f GB": "SD卡 %.1f GB",
|
||||
"Exit": "退出",
|
||||
"Install disabled...\nPlease enable installing via the install options.": "安装已禁用...\n请通过安装选项启用安装。",
|
||||
"No GameCard inserted": "未插入游戏卡",
|
||||
"GameCard is already trimmed!": "游戏卡已被修剪!",
|
||||
"WARNING: GameCard is already trimmed!": "警告:游戏卡已被修剪!",
|
||||
"Continue": "继续",
|
||||
"Gc install success!": "游戏安装成功。",
|
||||
"Gc install failed!": "游戏安装失败。",
|
||||
|
||||
"IRS (Infrared Joycon Camera)": "",
|
||||
"IRS": "",
|
||||
"IRS (Infrared Joycon Camera)": "IRS(Joycon红外摄像头)",
|
||||
"IRS": "IRS",
|
||||
"Irs": "红外成像",
|
||||
"Ambient Noise Level: ": "环境噪声等级:",
|
||||
"Controller": "控制器",
|
||||
"Pad ": "手柄 ",
|
||||
"HandHeld": "掌机模式",
|
||||
" (Available)": " (可用的)",
|
||||
" (Unsupported)": " (不支持的)",
|
||||
" (Available)": " (可用)",
|
||||
" (Unsupported)": " (不支持)",
|
||||
" (Unconnected)": " (未连接)",
|
||||
"Rotation": "旋转",
|
||||
"0 (Sideways)": "0度",
|
||||
@@ -159,7 +190,7 @@
|
||||
"Normal image": "正常图像",
|
||||
"Negative image": "负片图像",
|
||||
"Format": "格式",
|
||||
"Trimming Format": "裁剪格式",
|
||||
"Trimming Format": "修剪格式",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
@@ -168,6 +199,11 @@
|
||||
"External Light Filter": "外部光滤镜",
|
||||
"Load Default": "加载默认值",
|
||||
|
||||
"Web": "网络",
|
||||
"Select URL": "选择链接",
|
||||
"Enter custom URL": "输入自定义链接",
|
||||
"Enter URL": "输入网址",
|
||||
|
||||
"Advanced": "高级",
|
||||
"Advanced Options": "高级选项",
|
||||
"Logging": "日志",
|
||||
@@ -181,15 +217,17 @@
|
||||
"Restored hbmenu": "已恢复 hbmenu",
|
||||
"Restart Sphaira?": "重启 Sphaira?",
|
||||
"Press OK to restart Sphaira": "按OK键以重启shphaira菜单",
|
||||
"Boost CPU during transfer": "传输时提升CPU频率",
|
||||
"Text scroll speed": "文本滚动速度",
|
||||
"Slow": "慢",
|
||||
"Normal": "正常",
|
||||
"Fast": "快",
|
||||
"Set right-side menu": "",
|
||||
"Set left-side menu": "设置左侧菜单",
|
||||
"Set right-side menu": "设置右侧菜单",
|
||||
"Install options": "安装选项",
|
||||
"Install Options": "安装选项",
|
||||
"Enable sysmmc": "",
|
||||
"Enable emummc": "",
|
||||
"Enable sysmmc": "启用sysmmc",
|
||||
"Enable emummc": "启用emummc",
|
||||
"Show install warning": "显示安装警告",
|
||||
"Install location": "安装位置",
|
||||
"System memory": "主机内存",
|
||||
@@ -199,17 +237,24 @@
|
||||
"Skip if already installed": "若已安装则跳过",
|
||||
"Ticket only": "仅安装票据",
|
||||
"Skip base": "跳过基础部分",
|
||||
"Skip patch": "跳过补丁",
|
||||
"Skip dlc": "跳过 DLC(可下载内容)",
|
||||
"Skip patch": "跳过更新补丁(UPD)",
|
||||
"Skip dlc": "跳过追加内容(DLC)",
|
||||
"Skip data patch": "跳过数据补丁",
|
||||
"Skip ticket": "跳过票据",
|
||||
"Skip NCA hash verify": "",
|
||||
"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": "降低系统版本",
|
||||
"Dump options": "转储选项",
|
||||
"Dump Options": "转储选项",
|
||||
"Created nested folder": "已创建嵌套文件夹",
|
||||
"Append folder with .xci": "用.xci附加文件夹",
|
||||
"Trim XCI": "修剪 XCI",
|
||||
"Label trimmed XCI": "标记已修剪的XCI",
|
||||
"Multi-threaded USB transfer": "多线程USB传输",
|
||||
|
||||
"Homebrew": "应用列表",
|
||||
"Apps": "应用",
|
||||
@@ -222,15 +267,17 @@
|
||||
"Creating Control": "正在创建控制器",
|
||||
"Creating Meta": "正在创建元数据",
|
||||
"Writing Nca": "正在写入Nca",
|
||||
"Updating ncm databse": "正在更新ncm数据库",
|
||||
"Updating ncm database": "正在更新ncm数据库",
|
||||
"Pushing application record": "正在推送应用记录",
|
||||
"Failed to install forwarder": "前端应用安装失败",
|
||||
"Unstar": "取消星标",
|
||||
"Star": "星标",
|
||||
"Unstarred ": "取消星标 ",
|
||||
"Starred ": "已星标 ",
|
||||
"Failed to remove old forwarder, please manually remove it!": "",
|
||||
"Failed to remove old forwarder, please manually remove it!": "删除旧前端应用失败,请手动删除!",
|
||||
|
||||
"AppStore": "应用商店",
|
||||
"Appstore": "",
|
||||
"Appstore": "应用商店",
|
||||
"Store": "商店",
|
||||
"Filter: %s | Sort: %s | Order: %s": "筛选: %s | 排序: %s | 顺序: %s",
|
||||
"AppStore Options": "应用商店选项",
|
||||
@@ -257,41 +304,46 @@
|
||||
"Copy": "复制",
|
||||
"Copying ": "正在复制 ",
|
||||
"Paste": "粘贴",
|
||||
"Paste ": "粘贴 ",
|
||||
" file(s)?": "个文件(夹)?",
|
||||
"Paste file(s)?": "粘贴 个文件(夹)?",
|
||||
"Pasting ": "正在粘贴 ",
|
||||
"Pasting": "正在粘贴",
|
||||
"Rename": "重命名",
|
||||
"Set New File Name": "输入新命名",
|
||||
"Extract zip": "",
|
||||
"Extract Options": "",
|
||||
"Extract here": "",
|
||||
"Extract to root": "",
|
||||
"Are you sure you want to extract to root?": "",
|
||||
"Extract to...": "",
|
||||
"Enter the path to the folder to extract into": "",
|
||||
"Extracting ": "",
|
||||
"Extract success!": "",
|
||||
"Extract failed!": "",
|
||||
"Failed to delete directory": "删除目录失败",
|
||||
"Failed to delete file": "删除文件失败",
|
||||
"Extract zip": "解压 zip",
|
||||
"Extract Options": "解压选项",
|
||||
"Extract here": "解压到这里",
|
||||
"Extract to root": "解压到根目录",
|
||||
"Are you sure you want to extract to root?": "您确定要解压到根目录吗?",
|
||||
"Extract to...": "解压到...",
|
||||
"Enter the path to the folder to extract into": "输入要解压到的文件夹的路径",
|
||||
"Extracting ": "正在解压",
|
||||
"Extract success!": "解压成功!",
|
||||
"Extract failed!": "解压失败!",
|
||||
"Compress to zip": "压缩到zip",
|
||||
"Compress Options": "",
|
||||
"Compress": "",
|
||||
"Compress to...": "",
|
||||
"Compressing ": "",
|
||||
"Compress success!": "",
|
||||
"Compress failed!": "",
|
||||
"Compress Options": "压缩选项",
|
||||
"Compress": "压缩",
|
||||
"Compress to...": "压缩到...",
|
||||
"Compressing ": "正在压缩 ",
|
||||
"Compress success!": "压缩成功!",
|
||||
"Compress failed!": "压缩失败",
|
||||
"Create File": "新建文件",
|
||||
"Set File Name": "输入文件名",
|
||||
"Create Folder": "新建文件夹",
|
||||
"Set Folder Name": "输入文件夹名",
|
||||
"Creating ": "正在创建 ",
|
||||
"Upload": "",
|
||||
"Select upload location": "",
|
||||
"No upload locations set!": "",
|
||||
"Uploading": "",
|
||||
"Upload successfull!": "",
|
||||
"Upload failed!": "",
|
||||
"View as text (unfinished)": "以文本形式查看(未完善)",
|
||||
"Upload": "上传",
|
||||
"Select upload location": "选择上传位置",
|
||||
"No upload locations set!": "未设置上传位置",
|
||||
"Uploading": "正在上传",
|
||||
"Upload successfull!": "上传成功!",
|
||||
"Upload failed!": "上传失败!",
|
||||
"Hash": "哈希",
|
||||
"Hash Options": "哈希选项",
|
||||
"Hashing": "正在计算文件哈希",
|
||||
"Failed to hash file...": "计算文件哈希失败...",
|
||||
"Ignore read only": "忽略只读",
|
||||
"Mount": "挂载",
|
||||
"Sd": "SD卡",
|
||||
@@ -302,6 +354,7 @@
|
||||
"Launch ": "启动 ",
|
||||
"Launch option for: ": "启动选项:",
|
||||
"Select launcher for: ": "选择启动器用于:",
|
||||
"Close FileBrowser?": "是否关闭文件浏览器?",
|
||||
|
||||
"Sort By": "排序方式",
|
||||
"Sort Options": "排序选项",
|
||||
@@ -328,32 +381,32 @@
|
||||
"Ascending": "升序",
|
||||
"Ascending (Up)": "升序",
|
||||
"Asc": "升序",
|
||||
"Layout": "",
|
||||
"List": "",
|
||||
"Icon": "",
|
||||
"Grid": "",
|
||||
"Layout": "布局",
|
||||
"List": "列表",
|
||||
"Icon": "图标",
|
||||
"Grid": "网格",
|
||||
"Search": "搜索",
|
||||
|
||||
"Options": "选项",
|
||||
"Split": "拆分",
|
||||
"OK": "确定",
|
||||
"Back": "返回",
|
||||
"Select": "选择",
|
||||
"Open": "打开",
|
||||
"Close": "关闭",
|
||||
"Launch": "启动",
|
||||
"Restart": "重启",
|
||||
"Next": "下一项",
|
||||
"Prev": "上一项",
|
||||
"Unstar": "取消星标",
|
||||
"Star": "星标",
|
||||
"Yes": "是",
|
||||
"No": "否",
|
||||
"On": "",
|
||||
"Off": "",
|
||||
"On": "开启",
|
||||
"Off": "关闭",
|
||||
|
||||
"Install": "安装",
|
||||
"Install Selected files?": "安装所选文件?",
|
||||
"Installing ": "正在安装 ",
|
||||
"Installed ": "",
|
||||
"Installed ": "已安装",
|
||||
"Installed!": "安装完成!",
|
||||
"Trying to load ": "尝试加载 ",
|
||||
"Checking MD5": "正在校验 MD5",
|
||||
@@ -367,12 +420,13 @@
|
||||
"Remove": "删除",
|
||||
"Completely remove ": "彻底删除 ",
|
||||
"Removing ": "正在移除 ",
|
||||
"Removed ": "已移除 ",
|
||||
"Uninstalling ": "正在卸载 ",
|
||||
"Removed ": "已移除 ",
|
||||
|
||||
"Download": "下载",
|
||||
"Downloading ": "正在下载 ",
|
||||
"Downloaded ": "已下载 ",
|
||||
"Download via the Network options!": "通过网络选项下载!",
|
||||
|
||||
"Update": "更新",
|
||||
"Update avaliable: ": "有可用更新!",
|
||||
@@ -380,13 +434,13 @@
|
||||
"Updated to ": "更新至 ",
|
||||
"Failed to download update": "更新下载失败",
|
||||
|
||||
"%zu hours %zu minutes remaining": "",
|
||||
"%zu minutes %zu seconds remaining": "",
|
||||
"%zu seconds remaining": "",
|
||||
"%zu hours %zu minutes remaining": "剩余 %zu 小时 %zu 分钟",
|
||||
"%zu minutes %zu seconds remaining": "剩余 %zu 分钟 %zu 秒",
|
||||
"%zu seconds remaining": "剩余 %zu 秒",
|
||||
|
||||
"Loading...": "加载中...",
|
||||
"Loading": "加载中",
|
||||
"Empty!": "空空如野!",
|
||||
"Empty!": "空空如也!",
|
||||
"Not Ready...": "尚未准备好...",
|
||||
"Error loading page!": "页面加载失败!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ background = 0x2d2d2d
|
||||
grid = 0x46464630
|
||||
popup = 0x2d2d2d
|
||||
error = 0xfa5a3a
|
||||
focus = 0x000000b4
|
||||
|
||||
line = 0xfbfbfb
|
||||
line_separator = 0x707070
|
||||
|
||||
@@ -3,6 +3,7 @@ background = 0xebebeb
|
||||
grid = 0xf0f0f0
|
||||
popup = 0xebebeb
|
||||
error = 0xfa5a3a
|
||||
focus = 0xd3d3d3a0
|
||||
|
||||
line = 0x373737
|
||||
line_separator = 0x6d787a
|
||||
|
||||
30
assets/romfs/themes/default_theme.ini
Normal file
@@ -0,0 +1,30 @@
|
||||
[meta]
|
||||
name=Default
|
||||
author=ninstar
|
||||
version=1.0.0
|
||||
inherit=romfs:/themes/base_black_theme.ini
|
||||
|
||||
[theme]
|
||||
background = 0x111f28
|
||||
grid = 0x122430
|
||||
popup = 0x143144
|
||||
error = 0xfa3a5d
|
||||
|
||||
line = 0x335e77
|
||||
line_separator = 0x163951
|
||||
|
||||
text = 0xfbfbfb
|
||||
text_info = 0xbed0d6
|
||||
text_selected = 0x32ffcf
|
||||
selected_background = 0x143144
|
||||
|
||||
sidebar = 0x071013ef
|
||||
|
||||
scrollbar = 0x32ffcf
|
||||
scrollbar_background = ; hide the background
|
||||
|
||||
progressbar = 0x32ffcf
|
||||
progressbar_background = 0x0B1519
|
||||
|
||||
highlight_1 = 0x69ff8f
|
||||
highlight_2 = 0x5cbeff
|
||||
|
Before Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 116 KiB |
BIN
assets/screenshots/appstore.jpg
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
assets/screenshots/appstore_page.jpg
Normal file
|
After Width: | Height: | Size: 235 KiB |
BIN
assets/screenshots/file_browser.jpg
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
assets/screenshots/games.jpg
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
assets/screenshots/homebrew.jpg
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
assets/screenshots/launch_options.jpg
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
assets/screenshots/themezer.jpg
Normal file
|
After Width: | Height: | Size: 246 KiB |
BIN
assets/screenshots/web.jpg
Normal file
|
After Width: | Height: | Size: 100 KiB |
@@ -1,6 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
set(sphaira_VERSION 0.11.2)
|
||||
set(sphaira_VERSION 0.13.3)
|
||||
|
||||
project(sphaira
|
||||
VERSION ${sphaira_VERSION}
|
||||
@@ -40,13 +40,16 @@ add_executable(sphaira
|
||||
source/ui/menus/irs_menu.cpp
|
||||
source/ui/menus/main_menu.cpp
|
||||
source/ui/menus/menu_base.cpp
|
||||
source/ui/menus/save_menu.cpp
|
||||
source/ui/menus/themezer.cpp
|
||||
source/ui/menus/ghdl.cpp
|
||||
source/ui/menus/usb_menu.cpp
|
||||
source/ui/menus/ftp_menu.cpp
|
||||
source/ui/menus/mtp_menu.cpp
|
||||
source/ui/menus/gc_menu.cpp
|
||||
source/ui/menus/game_menu.cpp
|
||||
source/ui/menus/grid_menu_base.cpp
|
||||
source/ui/menus/install_stream_menu_base.cpp
|
||||
|
||||
source/ui/error_box.cpp
|
||||
source/ui/notification.cpp
|
||||
@@ -74,10 +77,13 @@ add_executable(sphaira
|
||||
source/nxlink.cpp
|
||||
source/owo.cpp
|
||||
source/swkbd.cpp
|
||||
source/web.cpp
|
||||
source/hasher.cpp
|
||||
source/i18n.cpp
|
||||
source/ftpsrv_helper.cpp
|
||||
source/haze_helper.cpp
|
||||
source/threaded_file_transfer.cpp
|
||||
source/minizip_helper.cpp
|
||||
|
||||
source/usb/base.cpp
|
||||
source/usb/usbds.cpp
|
||||
@@ -151,13 +157,13 @@ set(FETCHCONTENT_QUIET FALSE)
|
||||
FetchContent_Declare(ftpsrv
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
|
||||
# GIT_TAG 1.2.2
|
||||
GIT_TAG 8c18431
|
||||
GIT_TAG 8782f6b
|
||||
SOURCE_SUBDIR NONE
|
||||
)
|
||||
|
||||
FetchContent_Declare(libhaze
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git
|
||||
GIT_TAG 8e16df2
|
||||
GIT_TAG af69c0a
|
||||
)
|
||||
|
||||
FetchContent_Declare(libpulsar
|
||||
@@ -177,7 +183,7 @@ FetchContent_Declare(stb
|
||||
|
||||
FetchContent_Declare(yyjson
|
||||
GIT_REPOSITORY https://github.com/ibireme/yyjson.git
|
||||
GIT_TAG 0.10.0
|
||||
GIT_TAG 0.11.1
|
||||
)
|
||||
|
||||
FetchContent_Declare(minIni
|
||||
@@ -193,10 +199,22 @@ FetchContent_Declare(zstd
|
||||
|
||||
FetchContent_Declare(libusbhsfs
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/libusbhsfs.git
|
||||
GIT_TAG db2bf2a
|
||||
GIT_TAG d0a973e
|
||||
)
|
||||
|
||||
FetchContent_Declare(libnxtc
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/libnxtc.git
|
||||
GIT_TAG 0d369b8
|
||||
)
|
||||
|
||||
FetchContent_Declare(nvjpg
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/oss-nvjpg.git
|
||||
GIT_TAG 45680e7
|
||||
)
|
||||
|
||||
set(USE_NEW_ZSTD ON)
|
||||
# has issues with some homebrew and game icons (oxenfree, overwatch2).
|
||||
set(USE_NVJPG ON)
|
||||
|
||||
set(ZSTD_BUILD_STATIC ON)
|
||||
set(ZSTD_BUILD_SHARED OFF)
|
||||
@@ -248,17 +266,18 @@ FetchContent_MakeAvailable(
|
||||
yyjson
|
||||
zstd
|
||||
libusbhsfs
|
||||
libnxtc
|
||||
nvjpg
|
||||
)
|
||||
|
||||
set(FTPSRV_LIB_BUILD TRUE)
|
||||
set(FTPSRV_LIB_SOCK_UNISTD TRUE)
|
||||
set(FTPSRV_LIB_VFS_CUSTOM ${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs_nx.h)
|
||||
set(FTPSRV_LIB_PATH_SIZE 0x301)
|
||||
set(FTPSRV_LIB_SESSIONS 16)
|
||||
set(FTPSRV_LIB_BUF_SIZE 1024*64)
|
||||
|
||||
set(FTPSRV_LIB_CUSTOM_DEFINES
|
||||
USE_VFS_SAVE=$<BOOL:TRUE>
|
||||
USE_VFS_SAVE=$<BOOL:FALSE>
|
||||
USE_VFS_STORAGE=$<BOOL:TRUE>
|
||||
# disabled as it may conflict with the gamecard menu.
|
||||
USE_VFS_GC=$<BOOL:FALSE>
|
||||
@@ -268,6 +287,7 @@ set(FTPSRV_LIB_CUSTOM_DEFINES
|
||||
USE_VFS_USBHSFS_INIT=$<BOOL:FALSE>
|
||||
# disable romfs mounting as otherwise we cannot write / modify sphaira.nro
|
||||
USE_VFS_ROMFS=$<BOOL:FALSE>
|
||||
FTP_SOCKET_HEADER="${ftpsrv_SOURCE_DIR}/src/platform/nx/socket_nx.h"
|
||||
)
|
||||
|
||||
add_subdirectory(${ftpsrv_SOURCE_DIR} binary_dir)
|
||||
@@ -277,7 +297,6 @@ add_library(ftpsrv_helper
|
||||
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_none.c
|
||||
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_root.c
|
||||
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_fs.c
|
||||
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_save.c
|
||||
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_storage.c
|
||||
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_stdio.c
|
||||
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_hdd.c
|
||||
@@ -290,6 +309,23 @@ target_include_directories(ftpsrv_helper PUBLIC ${ftpsrv_SOURCE_DIR}/src/platfor
|
||||
add_library(stb INTERFACE)
|
||||
target_include_directories(stb INTERFACE ${stb_SOURCE_DIR})
|
||||
|
||||
add_library(libnxtc
|
||||
${libnxtc_SOURCE_DIR}/source/nxtc.c
|
||||
${libnxtc_SOURCE_DIR}/source/nxtc_log.c
|
||||
${libnxtc_SOURCE_DIR}/source/nxtc_utils.c
|
||||
)
|
||||
target_include_directories(libnxtc PUBLIC ${libnxtc_SOURCE_DIR}/include)
|
||||
|
||||
if (USE_NVJPG)
|
||||
add_library(nvjpg
|
||||
${nvjpg_SOURCE_DIR}/lib/decoder.cpp
|
||||
${nvjpg_SOURCE_DIR}/lib/image.cpp
|
||||
${nvjpg_SOURCE_DIR}/lib/surface.cpp
|
||||
)
|
||||
target_include_directories(nvjpg PUBLIC ${nvjpg_SOURCE_DIR}/include)
|
||||
set_target_properties(nvjpg PROPERTIES CXX_STANDARD 26)
|
||||
endif()
|
||||
|
||||
find_package(ZLIB REQUIRED)
|
||||
find_library(minizip_lib minizip REQUIRED)
|
||||
find_path(minizip_inc minizip REQUIRED)
|
||||
@@ -319,6 +355,7 @@ target_link_libraries(sphaira PRIVATE
|
||||
stb
|
||||
yyjson
|
||||
# libusbhsfs
|
||||
libnxtc
|
||||
|
||||
${minizip_lib}
|
||||
ZLIB::ZLIB
|
||||
@@ -335,6 +372,11 @@ else()
|
||||
target_include_directories(sphaira PRIVATE ${zstd_inc})
|
||||
endif()
|
||||
|
||||
if (USE_NVJPG)
|
||||
target_link_libraries(sphaira PRIVATE nvjpg)
|
||||
target_compile_definitions(sphaira PRIVATE USE_NVJPG)
|
||||
endif()
|
||||
|
||||
target_include_directories(sphaira PRIVATE
|
||||
include
|
||||
${minizip_inc}
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
#include "fs.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
#ifdef USE_NVJPG
|
||||
#include <nvjpg.hpp>
|
||||
#endif
|
||||
#include <switch.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
@@ -189,11 +192,59 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
static void SetBoostMode(bool enable, bool force = false) {
|
||||
static Mutex mutex{};
|
||||
static int ref_count{};
|
||||
|
||||
mutexLock(&mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&mutex));
|
||||
|
||||
if (enable) {
|
||||
ref_count++;
|
||||
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
|
||||
} else {
|
||||
if (ref_count) {
|
||||
ref_count--;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ref_count || force) {
|
||||
ref_count = 0;
|
||||
appletSetCpuBoostMode(ApmCpuBoostMode_Normal);
|
||||
}
|
||||
}
|
||||
|
||||
static auto GetAccountList() -> std::vector<AccountProfileBase> {
|
||||
std::vector<AccountProfileBase> out;
|
||||
|
||||
AccountUid uids[ACC_USER_LIST_SIZE];
|
||||
s32 account_count;
|
||||
if (R_SUCCEEDED(accountListAllUsers(uids, std::size(uids), &account_count))) {
|
||||
for (s32 i = 0; i < account_count; i++) {
|
||||
AccountProfile profile;
|
||||
if (R_SUCCEEDED(accountGetProfile(&profile, uids[i]))) {
|
||||
ON_SCOPE_EXIT(accountProfileClose(&profile));
|
||||
|
||||
AccountProfileBase base;
|
||||
if (R_SUCCEEDED(accountProfileGet(&profile, nullptr, &base))) {
|
||||
// sometimes the uid for the acc can differ to the base.
|
||||
base.uid = uids[i];
|
||||
log_write("[ACC] found uid: 0x%016lX%016lX\n", uids[i].uid[0], uids[i].uid[1]);
|
||||
log_write("[ACC] base uid: 0x%016lX%016lX\n", base.uid.uid[0], base.uid.uid[1]);
|
||||
out.emplace_back(base);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// private:
|
||||
static constexpr inline auto CONFIG_PATH = "/config/sphaira/config.ini";
|
||||
static constexpr inline auto PLAYLOG_PATH = "/config/sphaira/playlog.ini";
|
||||
static constexpr inline auto INI_SECTION = "config";
|
||||
static constexpr inline auto DEFAULT_THEME_PATH = "romfs:/themes/abyss_theme.ini";
|
||||
static constexpr inline auto DEFAULT_THEME_PATH = "romfs:/themes/default_theme.ini";
|
||||
|
||||
fs::FsPath m_app_path;
|
||||
u64 m_start_timestamp{};
|
||||
@@ -256,6 +307,7 @@ public:
|
||||
option::OptionBool m_skip_rsa_header_fixed_key_verify{INI_SECTION, "skip_rsa_header_fixed_key_verify", false};
|
||||
option::OptionBool m_skip_rsa_npdm_fixed_key_verify{INI_SECTION, "skip_rsa_npdm_fixed_key_verify", false};
|
||||
option::OptionBool m_ignore_distribution_bit{INI_SECTION, "ignore_distribution_bit", false};
|
||||
option::OptionBool m_convert_to_common_ticket{INI_SECTION, "convert_to_common_ticket", true};
|
||||
option::OptionBool m_convert_to_standard_crypto{INI_SECTION, "convert_to_standard_crypto", false};
|
||||
option::OptionBool m_lower_master_key{INI_SECTION, "lower_master_key", false};
|
||||
option::OptionBool m_lower_system_version{INI_SECTION, "lower_system_version", false};
|
||||
@@ -266,12 +318,17 @@ public:
|
||||
option::OptionBool m_dump_trim_xci{"dump", "trim_xci", false};
|
||||
option::OptionBool m_dump_label_trim_xci{"dump", "label_trim_xci", false};
|
||||
option::OptionBool m_dump_usb_transfer_stream{"dump", "usb_transfer_stream", true};
|
||||
option::OptionBool m_dump_convert_to_common_ticket{"dump", "convert_to_common_ticket", true};
|
||||
|
||||
// todo: move this into it's own menu
|
||||
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal
|
||||
|
||||
PLSR_PlayerSoundId m_sound_ids[SoundEffect_MAX]{};
|
||||
|
||||
#ifdef USE_NVJPG
|
||||
nj::Decoder m_decoder;
|
||||
#endif
|
||||
|
||||
private: // from nanovg decko3d example by adubbz
|
||||
static constexpr unsigned NumFramebuffers = 2;
|
||||
static constexpr unsigned StaticCmdSize = 0x1000;
|
||||
|
||||
@@ -174,6 +174,7 @@ enum {
|
||||
Module_Npln = 321,
|
||||
Module_Tspm = 499,
|
||||
Module_Devmenu = 500,
|
||||
Module_Sphaira = 505,
|
||||
};
|
||||
|
||||
enum SvcError {
|
||||
@@ -494,7 +495,269 @@ enum NcmError {
|
||||
NcmError_WriteToReadOnlyContentStorage = 0x17C05,
|
||||
};
|
||||
|
||||
#define R_SUCCEED() return 0
|
||||
enum class SphairaResult : Result {
|
||||
TransferCancelled,
|
||||
StreamBadSeek,
|
||||
|
||||
FsTooManyEntries,
|
||||
FsNewPathTooLarge,
|
||||
FsInvalidType,
|
||||
FsEmpty,
|
||||
FsAlreadyRoot,
|
||||
FsNoCurrentPath,
|
||||
FsBrokenCurrentPath,
|
||||
FsIndexOutOfBounds,
|
||||
FsFsNotActive,
|
||||
FsNewPathEmpty,
|
||||
FsLoadingCancelled,
|
||||
FsBrokenRoot,
|
||||
FsUnknownStdioError,
|
||||
FsReadOnly,
|
||||
FsNotActive,
|
||||
FsFailedStdioStat,
|
||||
FsFailedStdioOpendir,
|
||||
|
||||
NroBadMagic,
|
||||
NroBadSize,
|
||||
|
||||
AppFailedMusicDownload,
|
||||
CurlFailedEasyInit,
|
||||
DumpFailedNetworkUpload,
|
||||
|
||||
UnzOpen2_64,
|
||||
UnzGetGlobalInfo64,
|
||||
UnzLocateFile,
|
||||
UnzGoToFirstFile,
|
||||
UnzGoToNextFile,
|
||||
UnzOpenCurrentFile,
|
||||
UnzGetCurrentFileInfo64,
|
||||
UnzReadCurrentFile,
|
||||
|
||||
ZipOpen2_64,
|
||||
ZipOpenNewFileInZip,
|
||||
ZipWriteInFileInZip,
|
||||
|
||||
FileBrowserFailedUpload,
|
||||
FileBrowserDirNotDaybreak,
|
||||
|
||||
AppstoreFailedZipDownload,
|
||||
AppstoreFailedMd5,
|
||||
AppstoreFailedParseManifest,
|
||||
|
||||
GameBadReadForDump,
|
||||
GameEmptyMetaEntries,
|
||||
GameMultipleKeysFound,
|
||||
GameNoNspEntriesBuilt,
|
||||
|
||||
KeyMissingNcaKeyArea,
|
||||
KeyMissingTitleKek,
|
||||
KeyMissingMasterKey,
|
||||
KeyFailedDecyptETicketDeviceKey,
|
||||
|
||||
NcaFailedNcaHeaderHashVerify,
|
||||
NcaBadSigKeyGen,
|
||||
|
||||
GcBadReadForDump,
|
||||
GcEmptyGamecard,
|
||||
GcBadXciMagic,
|
||||
GcBadXciRomSize,
|
||||
GcFailedToGetSecurityInfo,
|
||||
|
||||
GhdlEmptyAsset,
|
||||
GhdlFailedToDownloadAsset,
|
||||
GhdlFailedToDownloadAssetJson,
|
||||
|
||||
ThemezerFailedToDownloadThemeMeta,
|
||||
ThemezerFailedToDownloadTheme,
|
||||
|
||||
MainFailedToDownloadUpdate,
|
||||
|
||||
UsbDsBadDeviceSpeed,
|
||||
|
||||
NspBadMagic,
|
||||
XciBadMagic,
|
||||
|
||||
EsBadTitleKeyType,
|
||||
EsPersonalisedTicketDeviceIdMissmatch,
|
||||
EsFailedDecryptPersonalisedTicket,
|
||||
EsBadDecryptedPersonalisedTicketSize,
|
||||
EsBadTicketSize,
|
||||
// found ticket has missmatching rights_id from it's name.
|
||||
EsInvalidTicketBadRightsId,
|
||||
EsInvalidTicketFromatVersion,
|
||||
EsInvalidTicketKeyType,
|
||||
EsInvalidTicketKeyRevision,
|
||||
|
||||
OwoBadArgs,
|
||||
|
||||
UsbCancelled,
|
||||
UsbBadMagic,
|
||||
UsbBadVersion,
|
||||
UsbBadCount,
|
||||
UsbBadTransferSize,
|
||||
UsbBadTotalSize,
|
||||
|
||||
UsbUploadBadMagic,
|
||||
UsbUploadExit,
|
||||
UsbUploadBadCount,
|
||||
UsbUploadBadTransferSize,
|
||||
UsbUploadBadTotalSize,
|
||||
UsbUploadBadCommand,
|
||||
|
||||
// unkown container for the source provided.
|
||||
YatiContainerNotFound,
|
||||
// nca required by the cnmt but not found in collection.
|
||||
YatiNcaNotFound,
|
||||
YatiInvalidNcaReadSize,
|
||||
YatiInvalidNcaSigKeyGen,
|
||||
YatiInvalidNcaMagic,
|
||||
YatiInvalidNcaSignature0,
|
||||
YatiInvalidNcaSignature1,
|
||||
// invalid sha256 over the entire nca.
|
||||
YatiInvalidNcaSha256,
|
||||
// section could not be found.
|
||||
YatiNczSectionNotFound,
|
||||
// section count == 0.
|
||||
YatiInvalidNczSectionCount,
|
||||
// block could not be found.
|
||||
YatiNczBlockNotFound,
|
||||
// block version != 2.
|
||||
YatiInvalidNczBlockVersion,
|
||||
// block type != 1.
|
||||
YatiInvalidNczBlockType,
|
||||
// block count == 0.
|
||||
YatiInvalidNczBlockTotal,
|
||||
// block size exponent < 14 || > 32.
|
||||
YatiInvalidNczBlockSizeExponent,
|
||||
// zstd error while decompressing ncz.
|
||||
YatiInvalidNczZstdError,
|
||||
// nca has rights_id but matching ticket wasn't found.
|
||||
YatiTicketNotFound,
|
||||
// found ticket has missmatching rights_id from it's name.
|
||||
YatiInvalidTicketBadRightsId,
|
||||
// cert not found for the ticket.
|
||||
YatiCertNotFound,
|
||||
// unable to fetch header from ncm database.
|
||||
YatiNcmDbCorruptHeader,
|
||||
// unable to total infos from ncm database.
|
||||
YatiNcmDbCorruptInfos,
|
||||
};
|
||||
|
||||
#define MAKE_SPHAIRA_RESULT_ENUM(x) Result_##x = MAKERESULT(Module_Sphaira, (Result)SphairaResult::x)
|
||||
|
||||
enum : Result {
|
||||
MAKE_SPHAIRA_RESULT_ENUM(TransferCancelled),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(StreamBadSeek),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsTooManyEntries),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsNewPathTooLarge),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsInvalidType),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsEmpty),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsAlreadyRoot),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsNoCurrentPath),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsBrokenCurrentPath),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsIndexOutOfBounds),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsFsNotActive),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsNewPathEmpty),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsLoadingCancelled),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsBrokenRoot),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsUnknownStdioError),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsReadOnly),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsNotActive),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsFailedStdioStat),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsFailedStdioOpendir),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(NroBadMagic),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(NroBadSize),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(AppFailedMusicDownload),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(CurlFailedEasyInit),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(DumpFailedNetworkUpload),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UnzOpen2_64),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UnzGetGlobalInfo64),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UnzLocateFile),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UnzGoToFirstFile),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UnzGoToNextFile),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UnzOpenCurrentFile),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UnzGetCurrentFileInfo64),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UnzReadCurrentFile),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(ZipOpen2_64),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(ZipOpenNewFileInZip),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(ZipWriteInFileInZip),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FileBrowserFailedUpload),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FileBrowserDirNotDaybreak),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(AppstoreFailedZipDownload),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(AppstoreFailedMd5),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(AppstoreFailedParseManifest),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(GameBadReadForDump),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(GameEmptyMetaEntries),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(GameMultipleKeysFound),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(GameNoNspEntriesBuilt),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(KeyMissingNcaKeyArea),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(KeyMissingTitleKek),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(KeyMissingMasterKey),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(KeyFailedDecyptETicketDeviceKey),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(NcaFailedNcaHeaderHashVerify),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(NcaBadSigKeyGen),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(GcBadReadForDump),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(GcEmptyGamecard),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(GcBadXciMagic),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(GcBadXciRomSize),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(GcFailedToGetSecurityInfo),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(GhdlEmptyAsset),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(GhdlFailedToDownloadAsset),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(GhdlFailedToDownloadAssetJson),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(ThemezerFailedToDownloadThemeMeta),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(ThemezerFailedToDownloadTheme),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(MainFailedToDownloadUpdate),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbDsBadDeviceSpeed),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(NspBadMagic),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(XciBadMagic),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(EsBadTitleKeyType),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(EsPersonalisedTicketDeviceIdMissmatch),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(EsFailedDecryptPersonalisedTicket),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(EsBadDecryptedPersonalisedTicketSize),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(EsBadTicketSize),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketBadRightsId),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketFromatVersion),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketKeyType),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(EsInvalidTicketKeyRevision),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(OwoBadArgs),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbCancelled),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbBadMagic),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbBadVersion),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbBadCount),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbBadTransferSize),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbBadTotalSize),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadMagic),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadExit),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadCount),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadTransferSize),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadTotalSize),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(UsbUploadBadCommand),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiContainerNotFound),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiNcaNotFound),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNcaReadSize),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNcaSigKeyGen),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNcaMagic),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNcaSignature0),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNcaSignature1),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNcaSha256),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiNczSectionNotFound),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNczSectionCount),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiNczBlockNotFound),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNczBlockVersion),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNczBlockType),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNczBlockTotal),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNczBlockSizeExponent),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidNczZstdError),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiTicketNotFound),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiInvalidTicketBadRightsId),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiCertNotFound),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiNcmDbCorruptHeader),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(YatiNcmDbCorruptInfos),
|
||||
};
|
||||
|
||||
#undef MAKE_SPHAIRA_RESULT_ENUM
|
||||
|
||||
#define R_SUCCEED() return (Result)0
|
||||
|
||||
#define R_THROW(_rc) return _rc
|
||||
|
||||
@@ -521,8 +784,8 @@ enum NcmError {
|
||||
#define ANONYMOUS_VARIABLE(pref) CONCATENATE(pref, __COUNTER__)
|
||||
|
||||
#define ON_SCOPE_EXIT(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { _f; }};
|
||||
#define ON_SCOPE_FAIL(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_FAILED(rc)) { _f; } }};
|
||||
#define ON_SCOPE_SUCCESS(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_SUCCEEDED(rc)) { _f; } }};
|
||||
// #define ON_SCOPE_FAIL(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_FAILED(rc)) { _f; } }};
|
||||
// #define ON_SCOPE_SUCCESS(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_SUCCEEDED(rc)) { _f; } }};
|
||||
|
||||
// threading helpers.
|
||||
#define PRIO_PREEMPTIVE 0x3B
|
||||
@@ -535,4 +798,6 @@ enum NcmError {
|
||||
#define THREAD_AFFINITY_ALL (THREAD_AFFINITY_CORE0|THREAD_AFFINITY_CORE1|THREAD_AFFINITY_CORE2)
|
||||
|
||||
// mutex helpers.
|
||||
#define SCOPED_MUTEX(mutex) mutexLock(mutex); ON_SCOPE_EXIT(mutexUnlock(mutex))
|
||||
#define SCOPED_MUTEX(mutex) \
|
||||
mutexLock(mutex); \
|
||||
ON_SCOPE_EXIT(mutexUnlock(mutex))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "fs.hpp"
|
||||
#include "location.hpp"
|
||||
#include <switch.h>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
@@ -30,6 +31,17 @@ enum DumpLocationFlag {
|
||||
DumpLocationFlag_All = DumpLocationFlag_SdCard | DumpLocationFlag_UsbS2S | DumpLocationFlag_DevNull | DumpLocationFlag_Stdio | DumpLocationFlag_Network,
|
||||
};
|
||||
|
||||
struct DumpEntry {
|
||||
DumpLocationType type;
|
||||
s32 index;
|
||||
};
|
||||
|
||||
struct DumpLocation {
|
||||
DumpEntry entry{};
|
||||
location::Entries network{};
|
||||
location::StdioEntries stdio{};
|
||||
};
|
||||
|
||||
struct BaseSource {
|
||||
virtual ~BaseSource() = default;
|
||||
virtual Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) = 0;
|
||||
@@ -40,7 +52,13 @@ struct BaseSource {
|
||||
|
||||
// called after dump has finished.
|
||||
using OnExit = std::function<void(Result rc)>;
|
||||
using OnLocation = std::function<void(const DumpLocation& loc)>;
|
||||
|
||||
// prompts the user to select dump location, calls on_loc on success with the selected location.
|
||||
void DumpGetLocation(const std::string& title, u32 location_flags, OnLocation on_loc);
|
||||
// dumps to a fetched location using DumpGetLocation().
|
||||
void Dump(std::shared_ptr<BaseSource> source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, OnExit on_exit);
|
||||
// DumpGetLocation() + Dump() all in one.
|
||||
void Dump(std::shared_ptr<BaseSource> source, const std::vector<fs::FsPath>& paths, OnExit on_exit = [](Result){}, u32 location_flags = DumpLocationFlag_All);
|
||||
|
||||
} // namespace sphaira::dump
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <string>
|
||||
#include <switch.h>
|
||||
#include <nxlink.h>
|
||||
#include <haze.h>
|
||||
#include "download.hpp"
|
||||
|
||||
namespace sphaira::evman {
|
||||
@@ -24,7 +23,6 @@ struct ExitEventData {
|
||||
using EventData = std::variant<
|
||||
LaunchNroEventData,
|
||||
ExitEventData,
|
||||
HazeCallbackData,
|
||||
NxlinkCallbackData,
|
||||
curl::DownloadEventData
|
||||
>;
|
||||
|
||||
@@ -12,10 +12,14 @@ namespace fs {
|
||||
|
||||
struct FsPath {
|
||||
FsPath() = default;
|
||||
constexpr FsPath(const auto& str) { From(str); }
|
||||
|
||||
constexpr FsPath(const FsPath& p) { From(p); }
|
||||
constexpr FsPath(const char* str) { From(str); }
|
||||
constexpr FsPath(const std::string& str) { From(str); }
|
||||
constexpr FsPath(const std::string_view& str) { From(str); }
|
||||
|
||||
constexpr void From(const FsPath& p) {
|
||||
*this = p;
|
||||
From(p.s);
|
||||
}
|
||||
|
||||
constexpr void From(const char* str) {
|
||||
@@ -40,6 +44,10 @@ struct FsPath {
|
||||
return s;
|
||||
}
|
||||
|
||||
constexpr auto starts_with(std::string_view str) const -> bool {
|
||||
return !strncasecmp(s, str.data(), str.length());
|
||||
}
|
||||
|
||||
constexpr auto empty() const {
|
||||
return s[0] == '\0';
|
||||
}
|
||||
@@ -65,6 +73,11 @@ struct FsPath {
|
||||
constexpr char& operator[](std::size_t idx) { return s[idx]; }
|
||||
constexpr const char& operator[](std::size_t idx) const { return s[idx]; }
|
||||
|
||||
constexpr FsPath& operator=(const FsPath& p) noexcept {
|
||||
From(p.s);
|
||||
return *this;
|
||||
}
|
||||
|
||||
constexpr FsPath operator+(const FsPath& v) const noexcept {
|
||||
FsPath r{*this};
|
||||
return r += v;
|
||||
@@ -186,15 +199,14 @@ struct File {
|
||||
FsFile m_native{};
|
||||
std::FILE* m_stdio{};
|
||||
s64 m_stdio_off{};
|
||||
// sadly, fatfs doesn't support fstat, so we have to manually
|
||||
// stat the file to get it's size.
|
||||
FsPath m_path{};
|
||||
u32 m_mode{};
|
||||
};
|
||||
|
||||
struct Dir {
|
||||
~Dir();
|
||||
|
||||
Result GetEntryCount(s64* out);
|
||||
Result Read(s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf);
|
||||
Result ReadAll(std::vector<FsDirectoryEntry>& buf);
|
||||
void Close();
|
||||
|
||||
@@ -261,22 +273,6 @@ Result FileGetSizeAndTimestamp(fs::Fs* fs, const FsPath& path, FsTimeStampRaw* t
|
||||
Result IsDirEmpty(fs::Fs* m_fs, const fs::FsPath& path, bool* out);
|
||||
|
||||
struct Fs {
|
||||
static constexpr inline u32 FsModule = 505;
|
||||
static constexpr inline Result ResultTooManyEntries = MAKERESULT(FsModule, 1);
|
||||
static constexpr inline Result ResultNewPathTooLarge = MAKERESULT(FsModule, 2);
|
||||
static constexpr inline Result ResultInvalidType = MAKERESULT(FsModule, 3);
|
||||
static constexpr inline Result ResultEmpty = MAKERESULT(FsModule, 4);
|
||||
static constexpr inline Result ResultAlreadyRoot = MAKERESULT(FsModule, 5);
|
||||
static constexpr inline Result ResultNoCurrentPath = MAKERESULT(FsModule, 6);
|
||||
static constexpr inline Result ResultBrokenCurrentPath = MAKERESULT(FsModule, 7);
|
||||
static constexpr inline Result ResultIndexOutOfBounds = MAKERESULT(FsModule, 8);
|
||||
static constexpr inline Result ResultFsNotActive = MAKERESULT(FsModule, 9);
|
||||
static constexpr inline Result ResultNewPathEmpty = MAKERESULT(FsModule, 10);
|
||||
static constexpr inline Result ResultLoadingCancelled = MAKERESULT(FsModule, 11);
|
||||
static constexpr inline Result ResultBrokenRoot = MAKERESULT(FsModule, 12);
|
||||
static constexpr inline Result ResultUnknownStdioError = MAKERESULT(FsModule, 13);
|
||||
static constexpr inline Result ResultReadOnly = MAKERESULT(FsModule, 14);
|
||||
|
||||
Fs(bool ignore_read_only = true) : m_ignore_read_only{ignore_read_only} {}
|
||||
virtual ~Fs() = default;
|
||||
|
||||
@@ -292,6 +288,7 @@ struct Fs {
|
||||
virtual Result GetEntryType(const FsPath& path, FsDirEntryType* out) = 0;
|
||||
virtual Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out) = 0;
|
||||
virtual Result SetTimestamp(const FsPath& path, const FsTimeStampRaw* ts) = 0;
|
||||
virtual Result Commit() = 0;
|
||||
virtual bool FileExists(const FsPath& path) = 0;
|
||||
virtual bool DirExists(const FsPath& path) = 0;
|
||||
virtual bool IsNative() const = 0;
|
||||
@@ -367,6 +364,9 @@ struct FsStdio : Fs {
|
||||
Result SetTimestamp(const FsPath& path, const FsTimeStampRaw *ts) override {
|
||||
return fs::SetTimestamp(path, ts);
|
||||
}
|
||||
Result Commit() override {
|
||||
R_SUCCEED();
|
||||
}
|
||||
bool FileExists(const FsPath& path) override {
|
||||
return fs::FileExists(path);
|
||||
}
|
||||
@@ -402,10 +402,6 @@ struct FsNative : Fs {
|
||||
}
|
||||
}
|
||||
|
||||
Result Commit() {
|
||||
return fsFsCommit(&m_fs);
|
||||
}
|
||||
|
||||
Result GetFreeSpace(const FsPath& path, s64* out) {
|
||||
return fsFsGetFreeSpace(&m_fs, path, out);
|
||||
}
|
||||
@@ -414,36 +410,6 @@ struct FsNative : Fs {
|
||||
return fsFsGetTotalSpace(&m_fs, path, out);
|
||||
}
|
||||
|
||||
// Result OpenDirectory(const FsPath& path, u32 mode, FsDir *out) {
|
||||
// return fsFsOpenDirectory(&m_fs, path, mode, out);
|
||||
// }
|
||||
|
||||
// void DirClose(FsDir *d) {
|
||||
// fsDirClose(d);
|
||||
// }
|
||||
|
||||
// Result DirGetEntryCount(FsDir *d, s64* out) {
|
||||
// return fsDirGetEntryCount(d, out);
|
||||
// }
|
||||
|
||||
// Result DirGetEntryCount(const FsPath& path, u32 mode, s64* out) {
|
||||
// FsDir d;
|
||||
// R_TRY(OpenDirectory(path, mode, &d));
|
||||
// ON_SCOPE_EXIT(DirClose(&d));
|
||||
// return DirGetEntryCount(&d, out);
|
||||
// }
|
||||
|
||||
// Result DirRead(FsDir *d, s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf) {
|
||||
// return fsDirRead(d, total_entries, max_entries, buf);
|
||||
// }
|
||||
|
||||
// Result DirRead(const FsPath& path, u32 mode, s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf) {
|
||||
// FsDir d;
|
||||
// R_TRY(OpenDirectory(path, mode, &d));
|
||||
// ON_SCOPE_EXIT(DirClose(&d));
|
||||
// return DirRead(&d, total_entries, max_entries, buf);
|
||||
// }
|
||||
|
||||
virtual bool IsFsActive() {
|
||||
return serviceIsActive(&m_fs.s);
|
||||
}
|
||||
@@ -488,6 +454,9 @@ struct FsNative : Fs {
|
||||
Result SetTimestamp(const FsPath& path, const FsTimeStampRaw *ts) override {
|
||||
return fs::SetTimestamp(&m_fs, path, ts);
|
||||
}
|
||||
Result Commit() override {
|
||||
return fsFsCommit(&m_fs);
|
||||
}
|
||||
bool FileExists(const FsPath& path) override {
|
||||
return fs::FileExists(&m_fs, path);
|
||||
}
|
||||
@@ -527,27 +496,41 @@ struct FsNativeSd final : FsNative {
|
||||
#endif
|
||||
|
||||
struct FsNativeBis final : FsNative {
|
||||
FsNativeBis(FsBisPartitionId id, const FsPath& string, bool ignore_read_only = true) : FsNative{ignore_read_only} {
|
||||
FsNativeBis(FsBisPartitionId id, const FsPath& string) {
|
||||
m_open_result = fsOpenBisFileSystem(&m_fs, id, string);
|
||||
}
|
||||
};
|
||||
|
||||
struct FsNativeImage final : FsNative {
|
||||
FsNativeImage(FsImageDirectoryId id, bool ignore_read_only = true) : FsNative{ignore_read_only} {
|
||||
FsNativeImage(FsImageDirectoryId id) {
|
||||
m_open_result = fsOpenImageDirectoryFileSystem(&m_fs, id);
|
||||
}
|
||||
};
|
||||
|
||||
struct FsNativeContentStorage final : FsNative {
|
||||
FsNativeContentStorage(FsContentStorageId id, bool ignore_read_only = true) : FsNative{ignore_read_only} {
|
||||
FsNativeContentStorage(FsContentStorageId id) {
|
||||
m_open_result = fsOpenContentStorageFileSystem(&m_fs, id);
|
||||
}
|
||||
};
|
||||
|
||||
struct FsNativeGameCard final : FsNative {
|
||||
FsNativeGameCard(const FsGameCardHandle* handle, FsGameCardPartition partition, bool ignore_read_only = true) : FsNative{ignore_read_only} {
|
||||
FsNativeGameCard(const FsGameCardHandle* handle, FsGameCardPartition partition) {
|
||||
m_open_result = fsOpenGameCardFileSystem(&m_fs, handle, partition);
|
||||
}
|
||||
};
|
||||
|
||||
struct FsNativeSave final : FsNative {
|
||||
FsNativeSave(FsSaveDataType data_type, FsSaveDataSpaceId save_data_space_id, const FsSaveDataAttribute *attr, bool read_only) {
|
||||
if (data_type == FsSaveDataType_System || data_type == FsSaveDataType_SystemBcat) {
|
||||
m_open_result = fsOpenSaveDataFileSystemBySystemSaveDataId(&m_fs, FsSaveDataSpaceId_System, attr);
|
||||
} else {
|
||||
if (read_only) {
|
||||
m_open_result = fsOpenReadOnlySaveDataFileSystem(&m_fs, save_data_space_id, attr);
|
||||
} else {
|
||||
m_open_result = fsOpenSaveDataFileSystem(&m_fs, save_data_space_id, attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace fs
|
||||
|
||||
@@ -7,11 +7,11 @@ namespace sphaira::ftpsrv {
|
||||
bool Init();
|
||||
void Exit();
|
||||
|
||||
using OnInstallStart = std::function<bool(void* user, const char* path)>;
|
||||
using OnInstallWrite = std::function<bool(void* user, const void* buf, size_t size)>;
|
||||
using OnInstallClose = std::function<void(void* user)>;
|
||||
using OnInstallStart = std::function<bool(const char* path)>;
|
||||
using OnInstallWrite = std::function<bool(const void* buf, size_t size)>;
|
||||
using OnInstallClose = std::function<void()>;
|
||||
|
||||
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close);
|
||||
void InitInstallMode(OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close);
|
||||
void DisableInstallMode();
|
||||
|
||||
unsigned GetPort();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "ui/progress_box.hpp"
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::hash {
|
||||
@@ -26,5 +27,6 @@ auto GetTypeStr(Type type) -> const char*;
|
||||
// returns the hash string.
|
||||
Result Hash(ui::ProgressBox* pbox, Type type, std::shared_ptr<BaseSource> source, std::string& out);
|
||||
Result Hash(ui::ProgressBox* pbox, Type type, fs::Fs* fs, const fs::FsPath& path, std::string& out);
|
||||
Result Hash(ui::ProgressBox* pbox, Type type, std::span<const u8> data, std::string& out);
|
||||
|
||||
} // namespace sphaira::hash
|
||||
|
||||
17
sphaira/include/haze_helper.hpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace sphaira::haze {
|
||||
|
||||
bool Init();
|
||||
void Exit();
|
||||
|
||||
using OnInstallStart = std::function<bool(const char* path)>;
|
||||
using OnInstallWrite = std::function<bool(const void* buf, size_t size)>;
|
||||
using OnInstallClose = std::function<void()>;
|
||||
|
||||
void InitInstallMode(OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close);
|
||||
void DisableInstallMode();
|
||||
|
||||
} // namespace sphaira::haze
|
||||
@@ -12,8 +12,14 @@ struct ImageResult {
|
||||
int w, h;
|
||||
};
|
||||
|
||||
auto ImageLoadFromMemory(std::span<const u8> data) -> ImageResult;
|
||||
auto ImageLoadFromFile(const fs::FsPath& file) -> ImageResult;
|
||||
enum ImageFlag {
|
||||
ImageFlag_None = 0,
|
||||
// set this if the image is a jpeg, will use oss-nvjpg to load.
|
||||
ImageFlag_JPEG = 1 << 0,
|
||||
};
|
||||
|
||||
auto ImageLoadFromMemory(std::span<const u8> data, u32 flags = ImageFlag_None) -> ImageResult;
|
||||
auto ImageLoadFromFile(const fs::FsPath& file, u32 flags = ImageFlag_None) -> ImageResult;
|
||||
auto ImageResize(std::span<const u8> data, int inx, int iny, int outx, int outy) -> ImageResult;
|
||||
auto ImageConvertToJpg(std::span<const u8> data, int x, int y) -> ImageResult;
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ extern "C" {
|
||||
bool log_file_init();
|
||||
bool log_nxlink_init();
|
||||
void log_file_exit();
|
||||
bool log_is_init();
|
||||
|
||||
void log_nxlink_exit();
|
||||
void log_write(const char* s, ...) __attribute__ ((format (printf, 1, 2)));
|
||||
void log_write_arg(const char* s, va_list* v);
|
||||
|
||||
24
sphaira/include/minizip_helper.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <minizip/ioapi.h>
|
||||
#include <vector>
|
||||
#include <span>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::mz {
|
||||
|
||||
struct MzMem {
|
||||
std::vector<u8> buf;
|
||||
size_t offset;
|
||||
};
|
||||
|
||||
struct MzSpan {
|
||||
std::span<const u8> buf;
|
||||
size_t offset;
|
||||
};
|
||||
|
||||
void FileFuncMem(MzMem* mem, zlib_filefunc64_def* funcs);
|
||||
void FileFuncSpan(MzSpan* span, zlib_filefunc64_def* funcs);
|
||||
void FileFuncStdio(zlib_filefunc64_def* funcs);
|
||||
|
||||
} // namespace sphaira::mz
|
||||
@@ -20,17 +20,6 @@ struct OwoConfig {
|
||||
std::vector<u8> program_nca{};
|
||||
};
|
||||
|
||||
enum {
|
||||
Module_Owo = 424,
|
||||
};
|
||||
|
||||
enum OwoError {
|
||||
OwoError_BadArgs = MAKERESULT(Module_Owo, 1),
|
||||
};
|
||||
|
||||
// fwd
|
||||
// struct ui::ProgressBox;
|
||||
|
||||
auto install_forwarder(OwoConfig& config, NcmStorageId storage_id) -> Result;
|
||||
auto install_forwarder(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId storage_id) -> Result;
|
||||
|
||||
|
||||
@@ -6,6 +6,15 @@
|
||||
|
||||
namespace sphaira::thread {
|
||||
|
||||
enum class Mode {
|
||||
// default, always multi-thread.
|
||||
MultiThreaded,
|
||||
// always single-thread.
|
||||
SingleThreaded,
|
||||
// check buffer size, if smaller, single thread.
|
||||
SingleThreadedIfSmaller,
|
||||
};
|
||||
|
||||
using ReadCallback = std::function<Result(void* data, s64 off, s64 size, u64* bytes_read)>;
|
||||
using WriteCallback = std::function<Result(const void* data, s64 off, s64 size)>;
|
||||
|
||||
@@ -23,10 +32,25 @@ using StartCallback = std::function<Result(PullCallback pull)>;
|
||||
using StartCallback2 = std::function<Result(StartThreadCallback start, PullCallback pull)>;
|
||||
|
||||
// reads data from rfunc into wfunc.
|
||||
Result Transfer(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, WriteCallback wfunc);
|
||||
Result Transfer(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, WriteCallback wfunc, Mode mode = Mode::MultiThreaded);
|
||||
|
||||
// reads data from rfunc, pull data from provided pull() callback.
|
||||
Result TransferPull(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, StartCallback sfunc);
|
||||
Result TransferPull(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, StartCallback2 sfunc);
|
||||
Result TransferPull(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, StartCallback sfunc, Mode mode = Mode::MultiThreaded);
|
||||
Result TransferPull(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, StartCallback2 sfunc, Mode mode = Mode::MultiThreaded);
|
||||
|
||||
// helper for extract zips.
|
||||
// this will multi-thread unzip if size >= 512KiB, otherwise it'll single pass.
|
||||
Result TransferUnzip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& path, s64 size, u32 crc32 = 0, Mode mode = Mode::SingleThreadedIfSmaller);
|
||||
|
||||
// same as above but for zipping files.
|
||||
Result TransferZip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& path, u32* crc32 = nullptr, Mode mode = Mode::SingleThreadedIfSmaller);
|
||||
|
||||
// passes the name inside the zip an final output path.
|
||||
using UnzipAllFilter = std::function<bool(const fs::FsPath& name, fs::FsPath& path)>;
|
||||
|
||||
// helper all-in-one unzip function that unzips a zip (either open or path provided).
|
||||
// the filter function can be used to modify the path and filter out unwanted files.
|
||||
Result TransferUnzipAll(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& base_path, UnzipAllFilter filter = nullptr, Mode mode = Mode::SingleThreadedIfSmaller);
|
||||
Result TransferUnzipAll(ui::ProgressBox* pbox, const fs::FsPath& zip_out, fs::Fs* fs, const fs::FsPath& base_path, UnzipAllFilter filter = nullptr, Mode mode = Mode::SingleThreadedIfSmaller);
|
||||
|
||||
} // namespace sphaira::thread
|
||||
|
||||
@@ -16,6 +16,7 @@ public:
|
||||
private:
|
||||
std::optional<Result> m_code{};
|
||||
std::string m_message{};
|
||||
std::string m_code_message{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui
|
||||
|
||||
@@ -108,8 +108,11 @@ private:
|
||||
std::shared_ptr<ScrollableText> m_details{};
|
||||
std::shared_ptr<ScrollableText> m_changelog{};
|
||||
std::shared_ptr<ScrollableText> m_detail_changelog{};
|
||||
std::unique_ptr<ScrollableText> m_manifest_list{};
|
||||
|
||||
bool m_show_changlog{};
|
||||
bool m_show_file_list{};
|
||||
ImageDownloadState m_file_list_state{};
|
||||
};
|
||||
|
||||
enum Filter {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "ui/scrolling_text.hpp"
|
||||
#include "ui/progress_box.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "option.hpp"
|
||||
@@ -154,6 +155,8 @@ struct FsDirCollection {
|
||||
|
||||
using FsDirCollections = std::vector<FsDirCollection>;
|
||||
|
||||
void SignalChange();
|
||||
|
||||
struct Menu;
|
||||
|
||||
struct FsView final : Widget {
|
||||
@@ -181,6 +184,10 @@ struct FsView final : Widget {
|
||||
|
||||
void SetSide(ViewSide side);
|
||||
|
||||
static Result DeleteAllCollections(ProgressBox* pbox, fs::Fs* fs, const FsDirCollections& collections, u32 mode = FsDirOpenMode_ReadDirs|FsDirOpenMode_ReadFiles);
|
||||
static auto get_collection(fs::Fs* fs, const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollection& out, bool inc_file, bool inc_dir, bool inc_size) -> Result;
|
||||
static auto get_collections(fs::Fs* fs, const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out, bool inc_size = false) -> Result;
|
||||
|
||||
private:
|
||||
void SetIndex(s64 index);
|
||||
void InstallForwarder();
|
||||
@@ -241,7 +248,7 @@ private:
|
||||
}
|
||||
|
||||
void Sort();
|
||||
void SortAndFindLastFile();
|
||||
void SortAndFindLastFile(bool scan = false);
|
||||
void SetIndexFromLastFile(const LastFile& last_file);
|
||||
|
||||
void OnDeleteCallback();
|
||||
@@ -249,9 +256,6 @@ private:
|
||||
void OnRenameCallback();
|
||||
auto CheckIfUpdateFolder() -> Result;
|
||||
|
||||
static auto get_collection(fs::Fs* fs, const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollection& out, bool inc_file, bool inc_dir, bool inc_size) -> Result;
|
||||
static auto get_collections(fs::Fs* fs, const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out, bool inc_size = false) -> Result;
|
||||
|
||||
auto get_collection(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollection& out, bool inc_file, bool inc_dir, bool inc_size) -> Result;
|
||||
auto get_collections(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out, bool inc_size = false) -> Result;
|
||||
|
||||
|
||||
@@ -1,56 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "yati/source/stream.hpp"
|
||||
#include "ui/menus/install_stream_menu_base.hpp"
|
||||
|
||||
namespace sphaira::ui::menu::ftp {
|
||||
|
||||
enum class State {
|
||||
// not connected.
|
||||
None,
|
||||
// just connected, starts the transfer.
|
||||
Connected,
|
||||
// set whilst transfer is in progress.
|
||||
Progress,
|
||||
// set when the transfer is finished.
|
||||
Done,
|
||||
// failed to connect.
|
||||
Failed,
|
||||
};
|
||||
|
||||
struct StreamFtp final : yati::source::Stream {
|
||||
StreamFtp(const fs::FsPath& path, std::stop_token token);
|
||||
|
||||
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
|
||||
bool Push(const void* buf, s64 size);
|
||||
void Disable();
|
||||
|
||||
// private:
|
||||
fs::FsPath m_path{};
|
||||
std::stop_token m_token{};
|
||||
std::vector<u8> m_buffer{};
|
||||
Mutex m_mutex{};
|
||||
bool m_active{};
|
||||
// bool m_push_exit{};
|
||||
};
|
||||
|
||||
struct Menu final : MenuBase {
|
||||
struct Menu final : stream::Menu {
|
||||
Menu(u32 flags);
|
||||
~Menu();
|
||||
|
||||
auto GetShortTitle() const -> const char* override { return "FTP"; };
|
||||
void Update(Controller* controller, TouchInfo* touch) override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
|
||||
// this should be private
|
||||
// private:
|
||||
std::shared_ptr<StreamFtp> m_source{};
|
||||
Thread m_thread{};
|
||||
Mutex m_mutex{};
|
||||
// the below are shared across threads, lock with the above mutex!
|
||||
State m_state{State::None};
|
||||
void OnDisableInstallMode() override;
|
||||
|
||||
private:
|
||||
const char* m_user{};
|
||||
const char* m_pass{};
|
||||
unsigned m_port{};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "option.hpp"
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <span>
|
||||
|
||||
namespace sphaira::ui::menu::game {
|
||||
|
||||
@@ -22,13 +23,12 @@ enum class NacpLoadStatus {
|
||||
|
||||
struct Entry {
|
||||
u64 app_id{};
|
||||
char display_version[0x10]{};
|
||||
NacpLanguageEntry lang{};
|
||||
int image{};
|
||||
bool selected{};
|
||||
|
||||
std::shared_ptr<NsApplicationControlData> control{};
|
||||
u64 control_size{};
|
||||
u64 jpeg_size{};
|
||||
NacpLoadStatus status{NacpLoadStatus::None};
|
||||
|
||||
auto GetName() const -> const char* {
|
||||
@@ -38,35 +38,38 @@ struct Entry {
|
||||
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]{};
|
||||
u64 jpeg_size{};
|
||||
NacpLanguageEntry lang{};
|
||||
NacpLoadStatus status{NacpLoadStatus::None};
|
||||
};
|
||||
|
||||
struct ThreadData {
|
||||
ThreadData();
|
||||
ThreadData(bool title_cache);
|
||||
|
||||
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);
|
||||
|
||||
auto IsRunning() const -> bool {
|
||||
return m_running;
|
||||
}
|
||||
|
||||
auto IsTitleCacheEnabled() const {
|
||||
return m_title_cache;
|
||||
}
|
||||
|
||||
private:
|
||||
UEvent m_uevent{};
|
||||
Mutex m_mutex_id{};
|
||||
Mutex m_mutex_result{};
|
||||
bool m_title_cache{};
|
||||
|
||||
// app_ids pushed to the queue, signal uevent when pushed.
|
||||
std::vector<u64> m_ids{};
|
||||
@@ -141,13 +144,14 @@ private:
|
||||
bool m_is_reversed{};
|
||||
bool m_dirty{};
|
||||
|
||||
ThreadData m_thread_data{};
|
||||
std::unique_ptr<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::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_Grid};
|
||||
option::OptionBool m_hide_forwarders{INI_SECTION, "hide_forwarders", false};
|
||||
option::OptionBool m_title_cache{INI_SECTION, "title_cache", true};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::game
|
||||
|
||||
@@ -25,6 +25,7 @@ enum OrderType {
|
||||
using LayoutType = grid::LayoutType;
|
||||
|
||||
auto GetNroEntries() -> std::span<const NroEntry>;
|
||||
void SignalChange();
|
||||
|
||||
struct Menu final : grid::Menu {
|
||||
Menu();
|
||||
@@ -47,7 +48,7 @@ private:
|
||||
void InstallHomebrew();
|
||||
void ScanHomebrew();
|
||||
void Sort();
|
||||
void SortAndFindLastFile();
|
||||
void SortAndFindLastFile(bool scan = false);
|
||||
void FreeEntries();
|
||||
void OnLayoutChange();
|
||||
|
||||
@@ -61,6 +62,7 @@ private:
|
||||
std::vector<NroEntry> m_entries{};
|
||||
s64 m_index{}; // where i am in the array
|
||||
std::unique_ptr<List> m_list{};
|
||||
bool m_dirty{};
|
||||
|
||||
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_AlphabeticalStar};
|
||||
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
|
||||
|
||||
64
sphaira/include/ui/menus/install_stream_menu_base.hpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "yati/source/stream.hpp"
|
||||
|
||||
namespace sphaira::ui::menu::stream {
|
||||
|
||||
enum class State {
|
||||
// not connected.
|
||||
None,
|
||||
// just connected, starts the transfer.
|
||||
Connected,
|
||||
// set whilst transfer is in progress.
|
||||
Progress,
|
||||
// set when the transfer is finished.
|
||||
Done,
|
||||
// failed to connect.
|
||||
Failed,
|
||||
};
|
||||
|
||||
using OnInstallStart = std::function<bool(const char* path)>;
|
||||
using OnInstallWrite = std::function<bool(const void* buf, size_t size)>;
|
||||
using OnInstallClose = std::function<void()>;
|
||||
|
||||
struct Stream final : yati::source::Stream {
|
||||
Stream(const fs::FsPath& path, std::stop_token token);
|
||||
|
||||
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
|
||||
bool Push(const void* buf, s64 size);
|
||||
void Disable();
|
||||
auto& GetPath() const { return m_path; }
|
||||
|
||||
private:
|
||||
fs::FsPath m_path{};
|
||||
std::stop_token m_token{};
|
||||
std::vector<u8> m_buffer{};
|
||||
CondVar m_can_read{};
|
||||
|
||||
public:
|
||||
Mutex m_mutex{};
|
||||
bool m_active{};
|
||||
};
|
||||
|
||||
struct Menu : MenuBase {
|
||||
Menu(const std::string& title, u32 flags);
|
||||
virtual ~Menu();
|
||||
|
||||
virtual void Update(Controller* controller, TouchInfo* touch);
|
||||
virtual void Draw(NVGcontext* vg, Theme* theme);
|
||||
virtual void OnDisableInstallMode() = 0;
|
||||
|
||||
protected:
|
||||
bool OnInstallStart(const char* path);
|
||||
bool OnInstallWrite(const void* buf, size_t size);
|
||||
void OnInstallClose();
|
||||
|
||||
private:
|
||||
std::shared_ptr<Stream> m_source{};
|
||||
Thread m_thread{};
|
||||
Mutex m_mutex{};
|
||||
State m_state{State::None};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::stream
|
||||
19
sphaira/include/ui/menus/mtp_menu.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/install_stream_menu_base.hpp"
|
||||
|
||||
namespace sphaira::ui::menu::mtp {
|
||||
|
||||
struct Menu final : stream::Menu {
|
||||
Menu(u32 flags);
|
||||
~Menu();
|
||||
|
||||
auto GetShortTitle() const -> const char* override { return "MTP"; };
|
||||
void Update(Controller* controller, TouchInfo* touch) override;
|
||||
void OnDisableInstallMode() override;
|
||||
|
||||
private:
|
||||
bool m_was_mtp_enabled{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::mtp
|
||||
158
sphaira/include/ui/menus/save_menu.hpp
Normal file
@@ -0,0 +1,158 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/grid_menu_base.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "option.hpp"
|
||||
#include "dumper.hpp"
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <span>
|
||||
|
||||
namespace sphaira::ui::menu::save {
|
||||
|
||||
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 final : FsSaveDataInfo {
|
||||
NacpLanguageEntry lang{};
|
||||
int image{};
|
||||
bool selected{};
|
||||
|
||||
std::shared_ptr<NsApplicationControlData> control{};
|
||||
u64 jpeg_size{};
|
||||
NacpLoadStatus status{NacpLoadStatus::None};
|
||||
|
||||
auto GetName() const -> const char* {
|
||||
return lang.name;
|
||||
}
|
||||
|
||||
auto GetAuthor() const -> const char* {
|
||||
return lang.author;
|
||||
}
|
||||
};
|
||||
|
||||
struct ThreadResultData {
|
||||
u64 id{};
|
||||
std::shared_ptr<NsApplicationControlData> control{};
|
||||
u64 jpeg_size{};
|
||||
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;
|
||||
|
||||
void SignalChange();
|
||||
|
||||
struct Menu final : grid::Menu {
|
||||
Menu(u32 flags);
|
||||
~Menu();
|
||||
|
||||
auto GetShortTitle() const -> const char* override { return "Saves"; };
|
||||
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 BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries);
|
||||
void RestoreSave();
|
||||
|
||||
auto BuildSavePath(const Entry& e, bool is_auto) const -> fs::FsPath;
|
||||
Result RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath& path) const;
|
||||
Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, const Entry& e, bool compressed, bool is_auto = false) const;
|
||||
|
||||
private:
|
||||
static constexpr inline const char* INI_SECTION = "saves";
|
||||
|
||||
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{};
|
||||
|
||||
std::vector<AccountProfileBase> m_accounts{};
|
||||
s64 m_account_index{};
|
||||
u8 m_data_type{FsSaveDataType_Account};
|
||||
|
||||
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_Grid};
|
||||
option::OptionBool m_auto_backup_on_restore{INI_SECTION, "auto_backup_on_restore", true};
|
||||
option::OptionBool m_compress_save_backup{INI_SECTION, "compress_save_backup", true};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::save
|
||||
@@ -30,7 +30,8 @@ void drawTextArgs(NVGcontext*, float x, float y, float size, int align, const NV
|
||||
|
||||
void drawTextBox(NVGcontext*, float x, float y, float size, float bound, const NVGcolor& c, const char* str, int align = NVG_ALIGN_LEFT | NVG_ALIGN_TOP, const char* end = nullptr);
|
||||
|
||||
void textBounds(NVGcontext*, float x, float y, float *bounds, const char* str, ...) __attribute__ ((format (printf, 5, 6)));
|
||||
void textBounds(NVGcontext*, float x, float y, float *bounds, const char* str);
|
||||
void textBoundsArgs(NVGcontext*, float x, float y, float *bounds, const char* str, ...) __attribute__ ((format (printf, 5, 6)));
|
||||
|
||||
auto getButton(Button button) -> const char*;
|
||||
void drawScrollbar(NVGcontext*, const Theme*, u32 index_off, u32 count, u32 max_per_page);
|
||||
|
||||
@@ -25,6 +25,7 @@ struct ProgressBox final : Widget {
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
|
||||
auto SetActionName(const std::string& action) -> ProgressBox&;
|
||||
auto SetTitle(const std::string& title) -> ProgressBox&;
|
||||
auto NewTransfer(const std::string& transfer) -> ProgressBox&;
|
||||
auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
|
||||
@@ -38,9 +39,9 @@ struct ProgressBox final : Widget {
|
||||
auto ShouldExitResult() -> Result;
|
||||
|
||||
// helper functions
|
||||
auto CopyFile(fs::Fs* fs_src, fs::Fs* fs_dst, const fs::FsPath& src, const fs::FsPath& dst) -> Result;
|
||||
auto CopyFile(fs::Fs* fs, const fs::FsPath& src, const fs::FsPath& dst) -> Result;
|
||||
auto CopyFile(const fs::FsPath& src, const fs::FsPath& dst) -> Result;
|
||||
auto CopyFile(fs::Fs* fs_src, fs::Fs* fs_dst, const fs::FsPath& src, const fs::FsPath& dst, bool single_threaded = false) -> Result;
|
||||
auto CopyFile(fs::Fs* fs, const fs::FsPath& src, const fs::FsPath& dst, bool single_threaded = false) -> Result;
|
||||
auto CopyFile(const fs::FsPath& src, const fs::FsPath& dst, bool single_threaded = false) -> Result;
|
||||
void Yield();
|
||||
|
||||
auto GetCpuId() const {
|
||||
|
||||
@@ -179,6 +179,8 @@ enum ThemeEntryID {
|
||||
ThemeEntryID_TEXT_SELECTED,
|
||||
// background colour of a selected item, can be an image (not recommended).
|
||||
ThemeEntryID_SELECTED_BACKGROUND,
|
||||
// colour of the split screen and selected item.
|
||||
ThemeEntryID_FOCUS,
|
||||
|
||||
// colour of line separators in a list.
|
||||
ThemeEntryID_LINE,
|
||||
|
||||
@@ -62,6 +62,12 @@ struct Widget : public Object {
|
||||
m_actions.clear();
|
||||
}
|
||||
|
||||
void RemoveActions(const Actions& actions) {
|
||||
for (auto& e : actions) {
|
||||
RemoveAction(e.first);
|
||||
}
|
||||
}
|
||||
|
||||
auto FireAction(Button button, u8 type = ActionType::DOWN) -> bool;
|
||||
|
||||
void SetPop(bool pop = true) {
|
||||
|
||||
@@ -8,12 +8,6 @@
|
||||
namespace sphaira::usb {
|
||||
|
||||
struct Base {
|
||||
enum { USBModule = 523 };
|
||||
|
||||
enum : Result {
|
||||
Result_Cancelled = MAKERESULT(USBModule, 100),
|
||||
};
|
||||
|
||||
Base(u64 transfer_timeout);
|
||||
virtual ~Base();
|
||||
|
||||
|
||||
@@ -10,17 +10,6 @@
|
||||
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();
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
#include "base.hpp"
|
||||
|
||||
// TODO: remove these when libnx pr is merged.
|
||||
enum { UsbDeviceSpeed_None = 0x0 };
|
||||
enum { UsbDeviceSpeed_Low = 0x1 };
|
||||
Result usbDsGetSpeed(UsbDeviceSpeed *out);
|
||||
|
||||
namespace sphaira::usb {
|
||||
|
||||
// Device Host
|
||||
|
||||
10
sphaira/include/web.hpp
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
#include <string>
|
||||
|
||||
namespace sphaira {
|
||||
|
||||
auto WebShow(const std::string& url) -> Result;
|
||||
|
||||
} // namespace sphaira
|
||||
@@ -2,49 +2,132 @@
|
||||
|
||||
#include <switch.h>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
#include "ncm.hpp"
|
||||
#include "keys.hpp"
|
||||
|
||||
namespace sphaira::es {
|
||||
|
||||
enum { TicketModule = 522 };
|
||||
|
||||
enum : Result {
|
||||
// found ticket has missmatching rights_id from it's name.
|
||||
Result_InvalidTicketBadRightsId = MAKERESULT(TicketModule, 71),
|
||||
Result_InvalidTicketVersion = MAKERESULT(TicketModule, 72),
|
||||
Result_InvalidTicketKeyType = MAKERESULT(TicketModule, 73),
|
||||
Result_InvalidTicketKeyRevision = MAKERESULT(TicketModule, 74),
|
||||
enum TitleKeyType : u8 {
|
||||
TitleKeyType_Common = 0,
|
||||
TitleKeyType_Personalized = 1,
|
||||
};
|
||||
|
||||
enum TicketSigantureType {
|
||||
TicketSigantureType_RSA_4096_SHA1 = 0x010000,
|
||||
TicketSigantureType_RSA_2048_SHA1 = 0x010001,
|
||||
TicketSigantureType_ECDSA_SHA1 = 0x010002,
|
||||
TicketSigantureType_RSA_4096_SHA256 = 0x010003,
|
||||
TicketSigantureType_RSA_2048_SHA256 = 0x010004,
|
||||
TicketSigantureType_ECDSA_SHA256 = 0x010005,
|
||||
TicketSigantureType_HMAC_SHA1_160 = 0x010006,
|
||||
enum SigType : u32 {
|
||||
SigType_Rsa4096Sha1 = 65536,
|
||||
SigType_Rsa2048Sha1 = 65537,
|
||||
SigType_Ecc480Sha1 = 65538,
|
||||
SigType_Rsa4096Sha256 = 65539,
|
||||
SigType_Rsa2048Sha256 = 65540,
|
||||
SigType_Ecc480Sha256 = 65541,
|
||||
SigType_Hmac160Sha1 = 65542
|
||||
};
|
||||
|
||||
enum TicketTitleKeyType {
|
||||
TicketTitleKeyType_Common = 0,
|
||||
TicketTitleKeyType_Personalized = 1,
|
||||
enum PubKeyType : u32 {
|
||||
PubKeyType_Rsa4096 = 0,
|
||||
PubKeyType_Rsa2048 = 1,
|
||||
PubKeyType_Ecc480 = 2
|
||||
};
|
||||
|
||||
enum TicketPropertiesBitfield {
|
||||
TicketPropertiesBitfield_None = 0,
|
||||
// temporary ticket, removed on restart
|
||||
TicketPropertiesBitfield_Temporary = 1 << 4,
|
||||
struct SignatureBlockRsa4096 {
|
||||
SigType sig_type;
|
||||
u8 sign[0x200];
|
||||
u8 reserved_1[0x3C];
|
||||
};
|
||||
static_assert(sizeof(SignatureBlockRsa4096) == 0x240);
|
||||
|
||||
struct SignatureBlockRsa2048 {
|
||||
SigType sig_type;
|
||||
u8 sign[0x100];
|
||||
u8 reserved_1[0x3C];
|
||||
};
|
||||
static_assert(sizeof(SignatureBlockRsa2048) == 0x140);
|
||||
|
||||
struct SignatureBlockEcc480 {
|
||||
SigType sig_type;
|
||||
u8 sign[0x3C];
|
||||
u8 reserved_1[0x40];
|
||||
};
|
||||
static_assert(sizeof(SignatureBlockEcc480) == 0x80);
|
||||
|
||||
struct SignatureBlockHmac160 {
|
||||
SigType sig_type;
|
||||
u8 sign[0x14];
|
||||
u8 reserved_1[0x28];
|
||||
};
|
||||
static_assert(sizeof(SignatureBlockHmac160) == 0x40);
|
||||
|
||||
struct CertHeader {
|
||||
char issuer[0x40];
|
||||
PubKeyType pub_key_type;
|
||||
char subject[0x40]; /* ServerId, DeviceId */
|
||||
u32 date;
|
||||
};
|
||||
static_assert(sizeof(CertHeader) == 0x88);
|
||||
|
||||
struct PublicKeyBlockRsa4096 {
|
||||
u8 public_key[0x200];
|
||||
u32 public_exponent;
|
||||
u8 reserved_1[0x34];
|
||||
};
|
||||
static_assert(sizeof(PublicKeyBlockRsa4096) == 0x238);
|
||||
|
||||
struct PublicKeyBlockRsa2048 {
|
||||
u8 public_key[0x100];
|
||||
u32 public_exponent;
|
||||
u8 reserved_1[0x34];
|
||||
};
|
||||
static_assert(sizeof(PublicKeyBlockRsa2048) == 0x138);
|
||||
|
||||
struct PublicKeyBlockEcc480 {
|
||||
u8 public_key[0x3C];
|
||||
u8 reserved_1[0x3C];
|
||||
};
|
||||
static_assert(sizeof(PublicKeyBlockEcc480) == 0x78);
|
||||
|
||||
template<typename Sig, typename Pub>
|
||||
struct Cert {
|
||||
Sig signature_block;
|
||||
CertHeader cert_header;
|
||||
Pub public_key_block;
|
||||
};
|
||||
|
||||
using CertRsa4096PubRsa4096 = Cert<SignatureBlockRsa4096, PublicKeyBlockRsa4096>;
|
||||
using CertRsa4096PubRsa2048 = Cert<SignatureBlockRsa4096, PublicKeyBlockRsa2048>;
|
||||
using CertRsa4096PubEcc480 = Cert<SignatureBlockRsa4096, PublicKeyBlockEcc480>;
|
||||
|
||||
using CertRsa2048PubRsa4096 = Cert<SignatureBlockRsa2048, PublicKeyBlockRsa4096>;
|
||||
using CertRsa2048PubRsa2048 = Cert<SignatureBlockRsa2048, PublicKeyBlockRsa2048>;
|
||||
using CertRsa2048PubEcc480 = Cert<SignatureBlockRsa2048, PublicKeyBlockEcc480>;
|
||||
|
||||
using CertEcc480PubRsa4096 = Cert<SignatureBlockEcc480, PublicKeyBlockRsa4096>;
|
||||
using CertEcc480PubRsa2048 = Cert<SignatureBlockEcc480, PublicKeyBlockRsa2048>;
|
||||
using CertEcc480PubEcc480 = Cert<SignatureBlockEcc480, PublicKeyBlockEcc480>;
|
||||
|
||||
using CertHmac160PubRsa4096 = Cert<SignatureBlockHmac160, PublicKeyBlockRsa4096>;
|
||||
using CertHmac160PubRsa2048 = Cert<SignatureBlockHmac160, PublicKeyBlockRsa2048>;
|
||||
using CertHmac160PubEcc480 = Cert<SignatureBlockHmac160, PublicKeyBlockEcc480>;
|
||||
|
||||
static_assert(sizeof(CertRsa4096PubRsa4096) == 0x500);
|
||||
static_assert(sizeof(CertRsa4096PubRsa2048) == 0x400);
|
||||
static_assert(sizeof(CertRsa4096PubEcc480) == 0x340);
|
||||
static_assert(sizeof(CertRsa2048PubRsa4096) == 0x400);
|
||||
static_assert(sizeof(CertRsa2048PubRsa2048) == 0x300);
|
||||
static_assert(sizeof(CertRsa2048PubEcc480) == 0x240);
|
||||
static_assert(sizeof(CertEcc480PubRsa4096) == 0x340);
|
||||
static_assert(sizeof(CertEcc480PubRsa2048) == 0x240);
|
||||
static_assert(sizeof(CertEcc480PubEcc480) == 0x180);
|
||||
static_assert(sizeof(CertHmac160PubRsa4096) == 0x300);
|
||||
static_assert(sizeof(CertHmac160PubRsa2048) == 0x200);
|
||||
static_assert(sizeof(CertHmac160PubEcc480) == 0x140);
|
||||
|
||||
struct TicketData {
|
||||
u8 issuer[0x40];
|
||||
char issuer[0x40];
|
||||
u8 title_key_block[0x100];
|
||||
u8 ticket_version1;
|
||||
u8 format_version;
|
||||
u8 title_key_type;
|
||||
u16 ticket_version2;
|
||||
u8 license_type;
|
||||
u16 version;
|
||||
TitleKeyType license_type;
|
||||
u8 master_key_revision;
|
||||
u16 properties_bitfield;
|
||||
u8 _0x148[0x8];
|
||||
@@ -52,10 +135,28 @@ struct TicketData {
|
||||
u64 device_id;
|
||||
FsRightsId rights_id;
|
||||
u32 account_id;
|
||||
u8 _0x174[0xC];
|
||||
u8 _0x180[0x140];
|
||||
u32 sect_total_size;
|
||||
u32 sect_hdr_offset;
|
||||
u16 sect_hdr_count;
|
||||
u16 sect_hdr_entry_size;
|
||||
};
|
||||
static_assert(sizeof(TicketData) == 0x2C0);
|
||||
static_assert(sizeof(TicketData) == 0x180);
|
||||
|
||||
template<typename Sig>
|
||||
struct Ticket {
|
||||
Sig signature_block;
|
||||
TicketData data;
|
||||
};
|
||||
|
||||
using TicketRsa4096 = Ticket<SignatureBlockRsa4096>;
|
||||
using TicketRsa2048 = Ticket<SignatureBlockRsa2048>;
|
||||
using TicketEcc480 = Ticket<SignatureBlockEcc480>;
|
||||
using TicketHmac160 = Ticket<SignatureBlockHmac160>;
|
||||
|
||||
static_assert(sizeof(TicketRsa4096) == 0x3C0);
|
||||
static_assert(sizeof(TicketRsa2048) == 0x2C0);
|
||||
static_assert(sizeof(TicketEcc480) == 0x200);
|
||||
static_assert(sizeof(TicketHmac160) == 0x1C0);
|
||||
|
||||
struct EticketRsaDeviceKey {
|
||||
u8 ctr[AES_128_KEY_SIZE];
|
||||
@@ -90,12 +191,16 @@ Result GetCommonTicketAndCertificateSize(u64 *tik_size_out, u64 *cert_size_out,
|
||||
Result GetCommonTicketAndCertificateData(u64 *tik_size_out, u64 *cert_size_out, void* tik_buf, u64 tik_size, void* cert_buf, u64 cert_size, const FsRightsId* rightsId); // [4.0.0+]
|
||||
|
||||
// ticket functions.
|
||||
Result GetTicketDataOffset(std::span<const u8> ticket, u64& out);
|
||||
Result GetTicketDataOffset(std::span<const u8> ticket, u64& out, bool is_cert = false);
|
||||
Result GetTicketData(std::span<const u8> ticket, es::TicketData* out);
|
||||
Result SetTicketData(std::span<u8> ticket, const es::TicketData* in);
|
||||
|
||||
// gets the title key and performs RSA-2048-OAEP if needed.
|
||||
Result GetTitleKey(keys::KeyEntry& out, const TicketData& data, const keys::Keys& keys);
|
||||
Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys);
|
||||
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys);
|
||||
Result EncryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys);
|
||||
|
||||
Result ShouldPatchTicket(const TicketData& data, std::span<const u8> ticket, std::span<const u8> cert_chain, bool patch_personalised, bool& should_patch);
|
||||
Result ShouldPatchTicket(std::span<const u8> ticket, std::span<const u8> cert_chain, bool patch_personalised, bool& should_patch);
|
||||
Result PatchTicket(std::vector<u8>& ticket, std::span<const u8> cert_chain, u8 key_gen, const keys::Keys& keys, bool patch_personalised);
|
||||
|
||||
} // namespace sphaira::es
|
||||
|
||||
@@ -46,19 +46,19 @@ struct Keys {
|
||||
}
|
||||
|
||||
auto GetNcaKeyArea(KeyEntry* out, u8 key, u8 index) const -> Result {
|
||||
R_UNLESS(HasNcaKeyArea(key, index), 0x1);
|
||||
R_UNLESS(HasNcaKeyArea(key, index), Result_KeyMissingNcaKeyArea);
|
||||
*out = key_area_key[index][FixKey(key)];
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
auto GetTitleKek(KeyEntry* out, u8 key) const -> Result {
|
||||
R_UNLESS(HasTitleKek(key), 0x1);
|
||||
R_UNLESS(HasTitleKek(key), Result_KeyMissingTitleKek);
|
||||
*out = titlekek[FixKey(key)];
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
auto GetMasterKey(KeyEntry* out, u8 key) const -> Result {
|
||||
R_UNLESS(HasMasterKey(key), 0x1);
|
||||
R_UNLESS(HasMasterKey(key), Result_KeyMissingMasterKey);
|
||||
*out = master_key[FixKey(key)];
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ 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);
|
||||
Result ParseControl(const fs::FsPath& path, u64 program_id, void* nacp_out = nullptr, s64 nacp_size = 0, std::vector<u8>* icon_out = nullptr, s64 nacp_off = 0);
|
||||
|
||||
auto GetKeyGenStr(u8 key_gen) -> const char*;
|
||||
|
||||
|
||||
@@ -11,16 +11,6 @@
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
struct Usb final : Base {
|
||||
enum { USBModule = 523 };
|
||||
|
||||
enum : Result {
|
||||
Result_BadMagic = MAKERESULT(USBModule, 0),
|
||||
Result_BadVersion = MAKERESULT(USBModule, 1),
|
||||
Result_BadCount = MAKERESULT(USBModule, 2),
|
||||
Result_BadTransferSize = MAKERESULT(USBModule, 3),
|
||||
Result_BadTotalSize = MAKERESULT(USBModule, 4),
|
||||
};
|
||||
|
||||
Usb(u64 transfer_timeout);
|
||||
~Usb();
|
||||
|
||||
|
||||
@@ -16,57 +16,6 @@
|
||||
|
||||
namespace sphaira::yati {
|
||||
|
||||
enum { YatiModule = 521 };
|
||||
|
||||
enum : Result {
|
||||
// unkown container for the source provided.
|
||||
Result_ContainerNotFound = MAKERESULT(YatiModule, 10),
|
||||
Result_Cancelled = MAKERESULT(YatiModule, 11),
|
||||
|
||||
// nca required by the cnmt but not found in collection.
|
||||
Result_NcaNotFound = MAKERESULT(YatiModule, 30),
|
||||
Result_InvalidNcaReadSize = MAKERESULT(YatiModule, 31),
|
||||
Result_InvalidNcaSigKeyGen = MAKERESULT(YatiModule, 32),
|
||||
Result_InvalidNcaMagic = MAKERESULT(YatiModule, 33),
|
||||
Result_InvalidNcaSignature0 = MAKERESULT(YatiModule, 34),
|
||||
Result_InvalidNcaSignature1 = MAKERESULT(YatiModule, 35),
|
||||
// invalid sha256 over the entire nca.
|
||||
Result_InvalidNcaSha256 = MAKERESULT(YatiModule, 36),
|
||||
|
||||
// section could not be found.
|
||||
Result_NczSectionNotFound = MAKERESULT(YatiModule, 50),
|
||||
// section count == 0.
|
||||
Result_InvalidNczSectionCount = MAKERESULT(YatiModule, 51),
|
||||
// block could not be found.
|
||||
Result_NczBlockNotFound = MAKERESULT(YatiModule, 52),
|
||||
// block version != 2.
|
||||
Result_InvalidNczBlockVersion = MAKERESULT(YatiModule, 53),
|
||||
// block type != 1.
|
||||
Result_InvalidNczBlockType = MAKERESULT(YatiModule, 54),
|
||||
// block count == 0.
|
||||
Result_InvalidNczBlockTotal = MAKERESULT(YatiModule, 55),
|
||||
// block size exponent < 14 || > 32.
|
||||
Result_InvalidNczBlockSizeExponent = MAKERESULT(YatiModule, 56),
|
||||
// zstd error while decompressing ncz.
|
||||
Result_InvalidNczZstdError = MAKERESULT(YatiModule, 57),
|
||||
|
||||
// nca has rights_id but matching ticket wasn't found.
|
||||
Result_TicketNotFound = MAKERESULT(YatiModule, 70),
|
||||
// found ticket has missmatching rights_id from it's name.
|
||||
Result_InvalidTicketBadRightsId = MAKERESULT(YatiModule, 71),
|
||||
Result_InvalidTicketVersion = MAKERESULT(YatiModule, 72),
|
||||
Result_InvalidTicketKeyType = MAKERESULT(YatiModule, 73),
|
||||
Result_InvalidTicketKeyRevision = MAKERESULT(YatiModule, 74),
|
||||
|
||||
// cert not found for the ticket.
|
||||
Result_CertNotFound = MAKERESULT(YatiModule, 90),
|
||||
|
||||
// unable to fetch header from ncm database.
|
||||
Result_NcmDbCorruptHeader = MAKERESULT(YatiModule, 110),
|
||||
// unable to total infos from ncm database.
|
||||
Result_NcmDbCorruptInfos = MAKERESULT(YatiModule, 111),
|
||||
};
|
||||
|
||||
struct Config {
|
||||
bool sd_card_install{};
|
||||
|
||||
@@ -99,6 +48,9 @@ struct Config {
|
||||
// if set, it will ignore the distribution bit in the nca header.
|
||||
bool ignore_distribution_bit{};
|
||||
|
||||
// converts a personalised ticket to common.
|
||||
bool convert_to_common_ticket{};
|
||||
|
||||
// converts titlekey to standard crypto, also known as "ticketless".
|
||||
// this will not work with addon (dlc), so, addon tickets will be installed.
|
||||
bool convert_to_standard_crypto{};
|
||||
@@ -120,6 +72,7 @@ struct ConfigOverride {
|
||||
std::optional<bool> skip_rsa_header_fixed_key_verify{};
|
||||
std::optional<bool> skip_rsa_npdm_fixed_key_verify{};
|
||||
std::optional<bool> ignore_distribution_bit{};
|
||||
std::optional<bool> convert_to_common_ticket{};
|
||||
std::optional<bool> convert_to_standard_crypto{};
|
||||
std::optional<bool> lower_master_key{};
|
||||
std::optional<bool> lower_system_version{};
|
||||
|
||||
@@ -19,11 +19,13 @@
|
||||
#include "defines.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "ftpsrv_helper.hpp"
|
||||
#include "haze_helper.hpp"
|
||||
#include "web.hpp"
|
||||
#include "swkbd.hpp"
|
||||
|
||||
#include <nanovg_dk.h>
|
||||
#include <minIni.h>
|
||||
#include <pulsar.h>
|
||||
#include <haze.h>
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
#include <cassert>
|
||||
@@ -49,7 +51,7 @@ constexpr const u8 DEFAULT_IMAGE_DATA[]{
|
||||
};
|
||||
|
||||
void download_default_music() {
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Downloading "_i18n, "default_music.bfstm", [](auto pbox){
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Downloading "_i18n, "default_music.bfstm", [](auto pbox) -> Result {
|
||||
const auto result = curl::Api().ToFile(
|
||||
curl::Url{DEFAULT_MUSIC_URL},
|
||||
curl::Path{DEFAULT_MUSIC_PATH},
|
||||
@@ -57,7 +59,7 @@ void download_default_music() {
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
R_THROW(0x1);
|
||||
R_THROW(Result_AppFailedMusicDownload);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
@@ -96,6 +98,7 @@ constexpr ThemeIdPair THEME_ENTRIES[] = {
|
||||
{ "selected_background", ThemeEntryID_SELECTED_BACKGROUND, ElementType::Colour },
|
||||
{ "error", ThemeEntryID_ERROR, ElementType::Colour },
|
||||
{ "popup", ThemeEntryID_POPUP, ElementType::Colour },
|
||||
{ "focus", ThemeEntryID_FOCUS, ElementType::Colour },
|
||||
{ "line", ThemeEntryID_LINE, ElementType::Colour },
|
||||
{ "line_separator", ThemeEntryID_LINE_SEPARATOR, ElementType::Colour },
|
||||
{ "sidebar", ThemeEntryID_SIDEBAR, ElementType::Colour },
|
||||
@@ -397,11 +400,6 @@ void LoadThemeInternal(ThemeMeta meta, ThemeData& theme_data, int inherit_level
|
||||
}
|
||||
}
|
||||
|
||||
void haze_callback(const HazeCallbackData *data) {
|
||||
App::NotifyFlashLed();
|
||||
evman::push(*data, false);
|
||||
}
|
||||
|
||||
void nxlink_callback(const NxlinkCallbackData *data) {
|
||||
App::NotifyFlashLed();
|
||||
evman::push(*data, false);
|
||||
@@ -457,9 +455,6 @@ void App::Loop() {
|
||||
} else if constexpr(std::is_same_v<T, evman::ExitEventData>) {
|
||||
log_write("[ExitEventData] got event\n");
|
||||
m_quit = true;
|
||||
} else if constexpr(std::is_same_v<T, HazeCallbackData>) {
|
||||
// log_write("[ExitEventData] got event\n");
|
||||
// m_quit = true;
|
||||
} else if constexpr(std::is_same_v<T, NxlinkCallbackData>) {
|
||||
switch (arg.type) {
|
||||
case NxlinkCallbackType_Connected:
|
||||
@@ -854,9 +849,9 @@ void App::SetMtpEnable(bool enable) {
|
||||
if (App::GetMtpEnable() != enable) {
|
||||
g_app->m_mtp_enabled.Set(enable);
|
||||
if (enable) {
|
||||
hazeInitialize(haze_callback, 0x2C, 2);
|
||||
haze::Init();
|
||||
} else {
|
||||
hazeExit();
|
||||
haze::Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -893,22 +888,18 @@ void App::SetTextScrollSpeed(long index) {
|
||||
}
|
||||
|
||||
auto App::Install(OwoConfig& config) -> Result {
|
||||
config.nro_path = nro_add_arg_file(config.nro_path);
|
||||
if (!config.icon.empty()) {
|
||||
config.icon = GetNroIcon(config.icon);
|
||||
}
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing Forwarder"_i18n, config.name, [config](auto pbox) mutable -> Result {
|
||||
return Install(pbox, config);
|
||||
}, [](Result rc){
|
||||
App::PushErrorBox(rc, "Failed to install forwarder"_i18n);
|
||||
|
||||
const auto rc = install_forwarder(config, App::GetInstallSdEnable() ? NcmStorageId_SdCard : NcmStorageId_BuiltInUser);
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::PlaySoundEffect(SoundEffect_Install);
|
||||
App::Notify("Installed!"_i18n);
|
||||
}
|
||||
}));
|
||||
|
||||
if (R_FAILED(rc)) {
|
||||
App::PlaySoundEffect(SoundEffect_Error);
|
||||
App::Push(std::make_shared<ui::ErrorBox>(rc, "Failed to install forwarder"_i18n));
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Install);
|
||||
App::Notify("Installed!"_i18n);
|
||||
}
|
||||
|
||||
return rc;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
auto App::Install(ui::ProgressBox* pbox, OwoConfig& config) -> Result {
|
||||
@@ -917,17 +908,15 @@ auto App::Install(ui::ProgressBox* pbox, OwoConfig& config) -> Result {
|
||||
config.icon = GetNroIcon(config.icon);
|
||||
}
|
||||
|
||||
const auto rc = install_forwarder(pbox, config, GetInstallSdEnable() ? NcmStorageId_SdCard : NcmStorageId_BuiltInUser);
|
||||
|
||||
if (R_FAILED(rc)) {
|
||||
App::PlaySoundEffect(SoundEffect_Error);
|
||||
App::Push(std::make_shared<ui::ErrorBox>(rc, "Failed to install forwarder"_i18n));
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Install);
|
||||
App::Notify("Installed!"_i18n);
|
||||
if (config.logo.empty()) {
|
||||
fs::FsNativeSd().read_entire_file("/config/sphaira/logo/NintendoLogo.png", config.logo);
|
||||
}
|
||||
|
||||
return rc;
|
||||
if (config.gif.empty()) {
|
||||
fs::FsNativeSd().read_entire_file("/config/sphaira/logo/StartupMovie.gif", config.gif);
|
||||
}
|
||||
|
||||
return install_forwarder(pbox, config, GetInstallSdEnable() ? NcmStorageId_SdCard : NcmStorageId_BuiltInUser);
|
||||
}
|
||||
|
||||
auto App::IsEmummc() -> bool {
|
||||
@@ -1286,8 +1275,8 @@ void App::ScanThemeEntries() {
|
||||
App::App(const char* argv0) {
|
||||
TimeStamp ts;
|
||||
|
||||
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
|
||||
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
|
||||
// boost mode is enabled in userAppInit().
|
||||
ON_SCOPE_EXIT(App::SetBoostMode(false));
|
||||
|
||||
g_app = this;
|
||||
m_start_timestamp = armGetSystemTick();
|
||||
@@ -1303,21 +1292,6 @@ App::App(const char* argv0) {
|
||||
__nx_applet_exit_mode = 1;
|
||||
}
|
||||
|
||||
// get emummc config.
|
||||
alignas(0x1000) AmsEmummcPaths 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);
|
||||
m_emummc_paths = paths;
|
||||
|
||||
log_write("emummc : %u\n", App::IsEmummc());
|
||||
if (App::IsEmummc()) {
|
||||
log_write("[emummc] file based path: %s\n", m_emummc_paths.file_based_path);
|
||||
log_write("[emummc] nintendo path: %s\n", m_emummc_paths.nintendo);
|
||||
}
|
||||
|
||||
fs::FsNativeSd fs;
|
||||
fs.CreateDirectoryRecursively("/config/sphaira");
|
||||
fs.CreateDirectory("/config/sphaira/assoc");
|
||||
@@ -1359,6 +1333,7 @@ App::App(const char* argv0) {
|
||||
else if (app->m_skip_rsa_header_fixed_key_verify.LoadFrom(Key, Value)) {}
|
||||
else if (app->m_skip_rsa_npdm_fixed_key_verify.LoadFrom(Key, Value)) {}
|
||||
else if (app->m_ignore_distribution_bit.LoadFrom(Key, Value)) {}
|
||||
else if (app->m_convert_to_common_ticket.LoadFrom(Key, Value)) {}
|
||||
else if (app->m_convert_to_standard_crypto.LoadFrom(Key, Value)) {}
|
||||
else if (app->m_lower_master_key.LoadFrom(Key, Value)) {}
|
||||
else if (app->m_lower_system_version.LoadFrom(Key, Value)) {}
|
||||
@@ -1373,14 +1348,58 @@ App::App(const char* argv0) {
|
||||
// loading each config one by one as it avoids re-opening the file multiple times.
|
||||
ini_browse(cb, this, CONFIG_PATH);
|
||||
|
||||
i18n::init(GetLanguage());
|
||||
|
||||
if (App::GetLogEnable()) {
|
||||
log_file_init();
|
||||
log_write("hello world v%s\n", APP_VERSION_HASH);
|
||||
App::Notify("Warning! Logs are enabled, Sphaira will run slowly!"_i18n);
|
||||
}
|
||||
|
||||
if (log_is_init()) {
|
||||
SetSysFirmwareVersion fw_version{};
|
||||
setsysInitialize();
|
||||
ON_SCOPE_EXIT(setsysExit());
|
||||
setsysGetFirmwareVersion(&fw_version);
|
||||
|
||||
log_write("[version] platform: %s\n", fw_version.platform);
|
||||
log_write("[version] version_hash: %s\n", fw_version.version_hash);
|
||||
log_write("[version] display_version: %s\n", fw_version.display_version);
|
||||
log_write("[version] display_title: %s\n", fw_version.display_title);
|
||||
|
||||
splInitialize();
|
||||
ON_SCOPE_EXIT(splExit());
|
||||
|
||||
u64 out{};
|
||||
splGetConfig((SplConfigItem)65000, &out);
|
||||
log_write("[ams] version: %lu.%lu.%lu\n", (out >> 56) & 0xFF, (out >> 48) & 0xFF, (out >> 40) & 0xFF);
|
||||
log_write("[ams] target version: %lu.%lu.%lu\n", (out >> 24) & 0xFF, (out >> 16) & 0xFF, (out >> 8) & 0xFF);
|
||||
log_write("[ams] key gen: %lu\n", (out >> 32) & 0xFF);
|
||||
|
||||
splGetConfig((SplConfigItem)65003, &out);
|
||||
log_write("[ams] hash: %lx\n", out);
|
||||
|
||||
splGetConfig((SplConfigItem)65010, &out);
|
||||
log_write("[ams] usb 3.0 enabled: %lu\n", out);
|
||||
}
|
||||
|
||||
// get emummc config.
|
||||
alignas(0x1000) AmsEmummcPaths 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);
|
||||
m_emummc_paths = paths;
|
||||
|
||||
log_write("[emummc] enabled: %u\n", App::IsEmummc());
|
||||
if (App::IsEmummc()) {
|
||||
log_write("[emummc] file based path: %s\n", m_emummc_paths.file_based_path);
|
||||
log_write("[emummc] nintendo path: %s\n", m_emummc_paths.nintendo);
|
||||
}
|
||||
|
||||
if (App::GetMtpEnable()) {
|
||||
hazeInitialize(haze_callback, PRIO_PREEMPTIVE, 2);
|
||||
haze::Init();
|
||||
}
|
||||
|
||||
if (App::GetFtpEnable()) {
|
||||
@@ -1401,6 +1420,12 @@ App::App(const char* argv0) {
|
||||
|
||||
curl::Init();
|
||||
|
||||
#ifdef USE_NVJPG
|
||||
// this has to be init before deko3d.
|
||||
nj::initialize();
|
||||
m_decoder.initialize();
|
||||
#endif
|
||||
|
||||
// get current size of the framebuffer
|
||||
const auto fb = GetFrameBufferSize();
|
||||
s_width = fb.size.x;
|
||||
@@ -1433,8 +1458,6 @@ App::App(const char* argv0) {
|
||||
this->renderer.emplace(s_width, s_height, this->device, this->queue, *this->pool_images, *this->pool_code, *this->pool_data);
|
||||
this->vg = nvgCreateDk(&*this->renderer, NVG_ANTIALIAS | NVG_STENCIL_STROKES);
|
||||
|
||||
i18n::init(GetLanguage());
|
||||
|
||||
// not sure if these are meant to be deleted or not...
|
||||
PlFontData font_standard, font_extended, font_lang;
|
||||
plGetSharedFontByType(&font_standard, PlSharedFontType_Standard);
|
||||
@@ -1624,6 +1647,34 @@ void App::DisplayMiscOptions(bool left_side) {
|
||||
App::Push(e.func(ui::menu::MenuFlag_None));
|
||||
}));
|
||||
}
|
||||
|
||||
if (App::IsApplication()) {
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("Web"_i18n, [](){
|
||||
// add some default entries, will use a config file soon so users can set their own.
|
||||
ui::PopupList::Items items;
|
||||
items.emplace_back("https://lite.duckduckgo.com/lite");
|
||||
items.emplace_back("https://dns.switchbru.com");
|
||||
items.emplace_back("https://gbatemp.net");
|
||||
items.emplace_back("https://github.com/ITotalJustice/sphaira/wiki");
|
||||
items.emplace_back("Enter custom URL"_i18n);
|
||||
|
||||
App::Push(std::make_shared<ui::PopupList>(
|
||||
"Select URL"_i18n, items, [items](auto op_index){
|
||||
if (op_index) {
|
||||
const auto index = *op_index;
|
||||
if (index == items.size() - 1) {
|
||||
std::string out;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, "Enter URL"_i18n.c_str(), "https://")) && !out.empty()) {
|
||||
WebShow(out);
|
||||
}
|
||||
} else {
|
||||
WebShow(items[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
));
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void App::DisplayAdvancedOptions(bool left_side) {
|
||||
@@ -1635,7 +1686,8 @@ 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;
|
||||
std::vector<std::string> menu_names;
|
||||
ui::SidebarEntryArray::Items menu_items;
|
||||
for (auto& e : ui::menu::main::GetMiscMenuEntries()) {
|
||||
if (!e.IsShortcut()) {
|
||||
continue;
|
||||
@@ -1646,16 +1698,7 @@ void App::DisplayAdvancedOptions(bool left_side) {
|
||||
}
|
||||
|
||||
menu_names.emplace_back(e.name);
|
||||
}
|
||||
|
||||
ui::SidebarEntryArray::Items side_menu_items;
|
||||
for (auto& str : menu_names) {
|
||||
side_menu_items.push_back(i18n::get(str));
|
||||
}
|
||||
|
||||
const auto it = std::find(menu_names.cbegin(), menu_names.cend(), g_app->m_right_menu.Get());
|
||||
if (it == menu_names.cend()) {
|
||||
g_app->m_right_menu.Set(menu_names[0]);
|
||||
menu_items.push_back(i18n::get(e.name));
|
||||
}
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [](bool& enable){
|
||||
@@ -1674,10 +1717,15 @@ void App::DisplayAdvancedOptions(bool left_side) {
|
||||
App::SetTextScrollSpeed(index_out);
|
||||
}, App::GetTextScrollSpeed()));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryArray>("Set left-side menu"_i18n, side_menu_items, [menu_names](s64& index_out){
|
||||
options->Add(std::make_shared<ui::SidebarEntryArray>("Set left-side menu"_i18n, menu_items, [menu_names](s64& index_out){
|
||||
const auto e = menu_names[index_out];
|
||||
if (g_app->m_left_menu.Get() != e) {
|
||||
// swap menus around.
|
||||
if (g_app->m_right_menu.Get() == e) {
|
||||
g_app->m_right_menu.Set(g_app->m_left_menu.Get());
|
||||
}
|
||||
g_app->m_left_menu.Set(e);
|
||||
|
||||
App::Push(std::make_shared<ui::OptionBox>(
|
||||
"Press OK to restart Sphaira"_i18n, "OK"_i18n, [](auto){
|
||||
App::ExitRestart();
|
||||
@@ -1686,10 +1734,15 @@ void App::DisplayAdvancedOptions(bool left_side) {
|
||||
}
|
||||
}, i18n::get(g_app->m_left_menu.Get())));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryArray>("Set right-side menu"_i18n, side_menu_items, [menu_names](s64& index_out){
|
||||
options->Add(std::make_shared<ui::SidebarEntryArray>("Set right-side menu"_i18n, menu_items, [menu_names](s64& index_out){
|
||||
const auto e = menu_names[index_out];
|
||||
if (g_app->m_right_menu.Get() != e) {
|
||||
// swap menus around.
|
||||
if (g_app->m_left_menu.Get() == e) {
|
||||
g_app->m_left_menu.Set(g_app->m_right_menu.Get());
|
||||
}
|
||||
g_app->m_right_menu.Set(e);
|
||||
|
||||
App::Push(std::make_shared<ui::OptionBox>(
|
||||
"Press OK to restart Sphaira"_i18n, "OK"_i18n, [](auto){
|
||||
App::ExitRestart();
|
||||
@@ -1779,6 +1832,10 @@ void App::DisplayInstallOptions(bool left_side) {
|
||||
App::GetApp()->m_ignore_distribution_bit.Set(enable);
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Convert to common ticket"_i18n, App::GetApp()->m_convert_to_common_ticket.Get(), [](bool& enable){
|
||||
App::GetApp()->m_convert_to_common_ticket.Set(enable);
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [](bool& enable){
|
||||
App::GetApp()->m_convert_to_standard_crypto.Set(enable);
|
||||
}));
|
||||
@@ -1815,26 +1872,36 @@ void App::DisplayDumpOptions(bool left_side) {
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Multi-threaded USB transfer"_i18n, App::GetApp()->m_dump_usb_transfer_stream.Get(), [](bool& enable){
|
||||
App::GetApp()->m_dump_usb_transfer_stream.Set(enable);
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Convert to common ticket"_i18n, App::GetApp()->m_dump_convert_to_common_ticket.Get(), [](bool& enable){
|
||||
App::GetApp()->m_dump_convert_to_common_ticket.Set(enable);
|
||||
}));
|
||||
}
|
||||
|
||||
App::~App() {
|
||||
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
|
||||
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
|
||||
// boost mode is disabled in userAppExit().
|
||||
App::SetBoostMode(true);
|
||||
|
||||
log_write("starting to exit\n");
|
||||
TimeStamp ts;
|
||||
|
||||
i18n::exit();
|
||||
curl::Exit();
|
||||
appletUnhook(&m_appletHookCookie);
|
||||
|
||||
// destroy this first as it seems to prevent a crash when exiting the appstore
|
||||
// when an image that was being drawn is displayed
|
||||
// replicate: saves -> homebrew -> misc -> appstore -> sphaira -> changelog -> exit
|
||||
// it will crash when deleting image 43.
|
||||
this->destroyFramebufferResources();
|
||||
|
||||
// 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();
|
||||
nvgDeleteImage(vg, m_default_image);
|
||||
|
||||
appletUnhook(&m_appletHookCookie);
|
||||
i18n::exit();
|
||||
curl::Exit();
|
||||
|
||||
ini_puts("config", "theme", m_theme.meta.ini_path, CONFIG_PATH);
|
||||
|
||||
CloseTheme();
|
||||
|
||||
// Free any loaded sound from memory
|
||||
@@ -1847,10 +1914,14 @@ App::~App() {
|
||||
// De-initialize our player
|
||||
plsrPlayerExit();
|
||||
|
||||
this->destroyFramebufferResources();
|
||||
nvgDeleteDk(this->vg);
|
||||
this->renderer.reset();
|
||||
|
||||
#ifdef USE_NVJPG
|
||||
m_decoder.finalize();
|
||||
nj::finalize();
|
||||
#endif
|
||||
|
||||
// backup hbmenu if it is not sphaira
|
||||
if (App::GetReplaceHbmenuEnable() && !IsHbmenu()) {
|
||||
NacpStruct hbmenu_nacp;
|
||||
@@ -1905,7 +1976,7 @@ App::~App() {
|
||||
|
||||
if (App::GetMtpEnable()) {
|
||||
log_write("closing mtp\n");
|
||||
hazeExit();
|
||||
haze::Exit();
|
||||
}
|
||||
|
||||
if (App::GetFtpEnable()) {
|
||||
@@ -1923,6 +1994,8 @@ App::~App() {
|
||||
usbHsFsExit();
|
||||
}
|
||||
|
||||
log_write("\t[EXIT] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
|
||||
|
||||
if (App::GetLogEnable()) {
|
||||
log_write("closing log\n");
|
||||
log_file_exit();
|
||||
|
||||
@@ -259,7 +259,7 @@ private:
|
||||
struct ThreadEntry {
|
||||
auto Create() -> Result {
|
||||
m_curl = curl_easy_init();
|
||||
R_UNLESS(m_curl != nullptr, 0x1);
|
||||
R_UNLESS(m_curl != nullptr, Result_CurlFailedEasyInit);
|
||||
|
||||
ueventCreate(&m_uevent, true);
|
||||
R_TRY(threadCreate(&m_thread, ThreadFunc, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
|
||||
@@ -332,7 +332,7 @@ struct ThreadQueue {
|
||||
}
|
||||
|
||||
auto Add(const Api& api, bool is_upload = false) -> bool {
|
||||
if (api.GetUrl().empty() || api.GetPath().empty() || !api.GetOnComplete()) {
|
||||
if (api.GetUrl().empty() || !api.GetOnComplete()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,17 +23,12 @@
|
||||
namespace sphaira::dump {
|
||||
namespace {
|
||||
|
||||
struct DumpEntry {
|
||||
DumpLocationType type;
|
||||
s32 index;
|
||||
};
|
||||
|
||||
struct DumpLocation {
|
||||
struct DumpLocationEntry {
|
||||
const DumpLocationType type;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
constexpr DumpLocation DUMP_LOCATIONS[]{
|
||||
constexpr DumpLocationEntry DUMP_LOCATIONS[]{
|
||||
{ DumpLocationType_SdCard, "microSD card (/dumps/)" },
|
||||
{ DumpLocationType_UsbS2S, "USB transfer (Switch 2 Switch)" },
|
||||
{ DumpLocationType_DevNull, "/dev/null (Speed Test)" },
|
||||
@@ -207,14 +202,14 @@ Result DumpToUsbS2S(ui::ProgressBox* pbox, BaseSource* source, std::span<const f
|
||||
|
||||
while (!pbox->ShouldExit()) {
|
||||
if (R_SUCCEEDED(usb->IsUsbConnected(timeout))) {
|
||||
pbox->NewTransfer("USB connected, sending file list");
|
||||
pbox->NewTransfer("USB connected, sending file list"_i18n);
|
||||
u8 flags = usb::tinfoil::USBFlag_NONE;
|
||||
if (App::GetApp()->m_dump_usb_transfer_stream.Get()) {
|
||||
flags |= usb::tinfoil::USBFlag_STREAM;
|
||||
}
|
||||
|
||||
if (R_SUCCEEDED(usb->WaitForConnection(timeout, flags, file_list))) {
|
||||
pbox->NewTransfer("Sent file list, waiting for command...");
|
||||
pbox->NewTransfer("Sent file list, waiting for command..."_i18n);
|
||||
|
||||
Result rc;
|
||||
if (flags & usb::tinfoil::USBFlag_STREAM) {
|
||||
@@ -232,7 +227,7 @@ Result DumpToUsbS2S(ui::ProgressBox* pbox, BaseSource* source, std::span<const f
|
||||
log_write("skipped polling for exit command\n");
|
||||
}
|
||||
|
||||
if (rc == usb->Result_Exit) {
|
||||
if (rc == Result_UsbUploadExit) {
|
||||
log_write("got exit command\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
@@ -240,7 +235,7 @@ Result DumpToUsbS2S(ui::ProgressBox* pbox, BaseSource* source, std::span<const f
|
||||
return rc;
|
||||
}
|
||||
} else {
|
||||
pbox->NewTransfer("waiting for usb connection...");
|
||||
pbox->NewTransfer("waiting for usb connection..."_i18n);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +303,7 @@ Result DumpToNetwork(ui::ProgressBox* pbox, const location::Entry& loc, BaseSour
|
||||
}
|
||||
);
|
||||
|
||||
R_UNLESS(result.success, 0x1);
|
||||
R_UNLESS(result.success, Result_DumpFailedNetworkUpload);
|
||||
R_SUCCEED();
|
||||
}
|
||||
));
|
||||
@@ -319,23 +314,24 @@ Result DumpToNetwork(ui::ProgressBox* pbox, const location::Entry& loc, BaseSour
|
||||
|
||||
} // namespace
|
||||
|
||||
void Dump(std::shared_ptr<BaseSource> source, const std::vector<fs::FsPath>& paths, OnExit on_exit, u32 location_flags) {
|
||||
void DumpGetLocation(const std::string& title, u32 location_flags, OnLocation on_loc) {
|
||||
DumpLocation out;
|
||||
ui::PopupList::Items items;
|
||||
std::vector<DumpEntry> dump_entries;
|
||||
|
||||
const auto network_locations = location::Load();
|
||||
out.network = location::Load();
|
||||
if (location_flags & (1 << DumpLocationType_Network)) {
|
||||
for (s32 i = 0; i < std::size(network_locations); i++) {
|
||||
for (s32 i = 0; i < std::size(out.network); i++) {
|
||||
dump_entries.emplace_back(DumpLocationType_Network, i);
|
||||
items.emplace_back(network_locations[i].name);
|
||||
items.emplace_back(out.network[i].name);
|
||||
}
|
||||
}
|
||||
|
||||
const auto stdio_locations = location::GetStdio(true);
|
||||
out.stdio = location::GetStdio(true);
|
||||
if (location_flags & (1 << DumpLocationType_Stdio)) {
|
||||
for (s32 i = 0; i < std::size(stdio_locations); i++) {
|
||||
for (s32 i = 0; i < std::size(out.stdio); i++) {
|
||||
dump_entries.emplace_back(DumpLocationType_Stdio, i);
|
||||
items.emplace_back(stdio_locations[i].name);
|
||||
items.emplace_back(out.stdio[i].name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,40 +343,44 @@ void Dump(std::shared_ptr<BaseSource> source, const std::vector<fs::FsPath>& pat
|
||||
}
|
||||
|
||||
App::Push(std::make_shared<ui::PopupList>(
|
||||
"Select dump location"_i18n, items, [source, paths, on_exit, network_locations, stdio_locations, dump_entries](auto op_index){
|
||||
if (!op_index) {
|
||||
on_exit(0xFFFF);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto dump_entry = dump_entries[*op_index];
|
||||
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Dumping"_i18n, "", [source, paths, network_locations, stdio_locations, dump_entry](auto pbox) -> Result {
|
||||
if (dump_entry.type == DumpLocationType_Network) {
|
||||
R_TRY(DumpToNetwork(pbox, network_locations[dump_entry.index], source.get(), paths));
|
||||
} else if (dump_entry.type == DumpLocationType_Stdio) {
|
||||
R_TRY(DumpToStdio(pbox, stdio_locations[dump_entry.index], source.get(), paths));
|
||||
} else if (dump_entry.type == DumpLocationType_SdCard) {
|
||||
R_TRY(DumpToFileNative(pbox, source.get(), paths));
|
||||
} else if (dump_entry.type == DumpLocationType_UsbS2S) {
|
||||
R_TRY(DumpToUsbS2S(pbox, source.get(), paths));
|
||||
} else if (dump_entry.type == DumpLocationType_DevNull) {
|
||||
R_TRY(DumpToDevNull(pbox, source.get(), paths));
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}, [on_exit](Result rc){
|
||||
App::PushErrorBox(rc, "Dump failed!"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Dump successfull!"_i18n);
|
||||
log_write("dump successfull!!!\n");
|
||||
}
|
||||
|
||||
on_exit(rc);
|
||||
}));
|
||||
title, items, [dump_entries, out, on_loc](auto op_index) mutable {
|
||||
out.entry = dump_entries[*op_index];
|
||||
on_loc(out);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
void Dump(std::shared_ptr<BaseSource> source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, OnExit on_exit) {
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Dumping"_i18n, "", [source, paths, location](auto pbox) -> Result {
|
||||
if (location.entry.type == DumpLocationType_Network) {
|
||||
R_TRY(DumpToNetwork(pbox, location.network[location.entry.index], source.get(), paths));
|
||||
} else if (location.entry.type == DumpLocationType_Stdio) {
|
||||
R_TRY(DumpToStdio(pbox, location.stdio[location.entry.index], source.get(), paths));
|
||||
} else if (location.entry.type == DumpLocationType_SdCard) {
|
||||
R_TRY(DumpToFileNative(pbox, source.get(), paths));
|
||||
} else if (location.entry.type == DumpLocationType_UsbS2S) {
|
||||
R_TRY(DumpToUsbS2S(pbox, source.get(), paths));
|
||||
} else if (location.entry.type == DumpLocationType_DevNull) {
|
||||
R_TRY(DumpToDevNull(pbox, source.get(), paths));
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}, [on_exit](Result rc){
|
||||
App::PushErrorBox(rc, "Dump failed!"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Dump successfull!"_i18n);
|
||||
log_write("dump successfull!!!\n");
|
||||
}
|
||||
|
||||
on_exit(rc);
|
||||
}));
|
||||
}
|
||||
|
||||
void Dump(std::shared_ptr<BaseSource> source, const std::vector<fs::FsPath>& paths, OnExit on_exit, u32 location_flags) {
|
||||
DumpGetLocation("Select dump location"_i18n, location_flags, [source, paths, on_exit](const DumpLocation& loc){
|
||||
Dump(source, loc, paths, on_exit);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace sphaira::dump
|
||||
|
||||
@@ -93,30 +93,52 @@ bool is_read_only(std::string_view path) {
|
||||
|
||||
} // namespace
|
||||
|
||||
FsPath AppendPath(const FsPath& root_path, const FsPath& file_path) {
|
||||
FsPath AppendPath(const FsPath& root_path, const FsPath& _file_path) {
|
||||
// strip leading '/' in file path.
|
||||
auto file_path = _file_path.s;
|
||||
while (file_path[0] == '/') {
|
||||
file_path++;
|
||||
}
|
||||
|
||||
FsPath path;
|
||||
if (root_path[std::strlen(root_path) - 1] != '/') {
|
||||
std::snprintf(path, sizeof(path), "%s/%s", root_path.s, file_path.s);
|
||||
std::snprintf(path, sizeof(path), "%s/%s", root_path.s, file_path);
|
||||
} else {
|
||||
std::snprintf(path, sizeof(path), "%s%s", root_path.s, file_path.s);
|
||||
std::snprintf(path, sizeof(path), "%s%s", root_path.s, file_path);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
Result CreateFile(FsFileSystem* fs, const FsPath& path, u64 size, u32 option, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only_root(path), Result_FsReadOnly);
|
||||
|
||||
return fsFsCreateFile(fs, path, size, option);
|
||||
R_TRY(fsFsCreateFile(fs, path, size, option));
|
||||
fsFsCommit(fs);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result CreateDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only_root(path), Result_FsReadOnly);
|
||||
|
||||
return fsFsCreateDirectory(fs, path);
|
||||
R_TRY(fsFsCreateDirectory(fs, path));
|
||||
fsFsCommit(fs);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& _path, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only_root(_path), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only_root(_path), Result_FsReadOnly);
|
||||
|
||||
// try and create the directory / see if it already exists before the loop.
|
||||
Result rc;
|
||||
if (fs) {
|
||||
rc = CreateDirectory(fs, _path, ignore_read_only);
|
||||
} else {
|
||||
rc = CreateDirectory(_path, ignore_read_only);
|
||||
}
|
||||
|
||||
if (R_SUCCEEDED(rc) || rc == FsError_PathAlreadyExists) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
auto path_view = std::string_view{_path};
|
||||
// todo: fix this for sdmc: and ums0:
|
||||
@@ -134,7 +156,6 @@ Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& _path, bool ig
|
||||
std::strncat(path, dir.data(), dir.size());
|
||||
log_write("[FS] dir creation path is now: %s\n", path.s);
|
||||
|
||||
Result rc;
|
||||
if (fs) {
|
||||
rc = CreateDirectory(fs, path, ignore_read_only);
|
||||
} else {
|
||||
@@ -153,64 +174,59 @@ Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& _path, bool ig
|
||||
}
|
||||
|
||||
Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& _path, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only_root(_path), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only_root(_path), Result_FsReadOnly);
|
||||
|
||||
size_t off = 0;
|
||||
while (true) {
|
||||
const auto first = std::strchr(_path + off, '/');
|
||||
if (!first) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
off = (first - _path.s) + 1;
|
||||
FsPath path;
|
||||
std::strncpy(path, _path, off);
|
||||
|
||||
Result rc;
|
||||
if (fs) {
|
||||
rc = CreateDirectory(fs, path, ignore_read_only);
|
||||
} else {
|
||||
rc = CreateDirectory(path, ignore_read_only);
|
||||
}
|
||||
|
||||
if (R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
||||
log_write("failed to create folder recursively: %s\n", path.s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
// log_write("created_directory recursively: %s\n", path);
|
||||
// strip file name form path.
|
||||
const auto last_slash = std::strrchr(_path, '/');
|
||||
if (!last_slash) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
FsPath new_path{};
|
||||
std::snprintf(new_path, sizeof(new_path), "%.*s", (int)(last_slash - _path.s), _path.s);
|
||||
R_TRY(CreateDirectoryRecursively(fs, new_path, ignore_read_only));
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result DeleteFile(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
|
||||
return fsFsDeleteFile(fs, path);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly);
|
||||
R_TRY(fsFsDeleteFile(fs, path));
|
||||
fsFsCommit(fs);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result DeleteDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly);
|
||||
|
||||
return fsFsDeleteDirectory(fs, path);
|
||||
R_TRY(fsFsDeleteDirectory(fs, path));
|
||||
fsFsCommit(fs);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result DeleteDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly);
|
||||
|
||||
return fsFsDeleteDirectoryRecursively(fs, path);
|
||||
R_TRY(fsFsDeleteDirectoryRecursively(fs, path));
|
||||
fsFsCommit(fs);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result RenameFile(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only(src), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(src), Result_FsReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly);
|
||||
|
||||
return fsFsRenameFile(fs, src, dst);
|
||||
R_TRY(fsFsRenameFile(fs, src, dst));
|
||||
fsFsCommit(fs);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result RenameDirectory(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only(src), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(src), Result_FsReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly);
|
||||
|
||||
return fsFsRenameDirectory(fs, src, dst);
|
||||
R_TRY(fsFsRenameDirectory(fs, src, dst));
|
||||
fsFsCommit(fs);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result GetEntryType(FsFileSystem* fs, const FsPath& path, FsDirEntryType* out) {
|
||||
@@ -258,10 +274,11 @@ Result read_entire_file(FsFileSystem* _fs, const FsPath& path, std::vector<u8>&
|
||||
}
|
||||
|
||||
Result write_entire_file(FsFileSystem* _fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly);
|
||||
|
||||
FsNative fs{_fs, false, ignore_read_only};
|
||||
R_TRY(fs.GetFsOpenResult());
|
||||
ON_SCOPE_EXIT(fs.Commit());
|
||||
|
||||
if (auto rc = fs.CreateFile(path, in.size(), 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
||||
return rc;
|
||||
@@ -278,7 +295,7 @@ Result write_entire_file(FsFileSystem* _fs, const FsPath& path, const std::vecto
|
||||
}
|
||||
|
||||
Result copy_entire_file(FsFileSystem* fs, const FsPath& dst, const FsPath& src, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly);
|
||||
|
||||
std::vector<u8> data;
|
||||
R_TRY(read_entire_file(fs, src, data));
|
||||
@@ -286,7 +303,7 @@ Result copy_entire_file(FsFileSystem* fs, const FsPath& dst, const FsPath& src,
|
||||
}
|
||||
|
||||
Result CreateFile(const FsPath& path, u64 size, u32 option, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only_root(path), Result_FsReadOnly);
|
||||
|
||||
auto fd = open(path, O_WRONLY | O_CREAT, DEFFILEMODE);
|
||||
if (fd == -1) {
|
||||
@@ -295,19 +312,19 @@ Result CreateFile(const FsPath& path, u64 size, u32 option, bool ignore_read_onl
|
||||
}
|
||||
|
||||
R_TRY(fsdevGetLastResult());
|
||||
return Fs::ResultUnknownStdioError;
|
||||
return Result_FsUnknownStdioError;
|
||||
}
|
||||
ON_SCOPE_EXIT(close(fd));
|
||||
|
||||
if (size) {
|
||||
R_UNLESS(!ftruncate(fd, size), Fs::ResultUnknownStdioError);
|
||||
R_UNLESS(!ftruncate(fd, size), Result_FsUnknownStdioError);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result CreateDirectory(const FsPath& path, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only_root(path), Result_FsReadOnly);
|
||||
|
||||
if (mkdir(path, ACCESSPERMS)) {
|
||||
if (errno == EEXIST) {
|
||||
@@ -315,46 +332,46 @@ Result CreateDirectory(const FsPath& path, bool ignore_read_only) {
|
||||
}
|
||||
|
||||
R_TRY(fsdevGetLastResult());
|
||||
return Fs::ResultUnknownStdioError;
|
||||
return Result_FsUnknownStdioError;
|
||||
}
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only_root(path), Result_FsReadOnly);
|
||||
|
||||
return CreateDirectoryRecursively(nullptr, path, ignore_read_only);
|
||||
}
|
||||
|
||||
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only_root(path), Result_FsReadOnly);
|
||||
|
||||
return CreateDirectoryRecursivelyWithPath(nullptr, path, ignore_read_only);
|
||||
}
|
||||
|
||||
Result DeleteFile(const FsPath& path, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly);
|
||||
|
||||
if (unlink(path)) {
|
||||
R_TRY(fsdevGetLastResult());
|
||||
return Fs::ResultUnknownStdioError;
|
||||
return Result_FsUnknownStdioError;
|
||||
}
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result DeleteDirectory(const FsPath& path, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly);
|
||||
|
||||
if (rmdir(path)) {
|
||||
R_TRY(fsdevGetLastResult());
|
||||
return Fs::ResultUnknownStdioError;
|
||||
return Result_FsUnknownStdioError;
|
||||
}
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
// ftw / ntfw isn't found by linker...
|
||||
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly);
|
||||
|
||||
#if 0
|
||||
// const auto unlink_cb = [](const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) -> int {
|
||||
@@ -365,7 +382,7 @@ Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only) {
|
||||
// if (nftw(path, unlink_cb, 16, FTW_DEPTH)) {
|
||||
if (ftw(path, unlink_cb, 16)) {
|
||||
R_TRY(fsdevGetLastResult());
|
||||
return Fs::ResultUnknownStdioError;
|
||||
return Result_FsUnknownStdioError;
|
||||
}
|
||||
R_SUCCEED();
|
||||
#else
|
||||
@@ -374,19 +391,19 @@ Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only) {
|
||||
}
|
||||
|
||||
Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only(src), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(src), Result_FsReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly);
|
||||
|
||||
if (rename(src, dst)) {
|
||||
R_TRY(fsdevGetLastResult());
|
||||
return Fs::ResultUnknownStdioError;
|
||||
return Result_FsUnknownStdioError;
|
||||
}
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only(src), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(src), Result_FsReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly);
|
||||
|
||||
return RenameFile(src, dst, ignore_read_only);
|
||||
}
|
||||
@@ -395,7 +412,7 @@ Result GetEntryType(const FsPath& path, FsDirEntryType* out) {
|
||||
struct stat st;
|
||||
if (stat(path, &st)) {
|
||||
R_TRY(fsdevGetLastResult());
|
||||
return Fs::ResultUnknownStdioError;
|
||||
return Result_FsUnknownStdioError;
|
||||
}
|
||||
*out = S_ISREG(st.st_mode) ? FsDirEntryType_File : FsDirEntryType_Dir;
|
||||
R_SUCCEED();
|
||||
@@ -405,7 +422,7 @@ Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out) {
|
||||
struct stat st;
|
||||
if (stat(path, &st)) {
|
||||
R_TRY(fsdevGetLastResult());
|
||||
return Fs::ResultUnknownStdioError;
|
||||
return Result_FsUnknownStdioError;
|
||||
}
|
||||
|
||||
out->is_valid = true;
|
||||
@@ -445,7 +462,7 @@ Result read_entire_file(const FsPath& path, std::vector<u8>& out) {
|
||||
auto f = std::fopen(path, "rb");
|
||||
if (!f) {
|
||||
R_TRY(fsdevGetLastResult());
|
||||
return Fs::ResultUnknownStdioError;
|
||||
return Result_FsUnknownStdioError;
|
||||
}
|
||||
ON_SCOPE_EXIT(std::fclose(f));
|
||||
|
||||
@@ -460,12 +477,12 @@ Result read_entire_file(const FsPath& path, std::vector<u8>& out) {
|
||||
}
|
||||
|
||||
Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly);
|
||||
|
||||
auto f = std::fopen(path, "wb");
|
||||
if (!f) {
|
||||
R_TRY(fsdevGetLastResult());
|
||||
return Fs::ResultUnknownStdioError;
|
||||
return Result_FsUnknownStdioError;
|
||||
}
|
||||
ON_SCOPE_EXIT(std::fclose(f));
|
||||
|
||||
@@ -474,7 +491,7 @@ Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ign
|
||||
}
|
||||
|
||||
Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only(dst), Fs::ResultReadOnly);
|
||||
R_UNLESS(ignore_read_only || !is_read_only(dst), Result_FsReadOnly);
|
||||
|
||||
std::vector<u8> data;
|
||||
R_TRY(read_entire_file(src, data));
|
||||
@@ -483,6 +500,7 @@ Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_o
|
||||
|
||||
Result OpenFile(fs::Fs* fs, const fs::FsPath& path, u32 mode, File* f) {
|
||||
f->m_fs = fs;
|
||||
f->m_mode = mode;
|
||||
|
||||
if (f->m_fs->IsNative()) {
|
||||
auto fs = (fs::FsNative*)f->m_fs;
|
||||
@@ -498,8 +516,7 @@ Result OpenFile(fs::Fs* fs, const fs::FsPath& path, u32 mode, File* f) {
|
||||
f->m_stdio = std::fopen(path, "rb+");
|
||||
}
|
||||
|
||||
R_UNLESS(f->m_stdio, Fs::ResultUnknownStdioError);
|
||||
std::strcpy(f->m_path, path);
|
||||
R_UNLESS(f->m_stdio, Result_FsUnknownStdioError);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
@@ -511,7 +528,7 @@ File::~File() {
|
||||
|
||||
Result File::Read( s64 off, void* buf, u64 read_size, u32 option, u64* bytes_read) {
|
||||
*bytes_read = 0;
|
||||
R_UNLESS(m_fs, 0x1);
|
||||
R_UNLESS(m_fs, Result_FsNotActive);
|
||||
|
||||
if (m_fs->IsNative()) {
|
||||
R_TRY(fsFileRead(&m_native, off, buf, read_size, option, bytes_read));
|
||||
@@ -526,7 +543,7 @@ Result File::Read( s64 off, void* buf, u64 read_size, u32 option, u64* bytes_rea
|
||||
// if we read less bytes than expected, check if there was an error (ignoring eof).
|
||||
if (*bytes_read < read_size) {
|
||||
if (!std::feof(m_stdio) && std::ferror(m_stdio)) {
|
||||
R_THROW(Fs::ResultUnknownStdioError);
|
||||
R_THROW(Result_FsUnknownStdioError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,7 +554,7 @@ Result File::Read( s64 off, void* buf, u64 read_size, u32 option, u64* bytes_rea
|
||||
}
|
||||
|
||||
Result File::Write(s64 off, const void* buf, u64 write_size, u32 option) {
|
||||
R_UNLESS(m_fs, 0x1);
|
||||
R_UNLESS(m_fs, Result_FsNotActive);
|
||||
|
||||
if (m_fs->IsNative()) {
|
||||
R_TRY(fsFileWrite(&m_native, off, buf, write_size, option));
|
||||
@@ -550,7 +567,7 @@ Result File::Write(s64 off, const void* buf, u64 write_size, u32 option) {
|
||||
|
||||
const auto result = std::fwrite(buf, 1, write_size, m_stdio);
|
||||
// log_write("[FS] fwrite res: %zu vs %zu\n", result, write_size);
|
||||
R_UNLESS(result == write_size, Fs::ResultUnknownStdioError);
|
||||
R_UNLESS(result == write_size, Result_FsUnknownStdioError);
|
||||
|
||||
m_stdio_off += write_size;
|
||||
}
|
||||
@@ -559,37 +576,27 @@ Result File::Write(s64 off, const void* buf, u64 write_size, u32 option) {
|
||||
}
|
||||
|
||||
Result File::SetSize(s64 sz) {
|
||||
R_UNLESS(m_fs, 0x1);
|
||||
R_UNLESS(m_fs, Result_FsNotActive);
|
||||
|
||||
if (m_fs->IsNative()) {
|
||||
R_TRY(fsFileSetSize(&m_native, sz));
|
||||
} else {
|
||||
const auto fd = fileno(m_stdio);
|
||||
R_UNLESS(fd > 0, Fs::ResultUnknownStdioError);
|
||||
R_UNLESS(!ftruncate(fd, sz), Fs::ResultUnknownStdioError);
|
||||
R_UNLESS(fd > 0, Result_FsUnknownStdioError);
|
||||
R_UNLESS(!ftruncate(fd, sz), Result_FsUnknownStdioError);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result File::GetSize(s64* out) {
|
||||
R_UNLESS(m_fs, 0x1);
|
||||
R_UNLESS(m_fs, Result_FsNotActive);
|
||||
|
||||
if (m_fs->IsNative()) {
|
||||
R_TRY(fsFileGetSize(&m_native, out));
|
||||
} else {
|
||||
struct stat st;
|
||||
const auto fd = fileno(m_stdio);
|
||||
bool did_stat{};
|
||||
|
||||
if (fd && !fstat(fd, &st)) {
|
||||
did_stat = true;
|
||||
}
|
||||
|
||||
if (!did_stat) {
|
||||
R_UNLESS(!lstat(m_path, &st), Fs::ResultUnknownStdioError);
|
||||
}
|
||||
|
||||
R_UNLESS(!fstat(fileno(m_stdio), &st), Result_FsUnknownStdioError);
|
||||
*out = st.st_size;
|
||||
}
|
||||
|
||||
@@ -604,6 +611,9 @@ void File::Close() {
|
||||
if (m_fs->IsNative()) {
|
||||
if (serviceIsActive(&m_native.s)) {
|
||||
fsFileClose(&m_native);
|
||||
if (m_mode & FsOpenMode_Write) {
|
||||
m_fs->Commit();
|
||||
}
|
||||
m_native = {};
|
||||
}
|
||||
} else {
|
||||
@@ -623,7 +633,7 @@ Result OpenDirectory(fs::Fs* fs, const fs::FsPath& path, u32 mode, Dir* d) {
|
||||
R_TRY(fsFsOpenDirectory(&fs->m_fs, path, mode, &d->m_native));
|
||||
} else {
|
||||
d->m_stdio = opendir(path);
|
||||
R_UNLESS(d->m_stdio, Fs::ResultUnknownStdioError);
|
||||
R_UNLESS(d->m_stdio, Result_FsUnknownStdioError);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
@@ -682,7 +692,7 @@ Dir::~Dir() {
|
||||
|
||||
Result Dir::GetEntryCount(s64* out) {
|
||||
*out = 0;
|
||||
R_UNLESS(m_fs, 0x1);
|
||||
R_UNLESS(m_fs, Result_FsNotActive);
|
||||
|
||||
if (m_fs->IsNative()) {
|
||||
R_TRY(fsDirGetEntryCount(&m_native, out));
|
||||
@@ -701,9 +711,50 @@ Result Dir::GetEntryCount(s64* out) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Dir::Read(s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf) {
|
||||
R_UNLESS(m_fs, Result_FsNotActive);
|
||||
*total_entries = 0;
|
||||
|
||||
if (m_fs->IsNative()) {
|
||||
R_TRY(fsDirRead(&m_native, total_entries, max_entries, buf));
|
||||
} else {
|
||||
while (auto d = readdir(m_stdio)) {
|
||||
if (!std::strcmp(d->d_name, ".") || !std::strcmp(d->d_name, "..")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
FsDirectoryEntry entry{};
|
||||
|
||||
if (d->d_type == DT_DIR) {
|
||||
if (!(m_mode & FsDirOpenMode_ReadDirs)) {
|
||||
continue;
|
||||
}
|
||||
entry.type = FsDirEntryType_Dir;
|
||||
} else if (d->d_type == DT_REG) {
|
||||
if (!(m_mode & FsDirOpenMode_ReadFiles)) {
|
||||
continue;
|
||||
}
|
||||
entry.type = FsDirEntryType_File;
|
||||
} else {
|
||||
log_write("[FS] WARNING: unknown type when reading dir: %u\n", d->d_type);
|
||||
continue;
|
||||
}
|
||||
|
||||
std::strcpy(entry.name, d->d_name);
|
||||
std::memcpy(&buf[*total_entries], &entry, sizeof(*buf));
|
||||
*total_entries = *total_entries + 1;
|
||||
if (*total_entries >= max_entries) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Dir::ReadAll(std::vector<FsDirectoryEntry>& buf) {
|
||||
buf.clear();
|
||||
R_UNLESS(m_fs, 0x1);
|
||||
R_UNLESS(m_fs, Result_FsNotActive);
|
||||
|
||||
if (m_fs->IsNative()) {
|
||||
s64 count;
|
||||
@@ -778,7 +829,7 @@ Result FileGetSizeAndTimestamp(fs::Fs* m_fs, const FsPath& path, FsTimeStampRaw*
|
||||
R_TRY(f.GetSize(size));
|
||||
} else {
|
||||
struct stat st;
|
||||
R_UNLESS(!lstat(path, &st), 0x1);
|
||||
R_UNLESS(!lstat(path, &st), Result_FsFailedStdioStat);
|
||||
|
||||
ts->is_valid = true;
|
||||
ts->created = st.st_ctim.tv_sec;
|
||||
@@ -801,7 +852,7 @@ Result IsDirEmpty(fs::Fs* m_fs, const fs::FsPath& path, bool* out) {
|
||||
*out = !count;
|
||||
} else {
|
||||
auto dir = opendir(path);
|
||||
R_UNLESS(dir, 0x1);
|
||||
R_UNLESS(dir, Result_FsFailedStdioOpendir);
|
||||
ON_SCOPE_EXIT(closedir(dir));
|
||||
|
||||
while (auto d = readdir(dir)) {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "fs.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
#include <mutex>
|
||||
#include <algorithm>
|
||||
#include <minIni.h>
|
||||
#include <ftpsrv.h>
|
||||
@@ -16,8 +15,7 @@ namespace sphaira::ftpsrv {
|
||||
namespace {
|
||||
|
||||
struct InstallSharedData {
|
||||
std::mutex mutex;
|
||||
|
||||
Mutex mutex;
|
||||
std::deque<std::string> queued_files;
|
||||
|
||||
void* user;
|
||||
@@ -36,7 +34,7 @@ FtpSrvConfig g_ftpsrv_config = {0};
|
||||
volatile bool g_should_exit = false;
|
||||
bool g_is_running{false};
|
||||
Thread g_thread;
|
||||
std::mutex g_mutex{};
|
||||
Mutex g_mutex{};
|
||||
InstallSharedData g_shared_data{};
|
||||
|
||||
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
|
||||
@@ -59,13 +57,13 @@ struct VfsUserData {
|
||||
// ive given up with good names.
|
||||
void on_thing() {
|
||||
log_write("[FTP] doing on_thing\n");
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
log_write("[FTP] locked on_thing\n");
|
||||
|
||||
if (!g_shared_data.in_progress) {
|
||||
if (!g_shared_data.queued_files.empty()) {
|
||||
log_write("[FTP] pushing new file data\n");
|
||||
if (!g_shared_data.on_start || !g_shared_data.on_start(g_shared_data.user, g_shared_data.queued_files[0].c_str())) {
|
||||
if (!g_shared_data.on_start || !g_shared_data.on_start(g_shared_data.queued_files[0].c_str())) {
|
||||
g_shared_data.queued_files.clear();
|
||||
} else {
|
||||
log_write("[FTP] success on new file push\n");
|
||||
@@ -77,7 +75,7 @@ void on_thing() {
|
||||
|
||||
int vfs_install_open(void* user, const char* path, enum FtpVfsOpenMode mode) {
|
||||
{
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
auto data = static_cast<VfsUserData*>(user);
|
||||
data->valid = 0;
|
||||
|
||||
@@ -133,7 +131,7 @@ int vfs_install_read(void* user, void* buf, size_t size) {
|
||||
}
|
||||
|
||||
int vfs_install_write(void* user, const void* buf, size_t size) {
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
if (!g_shared_data.enabled) {
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
@@ -145,7 +143,7 @@ int vfs_install_write(void* user, const void* buf, size_t size) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!g_shared_data.on_write || !g_shared_data.on_write(g_shared_data.user, buf, size)) {
|
||||
if (!g_shared_data.on_write || !g_shared_data.on_write(buf, size)) {
|
||||
errno = EIO;
|
||||
return -1;
|
||||
}
|
||||
@@ -159,13 +157,13 @@ int vfs_install_seek(void* user, const void* buf, size_t size, size_t off) {
|
||||
}
|
||||
|
||||
int vfs_install_isfile_open(void* user) {
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
auto data = static_cast<VfsUserData*>(user);
|
||||
return data->valid;
|
||||
}
|
||||
|
||||
int vfs_install_isfile_ready(void* user) {
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
auto data = static_cast<VfsUserData*>(user);
|
||||
const auto ready = !g_shared_data.queued_files.empty() && data->path == g_shared_data.queued_files[0];
|
||||
return ready;
|
||||
@@ -174,7 +172,7 @@ int vfs_install_isfile_ready(void* user) {
|
||||
int vfs_install_close(void* user) {
|
||||
{
|
||||
log_write("[FTP] closing file\n");
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
auto data = static_cast<VfsUserData*>(user);
|
||||
if (data->valid) {
|
||||
log_write("[FTP] closing valid file\n");
|
||||
@@ -184,7 +182,7 @@ int vfs_install_close(void* user) {
|
||||
if (it == g_shared_data.queued_files.cbegin()) {
|
||||
log_write("[FTP] closing current file\n");
|
||||
if (g_shared_data.on_close) {
|
||||
g_shared_data.on_close(g_shared_data.user);
|
||||
g_shared_data.on_close();
|
||||
}
|
||||
|
||||
g_shared_data.in_progress = false;
|
||||
@@ -296,7 +294,7 @@ void loop(void* arg) {
|
||||
} // namespace
|
||||
|
||||
bool Init() {
|
||||
std::scoped_lock lock{g_mutex};
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
if (g_is_running) {
|
||||
log_write("[FTP] already enabled, cannot open\n");
|
||||
return false;
|
||||
@@ -380,7 +378,7 @@ bool Init() {
|
||||
}
|
||||
|
||||
void Exit() {
|
||||
std::scoped_lock lock{g_mutex};
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
if (!g_is_running) {
|
||||
return;
|
||||
}
|
||||
@@ -398,9 +396,8 @@ void Exit() {
|
||||
log_write("[FTP] exitied\n");
|
||||
}
|
||||
|
||||
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close) {
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
g_shared_data.user = user;
|
||||
void InitInstallMode(OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close) {
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
g_shared_data.on_start = on_start;
|
||||
g_shared_data.on_write = on_write;
|
||||
g_shared_data.on_close = on_close;
|
||||
@@ -408,27 +405,27 @@ void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_writ
|
||||
}
|
||||
|
||||
void DisableInstallMode() {
|
||||
std::scoped_lock lock{g_shared_data.mutex};
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
g_shared_data.enabled = false;
|
||||
}
|
||||
|
||||
unsigned GetPort() {
|
||||
std::scoped_lock lock{g_mutex};
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
return g_ftpsrv_config.port;
|
||||
}
|
||||
|
||||
bool IsAnon() {
|
||||
std::scoped_lock lock{g_mutex};
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
return g_ftpsrv_config.anon;
|
||||
}
|
||||
|
||||
const char* GetUser() {
|
||||
std::scoped_lock lock{g_mutex};
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
return g_ftpsrv_config.user;
|
||||
}
|
||||
|
||||
const char* GetPass() {
|
||||
std::scoped_lock lock{g_mutex};
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
return g_ftpsrv_config.pass;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "app.hpp"
|
||||
#include "threaded_file_transfer.hpp"
|
||||
#include <mbedtls/md5.h>
|
||||
#include <utility>
|
||||
|
||||
namespace sphaira::hash {
|
||||
namespace {
|
||||
@@ -16,11 +17,11 @@ struct FileSource final : BaseSource {
|
||||
m_is_file_based_emummc = App::IsFileBaseEmummc();
|
||||
}
|
||||
|
||||
Result Size(s64* out) {
|
||||
Result Size(s64* out) override {
|
||||
return m_file.GetSize(out);
|
||||
}
|
||||
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
|
||||
const auto rc = m_file.Read(off, buf, size, 0, bytes_read);
|
||||
if (m_fs->IsNative() && m_is_file_based_emummc) {
|
||||
svcSleepThread(2e+6); // 2ms
|
||||
@@ -35,6 +36,25 @@ private:
|
||||
bool m_is_file_based_emummc{};
|
||||
};
|
||||
|
||||
struct MemSource final : BaseSource {
|
||||
MemSource(std::span<const u8> data) : m_data{data} { }
|
||||
|
||||
Result Size(s64* out) override {
|
||||
*out = m_data.size();
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
|
||||
size = std::min<s64>(size, m_data.size() - off);
|
||||
std::memcpy(buf, m_data.data() + off, size);
|
||||
*bytes_read = size;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
private:
|
||||
const std::span<const u8> m_data;
|
||||
};
|
||||
|
||||
struct HashSource {
|
||||
virtual ~HashSource() = default;
|
||||
virtual void Update(const void* buf, s64 size) = 0;
|
||||
@@ -42,11 +62,11 @@ struct HashSource {
|
||||
};
|
||||
|
||||
struct HashCrc32 final : HashSource {
|
||||
void Update(const void* buf, s64 size) {
|
||||
void Update(const void* buf, s64 size) override {
|
||||
m_seed = crc32CalculateWithSeed(m_seed, buf, size);
|
||||
}
|
||||
|
||||
void Get(std::string& out) {
|
||||
void Get(std::string& out) override {
|
||||
char str[CalculateHashStrLen(sizeof(m_seed))];
|
||||
std::snprintf(str, sizeof(str), "%08x", m_seed);
|
||||
out = str;
|
||||
@@ -66,11 +86,11 @@ struct HashMd5 final : HashSource {
|
||||
mbedtls_md5_free(&m_ctx);
|
||||
}
|
||||
|
||||
void Update(const void* buf, s64 size) {
|
||||
void Update(const void* buf, s64 size) override {
|
||||
mbedtls_md5_update_ret(&m_ctx, (const u8*)buf, size);
|
||||
}
|
||||
|
||||
void Get(std::string& out) {
|
||||
void Get(std::string& out) override {
|
||||
u8 hash[16];
|
||||
mbedtls_md5_finish_ret(&m_ctx, hash);
|
||||
|
||||
@@ -91,11 +111,11 @@ struct HashSha1 final : HashSource {
|
||||
sha1ContextCreate(&m_ctx);
|
||||
}
|
||||
|
||||
void Update(const void* buf, s64 size) {
|
||||
void Update(const void* buf, s64 size) override {
|
||||
sha1ContextUpdate(&m_ctx, buf, size);
|
||||
}
|
||||
|
||||
void Get(std::string& out) {
|
||||
void Get(std::string& out) override {
|
||||
u8 hash[SHA1_HASH_SIZE];
|
||||
sha1ContextGetHash(&m_ctx, hash);
|
||||
|
||||
@@ -116,11 +136,11 @@ struct HashSha256 final : HashSource {
|
||||
sha256ContextCreate(&m_ctx);
|
||||
}
|
||||
|
||||
void Update(const void* buf, s64 size) {
|
||||
void Update(const void* buf, s64 size) override {
|
||||
sha256ContextUpdate(&m_ctx, buf, size);
|
||||
}
|
||||
|
||||
void Get(std::string& out) {
|
||||
void Get(std::string& out) override {
|
||||
u8 hash[SHA256_HASH_SIZE];
|
||||
sha256ContextGetHash(&m_ctx, hash);
|
||||
|
||||
@@ -173,7 +193,7 @@ Result Hash(ui::ProgressBox* pbox, Type type, std::shared_ptr<BaseSource> source
|
||||
case Type::Sha1: return Hash(pbox, std::make_unique<HashSha1>(), source, out);
|
||||
case Type::Sha256: return Hash(pbox, std::make_unique<HashSha256>(), source, out);
|
||||
}
|
||||
R_THROW(0x1);
|
||||
std::unreachable();
|
||||
}
|
||||
|
||||
Result Hash(ui::ProgressBox* pbox, Type type, fs::Fs* fs, const fs::FsPath& path, std::string& out) {
|
||||
@@ -181,4 +201,9 @@ Result Hash(ui::ProgressBox* pbox, Type type, fs::Fs* fs, const fs::FsPath& path
|
||||
return Hash(pbox, type, source, out);
|
||||
}
|
||||
|
||||
Result Hash(ui::ProgressBox* pbox, Type type, std::span<const u8> data, std::string& out) {
|
||||
auto source = std::make_shared<MemSource>(data);
|
||||
return Hash(pbox, type, source, out);
|
||||
}
|
||||
|
||||
} // namespace sphaira::has
|
||||
|
||||
603
sphaira/source/haze_helper.cpp
Normal file
@@ -0,0 +1,603 @@
|
||||
#include "haze_helper.hpp"
|
||||
|
||||
#include "app.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "log.hpp"
|
||||
#include "evman.hpp"
|
||||
#include "i18n.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <haze.h>
|
||||
|
||||
namespace sphaira::haze {
|
||||
namespace {
|
||||
|
||||
struct InstallSharedData {
|
||||
Mutex mutex;
|
||||
std::string current_file;
|
||||
|
||||
void* user;
|
||||
OnInstallStart on_start;
|
||||
OnInstallWrite on_write;
|
||||
OnInstallClose on_close;
|
||||
|
||||
bool in_progress;
|
||||
bool enabled;
|
||||
};
|
||||
|
||||
constexpr int THREAD_PRIO = PRIO_PREEMPTIVE;
|
||||
constexpr int THREAD_CORE = 2;
|
||||
volatile bool g_should_exit = false;
|
||||
bool g_is_running{false};
|
||||
Mutex g_mutex{};
|
||||
InstallSharedData g_shared_data{};
|
||||
|
||||
const char* SUPPORTED_EXT[] = {
|
||||
".nsp", ".xci", ".nsz", ".xcz",
|
||||
};
|
||||
|
||||
// ive given up with good names.
|
||||
void on_thing() {
|
||||
log_write("[MTP] doing on_thing\n");
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
log_write("[MTP] locked on_thing\n");
|
||||
|
||||
if (!g_shared_data.in_progress) {
|
||||
if (!g_shared_data.current_file.empty()) {
|
||||
log_write("[MTP] pushing new file data\n");
|
||||
if (!g_shared_data.on_start || !g_shared_data.on_start(g_shared_data.current_file.c_str())) {
|
||||
g_shared_data.current_file.clear();
|
||||
} else {
|
||||
log_write("[MTP] success on new file push\n");
|
||||
g_shared_data.in_progress = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FsProxyBase : ::haze::FileSystemProxyImpl {
|
||||
FsProxyBase(const char* name, const char* display_name) : m_name{name}, m_display_name{display_name} {
|
||||
|
||||
}
|
||||
|
||||
auto FixPath(const char* path) const {
|
||||
fs::FsPath buf;
|
||||
const auto len = std::strlen(GetName());
|
||||
|
||||
if (len && !strncasecmp(path + 1, GetName(), len)) {
|
||||
std::snprintf(buf, sizeof(buf), "/%s", path + 1 + len);
|
||||
} else {
|
||||
std::strcpy(buf, path);
|
||||
}
|
||||
|
||||
// log_write("[FixPath] %s -> %s\n", path, buf.s);
|
||||
return buf;
|
||||
}
|
||||
|
||||
const char* GetName() const override {
|
||||
return m_name.c_str();
|
||||
}
|
||||
const char* GetDisplayName() const override {
|
||||
return m_display_name.c_str();
|
||||
}
|
||||
|
||||
protected:
|
||||
const std::string m_name;
|
||||
const std::string m_display_name;
|
||||
};
|
||||
|
||||
struct FsProxy final : FsProxyBase {
|
||||
FsProxy(std::shared_ptr<fs::Fs> fs, const char* name, const char* display_name) : FsProxyBase{name, display_name} {
|
||||
m_fs = fs;
|
||||
}
|
||||
|
||||
~FsProxy() {
|
||||
if (m_fs->IsNative()) {
|
||||
auto fs = (fs::FsNative*)m_fs.get();
|
||||
fsFsCommit(&fs->m_fs);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: impl this for stdio
|
||||
Result GetTotalSpace(const char *path, s64 *out) override {
|
||||
if (m_fs->IsNative()) {
|
||||
auto fs = (fs::FsNative*)m_fs.get();
|
||||
return fsFsGetTotalSpace(&fs->m_fs, FixPath(path), out);
|
||||
}
|
||||
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result GetFreeSpace(const char *path, s64 *out) override {
|
||||
if (m_fs->IsNative()) {
|
||||
auto fs = (fs::FsNative*)m_fs.get();
|
||||
return fsFsGetFreeSpace(&fs->m_fs, FixPath(path), out);
|
||||
}
|
||||
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) override {
|
||||
const auto rc = m_fs->GetEntryType(FixPath(path), out_entry_type);
|
||||
log_write("[HAZE] GetEntryType(%s) 0x%X\n", path, rc);
|
||||
return rc;
|
||||
}
|
||||
Result CreateFile(const char* path, s64 size, u32 option) override {
|
||||
log_write("[HAZE] CreateFile(%s)\n", path);
|
||||
return m_fs->CreateFile(FixPath(path), size, option);
|
||||
}
|
||||
Result DeleteFile(const char* path) override {
|
||||
log_write("[HAZE] DeleteFile(%s)\n", path);
|
||||
return m_fs->DeleteFile(FixPath(path));
|
||||
}
|
||||
Result RenameFile(const char *old_path, const char *new_path) override {
|
||||
log_write("[HAZE] RenameFile(%s -> %s)\n", old_path, new_path);
|
||||
return m_fs->RenameFile(FixPath(old_path), FixPath(new_path));
|
||||
}
|
||||
Result OpenFile(const char *path, u32 mode, FsFile *out_file) override {
|
||||
log_write("[HAZE] OpenFile(%s)\n", path);
|
||||
auto fptr = new fs::File();
|
||||
const auto rc = m_fs->OpenFile(FixPath(path), mode, fptr);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
std::memcpy(&out_file->s, &fptr, sizeof(fptr));
|
||||
} else {
|
||||
delete fptr;
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
Result GetFileSize(FsFile *file, s64 *out_size) override {
|
||||
log_write("[HAZE] GetFileSize()\n");
|
||||
fs::File* f;
|
||||
std::memcpy(&f, &file->s, sizeof(f));
|
||||
return f->GetSize(out_size);
|
||||
}
|
||||
Result SetFileSize(FsFile *file, s64 size) override {
|
||||
log_write("[HAZE] SetFileSize()\n");
|
||||
fs::File* f;
|
||||
std::memcpy(&f, &file->s, sizeof(f));
|
||||
return f->SetSize(size);
|
||||
}
|
||||
Result ReadFile(FsFile *file, s64 off, void *buf, u64 read_size, u32 option, u64 *out_bytes_read) override {
|
||||
log_write("[HAZE] ReadFile()\n");
|
||||
fs::File* f;
|
||||
std::memcpy(&f, &file->s, sizeof(f));
|
||||
return f->Read(off, buf, read_size, option, out_bytes_read);
|
||||
}
|
||||
Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) override {
|
||||
log_write("[HAZE] WriteFile()\n");
|
||||
fs::File* f;
|
||||
std::memcpy(&f, &file->s, sizeof(f));
|
||||
return f->Write(off, buf, write_size, option);
|
||||
}
|
||||
void CloseFile(FsFile *file) override {
|
||||
log_write("[HAZE] CloseFile()\n");
|
||||
fs::File* f;
|
||||
std::memcpy(&f, &file->s, sizeof(f));
|
||||
if (f) {
|
||||
delete f;
|
||||
}
|
||||
std::memset(file, 0, sizeof(*file));
|
||||
}
|
||||
|
||||
Result CreateDirectory(const char* path) override {
|
||||
log_write("[HAZE] DeleteFile(%s)\n", path);
|
||||
return m_fs->CreateDirectory(FixPath(path));
|
||||
}
|
||||
Result DeleteDirectoryRecursively(const char* path) override {
|
||||
log_write("[HAZE] DeleteDirectoryRecursively(%s)\n", path);
|
||||
return m_fs->DeleteDirectoryRecursively(FixPath(path));
|
||||
}
|
||||
Result RenameDirectory(const char *old_path, const char *new_path) override {
|
||||
log_write("[HAZE] RenameDirectory(%s -> %s)\n", old_path, new_path);
|
||||
return m_fs->RenameDirectory(FixPath(old_path), FixPath(new_path));
|
||||
}
|
||||
Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) override {
|
||||
auto fptr = new fs::Dir();
|
||||
const auto rc = m_fs->OpenDirectory(FixPath(path), mode, fptr);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
std::memcpy(&out_dir->s, &fptr, sizeof(fptr));
|
||||
} else {
|
||||
delete fptr;
|
||||
}
|
||||
|
||||
log_write("[HAZE] OpenDirectory(%s) 0x%X\n", path, rc);
|
||||
return rc;
|
||||
}
|
||||
Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) override {
|
||||
fs::Dir* f;
|
||||
std::memcpy(&f, &d->s, sizeof(f));
|
||||
const auto rc = f->Read(out_total_entries, max_entries, buf);
|
||||
log_write("[HAZE] ReadDirectory(%zd) 0x%X\n", *out_total_entries, rc);
|
||||
return rc;
|
||||
}
|
||||
Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override {
|
||||
fs::Dir* f;
|
||||
std::memcpy(&f, &d->s, sizeof(f));
|
||||
const auto rc = f->GetEntryCount(out_count);
|
||||
log_write("[HAZE] GetDirectoryEntryCount(%zd) 0x%X\n", *out_count, rc);
|
||||
return rc;
|
||||
}
|
||||
void CloseDirectory(FsDir *d) override {
|
||||
log_write("[HAZE] CloseDirectory()\n");
|
||||
fs::Dir* f;
|
||||
std::memcpy(&f, &d->s, sizeof(f));
|
||||
if (f) {
|
||||
delete f;
|
||||
}
|
||||
std::memset(d, 0, sizeof(*d));
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<fs::Fs> m_fs{};
|
||||
};
|
||||
|
||||
// fake fs that allows for files to create r/w on the root.
|
||||
// folders are not yet supported.
|
||||
struct FsProxyVfs : FsProxyBase {
|
||||
using FsProxyBase::FsProxyBase;
|
||||
virtual ~FsProxyVfs() = default;
|
||||
|
||||
auto GetFileName(const char* s) -> const char* {
|
||||
const auto file_name = std::strrchr(s, '/');
|
||||
if (!file_name || file_name[1] == '\0') {
|
||||
return nullptr;
|
||||
}
|
||||
return file_name + 1;
|
||||
}
|
||||
|
||||
virtual Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) {
|
||||
if (FixPath(path) == "/") {
|
||||
*out_entry_type = FsDirEntryType_Dir;
|
||||
R_SUCCEED();
|
||||
} else {
|
||||
const auto file_name = GetFileName(path);
|
||||
R_UNLESS(file_name, FsError_PathNotFound);
|
||||
|
||||
const auto it = std::ranges::find_if(m_entries, [file_name](auto& e){
|
||||
return !strcasecmp(file_name, e.name);
|
||||
});
|
||||
R_UNLESS(it != m_entries.end(), FsError_PathNotFound);
|
||||
|
||||
*out_entry_type = FsDirEntryType_File;
|
||||
R_SUCCEED();
|
||||
}
|
||||
}
|
||||
virtual Result CreateFile(const char* path, s64 size, u32 option) {
|
||||
const auto file_name = GetFileName(path);
|
||||
R_UNLESS(file_name, FsError_PathNotFound);
|
||||
|
||||
const auto it = std::ranges::find_if(m_entries, [file_name](auto& e){
|
||||
return !strcasecmp(file_name, e.name);
|
||||
});
|
||||
R_UNLESS(it == m_entries.end(), FsError_PathAlreadyExists);
|
||||
|
||||
FsDirectoryEntry entry{};
|
||||
std::strcpy(entry.name, file_name);
|
||||
entry.type = FsDirEntryType_File;
|
||||
entry.file_size = size;
|
||||
|
||||
m_entries.emplace_back(entry);
|
||||
R_SUCCEED();
|
||||
}
|
||||
virtual Result DeleteFile(const char* path) {
|
||||
const auto file_name = GetFileName(path);
|
||||
R_UNLESS(file_name, FsError_PathNotFound);
|
||||
|
||||
const auto it = std::ranges::find_if(m_entries, [file_name](auto& e){
|
||||
return !strcasecmp(file_name, e.name);
|
||||
});
|
||||
R_UNLESS(it != m_entries.end(), FsError_PathNotFound);
|
||||
|
||||
m_entries.erase(it);
|
||||
R_SUCCEED();
|
||||
}
|
||||
virtual Result RenameFile(const char *old_path, const char *new_path) {
|
||||
const auto file_name = GetFileName(old_path);
|
||||
R_UNLESS(file_name, FsError_PathNotFound);
|
||||
|
||||
const auto it = std::ranges::find_if(m_entries, [file_name](auto& e){
|
||||
return !strcasecmp(file_name, e.name);
|
||||
});
|
||||
R_UNLESS(it != m_entries.end(), FsError_PathNotFound);
|
||||
|
||||
const auto file_name_new = GetFileName(new_path);
|
||||
R_UNLESS(file_name_new, FsError_PathNotFound);
|
||||
|
||||
const auto new_it = std::ranges::find_if(m_entries, [file_name_new](auto& e){
|
||||
return !strcasecmp(file_name_new, e.name);
|
||||
});
|
||||
R_UNLESS(new_it == m_entries.end(), FsError_PathAlreadyExists);
|
||||
|
||||
std::strcpy(it->name, file_name_new);
|
||||
R_SUCCEED();
|
||||
}
|
||||
virtual Result OpenFile(const char *path, u32 mode, FsFile *out_file) {
|
||||
const auto file_name = GetFileName(path);
|
||||
R_UNLESS(file_name, FsError_PathNotFound);
|
||||
|
||||
const auto it = std::ranges::find_if(m_entries, [file_name](auto& e){
|
||||
return !strcasecmp(file_name, e.name);
|
||||
});
|
||||
R_UNLESS(it != m_entries.end(), FsError_PathNotFound);
|
||||
|
||||
out_file->s.object_id = std::distance(m_entries.begin(), it);
|
||||
out_file->s.own_handle = mode;
|
||||
R_SUCCEED();
|
||||
}
|
||||
virtual Result GetFileSize(FsFile *file, s64 *out_size) {
|
||||
auto& e = m_entries[file->s.object_id];
|
||||
*out_size = e.file_size;
|
||||
R_SUCCEED();
|
||||
}
|
||||
virtual Result SetFileSize(FsFile *file, s64 size) {
|
||||
auto& e = m_entries[file->s.object_id];
|
||||
e.file_size = size;
|
||||
R_SUCCEED();
|
||||
}
|
||||
virtual Result ReadFile(FsFile *file, s64 off, void *buf, u64 read_size, u32 option, u64 *out_bytes_read) {
|
||||
// stub for now as it may confuse users who think that the returned file is valid.
|
||||
// the code below can be used to benchmark mtp reads.
|
||||
R_THROW(FsError_NotImplemented);
|
||||
// auto& e = m_entries[file->s.object_id];
|
||||
// read_size = std::min<s64>(e.file_size - off, read_size);
|
||||
// std::memset(buf, 0, read_size);
|
||||
// *out_bytes_read = read_size;
|
||||
// R_SUCCEED();
|
||||
}
|
||||
virtual Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) {
|
||||
auto& e = m_entries[file->s.object_id];
|
||||
e.file_size = std::max<s64>(e.file_size, off + write_size);
|
||||
R_SUCCEED();
|
||||
}
|
||||
virtual void CloseFile(FsFile *file) {
|
||||
std::memset(file, 0, sizeof(*file));
|
||||
}
|
||||
|
||||
Result CreateDirectory(const char* path) override {
|
||||
R_THROW(FsError_NotImplemented);
|
||||
}
|
||||
Result DeleteDirectoryRecursively(const char* path) override {
|
||||
R_THROW(FsError_NotImplemented);
|
||||
}
|
||||
Result RenameDirectory(const char *old_path, const char *new_path) override {
|
||||
R_THROW(FsError_NotImplemented);
|
||||
}
|
||||
Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) override {
|
||||
std::memset(out_dir, 0, sizeof(*out_dir));
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) override {
|
||||
max_entries = std::min<s64>(m_entries.size()- d->s.object_id, max_entries);
|
||||
std::memcpy(buf, m_entries.data() + d->s.object_id, max_entries * sizeof(*buf));
|
||||
d->s.object_id += max_entries;
|
||||
*out_total_entries = max_entries;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override {
|
||||
*out_count = m_entries.size();
|
||||
R_SUCCEED();
|
||||
}
|
||||
void CloseDirectory(FsDir *d) override {
|
||||
std::memset(d, 0, sizeof(*d));
|
||||
}
|
||||
|
||||
protected:
|
||||
std::vector<FsDirectoryEntry> m_entries;
|
||||
};
|
||||
|
||||
struct FsDevNullProxy final : FsProxyVfs {
|
||||
using FsProxyVfs::FsProxyVfs;
|
||||
|
||||
Result GetTotalSpace(const char *path, s64 *out) override {
|
||||
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result GetFreeSpace(const char *path, s64 *out) override {
|
||||
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
|
||||
R_SUCCEED();
|
||||
}
|
||||
};
|
||||
|
||||
struct FsInstallProxy final : FsProxyVfs {
|
||||
using FsProxyVfs::FsProxyVfs;
|
||||
|
||||
Result FailedIfNotEnabled() {
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
if (!g_shared_data.enabled) {
|
||||
App::Notify("Please launch MTP install menu before trying to install"_i18n);
|
||||
R_THROW(FsError_NotImplemented);
|
||||
}
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result IsValidFileType(const char* name) {
|
||||
const char* ext = std::strrchr(name, '.');
|
||||
if (!ext) {
|
||||
R_THROW(FsError_NotImplemented);
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
for (size_t i = 0; i < std::size(SUPPORTED_EXT); i++) {
|
||||
if (!strcasecmp(ext, SUPPORTED_EXT[i])) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
R_THROW(FsError_NotImplemented);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result GetTotalSpace(const char *path, s64 *out) override {
|
||||
if (App::GetApp()->m_install_sd.Get()) {
|
||||
return fs::FsNativeContentStorage(FsContentStorageId_SdCard).GetTotalSpace("/", out);
|
||||
} else {
|
||||
return fs::FsNativeContentStorage(FsContentStorageId_User).GetTotalSpace("/", out);
|
||||
}
|
||||
}
|
||||
Result GetFreeSpace(const char *path, s64 *out) override {
|
||||
if (App::GetApp()->m_install_sd.Get()) {
|
||||
return fs::FsNativeContentStorage(FsContentStorageId_SdCard).GetFreeSpace("/", out);
|
||||
} else {
|
||||
return fs::FsNativeContentStorage(FsContentStorageId_User).GetFreeSpace("/", out);
|
||||
}
|
||||
}
|
||||
|
||||
Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) override {
|
||||
R_TRY(FsProxyVfs::GetEntryType(path, out_entry_type));
|
||||
if (*out_entry_type == FsDirEntryType_File) {
|
||||
R_TRY(FailedIfNotEnabled());
|
||||
}
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result CreateFile(const char* path, s64 size, u32 option) override {
|
||||
R_TRY(FailedIfNotEnabled());
|
||||
R_TRY(IsValidFileType(path));
|
||||
R_TRY(FsProxyVfs::CreateFile(path, size, option));
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result OpenFile(const char *path, u32 mode, FsFile *out_file) override {
|
||||
R_TRY(FailedIfNotEnabled());
|
||||
R_TRY(IsValidFileType(path));
|
||||
R_TRY(FsProxyVfs::OpenFile(path, mode, out_file));
|
||||
log_write("[MTP] done file open: %s mode: 0x%X\n", path, mode);
|
||||
|
||||
if (mode & FsOpenMode_Write) {
|
||||
const auto& e = m_entries[out_file->s.object_id];
|
||||
|
||||
// check if we already have this file queued.
|
||||
log_write("[MTP] checking if empty\n");
|
||||
R_UNLESS(g_shared_data.current_file.empty(), FsError_NotImplemented);
|
||||
log_write("[MTP] is empty\n");
|
||||
g_shared_data.current_file = e.name;
|
||||
on_thing();
|
||||
}
|
||||
|
||||
log_write("[MTP] got file: %s\n", path);
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) override {
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
if (!g_shared_data.enabled) {
|
||||
log_write("[MTP] failing as not enabled\n");
|
||||
R_THROW(FsError_NotImplemented);
|
||||
}
|
||||
|
||||
if (!g_shared_data.on_write || !g_shared_data.on_write(buf, write_size)) {
|
||||
log_write("[MTP] failing as not written\n");
|
||||
R_THROW(FsError_NotImplemented);
|
||||
}
|
||||
|
||||
R_TRY(FsProxyVfs::WriteFile(file, off, buf, write_size, option));
|
||||
R_SUCCEED();
|
||||
}
|
||||
void CloseFile(FsFile *file) override {
|
||||
bool update{};
|
||||
{
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
if (file->s.own_handle & FsOpenMode_Write) {
|
||||
log_write("[MTP] closing current file\n");
|
||||
if (g_shared_data.on_close) {
|
||||
g_shared_data.on_close();
|
||||
}
|
||||
|
||||
g_shared_data.in_progress = false;
|
||||
g_shared_data.current_file.clear();
|
||||
update = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (update) {
|
||||
on_thing();
|
||||
}
|
||||
|
||||
FsProxyVfs::CloseFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
::haze::FsEntries g_fs_entries{};
|
||||
|
||||
void haze_callback(const ::haze::CallbackData *data) {
|
||||
auto& e = *data;
|
||||
|
||||
switch (e.type) {
|
||||
case ::haze::CallbackType_OpenSession: log_write("[LIBHAZE] Opening Session\n"); break;
|
||||
case ::haze::CallbackType_CloseSession: log_write("[LIBHAZE] Closing Session\n"); break;
|
||||
|
||||
case ::haze::CallbackType_CreateFile: log_write("[LIBHAZE] Creating File: %s\n", e.file.filename); break;
|
||||
case ::haze::CallbackType_DeleteFile: log_write("[LIBHAZE] Deleting File: %s\n", e.file.filename); break;
|
||||
|
||||
case ::haze::CallbackType_RenameFile: log_write("[LIBHAZE] Rename File: %s -> %s\n", e.rename.filename, e.rename.newname); break;
|
||||
case ::haze::CallbackType_RenameFolder: log_write("[LIBHAZE] Rename Folder: %s -> %s\n", e.rename.filename, e.rename.newname); break;
|
||||
|
||||
case ::haze::CallbackType_CreateFolder: log_write("[LIBHAZE] Creating Folder: %s\n", e.file.filename); break;
|
||||
case ::haze::CallbackType_DeleteFolder: log_write("[LIBHAZE] Deleting Folder: %s\n", e.file.filename); break;
|
||||
|
||||
case ::haze::CallbackType_ReadBegin: log_write("[LIBHAZE] Reading File Begin: %s \n", e.file.filename); break;
|
||||
case ::haze::CallbackType_ReadProgress: log_write("\t[LIBHAZE] Reading File: offset: %lld size: %lld\n", e.progress.offset, e.progress.size); break;
|
||||
case ::haze::CallbackType_ReadEnd: log_write("[LIBHAZE] Reading File Finished: %s\n", e.file.filename); break;
|
||||
|
||||
case ::haze::CallbackType_WriteBegin: log_write("[LIBHAZE] Writing File Begin: %s \n", e.file.filename); break;
|
||||
case ::haze::CallbackType_WriteProgress: log_write("\t[LIBHAZE] Writing File: offset: %lld size: %lld\n", e.progress.offset, e.progress.size); break;
|
||||
case ::haze::CallbackType_WriteEnd: log_write("[LIBHAZE] Writing File Finished: %s\n", e.file.filename); break;
|
||||
}
|
||||
|
||||
App::NotifyFlashLed();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Init() {
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
if (g_is_running) {
|
||||
log_write("[MTP] already enabled, cannot open\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_shared<fs::FsNativeSd>(), "", "microSD card"));
|
||||
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_shared<fs::FsNativeImage>(FsImageDirectoryId_Nand), "image_nand", "Image nand"));
|
||||
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_shared<fs::FsNativeImage>(FsImageDirectoryId_Sd), "image_sd", "Image sd"));
|
||||
g_fs_entries.emplace_back(std::make_shared<FsDevNullProxy>("DevNull", "DevNull (Speed Test)"));
|
||||
g_fs_entries.emplace_back(std::make_shared<FsInstallProxy>("install", "Install (NSP, XCI, NSZ, XCZ)"));
|
||||
|
||||
g_should_exit = false;
|
||||
if (!::haze::Initialize(haze_callback, THREAD_PRIO, THREAD_CORE, g_fs_entries)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
log_write("[MTP] started\n");
|
||||
return g_is_running = true;
|
||||
}
|
||||
|
||||
void Exit() {
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
if (!g_is_running) {
|
||||
return;
|
||||
}
|
||||
|
||||
::haze::Exit();
|
||||
g_is_running = false;
|
||||
g_should_exit = true;
|
||||
g_fs_entries.clear();
|
||||
|
||||
log_write("[MTP] exitied\n");
|
||||
}
|
||||
|
||||
void InitInstallMode(OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close) {
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
g_shared_data.on_start = on_start;
|
||||
g_shared_data.on_write = on_write;
|
||||
g_shared_data.on_close = on_close;
|
||||
g_shared_data.enabled = true;
|
||||
}
|
||||
|
||||
void DisableInstallMode() {
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
g_shared_data.enabled = false;
|
||||
}
|
||||
|
||||
} // namespace sphaira::haze
|
||||
@@ -20,7 +20,11 @@
|
||||
#pragma GCC diagnostic pop
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
#include "app.hpp"
|
||||
#include "log.hpp"
|
||||
#ifdef USE_NVJPG
|
||||
#include <nvjpg.hpp>
|
||||
#endif
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira {
|
||||
@@ -30,7 +34,6 @@ constexpr int BPP = 4;
|
||||
|
||||
auto ImageLoadInternal(stbi_uc* image_data, int x, int y) -> ImageResult {
|
||||
if (image_data) {
|
||||
log_write("loaded image: w: %d h: %d\n", x, y);
|
||||
ImageResult result{};
|
||||
result.data.resize(x*y*BPP);
|
||||
result.w = x;
|
||||
@@ -44,17 +47,83 @@ auto ImageLoadInternal(stbi_uc* image_data, int x, int y) -> ImageResult {
|
||||
return {};
|
||||
}
|
||||
|
||||
#ifdef USE_NVJPG
|
||||
auto ImageLoadInternal(nj::Image&& image) -> ImageResult {
|
||||
if (!image.is_valid() || image.parse()) {
|
||||
log_write("[NVJPG] failed to parse image\n");
|
||||
return {};
|
||||
}
|
||||
|
||||
nj::Surface surf{image.width, image.height};
|
||||
if (surf.allocate()) {
|
||||
log_write("[NVJPG] failed to allocate surf\n");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (R_FAILED(App::GetApp()->m_decoder.render(image, surf, 255))) {
|
||||
log_write("[NVJPG] failed to render\n");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (R_FAILED(App::GetApp()->m_decoder.wait(surf))) {
|
||||
log_write("[NVJPG] failed to wait\n");
|
||||
return {};
|
||||
}
|
||||
|
||||
ImageResult result{};
|
||||
result.w = surf.width;
|
||||
result.h = surf.height;
|
||||
result.data.resize(surf.width * surf.height * surf.get_bpp());
|
||||
// std::printf("[NVJPG] w: %zu h: %zu bpp: %u pitch: %zu size: %zu size2: %u\n", surf.width, surf.height, surf.get_bpp(), surf.pitch, surf.size(), 256*256*4);
|
||||
|
||||
if (surf.width * surf.get_bpp() == surf.pitch) [[likely]] {
|
||||
std::memcpy(result.data.data(), surf.data(), result.data.size());
|
||||
} else {
|
||||
for (size_t i = 0; i < surf.height; i++) {
|
||||
const auto src_pitch = surf.pitch;
|
||||
const auto dst_pitch = surf.width * surf.get_bpp();
|
||||
std::memcpy(result.data.data() + i * dst_pitch, surf.data() + i * src_pitch, dst_pitch);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
auto ImageLoadFromMemory(std::span<const u8> data) -> ImageResult {
|
||||
int x, y, channels;
|
||||
return ImageLoadInternal(stbi_load_from_memory(data.data(), data.size(), &x, &y, &channels, BPP), x, y);
|
||||
auto ImageLoadFromMemory(std::span<const u8> data, u32 flags) -> ImageResult {
|
||||
#ifdef USE_NVJPG
|
||||
if (flags & ImageFlag_JPEG) {
|
||||
auto shared_vec = std::make_shared<std::vector<u8>>(data.size());
|
||||
std::memcpy(shared_vec->data(), data.data(), shared_vec->size());
|
||||
// don't make const as it prevents RTO.
|
||||
auto result = ImageLoadInternal(nj::Image{shared_vec});
|
||||
// if it failed, try again but without using oss-jpg.
|
||||
return result.data.empty() ? ImageLoadFromMemory(data, 0) : result;
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
int x, y, channels;
|
||||
return ImageLoadInternal(stbi_load_from_memory(data.data(), data.size(), &x, &y, &channels, BPP), x, y);
|
||||
}
|
||||
}
|
||||
|
||||
auto ImageLoadFromFile(const fs::FsPath& file) -> ImageResult {
|
||||
log_write("doing file load\n");
|
||||
int x, y, channels;
|
||||
return ImageLoadInternal(stbi_load(file, &x, &y, &channels, BPP), x, y);
|
||||
auto ImageLoadFromFile(const fs::FsPath& file, u32 flags) -> ImageResult {
|
||||
#ifdef USE_NVJPG
|
||||
if (flags & ImageFlag_JPEG) {
|
||||
// don't make const as it prevents RTO.
|
||||
auto result = ImageLoadInternal(nj::Image{file});
|
||||
// if it failed, try again but without using oss-jpg.
|
||||
return result.data.empty() ? ImageLoadFromFile(file, 0) : result;
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
int x, y, channels;
|
||||
return ImageLoadInternal(stbi_load(file, &x, &y, &channels, BPP), x, y);
|
||||
}
|
||||
}
|
||||
|
||||
auto ImageResize(std::span<const u8> data, int inx, int iny, int outx, int outy) -> ImageResult {
|
||||
|
||||
@@ -10,17 +10,27 @@ namespace {
|
||||
|
||||
constexpr const char* logpath = "/config/sphaira/log.txt";
|
||||
|
||||
std::FILE* file{};
|
||||
int nxlink_socket{};
|
||||
bool g_file_open{};
|
||||
std::mutex mutex{};
|
||||
|
||||
void log_write_arg_internal(const char* s, std::va_list* v) {
|
||||
if (file) {
|
||||
std::vfprintf(file, s, *v);
|
||||
std::fflush(file);
|
||||
const auto t = std::time(nullptr);
|
||||
const auto tm = std::localtime(&t);
|
||||
|
||||
static char buf[512];
|
||||
const auto len = std::snprintf(buf, sizeof(buf), "[%02u:%02u:%02u] -> ", tm->tm_hour, tm->tm_min, tm->tm_sec);
|
||||
std::vsnprintf(buf + len, sizeof(buf) - len, s, *v);
|
||||
|
||||
if (g_file_open) {
|
||||
auto file = std::fopen(logpath, "a");
|
||||
if (file) {
|
||||
std::fprintf(file, "%s", buf);
|
||||
std::fclose(file);
|
||||
}
|
||||
}
|
||||
if (nxlink_socket) {
|
||||
std::vprintf(s, *v);
|
||||
std::printf("%s", buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,12 +40,18 @@ extern "C" {
|
||||
|
||||
auto log_file_init() -> bool {
|
||||
std::scoped_lock lock{mutex};
|
||||
if (file) {
|
||||
if (g_file_open) {
|
||||
return false;
|
||||
}
|
||||
|
||||
file = std::fopen(logpath, "w");
|
||||
return file != nullptr;
|
||||
auto file = std::fopen(logpath, "w");
|
||||
if (file) {
|
||||
g_file_open = true;
|
||||
std::fclose(file);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
auto log_nxlink_init() -> bool {
|
||||
@@ -50,9 +66,8 @@ auto log_nxlink_init() -> bool {
|
||||
|
||||
void log_file_exit() {
|
||||
std::scoped_lock lock{mutex};
|
||||
if (file) {
|
||||
std::fclose(file);
|
||||
file = nullptr;
|
||||
if (g_file_open) {
|
||||
g_file_open = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,12 +79,17 @@ void log_nxlink_exit() {
|
||||
}
|
||||
}
|
||||
|
||||
void log_write(const char* s, ...) {
|
||||
bool log_is_init() {
|
||||
std::scoped_lock lock{mutex};
|
||||
if (!file && !nxlink_socket) {
|
||||
return g_file_open || nxlink_socket;
|
||||
}
|
||||
|
||||
void log_write(const char* s, ...) {
|
||||
if (!log_is_init()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::scoped_lock lock{mutex};
|
||||
std::va_list v{};
|
||||
va_start(v, s);
|
||||
log_write_arg_internal(s, &v);
|
||||
@@ -77,11 +97,11 @@ void log_write(const char* s, ...) {
|
||||
}
|
||||
|
||||
void log_write_arg(const char* s, va_list* v) {
|
||||
std::scoped_lock lock{mutex};
|
||||
if (!file && !nxlink_socket) {
|
||||
if (!log_is_init()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::scoped_lock lock{mutex};
|
||||
log_write_arg_internal(s, v);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ int main(int argc, char** argv) {
|
||||
extern "C" {
|
||||
|
||||
void userAppInit(void) {
|
||||
Result rc;
|
||||
sphaira::App::SetBoostMode(true);
|
||||
|
||||
// https://github.com/mtheall/ftpd/blob/e27898f0c3101522311f330e82a324861e0e3f7e/source/switch/init.c#L31
|
||||
const SocketInitConfig socket_config_application = {
|
||||
@@ -47,6 +47,7 @@ void userAppInit(void) {
|
||||
|
||||
const auto socket_config = is_application ? socket_config_application : socket_config_applet;
|
||||
|
||||
Result rc;
|
||||
if (R_FAILED(rc = appletLockExit()))
|
||||
diagAbortWithResult(rc);
|
||||
if (R_FAILED(rc = socketInitialize(&socket_config)))
|
||||
@@ -83,6 +84,12 @@ void userAppExit(void) {
|
||||
psmExit();
|
||||
plExit();
|
||||
socketExit();
|
||||
// NOTE (DMC): prevents exfat corruption.
|
||||
if (auto fs = fsdevGetDeviceFileSystem("sdmc:")) {
|
||||
fsFsCommit(fs);
|
||||
}
|
||||
|
||||
sphaira::App::SetBoostMode(false);
|
||||
appletUnlockExit();
|
||||
}
|
||||
|
||||
|
||||
210
sphaira/source/minizip_helper.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
#include "minizip_helper.hpp"
|
||||
#include <minizip/unzip.h>
|
||||
#include <minizip/zip.h>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
|
||||
namespace sphaira::mz {
|
||||
namespace {
|
||||
|
||||
voidpf minizip_open_file_func_mem(voidpf opaque, const void* filename, int mode) {
|
||||
return opaque;
|
||||
}
|
||||
|
||||
ZPOS64_T minizip_tell_file_func_mem(voidpf opaque, voidpf stream) {
|
||||
auto mem = static_cast<const MzMem*>(opaque);
|
||||
return mem->offset;
|
||||
}
|
||||
|
||||
long minizip_seek_file_func_mem(voidpf opaque, voidpf stream, ZPOS64_T offset, int origin) {
|
||||
auto mem = static_cast<MzMem*>(opaque);
|
||||
size_t new_offset = 0;
|
||||
|
||||
switch (origin) {
|
||||
case ZLIB_FILEFUNC_SEEK_SET: new_offset = offset; break;
|
||||
case ZLIB_FILEFUNC_SEEK_CUR: new_offset = mem->offset + offset; break;
|
||||
case ZLIB_FILEFUNC_SEEK_END: new_offset = (mem->buf.size() - 1) + offset; break;
|
||||
default: return -1;
|
||||
}
|
||||
|
||||
if (new_offset > mem->buf.size()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
mem->offset = new_offset;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uLong minizip_read_file_func_mem(voidpf opaque, voidpf stream, void* buf, uLong size) {
|
||||
auto mem = static_cast<MzMem*>(opaque);
|
||||
|
||||
size = std::min(size, mem->buf.size() - mem->offset);
|
||||
std::memcpy(buf, mem->buf.data() + mem->offset, size);
|
||||
mem->offset += size;
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
uLong minizip_write_file_func_mem(voidpf opaque, voidpf stream, const void* buf, uLong size) {
|
||||
auto mem = static_cast<MzMem*>(opaque);
|
||||
|
||||
// give it more memory
|
||||
if (mem->buf.capacity() < mem->offset + size) {
|
||||
mem->buf.reserve(mem->buf.capacity() + 1024*1024*64);
|
||||
}
|
||||
|
||||
if (mem->buf.size() < mem->offset + size) {
|
||||
mem->buf.resize(mem->offset + size);
|
||||
}
|
||||
|
||||
std::memcpy(mem->buf.data() + mem->offset, buf, size);
|
||||
mem->offset += size;
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
int minizip_close_file_func_mem(voidpf opaque, voidpf stream) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
constexpr zlib_filefunc64_def zlib_filefunc_mem = {
|
||||
.zopen64_file = minizip_open_file_func_mem,
|
||||
.zread_file = minizip_read_file_func_mem,
|
||||
.zwrite_file = minizip_write_file_func_mem,
|
||||
.ztell64_file = minizip_tell_file_func_mem,
|
||||
.zseek64_file = minizip_seek_file_func_mem,
|
||||
.zclose_file = minizip_close_file_func_mem,
|
||||
};
|
||||
|
||||
voidpf minizip_open_file_func_span(voidpf opaque, const void* filename, int mode) {
|
||||
return opaque;
|
||||
}
|
||||
|
||||
ZPOS64_T minizip_tell_file_func_span(voidpf opaque, voidpf stream) {
|
||||
auto mem = static_cast<const MzSpan*>(opaque);
|
||||
return mem->offset;
|
||||
}
|
||||
|
||||
long minizip_seek_file_func_span(voidpf opaque, voidpf stream, ZPOS64_T offset, int origin) {
|
||||
auto mem = static_cast<MzSpan*>(opaque);
|
||||
size_t new_offset = 0;
|
||||
|
||||
switch (origin) {
|
||||
case ZLIB_FILEFUNC_SEEK_SET: new_offset = offset; break;
|
||||
case ZLIB_FILEFUNC_SEEK_CUR: new_offset = mem->offset + offset; break;
|
||||
case ZLIB_FILEFUNC_SEEK_END: new_offset = (mem->buf.size() - 1) + offset; break;
|
||||
default: return -1;
|
||||
}
|
||||
|
||||
if (new_offset > mem->buf.size()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
mem->offset = new_offset;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uLong minizip_read_file_func_span(voidpf opaque, voidpf stream, void* buf, uLong size) {
|
||||
auto mem = static_cast<MzSpan*>(opaque);
|
||||
|
||||
size = std::min(size, mem->buf.size() - mem->offset);
|
||||
std::memcpy(buf, mem->buf.data() + mem->offset, size);
|
||||
mem->offset += size;
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
int minizip_close_file_func_span(voidpf opaque, voidpf stream) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
constexpr zlib_filefunc64_def zlib_filefunc_span = {
|
||||
.zopen64_file = minizip_open_file_func_span,
|
||||
.zread_file = minizip_read_file_func_span,
|
||||
.ztell64_file = minizip_tell_file_func_span,
|
||||
.zseek64_file = minizip_seek_file_func_span,
|
||||
.zclose_file = minizip_close_file_func_span,
|
||||
};
|
||||
|
||||
voidpf minizip_open_file_func_stdio(voidpf opaque, const void* filename, int mode) {
|
||||
const char* mode_fopen = NULL;
|
||||
if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER) == ZLIB_FILEFUNC_MODE_READ) {
|
||||
mode_fopen = "rb";
|
||||
} else if (mode & ZLIB_FILEFUNC_MODE_EXISTING) {
|
||||
mode_fopen = "r+b";
|
||||
} else if (mode & ZLIB_FILEFUNC_MODE_CREATE) {
|
||||
mode_fopen = "wb";
|
||||
} else {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto f = std::fopen((const char*)filename, mode_fopen);
|
||||
if (f) {
|
||||
std::setvbuf(f, nullptr, _IOFBF, 1024 * 512);
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
ZPOS64_T minizip_tell_file_func_stdio(voidpf opaque, voidpf stream) {
|
||||
auto file = static_cast<std::FILE*>(stream);
|
||||
return std::ftell(file);
|
||||
}
|
||||
|
||||
long minizip_seek_file_func_stdio(voidpf opaque, voidpf stream, ZPOS64_T offset, int origin) {
|
||||
auto file = static_cast<std::FILE*>(stream);
|
||||
return std::fseek(file, offset, origin);
|
||||
}
|
||||
|
||||
uLong minizip_read_file_func_stdio(voidpf opaque, voidpf stream, void* buf, uLong size) {
|
||||
auto file = static_cast<std::FILE*>(stream);
|
||||
return std::fread(buf, 1, size, file);
|
||||
}
|
||||
|
||||
uLong minizip_write_file_func_stdio(voidpf opaque, voidpf stream, const void* buf, uLong size) {
|
||||
auto file = static_cast<std::FILE*>(stream);
|
||||
return std::fwrite(buf, 1, size, file);
|
||||
}
|
||||
|
||||
int minizip_close_file_func_stdio(voidpf opaque, voidpf stream) {
|
||||
auto file = static_cast<std::FILE*>(stream);
|
||||
if (file) {
|
||||
return std::fclose(file);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int minizip_error_file_func_stdio(voidpf opaque, voidpf stream) {
|
||||
auto file = static_cast<std::FILE*>(stream);
|
||||
if (file) {
|
||||
return std::ferror(file);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
constexpr zlib_filefunc64_def zlib_filefunc_stdio = {
|
||||
.zopen64_file = minizip_open_file_func_stdio,
|
||||
.zread_file = minizip_read_file_func_stdio,
|
||||
.zwrite_file = minizip_write_file_func_stdio,
|
||||
.ztell64_file = minizip_tell_file_func_stdio,
|
||||
.zseek64_file = minizip_seek_file_func_stdio,
|
||||
.zclose_file = minizip_close_file_func_stdio,
|
||||
.zerror_file = minizip_error_file_func_stdio,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void FileFuncMem(MzMem* mem, zlib_filefunc64_def* funcs) {
|
||||
*funcs = zlib_filefunc_mem;
|
||||
funcs->opaque = mem;
|
||||
}
|
||||
|
||||
void FileFuncSpan(MzSpan* span, zlib_filefunc64_def* funcs) {
|
||||
*funcs = zlib_filefunc_span;
|
||||
funcs->opaque = span;
|
||||
}
|
||||
|
||||
void FileFuncStdio(zlib_filefunc64_def* funcs) {
|
||||
*funcs = zlib_filefunc_stdio;
|
||||
}
|
||||
|
||||
} // namespace sphaira::mz
|
||||
@@ -13,15 +13,6 @@
|
||||
namespace sphaira {
|
||||
namespace {
|
||||
|
||||
enum {
|
||||
Module_Nro = 421,
|
||||
};
|
||||
|
||||
enum NroError {
|
||||
NroError_BadMagic = MAKERESULT(Module_Nro, 1),
|
||||
NroError_BadSize = MAKERESULT(Module_Nro, 2),
|
||||
};
|
||||
|
||||
struct NroData {
|
||||
NroStart start;
|
||||
NroHeader header;
|
||||
@@ -47,11 +38,11 @@ auto nro_parse_internal(fs::Fs* fs, const fs::FsPath& path, NroEntry& entry) ->
|
||||
NroData data;
|
||||
u64 bytes_read;
|
||||
R_TRY(f.Read(0, &data, sizeof(data), FsReadOption_None, &bytes_read));
|
||||
R_UNLESS(data.header.magic == NROHEADER_MAGIC, NroError_BadMagic);
|
||||
R_UNLESS(data.header.magic == NROHEADER_MAGIC, Result_NroBadMagic);
|
||||
|
||||
NroAssetHeader asset;
|
||||
R_TRY(f.Read(data.header.size, &asset, sizeof(asset), FsReadOption_None, &bytes_read));
|
||||
// R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, NroError_BadMagic);
|
||||
// R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, Result_NroBadMagic);
|
||||
|
||||
// we can avoid a GetSize() call by calculating the size manually.
|
||||
entry.size = data.header.size;
|
||||
@@ -68,18 +59,20 @@ auto nro_parse_internal(fs::Fs* fs, const fs::FsPath& path, NroEntry& entry) ->
|
||||
std::strncpy(nacp.lang.name, file_name, file_name_len - 4);
|
||||
std::strcpy(nacp.lang.author, "Unknown");
|
||||
std::strcpy(nacp.display_version, "Unknown");
|
||||
|
||||
entry.icon_offset = entry.icon_size = 0;
|
||||
entry.is_nacp_valid = false;
|
||||
} else {
|
||||
entry.size += sizeof(asset) + asset.icon.size + asset.nacp.size + asset.romfs.size;
|
||||
R_TRY(f.Read(data.header.size + asset.nacp.offset, &nacp.lang, sizeof(nacp.lang), FsReadOption_None, &bytes_read));
|
||||
R_TRY(f.Read(data.header.size + asset.nacp.offset + offsetof(NacpStruct, display_version), nacp.display_version, sizeof(nacp.display_version), FsReadOption_None, &bytes_read));
|
||||
|
||||
// lazy load the icons
|
||||
entry.icon_size = asset.icon.size;
|
||||
entry.icon_offset = data.header.size + asset.icon.offset;
|
||||
entry.is_nacp_valid = true;
|
||||
}
|
||||
|
||||
// lazy load the icons
|
||||
entry.icon_size = asset.icon.size;
|
||||
entry.icon_offset = data.header.size + asset.icon.offset;
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
@@ -192,10 +185,10 @@ auto launch_internal(const std::string& path, const std::string& argv) -> Result
|
||||
|
||||
auto nro_verify(std::span<const u8> data) -> Result {
|
||||
NroData nro;
|
||||
R_UNLESS(data.size() >= sizeof(nro), NroError_BadSize);
|
||||
R_UNLESS(data.size() >= sizeof(nro), Result_NroBadSize);
|
||||
|
||||
memcpy(&nro, data.data(), sizeof(nro));
|
||||
R_UNLESS(nro.header.magic == NROHEADER_MAGIC, NroError_BadMagic);
|
||||
R_UNLESS(nro.header.magic == NROHEADER_MAGIC, Result_NroBadMagic);
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
@@ -243,9 +236,9 @@ auto nro_get_nacp(const fs::FsPath& path, NacpStruct& nacp) -> Result {
|
||||
R_TRY(fs.OpenFile(path, FsOpenMode_Read, &f));
|
||||
|
||||
R_TRY(f.Read(0, &data, sizeof(data), FsReadOption_None, &bytes_read));
|
||||
R_UNLESS(data.header.magic == NROHEADER_MAGIC, NroError_BadMagic);
|
||||
R_UNLESS(data.header.magic == NROHEADER_MAGIC, Result_NroBadMagic);
|
||||
R_TRY(f.Read(data.header.size, &asset, sizeof(asset), FsReadOption_None, &bytes_read));
|
||||
R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, NroError_BadMagic);
|
||||
R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, Result_NroBadMagic);
|
||||
R_TRY(f.Read(data.header.size + asset.nacp.offset, &nacp, sizeof(nacp), FsReadOption_None, &bytes_read));
|
||||
|
||||
R_SUCCEED();
|
||||
|
||||
@@ -842,7 +842,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
||||
pbox->SetTitle(config.name);
|
||||
pbox->SetImageDataConst(config.icon);
|
||||
|
||||
R_UNLESS(!config.nro_path.empty(), OwoError_BadArgs);
|
||||
R_UNLESS(!config.nro_path.empty(), Result_OwoBadArgs);
|
||||
// R_UNLESS(!config.icon.empty(), OwoError_BadArgs);
|
||||
|
||||
R_TRY(splCryptoInitialize());
|
||||
@@ -962,7 +962,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
||||
|
||||
// setup database
|
||||
{
|
||||
pbox->NewTransfer("Updating ncm databse"_i18n).UpdateTransfer(4, 8);
|
||||
pbox->NewTransfer("Updating ncm database"_i18n).UpdateTransfer(4, 8);
|
||||
NcmContentMetaDatabase db;
|
||||
R_TRY(ncmOpenContentMetaDatabase(&db, storage_id));
|
||||
ON_SCOPE_EXIT(ncmContentMetaDatabaseClose(&db));
|
||||
|
||||
@@ -2,19 +2,25 @@
|
||||
#include "log.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "app.hpp"
|
||||
#include "minizip_helper.hpp"
|
||||
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <minizip/unzip.h>
|
||||
#include <minizip/zip.h>
|
||||
|
||||
namespace sphaira::thread {
|
||||
namespace {
|
||||
|
||||
constexpr u64 READ_BUFFER_MAX = 1024*1024*4;
|
||||
// used for file based emummc and zip/unzip.
|
||||
constexpr u64 SMALL_BUFFER_SIZE = 1024 * 512;
|
||||
// used for everything else.
|
||||
constexpr u64 NORMAL_BUFFER_SIZE = 1024*1024*4;
|
||||
|
||||
struct ThreadBuffer {
|
||||
ThreadBuffer() {
|
||||
buf.reserve(READ_BUFFER_MAX);
|
||||
buf.reserve(NORMAL_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
std::vector<u8> buf;
|
||||
@@ -65,7 +71,7 @@ public:
|
||||
};
|
||||
|
||||
struct ThreadData {
|
||||
ThreadData(ui::ProgressBox* _pbox, s64 size, ReadCallback _rfunc, WriteCallback _wfunc);
|
||||
ThreadData(ui::ProgressBox* _pbox, s64 size, ReadCallback _rfunc, WriteCallback _wfunc, u64 buffer_size);
|
||||
|
||||
auto GetResults() -> Result;
|
||||
void WakeAllThreads();
|
||||
@@ -104,9 +110,9 @@ private:
|
||||
|
||||
private:
|
||||
// these need to be copied
|
||||
ui::ProgressBox* pbox{};
|
||||
ReadCallback rfunc{};
|
||||
WriteCallback wfunc{};
|
||||
ui::ProgressBox* const pbox;
|
||||
const ReadCallback rfunc;
|
||||
const WriteCallback wfunc;
|
||||
|
||||
// these need to be created
|
||||
Mutex mutex{};
|
||||
@@ -121,21 +127,24 @@ private:
|
||||
std::vector<u8> pull_buffer{};
|
||||
s64 pull_buffer_offset{};
|
||||
|
||||
u64 read_buffer_size{};
|
||||
u64 max_buffer_size{};
|
||||
const u64 read_buffer_size;
|
||||
const s64 write_size;
|
||||
|
||||
// these are shared between threads
|
||||
volatile s64 read_offset{};
|
||||
volatile s64 write_offset{};
|
||||
volatile s64 write_size{};
|
||||
|
||||
volatile Result read_result{};
|
||||
volatile Result write_result{};
|
||||
volatile Result pull_result{};
|
||||
};
|
||||
|
||||
ThreadData::ThreadData(ui::ProgressBox* _pbox, s64 size, ReadCallback _rfunc, WriteCallback _wfunc)
|
||||
: pbox{_pbox}, rfunc{_rfunc}, wfunc{_wfunc} {
|
||||
ThreadData::ThreadData(ui::ProgressBox* _pbox, s64 size, ReadCallback _rfunc, WriteCallback _wfunc, u64 buffer_size)
|
||||
: pbox{_pbox}
|
||||
, rfunc{_rfunc}
|
||||
, wfunc{_wfunc}
|
||||
, read_buffer_size{buffer_size}
|
||||
, write_size{size} {
|
||||
mutexInit(std::addressof(mutex));
|
||||
mutexInit(std::addressof(pull_mutex));
|
||||
|
||||
@@ -143,20 +152,10 @@ ThreadData::ThreadData(ui::ProgressBox* _pbox, s64 size, ReadCallback _rfunc, Wr
|
||||
condvarInit(std::addressof(can_write));
|
||||
condvarInit(std::addressof(can_pull));
|
||||
condvarInit(std::addressof(can_pull_write));
|
||||
|
||||
write_size = size;
|
||||
|
||||
if (App::IsFileBaseEmummc()) {
|
||||
read_buffer_size = 1024 * 512;
|
||||
max_buffer_size = 1024 * 512;
|
||||
} else {
|
||||
read_buffer_size = READ_BUFFER_MAX;
|
||||
max_buffer_size = READ_BUFFER_MAX;
|
||||
}
|
||||
}
|
||||
|
||||
auto ThreadData::GetResults() -> Result {
|
||||
R_UNLESS(!pbox->ShouldExit(), 0x1);
|
||||
R_UNLESS(!pbox->ShouldExit(), Result_TransferCancelled);
|
||||
R_TRY(read_result);
|
||||
R_TRY(write_result);
|
||||
R_TRY(pull_result);
|
||||
@@ -251,7 +250,7 @@ Result ThreadData::Pull(void* data, s64 size, u64* bytes_read) {
|
||||
Result ThreadData::readFuncInternal() {
|
||||
// the main buffer which data is read into.
|
||||
std::vector<u8> buf;
|
||||
buf.reserve(this->max_buffer_size);
|
||||
buf.reserve(this->read_buffer_size);
|
||||
|
||||
while (this->read_offset < this->write_size && R_SUCCEEDED(this->GetResults())) {
|
||||
// read more data
|
||||
@@ -265,14 +264,14 @@ Result ThreadData::readFuncInternal() {
|
||||
R_TRY(this->SetWriteBuf(buf, buf_size));
|
||||
}
|
||||
|
||||
log_write("read success\n");
|
||||
log_write("finished read thread success!\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
// write thread writes data to the nca placeholder.
|
||||
Result ThreadData::writeFuncInternal() {
|
||||
std::vector<u8> buf;
|
||||
buf.reserve(this->max_buffer_size);
|
||||
buf.reserve(this->read_buffer_size);
|
||||
|
||||
while (this->write_offset < this->write_size && R_SUCCEEDED(this->GetResults())) {
|
||||
s64 dummy_off;
|
||||
@@ -288,7 +287,7 @@ Result ThreadData::writeFuncInternal() {
|
||||
this->write_offset += size;
|
||||
}
|
||||
|
||||
log_write("finished write thread!\n");
|
||||
log_write("finished write thread success!\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
@@ -308,87 +307,275 @@ auto GetAlternateCore(int id) {
|
||||
return id == 1 ? 2 : 1;
|
||||
}
|
||||
|
||||
Result TransferInternal(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, WriteCallback wfunc, StartCallback2 sfunc) {
|
||||
App::SetAutoSleepDisabled(true);
|
||||
ON_SCOPE_EXIT(App::SetAutoSleepDisabled(false));
|
||||
Result TransferInternal(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, WriteCallback wfunc, StartCallback2 sfunc, Mode mode, u64 buffer_size = NORMAL_BUFFER_SIZE) {
|
||||
const auto is_file_based_emummc = App::IsFileBaseEmummc();
|
||||
|
||||
const auto WRITE_THREAD_CORE = sfunc ? pbox->GetCpuId() : GetAlternateCore(pbox->GetCpuId());
|
||||
const auto READ_THREAD_CORE = GetAlternateCore(WRITE_THREAD_CORE);
|
||||
if (is_file_based_emummc) {
|
||||
buffer_size = SMALL_BUFFER_SIZE;
|
||||
}
|
||||
|
||||
ThreadData t_data{pbox, size, rfunc, wfunc};
|
||||
if (mode == Mode::SingleThreadedIfSmaller) {
|
||||
if (size <= buffer_size) {
|
||||
mode = Mode::SingleThreaded;
|
||||
} else {
|
||||
mode = Mode::MultiThreaded;
|
||||
}
|
||||
}
|
||||
|
||||
Thread t_read{};
|
||||
R_TRY(threadCreate(&t_read, readFunc, std::addressof(t_data), nullptr, 1024*64, PRIO_PREEMPTIVE, READ_THREAD_CORE));
|
||||
ON_SCOPE_EXIT(threadClose(&t_read));
|
||||
// single threaded pull buffer is not supported.
|
||||
R_UNLESS(mode != Mode::MultiThreaded || !sfunc, 0x1);
|
||||
|
||||
Thread t_write{};
|
||||
R_TRY(threadCreate(&t_write, writeFunc, std::addressof(t_data), nullptr, 1024*64, PRIO_PREEMPTIVE, WRITE_THREAD_CORE));
|
||||
ON_SCOPE_EXIT(threadClose(&t_write));
|
||||
// todo: support single threaded pull buffer.
|
||||
if (mode == Mode::SingleThreaded) {
|
||||
std::vector<u8> buf(buffer_size);
|
||||
|
||||
s64 offset{};
|
||||
while (offset < size) {
|
||||
R_TRY(pbox->ShouldExitResult());
|
||||
|
||||
u64 bytes_read;
|
||||
const auto rsize = std::min<s64>(buf.size(), size - offset);
|
||||
R_TRY(rfunc(buf.data(), offset, rsize, &bytes_read));
|
||||
R_TRY(wfunc(buf.data(), offset, bytes_read));
|
||||
|
||||
offset += bytes_read;
|
||||
pbox->UpdateTransfer(offset, size);
|
||||
}
|
||||
|
||||
const auto start_threads = [&]() -> Result {
|
||||
log_write("starting threads\n");
|
||||
R_TRY(threadStart(std::addressof(t_read)));
|
||||
R_TRY(threadStart(std::addressof(t_write)));
|
||||
R_SUCCEED();
|
||||
};
|
||||
|
||||
ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_read)));
|
||||
ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_write)));
|
||||
|
||||
if (sfunc) {
|
||||
t_data.SetPullResult(sfunc(start_threads, [&](void* data, s64 size, u64* bytes_read) -> Result {
|
||||
R_TRY(t_data.GetResults());
|
||||
return t_data.Pull(data, size, bytes_read);
|
||||
}));
|
||||
} else {
|
||||
R_TRY(start_threads());
|
||||
|
||||
while (t_data.GetWriteOffset() != t_data.GetWriteSize() && R_SUCCEEDED(t_data.GetResults())) {
|
||||
pbox->UpdateTransfer(t_data.GetWriteOffset(), t_data.GetWriteSize());
|
||||
svcSleepThread(1e+6);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const auto WRITE_THREAD_CORE = sfunc ? pbox->GetCpuId() : GetAlternateCore(pbox->GetCpuId());
|
||||
const auto READ_THREAD_CORE = GetAlternateCore(WRITE_THREAD_CORE);
|
||||
|
||||
// wait for all threads to close.
|
||||
log_write("waiting for threads to close\n");
|
||||
for (;;) {
|
||||
t_data.WakeAllThreads();
|
||||
pbox->Yield();
|
||||
ThreadData t_data{pbox, size, rfunc, wfunc, buffer_size};
|
||||
|
||||
if (R_FAILED(waitSingleHandle(t_read.handle, 1000))) {
|
||||
continue;
|
||||
} else if (R_FAILED(waitSingleHandle(t_write.handle, 1000))) {
|
||||
continue;
|
||||
Thread t_read{};
|
||||
R_TRY(threadCreate(&t_read, readFunc, std::addressof(t_data), nullptr, 1024*256, 0x3B, READ_THREAD_CORE));
|
||||
ON_SCOPE_EXIT(threadClose(&t_read));
|
||||
|
||||
Thread t_write{};
|
||||
R_TRY(threadCreate(&t_write, writeFunc, std::addressof(t_data), nullptr, 1024*256, 0x3B, WRITE_THREAD_CORE));
|
||||
ON_SCOPE_EXIT(threadClose(&t_write));
|
||||
|
||||
const auto start_threads = [&]() -> Result {
|
||||
log_write("starting threads\n");
|
||||
R_TRY(threadStart(std::addressof(t_read)));
|
||||
R_TRY(threadStart(std::addressof(t_write)));
|
||||
R_SUCCEED();
|
||||
};
|
||||
|
||||
ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_read)));
|
||||
ON_SCOPE_EXIT(threadWaitForExit(std::addressof(t_write)));
|
||||
|
||||
if (sfunc) {
|
||||
log_write("[THREAD] doing sfuncn\n");
|
||||
t_data.SetPullResult(sfunc(start_threads, [&](void* data, s64 size, u64* bytes_read) -> Result {
|
||||
R_TRY(t_data.GetResults());
|
||||
return t_data.Pull(data, size, bytes_read);
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
log_write("threads closed\n");
|
||||
else {
|
||||
log_write("[THREAD] doing normal\n");
|
||||
R_TRY(start_threads());
|
||||
log_write("[THREAD] started threads\n");
|
||||
|
||||
// if any of the threads failed, wake up all threads so they can exit.
|
||||
if (R_FAILED(t_data.GetResults())) {
|
||||
log_write("some reads failed, waking threads\n");
|
||||
log_write("returning due to fail\n");
|
||||
while (t_data.GetWriteOffset() != t_data.GetWriteSize() && R_SUCCEEDED(t_data.GetResults())) {
|
||||
pbox->UpdateTransfer(t_data.GetWriteOffset(), t_data.GetWriteSize());
|
||||
svcSleepThread(1e+6);
|
||||
}
|
||||
}
|
||||
|
||||
// wait for all threads to close.
|
||||
log_write("waiting for threads to close\n");
|
||||
for (;;) {
|
||||
t_data.WakeAllThreads();
|
||||
pbox->Yield();
|
||||
|
||||
if (R_FAILED(waitSingleHandle(t_read.handle, 1000))) {
|
||||
continue;
|
||||
} else if (R_FAILED(waitSingleHandle(t_write.handle, 1000))) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
log_write("threads closed\n");
|
||||
|
||||
// if any of the threads failed, wake up all threads so they can exit.
|
||||
if (R_FAILED(t_data.GetResults())) {
|
||||
log_write("some reads failed, waking threads\n");
|
||||
log_write("returning due to fail\n");
|
||||
return t_data.GetResults();
|
||||
}
|
||||
|
||||
log_write("returning from thread func\n");
|
||||
return t_data.GetResults();
|
||||
}
|
||||
|
||||
return t_data.GetResults();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result Transfer(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, WriteCallback wfunc) {
|
||||
return TransferInternal(pbox, size, rfunc, wfunc, nullptr);
|
||||
Result Transfer(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, WriteCallback wfunc, Mode mode) {
|
||||
return TransferInternal(pbox, size, rfunc, wfunc, nullptr, mode);
|
||||
}
|
||||
|
||||
Result TransferPull(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, StartCallback sfunc) {
|
||||
Result TransferPull(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, StartCallback sfunc, Mode mode) {
|
||||
return TransferInternal(pbox, size, rfunc, nullptr, [sfunc](StartThreadCallback start, PullCallback pull) -> Result {
|
||||
R_TRY(start());
|
||||
return sfunc(pull);
|
||||
});
|
||||
}, mode);
|
||||
}
|
||||
|
||||
Result TransferPull(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, StartCallback2 sfunc) {
|
||||
return TransferInternal(pbox, size, rfunc, nullptr, sfunc);
|
||||
Result TransferPull(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, StartCallback2 sfunc, Mode mode) {
|
||||
return TransferInternal(pbox, size, rfunc, nullptr, sfunc, mode);
|
||||
}
|
||||
|
||||
Result TransferUnzip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& path, s64 size, u32 crc32, Mode mode) {
|
||||
Result rc;
|
||||
if (R_FAILED(rc = fs->CreateDirectoryRecursivelyWithPath(path)) && rc != FsError_PathAlreadyExists) {
|
||||
log_write("failed to create folder: %s 0x%04X\n", path.s, rc);
|
||||
R_THROW(rc);
|
||||
}
|
||||
|
||||
if (R_FAILED(rc = fs->CreateFile(path, size, 0)) && rc != FsError_PathAlreadyExists) {
|
||||
log_write("failed to create file: %s 0x%04X\n", path.s, rc);
|
||||
R_THROW(rc);
|
||||
}
|
||||
|
||||
fs::File f;
|
||||
R_TRY(fs->OpenFile(path, FsOpenMode_Write, &f));
|
||||
|
||||
// only update the size if this is an existing file.
|
||||
if (rc == FsError_PathAlreadyExists) {
|
||||
R_TRY(f.SetSize(size));
|
||||
}
|
||||
|
||||
// NOTES: do not use temp file with rename / delete after as it massively slows
|
||||
// down small file transfers (RA 21s -> 50s).
|
||||
u32 crc32_out{};
|
||||
R_TRY(thread::TransferInternal(pbox, size,
|
||||
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
|
||||
const auto result = unzReadCurrentFile(zfile, data, size);
|
||||
if (result <= 0) {
|
||||
log_write("failed to read zip file: %s %d\n", path.s, result);
|
||||
R_THROW(Result_UnzReadCurrentFile);
|
||||
}
|
||||
|
||||
if (crc32) {
|
||||
crc32_out = crc32CalculateWithSeed(crc32_out, data, result);
|
||||
}
|
||||
|
||||
*bytes_read = result;
|
||||
R_SUCCEED();
|
||||
},
|
||||
[&](const void* data, s64 off, s64 size) -> Result {
|
||||
return f.Write(off, data, size, FsWriteOption_None);
|
||||
},
|
||||
nullptr, mode, SMALL_BUFFER_SIZE
|
||||
));
|
||||
|
||||
// validate crc32 (if set in the info).
|
||||
R_UNLESS(!crc32 || crc32 == crc32_out, 0x8);
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result TransferZip(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& path, u32* crc32, Mode mode) {
|
||||
fs::File f;
|
||||
R_TRY(fs->OpenFile(path, FsOpenMode_Read, &f));
|
||||
|
||||
s64 file_size;
|
||||
R_TRY(f.GetSize(&file_size));
|
||||
|
||||
if (crc32) {
|
||||
*crc32 = 0;
|
||||
}
|
||||
|
||||
return thread::TransferInternal(pbox, file_size,
|
||||
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
|
||||
const auto rc = f.Read(off, data, size, FsReadOption_None, bytes_read);
|
||||
if (R_SUCCEEDED(rc) && crc32) {
|
||||
*crc32 = crc32CalculateWithSeed(*crc32, data, *bytes_read);
|
||||
}
|
||||
return rc;
|
||||
},
|
||||
[&](const void* data, s64 off, s64 size) -> Result {
|
||||
if (ZIP_OK != zipWriteInFileInZip(zfile, data, size)) {
|
||||
log_write("failed to write zip file: %s\n", path.s);
|
||||
R_THROW(Result_ZipWriteInFileInZip);
|
||||
}
|
||||
R_SUCCEED();
|
||||
},
|
||||
nullptr, mode, SMALL_BUFFER_SIZE
|
||||
);
|
||||
}
|
||||
|
||||
Result TransferUnzipAll(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs::FsPath& base_path, UnzipAllFilter filter, Mode mode) {
|
||||
unz_global_info64 ginfo;
|
||||
if (UNZ_OK != unzGetGlobalInfo64(zfile, &ginfo)) {
|
||||
R_THROW(Result_UnzGetGlobalInfo64);
|
||||
}
|
||||
|
||||
if (UNZ_OK != unzGoToFirstFile(zfile)) {
|
||||
R_THROW(Result_UnzGoToFirstFile);
|
||||
}
|
||||
|
||||
for (s64 i = 0; i < ginfo.number_entry; i++) {
|
||||
R_TRY(pbox->ShouldExitResult());
|
||||
|
||||
if (i > 0) {
|
||||
if (UNZ_OK != unzGoToNextFile(zfile)) {
|
||||
log_write("failed to unzGoToNextFile\n");
|
||||
R_THROW(Result_UnzGoToNextFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
|
||||
log_write("failed to open current file\n");
|
||||
R_THROW(Result_UnzOpenCurrentFile);
|
||||
}
|
||||
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
|
||||
|
||||
unz_file_info64 info;
|
||||
fs::FsPath name;
|
||||
if (UNZ_OK != unzGetCurrentFileInfo64(zfile, &info, name, sizeof(name), 0, 0, 0, 0)) {
|
||||
log_write("failed to get current info\n");
|
||||
R_THROW(Result_UnzGetCurrentFileInfo64);
|
||||
}
|
||||
|
||||
// check if we should skip this file.
|
||||
// don't make const as to allow the function to modify the path
|
||||
// this function is used for the updater to change sphaira.nro to exe path.
|
||||
auto path = fs::AppendPath(base_path, name);
|
||||
if (filter && !filter(name, path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pbox->NewTransfer(name);
|
||||
|
||||
if (path[std::strlen(path) -1] == '/') {
|
||||
Result rc;
|
||||
if (R_FAILED(rc = fs->CreateDirectoryRecursively(path)) && rc != FsError_PathAlreadyExists) {
|
||||
log_write("failed to create folder: %s 0x%04X\n", path.s, rc);
|
||||
R_THROW(rc);
|
||||
}
|
||||
} else {
|
||||
R_TRY(TransferUnzip(pbox, zfile, fs, path, info.uncompressed_size, info.crc, mode));
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result TransferUnzipAll(ui::ProgressBox* pbox, const fs::FsPath& zip_out, fs::Fs* fs, const fs::FsPath& base_path, UnzipAllFilter filter, Mode mode) {
|
||||
zlib_filefunc64_def file_func;
|
||||
mz::FileFuncStdio(&file_func);
|
||||
|
||||
auto zfile = unzOpen2_64(zip_out, &file_func);
|
||||
R_UNLESS(zfile, Result_UnzOpen2_64);
|
||||
ON_SCOPE_EXIT(unzClose(zfile));
|
||||
|
||||
return TransferUnzipAll(pbox, zfile, fs, base_path, filter, mode);
|
||||
}
|
||||
|
||||
} // namespace::thread
|
||||
|
||||
@@ -4,8 +4,129 @@
|
||||
#include "i18n.hpp"
|
||||
|
||||
namespace sphaira::ui {
|
||||
namespace {
|
||||
|
||||
auto GetCodeMessage(Result rc) -> const char* {
|
||||
switch (rc) {
|
||||
case FsError_PathNotFound: return "FsError_PathNotFound";
|
||||
case FsError_PathAlreadyExists: return "FsError_PathAlreadyExists";
|
||||
case FsError_TargetLocked: return "FsError_TargetLocked";
|
||||
|
||||
case Result_TransferCancelled: return "SphairaError_TransferCancelled";
|
||||
case Result_StreamBadSeek: return "SphairaError_StreamBadSeek";
|
||||
case Result_FsTooManyEntries: return "SphairaError_FsTooManyEntries";
|
||||
case Result_FsNewPathTooLarge: return "SphairaError_FsNewPathTooLarge";
|
||||
case Result_FsInvalidType: return "SphairaError_FsInvalidType";
|
||||
case Result_FsEmpty: return "SphairaError_FsEmpty";
|
||||
case Result_FsAlreadyRoot: return "SphairaError_FsAlreadyRoot";
|
||||
case Result_FsNoCurrentPath: return "SphairaError_FsNoCurrentPath";
|
||||
case Result_FsBrokenCurrentPath: return "SphairaError_FsBrokenCurrentPath";
|
||||
case Result_FsIndexOutOfBounds: return "SphairaError_FsIndexOutOfBounds";
|
||||
case Result_FsFsNotActive: return "SphairaError_FsFsNotActive";
|
||||
case Result_FsNewPathEmpty: return "SphairaError_FsNewPathEmpty";
|
||||
case Result_FsLoadingCancelled: return "SphairaError_FsLoadingCancelled";
|
||||
case Result_FsBrokenRoot: return "SphairaError_FsBrokenRoot";
|
||||
case Result_FsUnknownStdioError: return "SphairaError_FsUnknownStdioError";
|
||||
case Result_FsReadOnly: return "SphairaError_FsReadOnly";
|
||||
case Result_FsNotActive: return "SphairaError_FsNotActive";
|
||||
case Result_FsFailedStdioStat: return "SphairaError_FsFailedStdioStat";
|
||||
case Result_FsFailedStdioOpendir: return "SphairaError_FsFailedStdioOpendir";
|
||||
case Result_NroBadMagic: return "SphairaError_NroBadMagic";
|
||||
case Result_NroBadSize: return "SphairaError_NroBadSize";
|
||||
case Result_AppFailedMusicDownload: return "SphairaError_AppFailedMusicDownload";
|
||||
case Result_CurlFailedEasyInit: return "SphairaError_CurlFailedEasyInit";
|
||||
case Result_DumpFailedNetworkUpload: return "SphairaError_DumpFailedNetworkUpload";
|
||||
case Result_UnzOpen2_64: return "SphairaError_UnzOpen2_64";
|
||||
case Result_UnzGetGlobalInfo64: return "SphairaError_UnzGetGlobalInfo64";
|
||||
case Result_UnzLocateFile: return "SphairaError_UnzLocateFile";
|
||||
case Result_UnzGoToFirstFile: return "SphairaError_UnzGoToFirstFile";
|
||||
case Result_UnzGoToNextFile: return "SphairaError_UnzGoToNextFile";
|
||||
case Result_UnzOpenCurrentFile: return "SphairaError_UnzOpenCurrentFile";
|
||||
case Result_UnzGetCurrentFileInfo64: return "SphairaError_UnzGetCurrentFileInfo64";
|
||||
case Result_UnzReadCurrentFile: return "SphairaError_UnzReadCurrentFile";
|
||||
case Result_ZipOpen2_64: return "SphairaError_ZipOpen2_64";
|
||||
case Result_ZipOpenNewFileInZip: return "SphairaError_ZipOpenNewFileInZip";
|
||||
case Result_ZipWriteInFileInZip: return "SphairaError_ZipWriteInFileInZip";
|
||||
case Result_FileBrowserFailedUpload: return "SphairaError_FileBrowserFailedUpload";
|
||||
case Result_FileBrowserDirNotDaybreak: return "SphairaError_FileBrowserDirNotDaybreak";
|
||||
case Result_AppstoreFailedZipDownload: return "SphairaError_AppstoreFailedZipDownload";
|
||||
case Result_AppstoreFailedMd5: return "SphairaError_AppstoreFailedMd5";
|
||||
case Result_AppstoreFailedParseManifest: return "SphairaError_AppstoreFailedParseManifest";
|
||||
case Result_GameBadReadForDump: return "SphairaError_GameBadReadForDump";
|
||||
case Result_GameEmptyMetaEntries: return "SphairaError_GameEmptyMetaEntries";
|
||||
case Result_GameMultipleKeysFound: return "SphairaError_GameMultipleKeysFound";
|
||||
case Result_GameNoNspEntriesBuilt: return "SphairaError_GameNoNspEntriesBuilt";
|
||||
case Result_KeyMissingNcaKeyArea: return "SphairaError_KeyMissingNcaKeyArea";
|
||||
case Result_KeyMissingTitleKek: return "SphairaError_KeyMissingTitleKek";
|
||||
case Result_KeyMissingMasterKey: return "SphairaError_KeyMissingMasterKey";
|
||||
case Result_KeyFailedDecyptETicketDeviceKey: return "SphairaError_KeyFailedDecyptETicketDeviceKey";
|
||||
case Result_NcaFailedNcaHeaderHashVerify: return "SphairaError_NcaFailedNcaHeaderHashVerify";
|
||||
case Result_NcaBadSigKeyGen: return "SphairaError_NcaBadSigKeyGen";
|
||||
case Result_GcBadReadForDump: return "SphairaError_GcBadReadForDump";
|
||||
case Result_GcEmptyGamecard: return "SphairaError_GcEmptyGamecard";
|
||||
case Result_GcBadXciMagic: return "SphairaError_GcBadXciMagic";
|
||||
case Result_GcBadXciRomSize: return "SphairaError_GcBadXciRomSize";
|
||||
case Result_GcFailedToGetSecurityInfo: return "SphairaError_GcFailedToGetSecurityInfo";
|
||||
case Result_GhdlEmptyAsset: return "SphairaError_GhdlEmptyAsset";
|
||||
case Result_GhdlFailedToDownloadAsset: return "SphairaError_GhdlFailedToDownloadAsset";
|
||||
case Result_GhdlFailedToDownloadAssetJson: return "SphairaError_GhdlFailedToDownloadAssetJson";
|
||||
case Result_ThemezerFailedToDownloadThemeMeta: return "SphairaError_ThemezerFailedToDownloadThemeMeta";
|
||||
case Result_ThemezerFailedToDownloadTheme: return "SphairaError_ThemezerFailedToDownloadTheme";
|
||||
case Result_MainFailedToDownloadUpdate: return "SphairaError_MainFailedToDownloadUpdate";
|
||||
case Result_UsbDsBadDeviceSpeed: return "SphairaError_UsbDsBadDeviceSpeed";
|
||||
case Result_NspBadMagic: return "SphairaError_NspBadMagic";
|
||||
case Result_XciBadMagic: return "SphairaError_XciBadMagic";
|
||||
case Result_EsBadTitleKeyType: return "SphairaError_EsBadTitleKeyType";
|
||||
case Result_EsPersonalisedTicketDeviceIdMissmatch: return "SphairaError_EsPersonalisedTicketDeviceIdMissmatch";
|
||||
case Result_EsFailedDecryptPersonalisedTicket: return "SphairaError_EsFailedDecryptPersonalisedTicket";
|
||||
case Result_EsBadDecryptedPersonalisedTicketSize: return "SphairaError_EsBadDecryptedPersonalisedTicketSize";
|
||||
case Result_EsInvalidTicketBadRightsId: return "SphairaError_EsInvalidTicketBadRightsId";
|
||||
case Result_EsInvalidTicketFromatVersion: return "SphairaError_EsInvalidTicketFromatVersion";
|
||||
case Result_EsInvalidTicketKeyType: return "SphairaError_EsInvalidTicketKeyType";
|
||||
case Result_EsInvalidTicketKeyRevision: return "SphairaError_EsInvalidTicketKeyRevision";
|
||||
case Result_OwoBadArgs: return "SphairaError_OwoBadArgs";
|
||||
case Result_UsbCancelled: return "SphairaError_UsbCancelled";
|
||||
case Result_UsbBadMagic: return "SphairaError_UsbBadMagic";
|
||||
case Result_UsbBadVersion: return "SphairaError_UsbBadVersion";
|
||||
case Result_UsbBadCount: return "SphairaError_UsbBadCount";
|
||||
case Result_UsbBadTransferSize: return "SphairaError_UsbBadTransferSize";
|
||||
case Result_UsbBadTotalSize: return "SphairaError_UsbBadTotalSize";
|
||||
case Result_UsbUploadBadMagic: return "SphairaError_UsbUploadBadMagic";
|
||||
case Result_UsbUploadExit: return "SphairaError_UsbUploadExit";
|
||||
case Result_UsbUploadBadCount: return "SphairaError_UsbUploadBadCount";
|
||||
case Result_UsbUploadBadTransferSize: return "SphairaError_UsbUploadBadTransferSize";
|
||||
case Result_UsbUploadBadTotalSize: return "SphairaError_UsbUploadBadTotalSize";
|
||||
case Result_UsbUploadBadCommand: return "SphairaError_UsbUploadBadCommand";
|
||||
case Result_YatiContainerNotFound: return "SphairaError_YatiContainerNotFound";
|
||||
case Result_YatiNcaNotFound: return "SphairaError_YatiNcaNotFound";
|
||||
case Result_YatiInvalidNcaReadSize: return "SphairaError_YatiInvalidNcaReadSize";
|
||||
case Result_YatiInvalidNcaSigKeyGen: return "SphairaError_YatiInvalidNcaSigKeyGen";
|
||||
case Result_YatiInvalidNcaMagic: return "SphairaError_YatiInvalidNcaMagic";
|
||||
case Result_YatiInvalidNcaSignature0: return "SphairaError_YatiInvalidNcaSignature0";
|
||||
case Result_YatiInvalidNcaSignature1: return "SphairaError_YatiInvalidNcaSignature1";
|
||||
case Result_YatiInvalidNcaSha256: return "SphairaError_YatiInvalidNcaSha256";
|
||||
case Result_YatiNczSectionNotFound: return "SphairaError_YatiNczSectionNotFound";
|
||||
case Result_YatiInvalidNczSectionCount: return "SphairaError_YatiInvalidNczSectionCount";
|
||||
case Result_YatiNczBlockNotFound: return "SphairaError_YatiNczBlockNotFound";
|
||||
case Result_YatiInvalidNczBlockVersion: return "SphairaError_YatiInvalidNczBlockVersion";
|
||||
case Result_YatiInvalidNczBlockType: return "SphairaError_YatiInvalidNczBlockType";
|
||||
case Result_YatiInvalidNczBlockTotal: return "SphairaError_YatiInvalidNczBlockTotal";
|
||||
case Result_YatiInvalidNczBlockSizeExponent: return "SphairaError_YatiInvalidNczBlockSizeExponent";
|
||||
case Result_YatiInvalidNczZstdError: return "SphairaError_YatiInvalidNczZstdError";
|
||||
case Result_YatiTicketNotFound: return "SphairaError_YatiTicketNotFound";
|
||||
case Result_YatiInvalidTicketBadRightsId: return "SphairaError_YatiInvalidTicketBadRightsId";
|
||||
case Result_YatiCertNotFound: return "SphairaError_YatiCertNotFound";
|
||||
case Result_YatiNcmDbCorruptHeader: return "SphairaError_YatiNcmDbCorruptHeader";
|
||||
case Result_YatiNcmDbCorruptInfos: return "SphairaError_YatiNcmDbCorruptInfos";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ErrorBox::ErrorBox(const std::string& message) : m_message{message} {
|
||||
log_write("[ERROR] %s\n", m_message.c_str());
|
||||
|
||||
m_pos.w = 770.f;
|
||||
m_pos.h = 430.f;
|
||||
@@ -21,6 +142,8 @@ ErrorBox::ErrorBox(const std::string& message) : m_message{message} {
|
||||
|
||||
ErrorBox::ErrorBox(Result code, const std::string& message) : ErrorBox{message} {
|
||||
m_code = code;
|
||||
m_code_message = GetCodeMessage(code);
|
||||
log_write("[ERROR] Code: 0x%X Module: %u Description: %u\n", R_VALUE(code), R_MODULE(code), R_DESCRIPTION(code));
|
||||
}
|
||||
|
||||
auto ErrorBox::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||
@@ -37,7 +160,11 @@ auto ErrorBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
gfx::drawTextArgs(vg, center_x, 180, 63, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_ERROR), "\uE140");
|
||||
if (m_code.has_value()) {
|
||||
const auto code = m_code.value();
|
||||
gfx::drawTextArgs(vg, center_x, 270, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Code: 0x%X Module: %u Description: 0x%X", R_VALUE(code), R_MODULE(code), R_DESCRIPTION(code));
|
||||
if (m_code_message.empty()) {
|
||||
gfx::drawTextArgs(vg, center_x, 270, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Code: 0x%X Module: %u Description: 0x%X", R_VALUE(code), R_MODULE(code), R_DESCRIPTION(code));
|
||||
} else {
|
||||
gfx::drawTextArgs(vg, center_x, 270, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", m_code_message.c_str());
|
||||
}
|
||||
} else {
|
||||
gfx::drawTextArgs(vg, center_x, 270, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "An error occurred"_i18n.c_str());
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "ui/menus/appstore.hpp"
|
||||
#include "ui/menus/homebrew.hpp"
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/popup_list.hpp"
|
||||
#include "ui/progress_box.hpp"
|
||||
@@ -14,7 +15,10 @@
|
||||
#include "swkbd.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "hasher.hpp"
|
||||
#include "threaded_file_transfer.hpp"
|
||||
#include "nro.hpp"
|
||||
#include "web.hpp"
|
||||
#include "minizip_helper.hpp"
|
||||
|
||||
#include <minIni.h>
|
||||
#include <string>
|
||||
@@ -87,6 +91,12 @@ auto BuildBannerUrl(const Entry& e) -> std::string {
|
||||
return out;
|
||||
}
|
||||
|
||||
auto BuildManifestUrl(const Entry& e) -> std::string {
|
||||
char out[0x100];
|
||||
std::snprintf(out, sizeof(out), "%s/packages/%s/manifest.install", URL_BASE, e.name.c_str());
|
||||
return out;
|
||||
}
|
||||
|
||||
auto BuildZipUrl(const Entry& e) -> std::string {
|
||||
char out[0x100];
|
||||
std::snprintf(out, sizeof(out), "%s/zips/%s.zip", URL_BASE, e.name.c_str());
|
||||
@@ -320,8 +330,9 @@ auto UninstallApp(ProgressBox* pbox, const Entry& entry) -> Result {
|
||||
fs::FsNativeSd fs;
|
||||
|
||||
if (manifest.empty()) {
|
||||
R_UNLESS(!entry.binary.empty(), 0x1);
|
||||
fs.DeleteFile(entry.binary);
|
||||
if (!entry.binary.empty()) {
|
||||
R_TRY(fs.DeleteFile(entry.binary));
|
||||
}
|
||||
} else {
|
||||
for (auto& e : manifest) {
|
||||
pbox->NewTransfer(e.path);
|
||||
@@ -332,6 +343,7 @@ auto UninstallApp(ProgressBox* pbox, const Entry& entry) -> Result {
|
||||
log_write("failed to delete file: %s\n", safe_buf.s);
|
||||
} else {
|
||||
log_write("deleted file: %s\n", safe_buf.s);
|
||||
svcSleepThread(1e+5);
|
||||
// todo: delete empty directories!
|
||||
// fs::delete_directory(safe_buf);
|
||||
}
|
||||
@@ -363,19 +375,30 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> Result {
|
||||
fs::FsNativeSd fs;
|
||||
R_TRY(fs.GetFsOpenResult());
|
||||
|
||||
// check if we can download the entire zip to mem for faster download / extract times.
|
||||
// current limit is 300MiB, or disabled for applet mode.
|
||||
const auto file_download = App::IsApplet() || entry.filesize >= 1024 * 1024 * 300;
|
||||
curl::ApiResult api_result{};
|
||||
|
||||
// 1. download the zip
|
||||
if (!pbox->ShouldExit()) {
|
||||
pbox->NewTransfer("Downloading "_i18n + entry.title);
|
||||
log_write("starting download\n");
|
||||
|
||||
const auto url = BuildZipUrl(entry);
|
||||
const auto result = curl::Api().ToFile(
|
||||
curl::Api api{
|
||||
curl::Url{url},
|
||||
curl::Path{zip_out},
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||
);
|
||||
};
|
||||
|
||||
R_UNLESS(result.success, 0x1);
|
||||
if (file_download) {
|
||||
api.SetOption(curl::Path{zip_out});
|
||||
api_result = curl::ToFile(api);
|
||||
} else {
|
||||
api_result = curl::ToMemory(api);
|
||||
}
|
||||
|
||||
R_UNLESS(api_result.success, Result_AppstoreFailedZipDownload);
|
||||
}
|
||||
|
||||
ON_SCOPE_EXIT(fs.DeleteFile(zip_out));
|
||||
@@ -386,24 +409,36 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> Result {
|
||||
log_write("starting md5 check\n");
|
||||
|
||||
std::string hash_out;
|
||||
R_TRY(hash::Hash(pbox, hash::Type::Md5, &fs, zip_out, hash_out));
|
||||
if (file_download) {
|
||||
R_TRY(hash::Hash(pbox, hash::Type::Md5, &fs, zip_out, hash_out));
|
||||
} else {
|
||||
R_TRY(hash::Hash(pbox, hash::Type::Md5, api_result.data, hash_out));
|
||||
}
|
||||
|
||||
if (strncasecmp(hash_out.data(), entry.md5.data(), entry.md5.length())) {
|
||||
log_write("bad md5: %.*s vs %.*s\n", 32, hash_out.data(), 32, entry.md5.c_str());
|
||||
R_THROW(0x1);
|
||||
R_THROW(Result_AppstoreFailedMd5);
|
||||
}
|
||||
}
|
||||
|
||||
mz::MzSpan mz_span{api_result.data};
|
||||
zlib_filefunc64_def file_func;
|
||||
if (!file_download) {
|
||||
mz::FileFuncSpan(&mz_span, &file_func);
|
||||
} else {
|
||||
mz::FileFuncStdio(&file_func);
|
||||
}
|
||||
|
||||
// 3. extract the zip
|
||||
if (!pbox->ShouldExit()) {
|
||||
auto zfile = unzOpen64(zip_out);
|
||||
R_UNLESS(zfile, 0x1);
|
||||
auto zfile = unzOpen2_64(zip_out, &file_func);
|
||||
R_UNLESS(zfile, Result_UnzOpen2_64);
|
||||
ON_SCOPE_EXIT(unzClose(zfile));
|
||||
|
||||
// get manifest
|
||||
if (UNZ_END_OF_LIST_OF_FILE == unzLocateFile(zfile, "manifest.install", 0)) {
|
||||
log_write("failed to find manifest.install\n");
|
||||
R_THROW(0x1);
|
||||
R_THROW(Result_UnzLocateFile);
|
||||
}
|
||||
|
||||
ManifestEntries new_manifest;
|
||||
@@ -411,159 +446,89 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> Result {
|
||||
{
|
||||
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
|
||||
log_write("failed to open current file\n");
|
||||
R_THROW(0x1);
|
||||
R_THROW(Result_UnzOpenCurrentFile);
|
||||
}
|
||||
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
|
||||
|
||||
unz_file_info64 info;
|
||||
if (UNZ_OK != unzGetCurrentFileInfo64(zfile, &info, 0, 0, 0, 0, 0, 0)) {
|
||||
log_write("failed to get current info\n");
|
||||
R_THROW(0x1);
|
||||
R_THROW(Result_UnzGetGlobalInfo64);
|
||||
}
|
||||
|
||||
std::vector<char> manifest_data(info.uncompressed_size);
|
||||
if ((int)info.uncompressed_size != unzReadCurrentFile(zfile, manifest_data.data(), manifest_data.size())) {
|
||||
log_write("failed to read manifest file\n");
|
||||
R_THROW(0x1);
|
||||
R_THROW(Result_UnzReadCurrentFile);
|
||||
}
|
||||
|
||||
new_manifest = ParseManifest(manifest_data);
|
||||
if (new_manifest.empty()) {
|
||||
log_write("manifest is empty!\n");
|
||||
R_THROW(0x1);
|
||||
R_THROW(Result_AppstoreFailedParseManifest);
|
||||
}
|
||||
}
|
||||
|
||||
const auto unzip_to_file = [&](const unz_file_info64& info, const fs::FsPath& inzip, fs::FsPath output) -> Result {
|
||||
if (output[0] != '/') {
|
||||
output = fs::AppendPath("/", output);
|
||||
}
|
||||
|
||||
// create directories
|
||||
fs.CreateDirectoryRecursivelyWithPath(output);
|
||||
|
||||
Result rc;
|
||||
if (R_FAILED(rc = fs.CreateFile(output, info.uncompressed_size, 0)) && rc != FsError_PathAlreadyExists) {
|
||||
log_write("failed to create file: %s 0x%04X\n", output.s, rc);
|
||||
R_THROW(rc);
|
||||
}
|
||||
|
||||
fs::File f;
|
||||
R_TRY(fs.OpenFile(output, FsOpenMode_Write, &f));
|
||||
R_TRY(f.SetSize(info.uncompressed_size));
|
||||
|
||||
u64 offset{};
|
||||
while (offset < info.uncompressed_size) {
|
||||
R_TRY(pbox->ShouldExitResult());
|
||||
|
||||
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
|
||||
if (bytes_read <= 0) {
|
||||
log_write("failed to read zip file: %s\n", inzip.s);
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
R_TRY(f.Write(offset, buf.data(), bytes_read, FsWriteOption_None));
|
||||
|
||||
pbox->UpdateTransfer(offset, info.uncompressed_size);
|
||||
offset += bytes_read;
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
};
|
||||
|
||||
const auto unzip_to = [&](const fs::FsPath& inzip, const fs::FsPath& output) -> Result {
|
||||
pbox->NewTransfer(inzip);
|
||||
|
||||
if (UNZ_END_OF_LIST_OF_FILE == unzLocateFile(zfile, inzip, 0)) {
|
||||
log_write("failed to find %s\n", inzip.s);
|
||||
R_THROW(0x1);
|
||||
R_THROW(Result_UnzLocateFile);
|
||||
}
|
||||
|
||||
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
|
||||
log_write("failed to open current file\n");
|
||||
R_THROW(0x1);
|
||||
R_THROW(Result_UnzOpenCurrentFile);
|
||||
}
|
||||
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
|
||||
|
||||
unz_file_info64 info;
|
||||
if (UNZ_OK != unzGetCurrentFileInfo64(zfile, &info, 0, 0, 0, 0, 0, 0)) {
|
||||
log_write("failed to get current info\n");
|
||||
R_THROW(0x1);
|
||||
R_THROW(Result_UnzGetCurrentFileInfo64);
|
||||
}
|
||||
|
||||
return unzip_to_file(info, inzip, output);
|
||||
};
|
||||
|
||||
const auto unzip_all = [&](std::span<const ManifestEntry> entries) -> Result {
|
||||
unz_global_info64 ginfo;
|
||||
if (UNZ_OK != unzGetGlobalInfo64(zfile, &ginfo)) {
|
||||
R_THROW(0x1);
|
||||
auto path = output;
|
||||
if (path[0] != '/') {
|
||||
path = fs::AppendPath("/", path);
|
||||
}
|
||||
|
||||
if (UNZ_OK != unzGoToFirstFile(zfile)) {
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
for (s64 i = 0; i < ginfo.number_entry; i++) {
|
||||
R_TRY(pbox->ShouldExitResult());
|
||||
|
||||
if (i > 0) {
|
||||
if (UNZ_OK != unzGoToNextFile(zfile)) {
|
||||
log_write("failed to unzGoToNextFile\n");
|
||||
R_THROW(0x1);
|
||||
}
|
||||
}
|
||||
|
||||
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
|
||||
log_write("failed to open current file\n");
|
||||
R_THROW(0x1);
|
||||
}
|
||||
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");
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
const auto it = std::ranges::find_if(entries, [&name](auto& e){
|
||||
return !strcasecmp(name, e.path);
|
||||
});
|
||||
|
||||
if (it == entries.end()) [[unlikely]] {
|
||||
continue;
|
||||
}
|
||||
|
||||
pbox->NewTransfer(it->path);
|
||||
|
||||
switch (it->command) {
|
||||
case 'E': // both are the same?
|
||||
case 'U':
|
||||
break;
|
||||
|
||||
case 'G': { // checks if file exists, if not, extract
|
||||
if (fs.FileExists(fs::AppendPath("/", it->path))) {
|
||||
continue;
|
||||
}
|
||||
} break;
|
||||
|
||||
default:
|
||||
log_write("bad command: %c\n", it->command);
|
||||
continue;
|
||||
}
|
||||
|
||||
R_TRY(unzip_to_file(info, it->path, it->path));
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
return thread::TransferUnzip(pbox, zfile, &fs, path, info.uncompressed_size, info.crc);
|
||||
};
|
||||
|
||||
// unzip manifest, info and all entries.
|
||||
TimeStamp ts;
|
||||
#if 1
|
||||
R_TRY(unzip_to("info.json", BuildInfoCachePath(entry)));
|
||||
R_TRY(unzip_to("manifest.install", BuildManifestCachePath(entry)));
|
||||
R_TRY(unzip_all(new_manifest));
|
||||
#endif
|
||||
|
||||
R_TRY(thread::TransferUnzipAll(pbox, zfile, &fs, "/", [&](const fs::FsPath& name, fs::FsPath& path) -> bool {
|
||||
const auto it = std::ranges::find_if(new_manifest, [&name](auto& e){
|
||||
return !strcasecmp(name, e.path);
|
||||
});
|
||||
|
||||
if (it == new_manifest.end()) [[unlikely]] {
|
||||
return false;
|
||||
}
|
||||
|
||||
pbox->NewTransfer(it->path);
|
||||
|
||||
switch (it->command) {
|
||||
case 'E': // both are the same?
|
||||
case 'U':
|
||||
return true;
|
||||
|
||||
case 'G': // checks if file exists, if not, extract
|
||||
return !fs.FileExists(fs::AppendPath("/", it->path));
|
||||
|
||||
default:
|
||||
log_write("bad command: %c\n", it->command);
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
|
||||
log_write("\n\t[APPSTORE] finished extract new, time taken: %.2fs %zums\n\n", ts.GetSecondsD(), ts.GetMs());
|
||||
|
||||
// finally finally, remove files no longer in the manifest
|
||||
@@ -583,6 +548,7 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> Result {
|
||||
log_write("failed to delete: %s\n", safe_buf.s);
|
||||
} else {
|
||||
log_write("deleted file: %s\n", safe_buf.s);
|
||||
svcSleepThread(1e+5);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -621,12 +587,15 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::X, Action{"Options"_i18n, [this](){
|
||||
auto sidebar = std::make_shared<Sidebar>("Options"_i18n, Sidebar::Side::RIGHT);
|
||||
sidebar->Add(std::make_shared<SidebarEntryCallback>("More by Author"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Options"_i18n, Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("More by Author"_i18n, [this](){
|
||||
m_menu.SetAuthor();
|
||||
SetPop();
|
||||
}, true));
|
||||
sidebar->Add(std::make_shared<SidebarEntryCallback>("Leave Feedback"_i18n, [this](){
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Leave Feedback"_i18n, [this](){
|
||||
std::string out;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out)) && !out.empty()) {
|
||||
const auto post = "name=" "switch_user" "&package=" + m_entry.name + "&message=" + out;
|
||||
@@ -647,10 +616,44 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
|
||||
});
|
||||
}
|
||||
}, true));
|
||||
App::Push(sidebar);
|
||||
|
||||
if (App::IsApplication() && !m_entry.url.empty()) {
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Visit Website"_i18n, [this](){
|
||||
WebShow(m_entry.url);
|
||||
}));
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}}),
|
||||
std::make_pair(Button::L2, Action{"Files"_i18n, [this](){
|
||||
m_show_file_list ^= 1;
|
||||
|
||||
if (m_show_file_list && !m_manifest_list && m_file_list_state == ImageDownloadState::None) {
|
||||
m_file_list_state = ImageDownloadState::Progress;
|
||||
const auto path = BuildManifestCachePath(m_entry);
|
||||
std::vector<u8> data;
|
||||
|
||||
if (R_SUCCEEDED(fs::read_entire_file(path, data))) {
|
||||
m_file_list_state = ImageDownloadState::Done;
|
||||
data.push_back('\0');
|
||||
m_manifest_list = std::make_unique<ScrollableText>((const char*)data.data(), 0, 374, 250, 768, 18);
|
||||
} else {
|
||||
curl::Api().ToMemoryAsync(
|
||||
curl::Url{BuildManifestUrl(m_entry)},
|
||||
curl::StopToken{this->GetToken()},
|
||||
curl::OnComplete{[this](auto& result){
|
||||
if (result.success) {
|
||||
m_file_list_state = ImageDownloadState::Done;
|
||||
result.data.push_back('\0');
|
||||
m_manifest_list = std::make_unique<ScrollableText>((const char*)result.data.data(), 0, 374, 250, 768, 18);
|
||||
} else {
|
||||
m_file_list_state = ImageDownloadState::Failed;
|
||||
}
|
||||
}}
|
||||
);
|
||||
}
|
||||
}
|
||||
}})
|
||||
);
|
||||
|
||||
@@ -699,7 +702,13 @@ EntryMenu::~EntryMenu() {
|
||||
void EntryMenu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
|
||||
m_detail_changelog->Update(controller, touch);
|
||||
if (m_show_file_list) {
|
||||
if (m_manifest_list) {
|
||||
m_manifest_list->Update(controller, touch);
|
||||
}
|
||||
} else {
|
||||
m_detail_changelog->Update(controller, touch);
|
||||
}
|
||||
}
|
||||
|
||||
void EntryMenu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
@@ -754,12 +763,24 @@ void EntryMenu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
y -= block.h + 18;
|
||||
}
|
||||
|
||||
m_detail_changelog->Draw(vg, theme);
|
||||
if (m_show_file_list) {
|
||||
if (m_manifest_list) {
|
||||
m_manifest_list->Draw(vg, theme);
|
||||
} else if (m_file_list_state == ImageDownloadState::Progress) {
|
||||
gfx::drawText(vg, 110, 374, 18, theme->GetColour(ThemeEntryID_TEXT), "Loading..."_i18n.c_str());
|
||||
} else if (m_file_list_state == ImageDownloadState::Failed) {
|
||||
gfx::drawText(vg, 110, 374, 18, theme->GetColour(ThemeEntryID_TEXT), "Failed to download manifest"_i18n.c_str());
|
||||
}
|
||||
} else {
|
||||
m_detail_changelog->Draw(vg, theme);
|
||||
}
|
||||
}
|
||||
|
||||
void EntryMenu::ShowChangelogAction() {
|
||||
std::function<void()> func = std::bind(&EntryMenu::ShowChangelogAction, this);
|
||||
m_show_changlog ^= 1;
|
||||
m_show_file_list = false;
|
||||
|
||||
|
||||
if (m_show_changlog) {
|
||||
SetAction(Button::L, Action{"Details"_i18n, func});
|
||||
@@ -779,6 +800,7 @@ void EntryMenu::UpdateOptions() {
|
||||
App::Push(std::make_shared<ProgressBox>(m_entry.image.image, "Downloading "_i18n, m_entry.title, [this](auto pbox){
|
||||
return InstallApp(pbox, m_entry);
|
||||
}, [this](Result rc){
|
||||
homebrew::SignalChange();
|
||||
App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
@@ -794,6 +816,7 @@ void EntryMenu::UpdateOptions() {
|
||||
App::Push(std::make_shared<ProgressBox>(m_entry.image.image, "Uninstalling "_i18n, m_entry.title, [this](auto pbox){
|
||||
return UninstallApp(pbox, m_entry);
|
||||
}, [this](Result rc){
|
||||
homebrew::SignalChange();
|
||||
App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
@@ -1147,8 +1170,8 @@ void Menu::SetIndex(s64 index) {
|
||||
}
|
||||
|
||||
void Menu::ScanHomebrew() {
|
||||
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
|
||||
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
|
||||
App::SetBoostMode(true);
|
||||
ON_SCOPE_EXIT(App::SetBoostMode(false));
|
||||
|
||||
from_json(REPO_PATH, m_entries);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "hasher.hpp"
|
||||
#include "location.hpp"
|
||||
#include "threaded_file_transfer.hpp"
|
||||
#include "minizip_helper.hpp"
|
||||
|
||||
#include "yati/yati.hpp"
|
||||
#include "yati/source/file.hpp"
|
||||
@@ -43,6 +44,8 @@
|
||||
namespace sphaira::ui::menu::filebrowser {
|
||||
namespace {
|
||||
|
||||
constinit UEvent g_change_uevent;
|
||||
|
||||
constexpr FsEntry FS_ENTRY_DEFAULT{
|
||||
"microSD card", "/", FsType::Sd, FsEntryFlag_Assoc,
|
||||
};
|
||||
@@ -288,6 +291,10 @@ auto GetRomIcon(fs::Fs* fs, ProgressBox* pbox, std::string filename, const RomDa
|
||||
|
||||
} // namespace
|
||||
|
||||
void SignalChange() {
|
||||
ueventSignal(&g_change_uevent);
|
||||
}
|
||||
|
||||
FsView::FsView(Menu* menu, const fs::FsPath& path, const FsEntry& entry, ViewSide side) : m_menu{menu}, m_side{side} {
|
||||
this->SetActions(
|
||||
std::make_pair(Button::L2, Action{[this](){
|
||||
@@ -435,7 +442,6 @@ FsView::~FsView() {
|
||||
}
|
||||
|
||||
void FsView::Update(Controller* controller, TouchInfo* touch) {
|
||||
Widget::Update(controller, touch);
|
||||
m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this](bool touch, auto i) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
@@ -561,7 +567,7 @@ void FsView::SetSide(ViewSide side) {
|
||||
if (m_menu->IsSplitScreen()) {
|
||||
if (m_side == ViewSide::Left) {
|
||||
this->SetW(pos.w / 2 - pos.x / 2);
|
||||
this->SetX(pos.x / 2);
|
||||
this->SetX(pos.x / 2 + 20.f);
|
||||
} else if (m_side == ViewSide::Right) {
|
||||
this->SetW(pos.w / 2 - pos.x / 2);
|
||||
this->SetX(pos.x / 2 + SCREEN_WIDTH / 2);
|
||||
@@ -571,7 +577,7 @@ void FsView::SetSide(ViewSide side) {
|
||||
v.w -= v.x / 2;
|
||||
|
||||
if (m_side == ViewSide::Left) {
|
||||
v.x = v.x / 2;
|
||||
v.x = v.x / 2 + 20.f;
|
||||
} else if (m_side == ViewSide::Right) {
|
||||
v.x = v.x / 2 + SCREEN_WIDTH / 2;
|
||||
}
|
||||
@@ -671,7 +677,19 @@ void FsView::InstallForwarder() {
|
||||
config.icon = GetRomIcon(m_fs.get(), pbox, file_name, db_indexs, nro);
|
||||
pbox->SetImageDataConst(config.icon);
|
||||
|
||||
if (!db_indexs.empty()) {
|
||||
fs::FsNativeSd().read_entire_file("/config/sphaira/logo/rom/NintendoLogo.png", config.logo);
|
||||
fs::FsNativeSd().read_entire_file("/config/sphaira/logo/rom/StartupMovie.gif", config.gif);
|
||||
}
|
||||
|
||||
return App::Install(pbox, config);
|
||||
}, [this](Result rc){
|
||||
App::PushErrorBox(rc, "Failed to install forwarder"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::PlaySoundEffect(SoundEffect_Install);
|
||||
App::Notify("Installed!"_i18n);
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
log_write("pressed B to skip launch...\n");
|
||||
@@ -694,6 +712,8 @@ void FsView::InstallFiles() {
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}, [this](Result rc){
|
||||
App::PushErrorBox(rc, "File install failed!"_i18n);
|
||||
}));
|
||||
}
|
||||
}));
|
||||
@@ -708,75 +728,12 @@ void FsView::UnzipFiles(fs::FsPath dir_path) {
|
||||
}
|
||||
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Extracting "_i18n, "", [this, dir_path, targets](auto pbox) -> Result {
|
||||
constexpr auto chunk_size = 1024 * 512; // 512KiB
|
||||
const auto is_hdd_fs = m_fs->Root().starts_with("ums");
|
||||
|
||||
for (auto& e : targets) {
|
||||
pbox->SetTitle(e.GetName());
|
||||
|
||||
const auto zip_out = GetNewPath(e);
|
||||
auto zfile = unzOpen64(zip_out);
|
||||
R_UNLESS(zfile, 0x1);
|
||||
ON_SCOPE_EXIT(unzClose(zfile));
|
||||
|
||||
unz_global_info64 pglobal_info;
|
||||
if (UNZ_OK != unzGetGlobalInfo64(zfile, &pglobal_info)) {
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
for (s64 i = 0; i < pglobal_info.number_entry; i++) {
|
||||
if (i > 0) {
|
||||
if (UNZ_OK != unzGoToNextFile(zfile)) {
|
||||
log_write("failed to unzGoToNextFile\n");
|
||||
R_THROW(0x1);
|
||||
}
|
||||
}
|
||||
|
||||
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
|
||||
log_write("failed to open current file\n");
|
||||
R_THROW(0x1);
|
||||
}
|
||||
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");
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
const auto file_path = fs::AppendPath(dir_path, name);
|
||||
pbox->NewTransfer(name);
|
||||
|
||||
// create directories
|
||||
m_fs->CreateDirectoryRecursivelyWithPath(file_path);
|
||||
|
||||
Result rc;
|
||||
if (R_FAILED(rc = m_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);
|
||||
R_THROW(rc);
|
||||
}
|
||||
|
||||
fs::File f;
|
||||
R_TRY(m_fs->OpenFile(file_path, FsOpenMode_Write, &f));
|
||||
R_TRY(f.SetSize(info.uncompressed_size));
|
||||
|
||||
std::vector<char> buf(chunk_size);
|
||||
s64 offset{};
|
||||
while (offset < info.uncompressed_size) {
|
||||
R_TRY(pbox->ShouldExitResult());
|
||||
|
||||
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
|
||||
if (bytes_read <= 0) {
|
||||
log_write("failed to read zip file: %s\n", name);
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
R_TRY(f.Write(offset, buf.data(), bytes_read, FsWriteOption_None));
|
||||
|
||||
pbox->UpdateTransfer(offset, info.uncompressed_size);
|
||||
offset += bytes_read;
|
||||
}
|
||||
}
|
||||
R_TRY(thread::TransferUnzipAll(pbox, zip_out, m_fs.get(), dir_path, nullptr, is_hdd_fs ? thread::Mode::SingleThreaded : thread::Mode::SingleThreadedIfSmaller));
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
@@ -784,7 +741,7 @@ void FsView::UnzipFiles(fs::FsPath dir_path) {
|
||||
App::PushErrorBox(rc, "Extract failed!"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Extract success!");
|
||||
App::Notify("Extract success!"_i18n);
|
||||
}
|
||||
|
||||
Scan(m_path);
|
||||
@@ -829,10 +786,9 @@ void FsView::ZipFiles(fs::FsPath zip_out) {
|
||||
}
|
||||
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Compressing "_i18n, "", [this, zip_out, targets](auto pbox) -> Result {
|
||||
constexpr auto chunk_size = 1024 * 512; // 512KiB
|
||||
|
||||
const auto t = std::time(NULL);
|
||||
const auto tm = std::localtime(&t);
|
||||
const auto is_hdd_fs = m_fs->Root().starts_with("ums");
|
||||
|
||||
// pre-calculate the time rather than calculate it in the loop.
|
||||
zip_fileinfo zip_info{};
|
||||
@@ -843,53 +799,36 @@ void FsView::ZipFiles(fs::FsPath zip_out) {
|
||||
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);
|
||||
R_UNLESS(zfile, 0x1);
|
||||
zlib_filefunc64_def file_func;
|
||||
mz::FileFuncStdio(&file_func);
|
||||
|
||||
auto zfile = zipOpen2_64(zip_out, APPEND_STATUS_CREATE, nullptr, &file_func);
|
||||
R_UNLESS(zfile, Result_ZipOpen2_64);
|
||||
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_VERSION_HASH));
|
||||
|
||||
const auto zip_add = [&](const fs::FsPath& file_path) -> Result {
|
||||
// the file name needs to be relative to the current directory.
|
||||
const char* file_name_in_zip = file_path.s + std::strlen(m_path);
|
||||
|
||||
// strip root path (/ or ums0:)
|
||||
if (!std::strncmp(file_name_in_zip, m_fs->Root(), std::strlen(m_fs->Root()))) {
|
||||
file_name_in_zip += std::strlen(m_fs->Root());
|
||||
}
|
||||
|
||||
// root paths are banned in zips, they will warn when extracting otherwise.
|
||||
if (file_name_in_zip[0] == '/') {
|
||||
while (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)) {
|
||||
R_THROW(0x1);
|
||||
if (ZIP_OK != zipOpenNewFileInZip(zfile, file_name_in_zip, &zip_info, NULL, 0, NULL, 0, NULL, Z_DEFLATED, Z_DEFAULT_COMPRESSION)) {
|
||||
log_write("failed to add zip for %s\n", file_path.s);
|
||||
R_THROW(Result_ZipOpenNewFileInZip);
|
||||
}
|
||||
ON_SCOPE_EXIT(zipCloseFileInZip(zfile));
|
||||
|
||||
fs::File f;
|
||||
R_TRY(m_fs->OpenFile(file_path, FsOpenMode_Read, &f));
|
||||
|
||||
s64 file_size;
|
||||
R_TRY(f.GetSize(&file_size));
|
||||
|
||||
std::vector<char> buf(chunk_size);
|
||||
s64 offset{};
|
||||
while (offset < file_size) {
|
||||
R_TRY(pbox->ShouldExitResult());
|
||||
|
||||
u64 bytes_read;
|
||||
R_TRY(f.Read(offset, buf.data(), buf.size(), FsReadOption_None, &bytes_read));
|
||||
|
||||
if (ZIP_OK != zipWriteInFileInZip(zfile, buf.data(), bytes_read)) {
|
||||
log_write("failed to write zip file: %s\n", file_path.s);
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
pbox->UpdateTransfer(offset, file_size);
|
||||
offset += bytes_read;
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
return thread::TransferZip(pbox, zfile, m_fs.get(), file_path, nullptr, is_hdd_fs ? thread::Mode::SingleThreaded : thread::Mode::SingleThreadedIfSmaller);
|
||||
};
|
||||
|
||||
for (auto& e : targets) {
|
||||
@@ -915,7 +854,7 @@ void FsView::ZipFiles(fs::FsPath zip_out) {
|
||||
App::PushErrorBox(rc, "Compress failed!"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Compress success!");
|
||||
App::Notify("Compress success!"_i18n);
|
||||
}
|
||||
|
||||
Scan(m_path);
|
||||
@@ -991,7 +930,7 @@ void FsView::UploadFiles() {
|
||||
}
|
||||
);
|
||||
|
||||
R_UNLESS(result.success, 0x1);
|
||||
R_UNLESS(result.success, Result_FileBrowserFailedUpload);
|
||||
R_SUCCEED();
|
||||
}
|
||||
);
|
||||
@@ -1032,8 +971,8 @@ void FsView::UploadFiles() {
|
||||
}
|
||||
|
||||
auto FsView::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
|
||||
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
|
||||
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
|
||||
App::SetBoostMode(true);
|
||||
ON_SCOPE_EXIT(App::SetBoostMode(false));
|
||||
|
||||
log_write("new scan path: %s\n", new_path.s);
|
||||
if (!is_walk_up && !m_path.empty() && !m_entries_current.empty()) {
|
||||
@@ -1153,13 +1092,17 @@ void FsView::Sort() {
|
||||
std::sort(m_entries_current.begin(), m_entries_current.end(), sorter);
|
||||
}
|
||||
|
||||
void FsView::SortAndFindLastFile() {
|
||||
void FsView::SortAndFindLastFile(bool scan) {
|
||||
std::optional<LastFile> last_file;
|
||||
if (!m_path.empty() && !m_entries_current.empty()) {
|
||||
last_file = LastFile(GetEntry().name, m_index, m_list->GetYoff(), m_entries_current.size());
|
||||
}
|
||||
|
||||
Sort();
|
||||
if (scan) {
|
||||
Scan(m_path);
|
||||
} else {
|
||||
Sort();
|
||||
}
|
||||
|
||||
if (last_file.has_value()) {
|
||||
SetIndexFromLastFile(*last_file);
|
||||
@@ -1240,7 +1183,7 @@ void FsView::OnDeleteCallback() {
|
||||
}
|
||||
}
|
||||
|
||||
return DeleteAllCollections(pbox, src_fs, selected, collections);
|
||||
return DeleteAllCollectionsWithSelected(pbox, src_fs, selected, collections);
|
||||
}, [this](Result rc){
|
||||
App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n);
|
||||
|
||||
@@ -1267,6 +1210,7 @@ void FsView::OnPasteCallback() {
|
||||
App::Push(std::make_shared<ProgressBox>(0, "Pasting"_i18n, "", [this](auto pbox) -> Result {
|
||||
auto& selected = m_menu->m_selected;
|
||||
auto src_fs = selected.m_view->GetFs();
|
||||
const auto is_same_fs = selected.SameFs(this);
|
||||
|
||||
if (selected.SameFs(this) && selected.m_type == SelectedType::Cut) {
|
||||
for (const auto& p : selected.m_files) {
|
||||
@@ -1331,7 +1275,7 @@ void FsView::OnPasteCallback() {
|
||||
} else {
|
||||
pbox->SetTitle(p.name);
|
||||
pbox->NewTransfer("Copying "_i18n + src_path);
|
||||
R_TRY(pbox->CopyFile(src_fs, m_fs.get(), src_path, dst_path));
|
||||
R_TRY(pbox->CopyFile(src_fs, m_fs.get(), src_path, dst_path, is_same_fs));
|
||||
R_TRY(on_paste_file(src_path, dst_path));
|
||||
}
|
||||
}
|
||||
@@ -1361,7 +1305,7 @@ void FsView::OnPasteCallback() {
|
||||
|
||||
pbox->SetTitle(p.name);
|
||||
pbox->NewTransfer("Copying "_i18n + src_path);
|
||||
R_TRY(pbox->CopyFile(src_fs, m_fs.get(), src_path, dst_path));
|
||||
R_TRY(pbox->CopyFile(src_fs, m_fs.get(), src_path, dst_path, is_same_fs));
|
||||
R_TRY(on_paste_file(src_path, dst_path));
|
||||
}
|
||||
}
|
||||
@@ -1373,7 +1317,7 @@ void FsView::OnPasteCallback() {
|
||||
// the folders cannot be deleted until the end as they have to be removed in
|
||||
// reverse order so that the folder can be deleted (it must be empty).
|
||||
if (selected.m_type == SelectedType::Cut) {
|
||||
R_TRY(DeleteAllCollections(pbox, src_fs, selected, collections, FsDirOpenMode_ReadDirs));
|
||||
R_TRY(DeleteAllCollectionsWithSelected(pbox, src_fs, selected, collections, FsDirOpenMode_ReadDirs));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1392,8 +1336,8 @@ void FsView::OnRenameCallback() {
|
||||
}
|
||||
|
||||
auto FsView::CheckIfUpdateFolder() -> Result {
|
||||
R_UNLESS(IsSd(), FsError_InvalidMountName);
|
||||
R_UNLESS(m_fs->IsNative(), 0x1);
|
||||
R_UNLESS(IsSd(), Result_FileBrowserDirNotDaybreak);
|
||||
R_UNLESS(m_fs->IsNative(), Result_FileBrowserDirNotDaybreak);
|
||||
|
||||
// check if we have already tried to find daybreak
|
||||
if (m_daybreak_path.has_value() && m_daybreak_path.value().empty()) {
|
||||
@@ -1417,16 +1361,16 @@ auto FsView::CheckIfUpdateFolder() -> Result {
|
||||
}
|
||||
|
||||
// check that we have enough ncas and not too many
|
||||
R_UNLESS(m_entries.size() > 150 && m_entries.size() < 300, 0x1);
|
||||
R_UNLESS(m_entries.size() > 150 && m_entries.size() < 300, Result_FileBrowserDirNotDaybreak);
|
||||
|
||||
// check that all entries end in .nca
|
||||
const auto nca_ext = std::string_view{".nca"};
|
||||
for (auto& e : m_entries) {
|
||||
// check that we are at the bottom level
|
||||
R_UNLESS(e.type == FsDirEntryType_File, 0x1);
|
||||
R_UNLESS(e.type == FsDirEntryType_File, Result_FileBrowserDirNotDaybreak);
|
||||
|
||||
const auto ext = std::strrchr(e.name, '.');
|
||||
R_UNLESS(ext && ext == nca_ext, 0x1);
|
||||
R_UNLESS(ext && ext == nca_ext, Result_FileBrowserDirNotDaybreak);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
@@ -1484,7 +1428,7 @@ auto FsView::get_collections(const fs::FsPath& path, const fs::FsPath& parent_na
|
||||
return get_collections(m_fs.get(), path, parent_name, out, inc_size);
|
||||
}
|
||||
|
||||
static Result DeleteAllCollections(ProgressBox* pbox, fs::Fs* fs, const SelectedStash& selected, const FsDirCollections& collections, u32 mode = FsDirOpenMode_ReadDirs|FsDirOpenMode_ReadFiles) {
|
||||
Result FsView::DeleteAllCollections(ProgressBox* pbox, fs::Fs* fs, const FsDirCollections& collections, u32 mode) {
|
||||
// delete everything in collections, reversed
|
||||
for (const auto& c : std::views::reverse(collections)) {
|
||||
const auto delete_func = [&](auto& array) -> Result {
|
||||
@@ -1498,9 +1442,11 @@ static Result DeleteAllCollections(ProgressBox* pbox, fs::Fs* fs, const Selected
|
||||
if ((mode & FsDirOpenMode_ReadDirs) && p.type == FsDirEntryType_Dir) {
|
||||
log_write("deleting dir: %s\n", full_path.s);
|
||||
R_TRY(fs->DeleteDirectory(full_path));
|
||||
svcSleepThread(1e+5);
|
||||
} else if ((mode & FsDirOpenMode_ReadFiles) && p.type == FsDirEntryType_File) {
|
||||
log_write("deleting file: %s\n", full_path.s);
|
||||
R_TRY(fs->DeleteFile(full_path));
|
||||
svcSleepThread(1e+5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1511,6 +1457,12 @@ static Result DeleteAllCollections(ProgressBox* pbox, fs::Fs* fs, const Selected
|
||||
R_TRY(delete_func(c.dirs));
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
static Result DeleteAllCollectionsWithSelected(ProgressBox* pbox, fs::Fs* fs, const SelectedStash& selected, const FsDirCollections& collections, u32 mode = FsDirOpenMode_ReadDirs|FsDirOpenMode_ReadFiles) {
|
||||
R_TRY(FsView::DeleteAllCollections(pbox, fs, collections, mode));
|
||||
|
||||
for (const auto& p : selected.m_files) {
|
||||
pbox->Yield();
|
||||
R_TRY(pbox->ShouldExitResult());
|
||||
@@ -1824,7 +1776,7 @@ void FsView::DisplayAdvancedOptions() {
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Mount"_i18n, mount_items, [this, fs_entries](s64& index_out){
|
||||
App::PopToMenu();
|
||||
SetFs(fs_entries[index_out].root, fs_entries[index_out]);
|
||||
}, m_fs_entry.name));
|
||||
}, i18n::get(m_fs_entry.name)));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Create File"_i18n, [this](){
|
||||
std::string out;
|
||||
@@ -1919,23 +1871,38 @@ Menu::Menu(u32 flags) : MenuBase{"FileBrowser"_i18n, flags} {
|
||||
}
|
||||
|
||||
view = view_left = std::make_shared<FsView>(this, ViewSide::Left);
|
||||
ueventCreate(&g_change_uevent, true);
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
if (R_SUCCEEDED(waitSingle(waiterForUEvent(&g_change_uevent), 0))) {
|
||||
if (IsSplitScreen()) {
|
||||
view_left->SortAndFindLastFile(true);
|
||||
view_right->SortAndFindLastFile(true);
|
||||
} else {
|
||||
view->SortAndFindLastFile(true);
|
||||
}
|
||||
}
|
||||
|
||||
// workaround the buttons not being display properly.
|
||||
// basically, inherit all actions from the view, draw them,
|
||||
// then restore state after.
|
||||
const auto view_actions = view->GetActions();
|
||||
m_actions.insert_range(view_actions);
|
||||
ON_SCOPE_EXIT(RemoveActions(view_actions));
|
||||
|
||||
MenuBase::Update(controller, touch);
|
||||
view->Update(controller, touch);
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
// workaround the buttons not being display properly.
|
||||
// basically, inherit all actions from the view, draw them,
|
||||
// then restore state after.
|
||||
const auto actions_copy = GetActions();
|
||||
ON_SCOPE_EXIT(m_actions = actions_copy);
|
||||
m_actions.insert_range(view->GetActions());
|
||||
// see Menu::Update().
|
||||
const auto view_actions = view->GetActions();
|
||||
m_actions.insert_range(view_actions);
|
||||
ON_SCOPE_EXIT(RemoveActions(view_actions));
|
||||
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
@@ -1944,9 +1911,9 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
view_right->Draw(vg, theme);
|
||||
|
||||
if (view == view_left) {
|
||||
gfx::drawRect(vg, view_right->GetPos(), nvgRGBA(0, 0, 0, 180), 5);
|
||||
gfx::drawRect(vg, view_right->GetPos(), theme->GetColour(ThemeEntryID_FOCUS), 5);
|
||||
} else {
|
||||
gfx::drawRect(vg, view_left->GetPos(), nvgRGBA(0, 0, 0, 180), 5);
|
||||
gfx::drawRect(vg, view_left->GetPos(), theme->GetColour(ThemeEntryID_FOCUS), 5);
|
||||
}
|
||||
|
||||
gfx::drawRect(vg, SCREEN_WIDTH/2, GetY(), 1, GetH(), theme->GetColour(ThemeEntryID_LINE));
|
||||
|
||||
@@ -1,166 +1,25 @@
|
||||
#include "ui/menus/ftp_menu.hpp"
|
||||
#include "yati/yati.hpp"
|
||||
#include "app.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "ftpsrv_helper.hpp"
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
|
||||
namespace sphaira::ui::menu::ftp {
|
||||
namespace {
|
||||
|
||||
constexpr u64 MAX_BUFFER_SIZE = 1024*1024*32;
|
||||
constexpr u64 SLEEPNS = 1000;
|
||||
volatile bool IN_PUSH_THREAD{};
|
||||
|
||||
bool OnInstallStart(void* user, const char* path) {
|
||||
auto menu = (Menu*)user;
|
||||
log_write("[INSTALL] inside OnInstallStart()\n");
|
||||
|
||||
for (;;) {
|
||||
mutexLock(&menu->m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex));
|
||||
|
||||
if (menu->m_state != State::Progress) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (menu->GetToken().stop_requested()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
svcSleepThread(1e+6);
|
||||
}
|
||||
|
||||
log_write("[INSTALL] OnInstallStart() got state: %u\n", (u8)menu->m_state);
|
||||
|
||||
if (menu->m_source) {
|
||||
log_write("[INSTALL] OnInstallStart() we have source\n");
|
||||
for (;;) {
|
||||
mutexLock(&menu->m_source->m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&menu->m_source->m_mutex));
|
||||
|
||||
if (!IN_PUSH_THREAD) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (menu->GetToken().stop_requested()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
svcSleepThread(1e+6);
|
||||
}
|
||||
|
||||
log_write("[INSTALL] OnInstallStart() stopped polling source\n");
|
||||
}
|
||||
|
||||
log_write("[INSTALL] OnInstallStart() doing make_shared\n");
|
||||
menu->m_source = std::make_shared<StreamFtp>(path, menu->GetToken());
|
||||
|
||||
mutexLock(&menu->m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex));
|
||||
menu->m_state = State::Connected;
|
||||
log_write("[INSTALL] OnInstallStart() done make shared\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OnInstallWrite(void* user, const void* buf, size_t size) {
|
||||
auto menu = (Menu*)user;
|
||||
|
||||
return menu->m_source->Push(buf, size);
|
||||
}
|
||||
|
||||
void OnInstallClose(void* user) {
|
||||
auto menu = (Menu*)user;
|
||||
menu->m_source->Disable();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
StreamFtp::StreamFtp(const fs::FsPath& path, std::stop_token token) {
|
||||
m_path = path;
|
||||
m_token = token;
|
||||
m_buffer.reserve(MAX_BUFFER_SIZE);
|
||||
m_active = true;
|
||||
}
|
||||
|
||||
Result StreamFtp::ReadChunk(void* buf, s64 size, u64* bytes_read) {
|
||||
while (!m_token.stop_requested()) {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
if (m_buffer.empty()) {
|
||||
if (!m_active) {
|
||||
break;
|
||||
}
|
||||
|
||||
svcSleepThread(SLEEPNS);
|
||||
} else {
|
||||
size = std::min<s64>(size, m_buffer.size());
|
||||
std::memcpy(buf, m_buffer.data(), size);
|
||||
m_buffer.erase(m_buffer.begin(), m_buffer.begin() + size);
|
||||
*bytes_read = size;
|
||||
R_SUCCEED();
|
||||
}
|
||||
}
|
||||
|
||||
return 0x1;
|
||||
}
|
||||
|
||||
bool StreamFtp::Push(const void* buf, s64 size) {
|
||||
IN_PUSH_THREAD = true;
|
||||
ON_SCOPE_EXIT(IN_PUSH_THREAD = false);
|
||||
|
||||
while (!m_token.stop_requested()) {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
if (!m_active) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (m_buffer.size() + size >= MAX_BUFFER_SIZE) {
|
||||
svcSleepThread(SLEEPNS);
|
||||
} else {
|
||||
const auto offset = m_buffer.size();
|
||||
m_buffer.resize(offset + size);
|
||||
std::memcpy(m_buffer.data() + offset, buf, size);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void StreamFtp::Disable() {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
m_active = false;
|
||||
}
|
||||
|
||||
Menu::Menu(u32 flags) : MenuBase{"FTP Install (EXPERIMENTAL)"_i18n, flags} {
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}});
|
||||
|
||||
SetAction(Button::X, Action{"Options"_i18n, [this](){
|
||||
App::DisplayInstallOptions(false);
|
||||
}});
|
||||
|
||||
App::SetAutoSleepDisabled(true);
|
||||
|
||||
mutexInit(&m_mutex);
|
||||
Menu::Menu(u32 flags) : stream::Menu{"FTP Install"_i18n, flags} {
|
||||
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);
|
||||
ftpsrv::InitInstallMode(
|
||||
[this](const char* path){ return OnInstallStart(path); },
|
||||
[this](const void *buf, size_t size){ return OnInstallWrite(buf, size); },
|
||||
[this](){ return OnInstallClose(); }
|
||||
);
|
||||
|
||||
m_port = ftpsrv::GetPort();
|
||||
m_anon = ftpsrv::IsAnon();
|
||||
@@ -173,71 +32,19 @@ Menu::Menu(u32 flags) : MenuBase{"FTP Install (EXPERIMENTAL)"_i18n, flags} {
|
||||
Menu::~Menu() {
|
||||
// signal for thread to exit and wait.
|
||||
ftpsrv::DisableInstallMode();
|
||||
m_stop_source.request_stop();
|
||||
|
||||
if (m_source) {
|
||||
m_source->Disable();
|
||||
}
|
||||
|
||||
if (!m_was_ftp_enabled) {
|
||||
log_write("[FTP] disabling on exit\n");
|
||||
App::SetFtpEnable(false);
|
||||
}
|
||||
|
||||
App::SetAutoSleepDisabled(false);
|
||||
log_write("closing data!!!!\n");
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
switch (m_state) {
|
||||
case State::None:
|
||||
break;
|
||||
|
||||
case State::Connected:
|
||||
log_write("set to progress\n");
|
||||
m_state = State::Progress;
|
||||
log_write("got connection\n");
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) -> Result {
|
||||
log_write("inside progress box\n");
|
||||
const auto rc = yati::InstallFromSource(pbox, m_source, m_source->m_path);
|
||||
if (R_FAILED(rc)) {
|
||||
m_source->Disable();
|
||||
R_THROW(rc);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}, [this](Result rc){
|
||||
App::PushErrorBox(rc, "Ftp install failed!"_i18n);
|
||||
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Ftp install success!"_i18n);
|
||||
m_state = State::Done;
|
||||
} else {
|
||||
m_state = State::Failed;
|
||||
}
|
||||
}));
|
||||
break;
|
||||
|
||||
case State::Progress:
|
||||
case State::Done:
|
||||
case State::Failed:
|
||||
break;
|
||||
}
|
||||
stream::Menu::Update(controller, touch);
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
stream::Menu::Draw(vg, theme);
|
||||
|
||||
const auto pdata = GetPolledData();
|
||||
if (pdata.ip) {
|
||||
@@ -257,6 +64,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
float bounds[4];
|
||||
|
||||
nvgFontSize(vg, font_size);
|
||||
nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
||||
|
||||
// note: textbounds strips spaces...todo: use nvgTextGlyphPositions() instead.
|
||||
#define draw(key, ...) \
|
||||
@@ -286,31 +94,10 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
}
|
||||
|
||||
#undef draw
|
||||
|
||||
switch (m_state) {
|
||||
case State::None:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Connected:
|
||||
break;
|
||||
|
||||
case State::Progress:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Transferring data..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Done:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Press B to exit..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Failed:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to install via FTP, press B to exit..."_i18n.c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::OnFocusGained() {
|
||||
MenuBase::OnFocusGained();
|
||||
void Menu::OnDisableInstallMode() {
|
||||
ftpsrv::DisableInstallMode();
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::ftp
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "dumper.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "image.hpp"
|
||||
#include "swkbd.hpp"
|
||||
|
||||
#include "ui/menus/game_menu.hpp"
|
||||
#include "ui/sidebar.hpp"
|
||||
@@ -23,6 +25,7 @@
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
#include <minIni.h>
|
||||
#include <nxtc.h>
|
||||
|
||||
namespace sphaira::ui::menu::game {
|
||||
namespace {
|
||||
@@ -30,6 +33,34 @@ namespace {
|
||||
constexpr int THREAD_PRIO = PRIO_PREEMPTIVE;
|
||||
constexpr int THREAD_CORE = 1;
|
||||
|
||||
// taken from nxtc
|
||||
constexpr u8 g_nacpLangTable[SetLanguage_Total] = {
|
||||
[SetLanguage_JA] = 2,
|
||||
[SetLanguage_ENUS] = 0,
|
||||
[SetLanguage_FR] = 3,
|
||||
[SetLanguage_DE] = 4,
|
||||
[SetLanguage_IT] = 7,
|
||||
[SetLanguage_ES] = 6,
|
||||
[SetLanguage_ZHCN] = 14,
|
||||
[SetLanguage_KO] = 12,
|
||||
[SetLanguage_NL] = 8,
|
||||
[SetLanguage_PT] = 10,
|
||||
[SetLanguage_RU] = 11,
|
||||
[SetLanguage_ZHTW] = 13,
|
||||
[SetLanguage_ENGB] = 1,
|
||||
[SetLanguage_FRCA] = 9,
|
||||
[SetLanguage_ES419] = 5,
|
||||
[SetLanguage_ZHHANS] = 14,
|
||||
[SetLanguage_ZHHANT] = 13,
|
||||
[SetLanguage_PTBR] = 15
|
||||
};
|
||||
|
||||
auto GetNacpLangEntryIndex() -> u8 {
|
||||
SetLanguage lang{SetLanguage_ENUS};
|
||||
nxtcGetCacheLanguage(&lang);
|
||||
return g_nacpLangTable[lang];
|
||||
}
|
||||
|
||||
constexpr u32 ContentMetaTypeToContentFlag(u8 meta_type) {
|
||||
if (meta_type & 0x80) {
|
||||
return 1 << (meta_type - 0x80);
|
||||
@@ -108,11 +139,12 @@ using MetaEntries = std::vector<NsApplicationContentMetaStatus>;
|
||||
struct ContentInfoEntry {
|
||||
NsApplicationContentMetaStatus status{};
|
||||
std::vector<NcmContentInfo> content_infos{};
|
||||
std::vector<FsRightsId> rights_ids{};
|
||||
std::vector<NcmRightsId> ncm_rights_id{};
|
||||
};
|
||||
|
||||
struct TikEntry {
|
||||
FsRightsId id{};
|
||||
u8 key_gen{};
|
||||
std::vector<u8> tik_data{};
|
||||
std::vector<u8> cert_data{};
|
||||
};
|
||||
@@ -162,7 +194,7 @@ struct NspEntry {
|
||||
const auto it = std::ranges::find_if(tickets, [&id](auto& e){
|
||||
return !std::memcmp(&id, &e.id, sizeof(id));
|
||||
});
|
||||
R_UNLESS(it != tickets.end(), 0x1);
|
||||
R_UNLESS(it != tickets.end(), Result_GameBadReadForDump);
|
||||
|
||||
const auto& data = collection.name.ends_with(".tik") ? it->tik_data : it->cert_data;
|
||||
std::memcpy(buf, data.data() + off, size);
|
||||
@@ -194,7 +226,7 @@ struct NspSource final : dump::BaseSource {
|
||||
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
|
||||
return path.find(e.path.s) != path.npos;
|
||||
});
|
||||
R_UNLESS(it != m_entries.end(), 0x1);
|
||||
R_UNLESS(it != m_entries.end(), Result_GameBadReadForDump);
|
||||
|
||||
const auto rc = it->Read(buf, off, size, bytes_read);
|
||||
if (m_is_file_based_emummc) {
|
||||
@@ -284,31 +316,27 @@ void FakeNacpEntry(ThreadResultData& e) {
|
||||
// fake the nacp entry
|
||||
std::strcpy(e.lang.name, "Corrupted");
|
||||
std::strcpy(e.lang.author, "Corrupted");
|
||||
std::strcpy(e.display_version, "0.0.0");
|
||||
e.control.reset();
|
||||
}
|
||||
|
||||
bool LoadControlImage(Entry& e) {
|
||||
if (!e.image && e.control) {
|
||||
ON_SCOPE_EXIT(e.control.reset());
|
||||
|
||||
TimeStamp ts;
|
||||
const auto jpeg_size = e.control_size - sizeof(NacpStruct);
|
||||
e.image = nvgCreateImageMem(App::GetVg(), 0, e.control->icon, jpeg_size);
|
||||
e.control.reset();
|
||||
log_write("\t\t[image load] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
|
||||
return true;
|
||||
const auto image = ImageLoadFromMemory({e.control->icon, e.jpeg_size}, ImageFlag_JPEG);
|
||||
if (!image.data.empty()) {
|
||||
e.image = nvgCreateImageRGBA(App::GetVg(), image.w, image.h, 0, image.data.data());
|
||||
log_write("\t[image load] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Result LoadControlManual(u64 id, ThreadResultData& data) {
|
||||
TimeStamp ts;
|
||||
|
||||
MetaEntries entries;
|
||||
R_TRY(GetMetaEntries(id, entries));
|
||||
R_UNLESS(!entries.empty(), 0x1);
|
||||
|
||||
const auto& ee = entries.back();
|
||||
Result GetControlPathFromStatus(const NsApplicationContentMetaStatus& status, u64* out_program_id, fs::FsPath* out_path) {
|
||||
const auto& ee = status;
|
||||
if (ee.storageID != NcmStorageId_SdCard && ee.storageID != NcmStorageId_BuiltInUser && ee.storageID != NcmStorageId_GameCard) {
|
||||
return 0x1;
|
||||
}
|
||||
@@ -322,17 +350,28 @@ Result LoadControlManual(u64 id, ThreadResultData& data) {
|
||||
NcmContentId content_id;
|
||||
R_TRY(ncmContentMetaDatabaseGetContentIdByType(&db, &content_id, &key, NcmContentType_Control));
|
||||
|
||||
u64 program_id;
|
||||
R_TRY(ncmContentStorageGetProgramId(&cs, &program_id, &content_id, FsContentAttributes_All));
|
||||
R_TRY(ncmContentStorageGetProgramId(&cs, out_program_id, &content_id, FsContentAttributes_All));
|
||||
|
||||
R_TRY(ncmContentStorageGetPath(&cs, out_path->s, sizeof(*out_path), &content_id));
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result LoadControlManual(u64 id, ThreadResultData& data) {
|
||||
TimeStamp ts;
|
||||
|
||||
MetaEntries entries;
|
||||
R_TRY(GetMetaEntries(id, entries));
|
||||
R_UNLESS(!entries.empty(), Result_GameEmptyMetaEntries);
|
||||
|
||||
u64 program_id;
|
||||
fs::FsPath path;
|
||||
R_TRY(ncmContentStorageGetPath(&cs, path, sizeof(path), &content_id));
|
||||
R_TRY(GetControlPathFromStatus(entries.back(), &program_id, &path));
|
||||
|
||||
std::vector<u8> icon;
|
||||
R_TRY(nca::ParseControl(path, program_id, &data.control->nacp, sizeof(data.control->nacp), &icon));
|
||||
R_TRY(nca::ParseControl(path, program_id, &data.control->nacp.lang[GetNacpLangEntryIndex()], sizeof(NacpLanguageEntry), &icon));
|
||||
std::memcpy(data.control->icon, icon.data(), icon.size());
|
||||
|
||||
data.control_size = sizeof(data.control->nacp) + icon.size();
|
||||
data.jpeg_size = icon.size();
|
||||
log_write("\t\t[manual control] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
|
||||
|
||||
R_SUCCEED();
|
||||
@@ -347,8 +386,10 @@ auto LoadControlEntry(u64 id) -> ThreadResultData {
|
||||
bool manual_load = true;
|
||||
if (hosversionBefore(20,0,0)) {
|
||||
TimeStamp ts;
|
||||
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, id, data.control.get(), sizeof(NsApplicationControlData), &data.control_size))) {
|
||||
u64 actual_size;
|
||||
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, id, data.control.get(), sizeof(NsApplicationControlData), &actual_size))) {
|
||||
manual_load = false;
|
||||
data.jpeg_size = actual_size - sizeof(NacpStruct);
|
||||
log_write("\t\t[ns control cache] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
|
||||
}
|
||||
}
|
||||
@@ -360,17 +401,16 @@ auto LoadControlEntry(u64 id) -> ThreadResultData {
|
||||
Result rc{};
|
||||
if (!manual_load) {
|
||||
TimeStamp ts;
|
||||
rc = nsGetApplicationControlData(NsApplicationControlSource_Storage, id, data.control.get(), sizeof(NsApplicationControlData), &data.control_size);
|
||||
log_write("\t\t[ns control storage] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
|
||||
u64 actual_size;
|
||||
if (R_SUCCEEDED(rc = nsGetApplicationControlData(NsApplicationControlSource_Storage, id, data.control.get(), sizeof(NsApplicationControlData), &actual_size))) {
|
||||
data.jpeg_size = actual_size - sizeof(NacpStruct);
|
||||
log_write("\t\t[ns control storage] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
|
||||
}
|
||||
}
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
NacpLanguageEntry* lang{};
|
||||
if (R_SUCCEEDED(rc = nsGetApplicationDesiredLanguage(&data.control->nacp, &lang)) && lang) {
|
||||
data.lang = *lang;
|
||||
std::memcpy(data.display_version, data.control->nacp.display_version, sizeof(data.display_version));
|
||||
data.status = NacpLoadStatus::Loaded;
|
||||
}
|
||||
data.lang = data.control->nacp.lang[GetNacpLangEntryIndex()];
|
||||
data.status = NacpLoadStatus::Loaded;
|
||||
}
|
||||
|
||||
if (R_FAILED(rc)) {
|
||||
@@ -383,8 +423,7 @@ auto LoadControlEntry(u64 id) -> ThreadResultData {
|
||||
void LoadResultIntoEntry(Entry& e, const ThreadResultData& result) {
|
||||
e.status = result.status;
|
||||
e.control = result.control;
|
||||
e.control_size = result.control_size;
|
||||
std::memcpy(e.display_version, result.display_version, sizeof(result.display_version));
|
||||
e.jpeg_size= result.jpeg_size;
|
||||
e.lang = result.lang;
|
||||
e.status = result.status;
|
||||
}
|
||||
@@ -463,7 +502,14 @@ auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status)
|
||||
|
||||
char version[sizeof(NacpStruct::display_version) + 1]{};
|
||||
if (status.meta_type == NcmContentMetaType_Patch) {
|
||||
std::snprintf(version, sizeof(version), "%s ", e.GetDisplayVersion());
|
||||
u64 program_id;
|
||||
fs::FsPath path;
|
||||
if (R_SUCCEEDED(GetControlPathFromStatus(status, &program_id, &path))) {
|
||||
char display_version[0x10];
|
||||
if (R_SUCCEEDED(nca::ParseControl(path, program_id, display_version, sizeof(display_version), nullptr, offsetof(NacpStruct, display_version)))) {
|
||||
std::snprintf(version, sizeof(version), "%s ", display_version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::FsPath path;
|
||||
@@ -495,8 +541,8 @@ Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentIn
|
||||
NcmContentMetaKey key;
|
||||
R_TRY(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(meta_total), std::addressof(meta_entries_written), std::addressof(key), 1, (NcmContentMetaType)status.meta_type, app_id, id_min, id_max, NcmContentInstallType_Full));
|
||||
log_write("ncmContentMetaDatabaseList(): AppId: %016lX Id: %016lX total: %d written: %d storageID: %u key.id %016lX\n", app_id, status.application_id, meta_total, meta_entries_written, status.storageID, key.id);
|
||||
R_UNLESS(meta_total == 1, 0x1);
|
||||
R_UNLESS(meta_entries_written == 1, 0x1);
|
||||
R_UNLESS(meta_total == 1, Result_GameMultipleKeysFound);
|
||||
R_UNLESS(meta_entries_written == 1, Result_GameMultipleKeysFound);
|
||||
|
||||
std::vector<NcmContentInfo> cnmt_infos;
|
||||
for (s32 i = 0; ; i++) {
|
||||
@@ -512,14 +558,13 @@ Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentIn
|
||||
NcmRightsId ncm_rights_id;
|
||||
R_TRY(ncmContentStorageGetRightsIdFromContentId(std::addressof(cs), std::addressof(ncm_rights_id), std::addressof(info_out.content_id), FsContentAttributes_All));
|
||||
|
||||
const auto rights_id = ncm_rights_id.rights_id;
|
||||
if (isRightsIdValid(rights_id)) {
|
||||
const auto it = std::ranges::find_if(out.rights_ids, [&rights_id](auto& e){
|
||||
return !std::memcmp(&e, &rights_id, sizeof(rights_id));
|
||||
if (isRightsIdValid(ncm_rights_id.rights_id)) {
|
||||
const auto it = std::ranges::find_if(out.ncm_rights_id, [&ncm_rights_id](auto& e){
|
||||
return !std::memcmp(&e, &ncm_rights_id, sizeof(ncm_rights_id));
|
||||
});
|
||||
|
||||
if (it == out.rights_ids.end()) {
|
||||
out.rights_ids.emplace_back(rights_id);
|
||||
if (it == out.ncm_rights_id.end()) {
|
||||
out.ncm_rights_id.emplace_back(ncm_rights_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,13 +581,27 @@ Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentIn
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, NspEntry& out) {
|
||||
Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::Keys& keys, NspEntry& out) {
|
||||
out.application_name = e.GetName();
|
||||
out.path = BuildNspPath(e, info.status);
|
||||
s64 offset{};
|
||||
|
||||
for (auto& rights_id : info.rights_ids) {
|
||||
TikEntry entry{rights_id};
|
||||
for (auto& e : info.content_infos) {
|
||||
char nca_name[0x200];
|
||||
std::snprintf(nca_name, sizeof(nca_name), "%s%s", hexIdToStr(e.content_id).str, e.content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca");
|
||||
|
||||
u64 size;
|
||||
ncmContentInfoSizeToU64(std::addressof(e), std::addressof(size));
|
||||
|
||||
out.collections.emplace_back(nca_name, offset, size);
|
||||
offset += size;
|
||||
}
|
||||
|
||||
for (auto& ncm_rights_id : info.ncm_rights_id) {
|
||||
const auto rights_id = ncm_rights_id.rights_id;
|
||||
const auto key_gen = ncm_rights_id.key_generation;
|
||||
|
||||
TikEntry entry{rights_id, key_gen};
|
||||
log_write("rights id is valid, fetching common ticket and cert\n");
|
||||
|
||||
u64 tik_size;
|
||||
@@ -555,6 +614,9 @@ Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, NspEntry& out
|
||||
R_TRY(es::GetCommonTicketAndCertificateData(&tik_size, &cert_size, entry.tik_data.data(), entry.tik_data.size(), entry.cert_data.data(), entry.cert_data.size(), &rights_id));
|
||||
log_write("got tik_data: %zu cert_data: %zu\n", tik_size, cert_size);
|
||||
|
||||
// patch fake ticket / convert personalised to common if needed.
|
||||
R_TRY(es::PatchTicket(entry.tik_data, entry.cert_data, key_gen, keys, App::GetApp()->m_dump_convert_to_common_ticket.Get()));
|
||||
|
||||
char tik_name[0x200];
|
||||
std::snprintf(tik_name, sizeof(tik_name), "%s%s", hexIdToStr(rights_id).str, ".tik");
|
||||
|
||||
@@ -570,17 +632,6 @@ Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, NspEntry& out
|
||||
out.tickets.emplace_back(entry);
|
||||
}
|
||||
|
||||
for (auto& e : info.content_infos) {
|
||||
char nca_name[0x200];
|
||||
std::snprintf(nca_name, sizeof(nca_name), "%s%s", hexIdToStr(e.content_id).str, e.content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca");
|
||||
|
||||
u64 size;
|
||||
ncmContentInfoSizeToU64(std::addressof(e), std::addressof(size));
|
||||
|
||||
out.collections.emplace_back(nca_name, offset, size);
|
||||
offset += size;
|
||||
}
|
||||
|
||||
out.nsp_data = yati::container::Nsp::Build(out.collections, out.nsp_size);
|
||||
out.cs = GetNcmCs(info.status.storageID);
|
||||
|
||||
@@ -593,16 +644,19 @@ Result BuildNspEntries(Entry& e, u32 flags, std::vector<NspEntry>& out) {
|
||||
MetaEntries meta_entries;
|
||||
R_TRY(GetMetaEntries(e, meta_entries, flags));
|
||||
|
||||
keys::Keys keys;
|
||||
R_TRY(keys::parse_keys(keys, true));
|
||||
|
||||
for (const auto& status : meta_entries) {
|
||||
ContentInfoEntry info;
|
||||
R_TRY(BuildContentEntry(status, info));
|
||||
|
||||
NspEntry nsp;
|
||||
R_TRY(BuildNspEntry(e, info, nsp));
|
||||
R_TRY(BuildNspEntry(e, info, keys, nsp));
|
||||
out.emplace_back(nsp).icon = e.image;
|
||||
}
|
||||
|
||||
R_UNLESS(!out.empty(), 0x1);
|
||||
R_UNLESS(!out.empty(), Result_GameNoNspEntriesBuilt);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
@@ -619,6 +673,11 @@ void LaunchEntry(const Entry& e) {
|
||||
void ThreadFunc(void* user) {
|
||||
auto data = static_cast<ThreadData*>(user);
|
||||
|
||||
if (data->IsTitleCacheEnabled() && !nxtcInitialize()) {
|
||||
log_write("[NXTC] failed to init cache\n");
|
||||
}
|
||||
ON_SCOPE_EXIT(nxtcExit());
|
||||
|
||||
while (data->IsRunning()) {
|
||||
data->Run();
|
||||
}
|
||||
@@ -626,21 +685,23 @@ void ThreadFunc(void* user) {
|
||||
|
||||
} // namespace
|
||||
|
||||
ThreadData::ThreadData() {
|
||||
ThreadData::ThreadData(bool title_cache) : m_title_cache{title_cache} {
|
||||
ueventCreate(&m_uevent, true);
|
||||
mutexInit(&m_mutex_id);
|
||||
mutexInit(&m_mutex_result);
|
||||
m_running = true;
|
||||
}
|
||||
|
||||
auto ThreadData::IsRunning() const -> bool {
|
||||
return m_running;
|
||||
}
|
||||
|
||||
void ThreadData::Run() {
|
||||
const auto waiter = waiterForUEvent(&m_uevent);
|
||||
while (IsRunning()) {
|
||||
const auto waiter = waiterForUEvent(&m_uevent);
|
||||
waitSingle(waiter, UINT64_MAX);
|
||||
const auto rc = waitSingle(waiter, 3e+9);
|
||||
|
||||
// if we timed out, flush the cache and poll again.
|
||||
if (R_FAILED(rc)) {
|
||||
nxtcFlushCacheFile();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IsRunning()) {
|
||||
return;
|
||||
@@ -658,10 +719,28 @@ void ThreadData::Run() {
|
||||
return;
|
||||
}
|
||||
|
||||
// sleep after every other entry loaded.
|
||||
svcSleepThread(2e+6); // 2ms
|
||||
ThreadResultData result{ids[i]};
|
||||
TimeStamp ts;
|
||||
if (auto data = nxtcGetApplicationMetadataEntryById(ids[i])) {
|
||||
log_write("[NXTC] loaded from cache time taken: %.2fs %zums %zuns\n", ts.GetSecondsD(), ts.GetMs(), ts.GetNs());
|
||||
ON_SCOPE_EXIT(nxtcFreeApplicationMetadata(&data));
|
||||
|
||||
result.control = std::make_unique<NsApplicationControlData>();
|
||||
result.status = NacpLoadStatus::Loaded;
|
||||
std::strcpy(result.lang.name, data->name);
|
||||
std::strcpy(result.lang.author, data->publisher);
|
||||
std::memcpy(result.control->icon, data->icon_data, data->icon_size);
|
||||
result.jpeg_size = data->icon_size;
|
||||
} else {
|
||||
// sleep after every other entry loaded.
|
||||
svcSleepThread(2e+6); // 2ms
|
||||
|
||||
result = LoadControlEntry(ids[i]);
|
||||
if (result.status == NacpLoadStatus::Loaded) {
|
||||
nxtcAddEntry(ids[i], &result.control->nacp, result.jpeg_size, result.control->icon, true);
|
||||
}
|
||||
}
|
||||
|
||||
const auto result = LoadControlEntry(ids[i]);
|
||||
mutexLock(&m_mutex_result);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex_result));
|
||||
m_result.emplace_back(result);
|
||||
@@ -871,6 +950,37 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
|
||||
));
|
||||
}, true));
|
||||
}
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Title cache"_i18n, m_title_cache.Get(), [this](bool& v_out){
|
||||
m_title_cache.Set(v_out);
|
||||
}));
|
||||
|
||||
// todo: impl this.
|
||||
#if 0
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Create save"_i18n, [this](){
|
||||
ui::PopupList::Items items{};
|
||||
const auto accounts = App::GetAccountList();
|
||||
for (auto& p : accounts) {
|
||||
items.emplace_back(p.nickname);
|
||||
}
|
||||
|
||||
fsCreateSaveDataFileSystem;
|
||||
|
||||
App::Push(std::make_shared<ui::PopupList>(
|
||||
"Select user to create save for"_i18n, items, [accounts](auto op_index){
|
||||
if (op_index) {
|
||||
s64 out;
|
||||
if (R_SUCCEEDED(swkbd::ShowNumPad(out, "Enter the save size"_i18n.c_str()))) {
|
||||
}
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
// 1. Select user to create save for.
|
||||
// 2. Enter the save size.
|
||||
// 3. Enter the journal size (0 for default).
|
||||
}));
|
||||
#endif
|
||||
}})
|
||||
);
|
||||
|
||||
@@ -883,13 +993,14 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
|
||||
e.Open();
|
||||
}
|
||||
|
||||
threadCreate(&m_thread, ThreadFunc, &m_thread_data, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE);
|
||||
m_thread_data = std::make_unique<ThreadData>(m_title_cache.Get());
|
||||
threadCreate(&m_thread, ThreadFunc, m_thread_data.get(), nullptr, 1024*32, THREAD_PRIO, THREAD_CORE);
|
||||
svcSetThreadCoreMask(m_thread.handle, THREAD_CORE, THREAD_AFFINITY_DEFAULT(THREAD_CORE));
|
||||
threadStart(&m_thread);
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
m_thread_data.Close();
|
||||
m_thread_data->Close();
|
||||
|
||||
for (auto& e : ncm_entries) {
|
||||
e.Close();
|
||||
@@ -923,12 +1034,17 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
if (m_entries.empty()) {
|
||||
gfx::drawTextArgs(vg, GetX() + GetW() / 2.f, GetY() + GetH() / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Empty..."_i18n.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// max images per frame, in order to not hit io / gpu too hard.
|
||||
const int image_load_max = 2;
|
||||
int image_load_count = 0;
|
||||
|
||||
std::vector<ThreadResultData> data;
|
||||
m_thread_data.Pop(data);
|
||||
m_thread_data->Pop(data);
|
||||
|
||||
for (const auto& d : data) {
|
||||
const auto it = std::ranges::find_if(m_entries, [&d](auto& e) {
|
||||
@@ -945,7 +1061,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
auto& e = m_entries[pos];
|
||||
|
||||
if (e.status == NacpLoadStatus::None) {
|
||||
m_thread_data.Push(e.app_id);
|
||||
m_thread_data->Push(e.app_id);
|
||||
e.status = NacpLoadStatus::Progress;
|
||||
}
|
||||
|
||||
@@ -956,11 +1072,14 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
}
|
||||
}
|
||||
|
||||
char title_id[33];
|
||||
std::snprintf(title_id, sizeof(title_id), "%016lX", e.app_id);
|
||||
|
||||
const auto selected = pos == m_index;
|
||||
DrawEntry(vg, theme, m_layout.Get(), v, selected, e.image, e.GetName(), e.GetAuthor(), e.GetDisplayVersion());
|
||||
DrawEntry(vg, theme, m_layout.Get(), v, selected, e.image, e.GetName(), e.GetAuthor(), title_id);
|
||||
|
||||
if (e.selected) {
|
||||
gfx::drawRect(vg, v, nvgRGBA(0, 0, 0, 180), 5);
|
||||
gfx::drawRect(vg, v, theme->GetColour(ThemeEntryID_FOCUS), 5);
|
||||
gfx::drawText(vg, x + w / 2, y + h / 2, 24.f, "\uE14B", nullptr, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED));
|
||||
}
|
||||
});
|
||||
@@ -990,8 +1109,8 @@ void Menu::ScanHomebrew() {
|
||||
const auto hide_forwarders = m_hide_forwarders.Get();
|
||||
TimeStamp ts;
|
||||
|
||||
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
|
||||
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
|
||||
App::SetBoostMode(true);
|
||||
ON_SCOPE_EXIT(App::SetBoostMode(false));
|
||||
|
||||
FreeEntries();
|
||||
m_entries.reserve(ENTRY_CHUNK_COUNT);
|
||||
@@ -1018,7 +1137,7 @@ void Menu::ScanHomebrew() {
|
||||
u64 unk_x11;// = e.unk_x11;
|
||||
memcpy(&unk_x0a, e.unk_x0a, sizeof(e.unk_x0a));
|
||||
memcpy(&unk_x11, e.unk_x11, sizeof(e.unk_x11));
|
||||
log_write("ID: %016lx got type: %u unk_x09: %u unk_x0a: %zu unk_x10: %u unk_x11: %zu\n", e.application_id, e.type,
|
||||
log_write("ID: %016lx got type: %u unk_x09: %u unk_x0a: %zu unk_x10: %u unk_x11: %zu\n", e.app_id, e.type,
|
||||
unk_x09,
|
||||
unk_x0a,
|
||||
unk_x10,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "i18n.hpp"
|
||||
#include "download.hpp"
|
||||
#include "dumper.hpp"
|
||||
#include "image.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
@@ -220,7 +221,7 @@ struct XciSource final : dump::BaseSource {
|
||||
span = initial;
|
||||
}
|
||||
|
||||
R_UNLESS(!span.empty(), 0x1);
|
||||
R_UNLESS(!span.empty(), Result_GcBadReadForDump);
|
||||
|
||||
size = ClipSize(off, size, span.size());
|
||||
*bytes_read = size;
|
||||
@@ -375,7 +376,7 @@ Result GcSource::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||
}
|
||||
|
||||
// this will never fail, unless i break something in yati.
|
||||
R_UNLESS(found, 0x1);
|
||||
R_UNLESS(found, Result_GcBadReadForDump);
|
||||
}
|
||||
|
||||
return m_file.Read(off - m_offset, buf, size, 0, bytes_read);
|
||||
@@ -404,6 +405,55 @@ auto ApplicationEntry::GetSize() const -> s64 {
|
||||
|
||||
Menu::Menu(u32 flags) : MenuBase{"GameCard"_i18n, flags} {
|
||||
this->SetActions(
|
||||
std::make_pair(Button::A, Action{"OK"_i18n, [this](){
|
||||
if (m_option_index == 2) {
|
||||
SetPop();
|
||||
} else {
|
||||
if (!m_mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_option_index == 0) {
|
||||
if (!App::GetInstallEnable()) {
|
||||
App::Push(std::make_shared<ui::OptionBox>(
|
||||
"Install disabled...\n"
|
||||
"Please enable installing via the install options."_i18n,
|
||||
"OK"_i18n
|
||||
));
|
||||
} else {
|
||||
log_write("[GC] doing install A\n");
|
||||
App::Push(std::make_shared<ui::ProgressBox>(m_icon, "Installing "_i18n, m_entries[m_entry_index].lang_entry.name, [this](auto pbox) -> Result {
|
||||
auto source = std::make_shared<GcSource>(m_entries[m_entry_index], m_fs.get());
|
||||
return yati::InstallFromCollections(pbox, source, source->m_collections, source->m_config);
|
||||
}, [this](Result rc){
|
||||
App::PushErrorBox(rc, "Gc install failed!"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Gc install success!"_i18n);
|
||||
}
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
auto options = std::make_shared<Sidebar>("Select content to dump"_i18n, Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
const auto add = [&](const std::string& name, u32 flags){
|
||||
options->Add(std::make_shared<SidebarEntryCallback>(name, [this, flags](){
|
||||
DumpGames(flags);
|
||||
m_dirty = true;
|
||||
}, true));
|
||||
};
|
||||
|
||||
add("Dump All"_i18n, DumpFileFlag_All);
|
||||
add("Dump All Bins"_i18n, DumpFileFlag_AllBin);
|
||||
add("Dump XCI"_i18n, DumpFileFlag_XCI);
|
||||
add("Dump Card ID Set"_i18n, DumpFileFlag_Set);
|
||||
add("Dump Card UID"_i18n, DumpFileFlag_UID);
|
||||
add("Dump Certificate"_i18n, DumpFileFlag_Cert);
|
||||
add("Dump Initial Data"_i18n, DumpFileFlag_Initial);
|
||||
}
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}}),
|
||||
@@ -528,7 +578,7 @@ Result Menu::GcMount() {
|
||||
// the fs, same as mounting storage.
|
||||
for (u32 i = 0; i < REMOUNT_ATTEMPT_MAX; i++) {
|
||||
R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(m_handle)));
|
||||
m_fs = std::make_unique<fs::FsNativeGameCard>(std::addressof(m_handle), FsGameCardPartition_Secure, false);
|
||||
m_fs = std::make_unique<fs::FsNativeGameCard>(std::addressof(m_handle), FsGameCardPartition_Secure);
|
||||
if (R_SUCCEEDED(m_fs->GetFsOpenResult())) {
|
||||
break;
|
||||
}
|
||||
@@ -592,7 +642,7 @@ Result Menu::GcMount() {
|
||||
return !std::strncmp(str.str, e.name, std::strlen(str.str));
|
||||
});
|
||||
|
||||
R_UNLESS(it != buf.cend(), yati::Result_NcaNotFound);
|
||||
R_UNLESS(it != buf.cend(), Result_YatiNcaNotFound);
|
||||
collections.emplace_back(it->name, it->file_size, info.content_type, info.id_offset);
|
||||
}
|
||||
|
||||
@@ -623,7 +673,7 @@ Result Menu::GcMount() {
|
||||
}
|
||||
}
|
||||
|
||||
R_UNLESS(m_entries.size(), 0x1);
|
||||
R_UNLESS(m_entries.size(), Result_GcEmptyGamecard);
|
||||
|
||||
// append tickets to every application, yati will ignore if undeeded.
|
||||
for (auto& e : m_entries) {
|
||||
@@ -642,57 +692,6 @@ Result Menu::GcMount() {
|
||||
}
|
||||
}
|
||||
|
||||
SetAction(Button::A, Action{"OK"_i18n, [this](){
|
||||
if (m_option_index == 2) {
|
||||
SetPop();
|
||||
} else {
|
||||
log_write("[GC] pressed A\n");
|
||||
if (!m_mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_option_index == 0) {
|
||||
if (!App::GetInstallEnable()) {
|
||||
App::Push(std::make_shared<ui::OptionBox>(
|
||||
"Install disabled...\n"
|
||||
"Please enable installing via the install options."_i18n,
|
||||
"OK"_i18n
|
||||
));
|
||||
} else {
|
||||
log_write("[GC] doing install A\n");
|
||||
App::Push(std::make_shared<ui::ProgressBox>(m_icon, "Installing "_i18n, m_entries[m_entry_index].lang_entry.name, [this](auto pbox) -> Result {
|
||||
auto source = std::make_shared<GcSource>(m_entries[m_entry_index], m_fs.get());
|
||||
return yati::InstallFromCollections(pbox, source, source->m_collections, source->m_config);
|
||||
}, [this](Result rc){
|
||||
App::PushErrorBox(rc, "Gc install failed"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Gc install success!"_i18n);
|
||||
}
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
auto options = std::make_shared<Sidebar>("Select content to dump"_i18n, Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
const auto add = [&](const std::string& name, u32 flags){
|
||||
options->Add(std::make_shared<SidebarEntryCallback>(name, [this, flags](){
|
||||
DumpGames(flags);
|
||||
m_dirty = true;
|
||||
}, true));
|
||||
};
|
||||
|
||||
add("Dump All"_i18n, DumpFileFlag_All);
|
||||
add("Dump All Bins"_i18n, DumpFileFlag_AllBin);
|
||||
add("Dump XCI"_i18n, DumpFileFlag_XCI);
|
||||
add("Dump Card ID Set"_i18n, DumpFileFlag_Set);
|
||||
add("Dump Card UID"_i18n, DumpFileFlag_UID);
|
||||
add("Dump Certificate"_i18n, DumpFileFlag_Cert);
|
||||
add("Dump Initial Data"_i18n, DumpFileFlag_Initial);
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
if (m_entries.size() > 1) {
|
||||
SetAction(Button::L2, Action{"Prev"_i18n, [this](){
|
||||
if (m_entry_index != 0) {
|
||||
@@ -741,12 +740,12 @@ Result Menu::GcMountStorage() {
|
||||
std::memcpy(&trim_size, header + 0x118, sizeof(trim_size));
|
||||
std::memcpy(&m_package_id, header + 0x110, sizeof(m_package_id));
|
||||
std::memcpy(m_initial_data_hash, header + 0x160, sizeof(m_initial_data_hash));
|
||||
R_UNLESS(magic == XCI_MAGIC, 0x1);
|
||||
R_UNLESS(magic == XCI_MAGIC, Result_GcBadXciMagic);
|
||||
|
||||
// calculate the reported size, error if not found.
|
||||
m_storage_full_size = GetXciSizeFromRomSize(rom_size);
|
||||
log_write("[GC] m_storage_full_size: %zd rom_size: 0x%X\n", m_storage_full_size, rom_size);
|
||||
R_UNLESS(m_storage_full_size > 0, 0x1);
|
||||
R_UNLESS(m_storage_full_size > 0, Result_GcBadXciRomSize);
|
||||
|
||||
R_TRY(fsStorageGetSize(&m_storage, &m_parition_normal_size));
|
||||
R_TRY(GcMountPartition(FsGameCardPartitionRaw_Secure));
|
||||
@@ -965,7 +964,13 @@ void Menu::OnChangeIndex(s64 new_index) {
|
||||
|
||||
const auto& e = m_entries[m_entry_index];
|
||||
const auto jpeg_size = e.control_size - sizeof(NacpStruct);
|
||||
m_icon = nvgCreateImageMem(App::GetVg(), 0, e.control->icon, jpeg_size);
|
||||
|
||||
TimeStamp ts;
|
||||
const auto image = ImageLoadFromMemory({e.control->icon, jpeg_size}, ImageFlag_JPEG);
|
||||
if (!image.data.empty()) {
|
||||
m_icon = nvgCreateImageRGBA(App::GetVg(), image.w, image.h, 0, image.data.data());
|
||||
log_write("\t[image load] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -975,8 +980,8 @@ Result Menu::DumpGames(u32 flags) {
|
||||
R_TRY(GcMountStorage());
|
||||
|
||||
const auto do_dump = [this](u32 flags) -> Result {
|
||||
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
|
||||
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
|
||||
App::SetBoostMode(true);
|
||||
ON_SCOPE_EXIT(App::SetBoostMode(false));
|
||||
|
||||
u32 location_flags = dump::DumpLocationFlag_All;
|
||||
|
||||
@@ -1040,25 +1045,35 @@ Result Menu::DumpGames(u32 flags) {
|
||||
bool is_trimmed = false;
|
||||
Result trim_rc = 0;
|
||||
if ((flags & DumpFileFlag_XCI) && m_storage_trimmed_size < m_storage_total_size) {
|
||||
u8 temp{};
|
||||
if (R_FAILED(trim_rc = GcStorageRead(&temp, m_storage_trimmed_size, sizeof(temp)))) {
|
||||
log_write("[GC] WARNING! GameCard is already trimmed: 0x%X FlashError: %u\n", trim_rc, trim_rc == 0x13D002);
|
||||
const auto start_offset = std::min<s64>(0, m_storage_trimmed_size - 0x4000);
|
||||
// works on fw 1.2.0 and below.
|
||||
std::vector<u8> temp(1024*1024*1);
|
||||
if (R_FAILED(trim_rc = GcStorageRead(temp.data(), m_storage_trimmed_size, std::min<s64>(temp.size(), m_storage_total_size - start_offset)))) {
|
||||
log_write("[GC] WARNING1! GameCard is already trimmed: 0x%X FlashError: %u\n", trim_rc, trim_rc == 0x13D002);
|
||||
is_trimmed = true;
|
||||
}
|
||||
|
||||
if (!is_trimmed) {
|
||||
// works on fw 1.2.0 and below.
|
||||
if (R_FAILED(trim_rc = GcStorageRead(temp.data(), m_storage_total_size - temp.size(), temp.size()))) {
|
||||
log_write("[GC] WARNING2! GameCard is already trimmed: 0x%X FlashError: %u\n", trim_rc, trim_rc == 0x13D002);
|
||||
is_trimmed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if trimmed and the user wants to dump the full xci, error.
|
||||
if ((flags & DumpFileFlag_XCI) && is_trimmed && App::GetApp()->m_dump_trim_xci.Get()) {
|
||||
App::PushErrorBox(trim_rc, "GameCard is already trimmed!"_i18n);
|
||||
} else if ((flags & DumpFileFlag_XCI) && is_trimmed) {
|
||||
App::Push(std::make_shared<ui::OptionBox>(
|
||||
"WARNING: GameCard is already trimmed!"_i18n,
|
||||
"Back"_i18n, "Continue"_i18n, 0, [&](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
do_dump(flags);
|
||||
}
|
||||
}
|
||||
}, m_icon
|
||||
));
|
||||
} else if ((flags & DumpFileFlag_XCI) && is_trimmed) {
|
||||
App::PushErrorBox(trim_rc, "GameCard is trimmed, full dump is not possible!"_i18n);
|
||||
} else {
|
||||
do_dump(flags);
|
||||
}
|
||||
@@ -1134,7 +1149,7 @@ Result Menu::GcGetSecurityInfo(GameCardSecurityInformation& out) {
|
||||
}
|
||||
}
|
||||
|
||||
R_THROW(0x1);
|
||||
R_THROW(Result_GcFailedToGetSecurityInfo);
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::gc
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
#include "ui/menus/ghdl.hpp"
|
||||
#include "ui/menus/homebrew.hpp"
|
||||
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/option_box.hpp"
|
||||
#include "ui/popup_list.hpp"
|
||||
#include "ui/progress_box.hpp"
|
||||
#include "ui/error_box.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
|
||||
#include "log.hpp"
|
||||
#include "app.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "image.hpp"
|
||||
#include "download.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "yyjson_helper.hpp"
|
||||
#include "threaded_file_transfer.hpp"
|
||||
|
||||
#include <minIni.h>
|
||||
#include <minizip/unzip.h>
|
||||
#include <dirent.h>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
@@ -83,13 +85,12 @@ void from_json(const fs::FsPath& path, GhApiEntry& e) {
|
||||
|
||||
auto DownloadApp(ProgressBox* pbox, const GhApiAsset& gh_asset, const AssetEntry* entry) -> Result {
|
||||
static const fs::FsPath temp_file{"/switch/sphaira/cache/github/ghdl.temp"};
|
||||
constexpr auto chunk_size = 1024 * 512; // 512KiB
|
||||
|
||||
fs::FsNativeSd fs;
|
||||
R_TRY(fs.GetFsOpenResult());
|
||||
ON_SCOPE_EXIT(fs.DeleteFile(temp_file));
|
||||
|
||||
R_UNLESS(!gh_asset.browser_download_url.empty(), 0x1);
|
||||
R_UNLESS(!gh_asset.browser_download_url.empty(), Result_GhdlEmptyAsset);
|
||||
|
||||
// 2. download the asset
|
||||
if (!pbox->ShouldExit()) {
|
||||
@@ -102,7 +103,7 @@ auto DownloadApp(ProgressBox* pbox, const GhApiAsset& gh_asset, const AssetEntry
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||
);
|
||||
|
||||
R_UNLESS(result.success, 0x1);
|
||||
R_UNLESS(result.success, Result_GhdlFailedToDownloadAsset);
|
||||
}
|
||||
|
||||
fs::FsPath root_path{"/"};
|
||||
@@ -113,77 +114,7 @@ auto DownloadApp(ProgressBox* pbox, const GhApiAsset& gh_asset, const AssetEntry
|
||||
// 3. extract the zip / file
|
||||
if (gh_asset.content_type.find("zip") != gh_asset.content_type.npos) {
|
||||
log_write("found zip\n");
|
||||
auto zfile = unzOpen64(temp_file);
|
||||
R_UNLESS(zfile, 0x1);
|
||||
ON_SCOPE_EXIT(unzClose(zfile));
|
||||
|
||||
unz_global_info64 pglobal_info;
|
||||
if (UNZ_OK != unzGetGlobalInfo64(zfile, &pglobal_info)) {
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < pglobal_info.number_entry; i++) {
|
||||
if (i > 0) {
|
||||
if (UNZ_OK != unzGoToNextFile(zfile)) {
|
||||
log_write("failed to unzGoToNextFile\n");
|
||||
R_THROW(0x1);
|
||||
}
|
||||
}
|
||||
|
||||
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
|
||||
log_write("failed to open current file\n");
|
||||
R_THROW(0x1);
|
||||
}
|
||||
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
|
||||
|
||||
unz_file_info64 info;
|
||||
fs::FsPath file_path;
|
||||
if (UNZ_OK != unzGetCurrentFileInfo64(zfile, &info, file_path, sizeof(file_path), 0, 0, 0, 0)) {
|
||||
log_write("failed to get current info\n");
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
file_path = fs::AppendPath(root_path, file_path);
|
||||
|
||||
Result rc;
|
||||
if (file_path[strlen(file_path) -1] == '/') {
|
||||
if (R_FAILED(rc = fs.CreateDirectoryRecursively(file_path)) && rc != FsError_PathAlreadyExists) {
|
||||
log_write("failed to create folder: %s 0x%04X\n", file_path.s, rc);
|
||||
R_THROW(rc);
|
||||
}
|
||||
} else {
|
||||
if (R_FAILED(rc = fs.CreateDirectoryRecursivelyWithPath(file_path)) && rc != FsError_PathAlreadyExists) {
|
||||
log_write("failed to create folder: %s 0x%04X\n", file_path.s, rc);
|
||||
R_THROW(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);
|
||||
R_THROW(rc);
|
||||
}
|
||||
|
||||
fs::File f;
|
||||
R_TRY(fs.OpenFile(file_path, FsOpenMode_Write, &f));
|
||||
R_TRY(f.SetSize(info.uncompressed_size));
|
||||
|
||||
std::vector<char> buf(chunk_size);
|
||||
s64 offset{};
|
||||
while (offset < info.uncompressed_size) {
|
||||
R_TRY(pbox->ShouldExitResult());
|
||||
|
||||
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
|
||||
if (bytes_read <= 0) {
|
||||
log_write("failed to read zip file: %s\n", file_path.s);
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
R_TRY(f.Write(offset, buf.data(), bytes_read, FsWriteOption_None));
|
||||
|
||||
pbox->UpdateTransfer(offset, info.uncompressed_size);
|
||||
offset += bytes_read;
|
||||
}
|
||||
}
|
||||
}
|
||||
R_TRY(thread::TransferUnzipAll(pbox, temp_file, &fs, root_path));
|
||||
} else {
|
||||
fs.CreateDirectoryRecursivelyWithPath(root_path);
|
||||
fs.DeleteFile(root_path);
|
||||
@@ -212,11 +143,11 @@ auto DownloadAssetJson(ProgressBox* pbox, const std::string& url, GhApiEntry& ou
|
||||
}
|
||||
);
|
||||
|
||||
R_UNLESS(result.success, 0x1);
|
||||
R_UNLESS(result.success, Result_GhdlFailedToDownloadAssetJson);
|
||||
from_json(result.path, out);
|
||||
}
|
||||
|
||||
R_UNLESS(!out.assets.empty(), 0x1);
|
||||
R_UNLESS(!out.assets.empty(), Result_GhdlEmptyAsset);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
@@ -288,6 +219,7 @@ Menu::Menu(u32 flags) : MenuBase{"GitHub"_i18n, flags} {
|
||||
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, GetEntry().repo, [this, &asset_entry, ptr](auto pbox) -> Result {
|
||||
return DownloadApp(pbox, asset_entry, ptr);
|
||||
}, [this, ptr](Result rc){
|
||||
homebrew::SignalChange();
|
||||
App::PushErrorBox(rc, "Failed to download app!"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "owo.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "image.hpp"
|
||||
|
||||
#include <minIni.h>
|
||||
#include <utility>
|
||||
@@ -19,6 +20,7 @@ namespace sphaira::ui::menu::homebrew {
|
||||
namespace {
|
||||
|
||||
Menu* g_menu{};
|
||||
constinit UEvent g_change_uevent;
|
||||
|
||||
auto GenerateStarPath(const fs::FsPath& nro_path) -> fs::FsPath {
|
||||
fs::FsPath out{};
|
||||
@@ -34,6 +36,10 @@ void FreeEntry(NVGcontext* vg, NroEntry& e) {
|
||||
|
||||
} // namespace
|
||||
|
||||
void SignalChange() {
|
||||
ueventSignal(&g_change_uevent);
|
||||
}
|
||||
|
||||
auto GetNroEntries() -> std::span<const NroEntry> {
|
||||
if (!g_menu) {
|
||||
return {};
|
||||
@@ -138,6 +144,7 @@ Menu::Menu() : grid::Menu{"Homebrew"_i18n, MenuFlag_Tab} {
|
||||
);
|
||||
|
||||
OnLayoutChange();
|
||||
ueventCreate(&g_change_uevent, true);
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
@@ -146,6 +153,14 @@ Menu::~Menu() {
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
if (R_SUCCEEDED(waitSingle(waiterForUEvent(&g_change_uevent), 0))) {
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
if (m_dirty) {
|
||||
SortAndFindLastFile(true);
|
||||
}
|
||||
|
||||
MenuBase::Update(controller, touch);
|
||||
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
|
||||
if (touch && m_index == i) {
|
||||
@@ -175,9 +190,17 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
// really, switch-tools should handle this by resizing the image before
|
||||
// adding it to the nro, as well as validate its a valid jpeg.
|
||||
const auto icon = nro_get_icon(e.path, e.icon_size, e.icon_offset);
|
||||
TimeStamp ts;
|
||||
if (!icon.empty()) {
|
||||
e.image = nvgCreateImageMem(vg, 0, icon.data(), icon.size());
|
||||
image_load_count++;
|
||||
const auto image = ImageLoadFromMemory(icon, ImageFlag_JPEG);
|
||||
if (!image.data.empty()) {
|
||||
e.image = nvgCreateImageRGBA(vg, image.w, image.h, 0, image.data.data());
|
||||
log_write("\t[image load] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
|
||||
image_load_count++;
|
||||
} else {
|
||||
// prevent loading of this icon again as it's already failed.
|
||||
e.icon_offset = e.icon_size = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,6 +311,7 @@ void Menu::ScanHomebrew() {
|
||||
|
||||
this->Sort();
|
||||
SetIndex(0);
|
||||
m_dirty = false;
|
||||
}
|
||||
|
||||
void Menu::Sort() {
|
||||
@@ -382,9 +406,13 @@ void Menu::Sort() {
|
||||
std::sort(m_entries.begin(), m_entries.end(), sorter);
|
||||
}
|
||||
|
||||
void Menu::SortAndFindLastFile() {
|
||||
void Menu::SortAndFindLastFile(bool scan) {
|
||||
const auto path = m_entries[m_index].path;
|
||||
Sort();
|
||||
if (scan) {
|
||||
ScanHomebrew();
|
||||
} else {
|
||||
Sort();
|
||||
}
|
||||
SetIndex(0);
|
||||
|
||||
s64 index = -1;
|
||||
|
||||
261
sphaira/source/ui/menus/install_stream_menu_base.cpp
Normal file
@@ -0,0 +1,261 @@
|
||||
#include "ui/menus/install_stream_menu_base.hpp"
|
||||
#include "yati/yati.hpp"
|
||||
#include "app.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::ui::menu::stream {
|
||||
namespace {
|
||||
|
||||
enum class InstallState {
|
||||
None,
|
||||
Progress,
|
||||
Finished,
|
||||
};
|
||||
|
||||
constexpr u64 MAX_BUFFER_SIZE = 1024ULL*1024ULL*8ULL;
|
||||
constexpr u64 MAX_BUFFER_RESERVE_SIZE = 1024ULL*1024ULL*32ULL;
|
||||
volatile InstallState INSTALL_STATE{InstallState::None};
|
||||
|
||||
} // namespace
|
||||
|
||||
Stream::Stream(const fs::FsPath& path, std::stop_token token) {
|
||||
m_path = path;
|
||||
m_token = token;
|
||||
m_active = true;
|
||||
m_buffer.reserve(MAX_BUFFER_RESERVE_SIZE);
|
||||
|
||||
mutexInit(&m_mutex);
|
||||
condvarInit(&m_can_read);
|
||||
}
|
||||
|
||||
Result Stream::ReadChunk(void* buf, s64 size, u64* bytes_read) {
|
||||
log_write("[Stream::ReadChunk] inside\n");
|
||||
ON_SCOPE_EXIT(
|
||||
log_write("[Stream::ReadChunk] exiting\n");
|
||||
);
|
||||
|
||||
while (!m_token.stop_requested()) {
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
if (m_active && m_buffer.empty()) {
|
||||
condvarWait(&m_can_read, &m_mutex);
|
||||
}
|
||||
|
||||
if ((!m_active && m_buffer.empty()) || m_token.stop_requested()) {
|
||||
break;
|
||||
}
|
||||
|
||||
size = std::min<s64>(size, m_buffer.size());
|
||||
std::memcpy(buf, m_buffer.data(), size);
|
||||
m_buffer.erase(m_buffer.begin(), m_buffer.begin() + size);
|
||||
*bytes_read = size;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
log_write("[Stream::ReadChunk] failed to read\n");
|
||||
R_THROW(Result_TransferCancelled);
|
||||
}
|
||||
|
||||
bool Stream::Push(const void* buf, s64 size) {
|
||||
log_write("[Stream::Push] inside\n");
|
||||
ON_SCOPE_EXIT(
|
||||
log_write("[Stream::Push] exiting\n");
|
||||
);
|
||||
|
||||
while (!m_token.stop_requested()) {
|
||||
if (INSTALL_STATE == InstallState::Finished) {
|
||||
log_write("[Stream::Push] install has finished\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
// don't use condivar here as windows mtp is very broken.
|
||||
// stalling for too longer (3s+) and having too varied transfer speeds
|
||||
// results in windows stalling the transfer for 1m until it kills it via timeout.
|
||||
// the workaround is to always accept new data, but stall for 1s.
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
if (m_active && m_buffer.size() >= MAX_BUFFER_SIZE) {
|
||||
// unlock the mutex and wait for 1s to bring transfer speed down to 1MiB/s.
|
||||
log_write("[Stream::Push] buffer is full, delaying\n");
|
||||
mutexUnlock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexLock(&m_mutex));
|
||||
|
||||
svcSleepThread(1e+9);
|
||||
}
|
||||
|
||||
if (!m_active) {
|
||||
log_write("[Stream::Push] file not active\n");
|
||||
break;
|
||||
}
|
||||
|
||||
const auto offset = m_buffer.size();
|
||||
m_buffer.resize(offset + size);
|
||||
std::memcpy(m_buffer.data() + offset, buf, size);
|
||||
condvarWakeOne(&m_can_read);
|
||||
return true;
|
||||
}
|
||||
|
||||
log_write("[Stream::Push] failed to push\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
void Stream::Disable() {
|
||||
log_write("[Stream::Disable] disabling file\n");
|
||||
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
m_active = false;
|
||||
condvarWakeOne(&m_can_read);
|
||||
}
|
||||
|
||||
Menu::Menu(const std::string& title, u32 flags) : MenuBase{title, flags} {
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}});
|
||||
|
||||
SetAction(Button::X, Action{"Options"_i18n, [this](){
|
||||
App::DisplayInstallOptions(false);
|
||||
}});
|
||||
|
||||
App::SetAutoSleepDisabled(true);
|
||||
mutexInit(&m_mutex);
|
||||
|
||||
INSTALL_STATE = InstallState::None;
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
// signal for thread to exit and wait.
|
||||
m_stop_source.request_stop();
|
||||
|
||||
if (m_source) {
|
||||
m_source->Disable();
|
||||
}
|
||||
|
||||
App::SetAutoSleepDisabled(false);
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
|
||||
if (m_state == State::Connected) {
|
||||
m_state = State::Progress;
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, m_source->GetPath(), [this](auto pbox) -> Result {
|
||||
INSTALL_STATE = InstallState::Progress;
|
||||
const auto rc = yati::InstallFromSource(pbox, m_source, m_source->GetPath());
|
||||
INSTALL_STATE = InstallState::Finished;
|
||||
|
||||
if (R_FAILED(rc)) {
|
||||
m_source->Disable();
|
||||
R_THROW(rc);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}, [this](Result rc){
|
||||
App::PushErrorBox(rc, "Install failed!"_i18n);
|
||||
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Install success!"_i18n);
|
||||
m_state = State::Done;
|
||||
} else {
|
||||
m_state = State::Failed;
|
||||
OnDisableInstallMode();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
|
||||
switch (m_state) {
|
||||
case State::None:
|
||||
case State::Done:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Drag'n'Drop (NSP, XCI, NSZ, XCZ) to the install folder"_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Connected:
|
||||
case State::Progress:
|
||||
break;
|
||||
|
||||
case State::Failed:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to install, press B to exit..."_i18n.c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool Menu::OnInstallStart(const char* path) {
|
||||
log_write("[Menu::OnInstallStart] inside\n");
|
||||
|
||||
for (;;) {
|
||||
{
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
|
||||
if (m_state != State::Progress) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (GetToken().stop_requested()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
svcSleepThread(1e+6);
|
||||
}
|
||||
|
||||
log_write("[Menu::OnInstallStart] got state: %u\n", (u8)m_state);
|
||||
|
||||
if (m_source) {
|
||||
log_write("[Menu::OnInstallStart] we have source\n");
|
||||
for (;;) {
|
||||
{
|
||||
SCOPED_MUTEX(&m_source->m_mutex);
|
||||
|
||||
if (!m_source->m_active && INSTALL_STATE != InstallState::Progress) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (GetToken().stop_requested()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
svcSleepThread(1e+6);
|
||||
}
|
||||
|
||||
log_write("[Menu::OnInstallStart] stopped polling source\n");
|
||||
}
|
||||
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
|
||||
m_source = std::make_shared<Stream>(path, GetToken());
|
||||
INSTALL_STATE = InstallState::None;
|
||||
m_state = State::Connected;
|
||||
log_write("[Menu::OnInstallStart] exiting\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Menu::OnInstallWrite(const void* buf, size_t size) {
|
||||
log_write("[Menu::OnInstallWrite] inside\n");
|
||||
return m_source->Push(buf, size);
|
||||
}
|
||||
|
||||
void Menu::OnInstallClose() {
|
||||
log_write("[Menu::OnInstallClose] inside\n");
|
||||
|
||||
m_source->Disable();
|
||||
|
||||
// wait until the install has finished before returning.
|
||||
while (INSTALL_STATE == InstallState::Progress) {
|
||||
svcSleepThread(1e+7);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::stream
|
||||
@@ -118,12 +118,12 @@ Menu::Menu(u32 flags) : MenuBase{"Irs"_i18n, flags} {
|
||||
is_negative_image_used_str.emplace_back("Negative image"_i18n);
|
||||
|
||||
SidebarEntryArray::Items format_str;
|
||||
format_str.emplace_back("320x240"_i18n);
|
||||
format_str.emplace_back("160x120"_i18n);
|
||||
format_str.emplace_back("80x60"_i18n);
|
||||
format_str.emplace_back("320\u00D7240");
|
||||
format_str.emplace_back("160\u00D7120");
|
||||
format_str.emplace_back("80\u00D760");
|
||||
if (hosversionAtLeast(4,0,0)) {
|
||||
format_str.emplace_back("40x30"_i18n);
|
||||
format_str.emplace_back("20x15"_i18n);
|
||||
format_str.emplace_back("40\u00D730");
|
||||
format_str.emplace_back("20\u00D715");
|
||||
}
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Controller"_i18n, controller_str, [this](s64& index){
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
#include "ui/menus/ghdl.hpp"
|
||||
#include "ui/menus/usb_menu.hpp"
|
||||
#include "ui/menus/ftp_menu.hpp"
|
||||
#include "ui/menus/mtp_menu.hpp"
|
||||
#include "ui/menus/gc_menu.hpp"
|
||||
#include "ui/menus/game_menu.hpp"
|
||||
#include "ui/menus/save_menu.hpp"
|
||||
#include "ui/menus/appstore.hpp"
|
||||
|
||||
#include "app.hpp"
|
||||
@@ -22,9 +24,9 @@
|
||||
#include "download.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "threaded_file_transfer.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <minizip/unzip.h>
|
||||
#include <yyjson.h>
|
||||
|
||||
namespace sphaira::ui::menu::main {
|
||||
@@ -49,9 +51,11 @@ 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 = "FileBrowser", .title = "FileBrowser", .func = MiscMenuFuncGenerator<ui::menu::filebrowser::Menu>, .flag = MiscMenuFlag_Shortcut },
|
||||
{ .name = "Saves", .title = "Saves", .func = MiscMenuFuncGenerator<ui::menu::save::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 = "MTP", .title = "MTP Install", .func = MiscMenuFuncGenerator<ui::menu::mtp::Menu>, .flag = MiscMenuFlag_Install },
|
||||
{ .name = "USB", .title = "USB Install", .func = MiscMenuFuncGenerator<ui::menu::usb::Menu>, .flag = MiscMenuFlag_Install },
|
||||
{ .name = "GameCard", .title = "GameCard", .func = MiscMenuFuncGenerator<ui::menu::gc::Menu>, .flag = MiscMenuFlag_Shortcut },
|
||||
{ .name = "IRS", .title = "IRS (Infrared Joycon Camera)", .func = MiscMenuFuncGenerator<ui::menu::irs::Menu>, .flag = MiscMenuFlag_Shortcut },
|
||||
@@ -59,7 +63,6 @@ const MiscMenuEntry MISC_MENU_ENTRIES[] = {
|
||||
|
||||
auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string version) -> Result {
|
||||
static fs::FsPath zip_out{"/switch/sphaira/cache/update.zip"};
|
||||
constexpr auto chunk_size = 1024 * 512; // 512KiB
|
||||
|
||||
fs::FsNativeSd fs;
|
||||
R_TRY(fs.GetFsOpenResult());
|
||||
@@ -75,102 +78,41 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||
);
|
||||
|
||||
R_UNLESS(result.success, 0x1);
|
||||
R_UNLESS(result.success, Result_MainFailedToDownloadUpdate);
|
||||
}
|
||||
|
||||
ON_SCOPE_EXIT(fs.DeleteFile(zip_out));
|
||||
|
||||
// 2. extract the zip
|
||||
if (!pbox->ShouldExit()) {
|
||||
auto zfile = unzOpen64(zip_out);
|
||||
R_UNLESS(zfile, 0x1);
|
||||
ON_SCOPE_EXIT(unzClose(zfile));
|
||||
const auto exe_path = App::GetExePath();
|
||||
bool found_exe{};
|
||||
|
||||
unz_global_info64 pglobal_info;
|
||||
if (UNZ_OK != unzGetGlobalInfo64(zfile, &pglobal_info)) {
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
for (s64 i = 0; i < pglobal_info.number_entry; i++) {
|
||||
if (i > 0) {
|
||||
if (UNZ_OK != unzGoToNextFile(zfile)) {
|
||||
log_write("failed to unzGoToNextFile\n");
|
||||
R_THROW(0x1);
|
||||
}
|
||||
R_TRY(thread::TransferUnzipAll(pbox, zip_out, &fs, "/", [&](const fs::FsPath& name, fs::FsPath& path) -> bool {
|
||||
if (std::strstr(path, "sphaira.nro")) {
|
||||
path = exe_path;
|
||||
found_exe = true;
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
|
||||
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
|
||||
log_write("failed to open current file\n");
|
||||
R_THROW(0x1);
|
||||
}
|
||||
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
|
||||
|
||||
unz_file_info64 info;
|
||||
fs::FsPath file_path;
|
||||
if (UNZ_OK != unzGetCurrentFileInfo64(zfile, &info, file_path, sizeof(file_path), 0, 0, 0, 0)) {
|
||||
log_write("failed to get current info\n");
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
if (file_path[0] != '/') {
|
||||
file_path = fs::AppendPath("/", file_path);
|
||||
}
|
||||
|
||||
if (std::strstr(file_path, "sphaira.nro")) {
|
||||
file_path = App::GetExePath();
|
||||
}
|
||||
|
||||
Result rc;
|
||||
if (file_path[std::strlen(file_path) -1] == '/') {
|
||||
if (R_FAILED(rc = fs.CreateDirectoryRecursively(file_path)) && rc != FsError_PathAlreadyExists) {
|
||||
log_write("failed to create folder: %s 0x%04X\n", file_path.s, rc);
|
||||
R_THROW(rc);
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
R_THROW(rc);
|
||||
// check if we have sphaira installed in other locations and update them.
|
||||
if (found_exe) {
|
||||
for (auto& path : SPHAIRA_PATHS) {
|
||||
log_write("[UPD] checking path: %s\n", path.s);
|
||||
// skip if we already updated this path.
|
||||
if (exe_path == path) {
|
||||
log_write("[UPD] skipped as already updated\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
fs::File f;
|
||||
R_TRY(fs.OpenFile(file_path, FsOpenMode_Write, &f));
|
||||
R_TRY(f.SetSize(info.uncompressed_size));
|
||||
|
||||
std::vector<char> buf(chunk_size);
|
||||
s64 offset{};
|
||||
while (offset < info.uncompressed_size) {
|
||||
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
|
||||
if (bytes_read <= 0) {
|
||||
// log_write("failed to read zip file: %s\n", inzip.c_str());
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
R_TRY(f.Write(offset, buf.data(), bytes_read, FsWriteOption_None));
|
||||
|
||||
pbox->UpdateTransfer(offset, info.uncompressed_size);
|
||||
offset += bytes_read;
|
||||
}
|
||||
}
|
||||
|
||||
// check if we have sphaira installed in other locations and update them.
|
||||
if (file_path == App::GetExePath()) {
|
||||
for (auto& path : SPHAIRA_PATHS) {
|
||||
log_write("[UPD] checking path: %s\n", path.s);
|
||||
// skip if we already updated this path.
|
||||
if (file_path == path) {
|
||||
log_write("[UPD] skipped as already updated\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
// check that this is really sphaira.
|
||||
log_write("[UPD] checking nacp\n");
|
||||
NacpStruct nacp;
|
||||
if (R_SUCCEEDED(nro_get_nacp(path, nacp)) && !std::strcmp(nacp.lang[0].name, "sphaira")) {
|
||||
log_write("[UPD] found, updating\n");
|
||||
pbox->NewTransfer(path);
|
||||
R_TRY(pbox->CopyFile(&fs, file_path, path));
|
||||
}
|
||||
// check that this is really sphaira.
|
||||
log_write("[UPD] checking nacp\n");
|
||||
NacpStruct nacp;
|
||||
if (R_SUCCEEDED(nro_get_nacp(path, nacp)) && !std::strcmp(nacp.lang[0].name, "sphaira")) {
|
||||
log_write("[UPD] found, updating\n");
|
||||
pbox->NewTransfer(path);
|
||||
R_TRY(pbox->CopyFile(&fs, exe_path, path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
|
||||
if (fixed) { \
|
||||
start_x -= fixed; \
|
||||
} else { \
|
||||
gfx::textBounds(vg, 0, 0, bounds, __VA_ARGS__); \
|
||||
gfx::textBoundsArgs(vg, 0, 0, bounds, __VA_ARGS__); \
|
||||
start_x -= spacing + (bounds[2] - bounds[0]); \
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
|
||||
|
||||
gfx::drawTextArgs(vg, 80, start_y, 28.f, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT), m_title.c_str());
|
||||
m_scroll_title_sub_heading.Draw(vg, true, title_sub_x, start_y, text_w - title_sub_x, 16, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), m_title_sub_heading.c_str());
|
||||
m_scroll_sub_heading.Draw(vg, true, 80, 685, text_w - 80, 18, NVG_ALIGN_LEFT, theme->GetColour(ThemeEntryID_TEXT), m_sub_heading.c_str());
|
||||
m_scroll_sub_heading.Draw(vg, true, 80, 685, text_w - 160, 18, NVG_ALIGN_LEFT, theme->GetColour(ThemeEntryID_TEXT), m_sub_heading.c_str());
|
||||
}
|
||||
|
||||
void MenuBase::SetTitle(std::string title) {
|
||||
|
||||
89
sphaira/source/ui/menus/mtp_menu.cpp
Normal file
@@ -0,0 +1,89 @@
|
||||
#include "ui/menus/mtp_menu.hpp"
|
||||
#include "usb/usbds.hpp"
|
||||
#include "app.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "haze_helper.hpp"
|
||||
|
||||
namespace sphaira::ui::menu::mtp {
|
||||
namespace {
|
||||
|
||||
auto GetUsbStateStr(UsbState state) -> const char* {
|
||||
switch (state) {
|
||||
case UsbState_Detached: return "Detached";
|
||||
case UsbState_Attached: return "Attached";
|
||||
case UsbState_Powered: return "Powered";
|
||||
case UsbState_Default: return "Default";
|
||||
case UsbState_Address: return "Address";
|
||||
case UsbState_Configured: return "Configured";
|
||||
case UsbState_Suspended: return "Suspended";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
auto GetUsbSpeedStr(UsbDeviceSpeed speed) -> const char* {
|
||||
// todo: remove this cast when libnx pr is merged.
|
||||
switch ((u32)speed) {
|
||||
case UsbDeviceSpeed_None: return "None";
|
||||
case UsbDeviceSpeed_Low: return "USB 1.0 Low Speed";
|
||||
case UsbDeviceSpeed_Full: return "USB 1.1 Full Speed";
|
||||
case UsbDeviceSpeed_High: return "USB 2.0 High Speed";
|
||||
case UsbDeviceSpeed_Super: return "USB 3.0 Super Speed";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Menu::Menu(u32 flags) : stream::Menu{"MTP Install"_i18n, flags} {
|
||||
m_was_mtp_enabled = App::GetMtpEnable();
|
||||
if (!m_was_mtp_enabled) {
|
||||
log_write("[MTP] wasn't enabled, forcefully enabling\n");
|
||||
App::SetMtpEnable(true);
|
||||
}
|
||||
|
||||
haze::InitInstallMode(
|
||||
[this](const char* path){ return OnInstallStart(path); },
|
||||
[this](const void *buf, size_t size){ return OnInstallWrite(buf, size); },
|
||||
[this](){ return OnInstallClose(); }
|
||||
);
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
// signal for thread to exit and wait.
|
||||
haze::DisableInstallMode();
|
||||
|
||||
if (!m_was_mtp_enabled) {
|
||||
log_write("[MTP] disabling on exit\n");
|
||||
App::SetMtpEnable(false);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
stream::Menu::Update(controller, touch);
|
||||
|
||||
static TimeStamp poll_ts;
|
||||
if (poll_ts.GetSeconds() >= 1) {
|
||||
poll_ts.Update();
|
||||
|
||||
UsbState state{UsbState_Detached};
|
||||
usbDsGetState(&state);
|
||||
|
||||
UsbDeviceSpeed speed{(UsbDeviceSpeed)UsbDeviceSpeed_None};
|
||||
usbDsGetSpeed(&speed);
|
||||
|
||||
char buf[128];
|
||||
std::snprintf(buf, sizeof(buf), "State: %s | Speed: %s", i18n::get(GetUsbStateStr(state)).c_str(), i18n::get(GetUsbSpeedStr(speed)).c_str());
|
||||
SetSubHeading(buf);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::OnDisableInstallMode() {
|
||||
haze::DisableInstallMode();
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::mtp
|
||||
1483
sphaira/source/ui/menus/save_menu.cpp
Normal file
@@ -11,11 +11,12 @@
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "swkbd.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "threaded_file_transfer.hpp"
|
||||
#include "image.hpp"
|
||||
|
||||
#include <minIni.h>
|
||||
#include <stb_image.h>
|
||||
#include <cstring>
|
||||
#include <minizip/unzip.h>
|
||||
#include <yyjson.h>
|
||||
#include "yyjson_helper.hpp"
|
||||
|
||||
@@ -134,19 +135,14 @@ auto loadThemeImage(ThemeEntry& e) -> bool {
|
||||
}
|
||||
auto vg = App::GetVg();
|
||||
|
||||
fs::FsNativeSd fs;
|
||||
std::vector<u8> image_buf;
|
||||
|
||||
const auto path = apiBuildIconCache(e);
|
||||
if (R_FAILED(fs.read_entire_file(path, image_buf))) {
|
||||
log_write("failed to load image from file: %s\n", path.s);
|
||||
} else {
|
||||
int channels_in_file;
|
||||
auto buf = stbi_load_from_memory(image_buf.data(), image_buf.size(), &image.w, &image.h, &channels_in_file, 4);
|
||||
if (buf) {
|
||||
ON_SCOPE_EXIT(stbi_image_free(buf));
|
||||
image.image = nvgCreateImageRGBA(vg, image.w, image.h, 0, buf);
|
||||
}
|
||||
TimeStamp ts;
|
||||
const auto data = ImageLoadFromFile(path, ImageFlag_JPEG);
|
||||
if (!data.data.empty()) {
|
||||
image.w = data.w;
|
||||
image.h = data.h;
|
||||
image.image = nvgCreateImageRGBA(vg, data.w, data.h, 0, data.data.data());
|
||||
log_write("\t[image load] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
|
||||
}
|
||||
|
||||
if (!image.image) {
|
||||
@@ -222,7 +218,6 @@ void from_json(const fs::FsPath& path, PackList& e) {
|
||||
|
||||
auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> Result {
|
||||
static const fs::FsPath zip_out{"/switch/sphaira/cache/themezer/temp.zip"};
|
||||
constexpr auto chunk_size = 1024 * 512; // 512KiB
|
||||
|
||||
fs::FsNativeSd fs;
|
||||
R_TRY(fs.GetFsOpenResult());
|
||||
@@ -243,7 +238,7 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> Result {
|
||||
|
||||
if (!result.success || result.data.empty()) {
|
||||
log_write("error with download: %s\n", url.c_str());
|
||||
R_THROW(0x1);
|
||||
R_THROW(Result_ThemezerFailedToDownloadThemeMeta);
|
||||
}
|
||||
|
||||
from_json(result.data, download_pack);
|
||||
@@ -260,7 +255,7 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> Result {
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||
);
|
||||
|
||||
R_UNLESS(result.success, 0x1);
|
||||
R_UNLESS(result.success, Result_ThemezerFailedToDownloadTheme);
|
||||
}
|
||||
|
||||
ON_SCOPE_EXIT(fs.DeleteFile(zip_out));
|
||||
@@ -272,66 +267,7 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> Result {
|
||||
|
||||
// 3. extract the zip
|
||||
if (!pbox->ShouldExit()) {
|
||||
auto zfile = unzOpen64(zip_out);
|
||||
R_UNLESS(zfile, 0x1);
|
||||
ON_SCOPE_EXIT(unzClose(zfile));
|
||||
|
||||
unz_global_info64 pglobal_info;
|
||||
if (UNZ_OK != unzGetGlobalInfo64(zfile, &pglobal_info)) {
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < pglobal_info.number_entry; i++) {
|
||||
if (i > 0) {
|
||||
if (UNZ_OK != unzGoToNextFile(zfile)) {
|
||||
log_write("failed to unzGoToNextFile\n");
|
||||
R_THROW(0x1);
|
||||
}
|
||||
}
|
||||
|
||||
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
|
||||
log_write("failed to open current file\n");
|
||||
R_THROW(0x1);
|
||||
}
|
||||
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");
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
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) {
|
||||
log_write("failed to create file: %s 0x%04X\n", file_path.s, rc);
|
||||
R_THROW(rc);
|
||||
}
|
||||
|
||||
fs::File f;
|
||||
R_TRY(fs.OpenFile(file_path, FsOpenMode_Write, &f));
|
||||
R_TRY(f.SetSize(info.uncompressed_size));
|
||||
|
||||
std::vector<char> buf(chunk_size);
|
||||
s64 offset{};
|
||||
while (offset < info.uncompressed_size) {
|
||||
R_TRY(pbox->ShouldExitResult());
|
||||
|
||||
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
|
||||
if (bytes_read <= 0) {
|
||||
// log_write("failed to read zip file: %s\n", inzip.c_str());
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
R_TRY(f.Write(offset, buf.data(), bytes_read, FsWriteOption_None));
|
||||
|
||||
pbox->UpdateTransfer(offset, info.uncompressed_size);
|
||||
offset += bytes_read;
|
||||
}
|
||||
}
|
||||
R_TRY(thread::TransferUnzipAll(pbox, zip_out, &fs, dir_path));
|
||||
}
|
||||
|
||||
log_write("finished install :)\n");
|
||||
@@ -645,8 +581,8 @@ void Menu::PackListDownload() {
|
||||
curl::Flags{curl::Flag_Cache},
|
||||
curl::StopToken{this->GetToken()},
|
||||
curl::OnComplete{[this, page_index](auto& result){
|
||||
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
|
||||
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
|
||||
App::SetBoostMode(true);
|
||||
ON_SCOPE_EXIT(App::SetBoostMode(false));
|
||||
|
||||
log_write("got themezer data\n");
|
||||
if (!result.success) {
|
||||
|
||||