51 Commits

Author SHA1 Message Date
ITotalJustice
aeb6c64077 bump version for new release 0.10.3 -> 0.11.0 2025-05-27 17:44:48 +01:00
⭐️NINIKA⭐️
9050229b12 Change NSP padding logic to follow nxdt and nsfw validation (#147) 2025-05-27 17:05:28 +01:00
ITotalJustice
af4aa836a9 add gamecard trim detection. remove duplicate value in error box. 2025-05-27 15:35:52 +01:00
ITotalJustice
fb2272546a bump usbhsfs git tag to include hack for adaptor mounting. fix applet mode text being draw at wrong offset. 2025-05-27 00:55:26 +01:00
ITotalJustice
4b25b2c5eb use usb mount flags to detect if its read only, rather than checking global config. 2025-05-26 23:18:14 +01:00
ITotalJustice
c11d9edc4e add support for changing the left-side menu.
fixes #148
2025-05-26 22:55:06 +01:00
ITotalJustice
e2a5454263 set fs view to the right side when split mode is enabled. 2025-05-26 20:27:54 +01:00
ITotalJustice
3ca82463cc mount hdd by default, add hdd write protect option. 2025-05-26 19:25:09 +01:00
ITotalJustice
793b36fd59 add hotkey for file browser to jump to advanced options (R2 + X). 2025-05-26 18:01:06 +01:00
ITotalJustice
66fe526754 add option to label trimmed xci dumps (disabled by default). 2025-05-26 17:55:54 +01:00
ITotalJustice
7c45d60e60 add nxmp and switch wave file assoc, remove old sphaira file assoc, replace ext/db assoc parse code with views::split 2025-05-26 17:42:32 +01:00
ITotalJustice
a9931a975d many more optimisations. cleaned up fs code. bug fixes etc (see below).
- fix usb using the wrong year when polling the timestamp.
- fs file/dir has been re-written to allow for simplified calling and remove the need of manually closing.
- add SetSize for stdio by using ftruncate.
- don't truncate the file when opened in stdio.
- add getcount api for stdio.
- display file/dir count in filebrowser for non-native fs.
- allow hash to be used on non-native fs.
- slightly optimise nro parsing by manually calculating nro size rather than doing an os call.
- slightly optimise nro parsing by keeping the fs struct alive between calls, rather than creating a new one on the stack.
- fix filebrowser peeking into zip files that are stored on non-sd fs.
- set the timestamp of a file moved to a stdio location (cut/paste).
- slightly optimise daybreak update folder detection by skipping opening/polling the dir size.
- set the fullpath of the file thats being hashed in progress box.
2025-05-26 17:06:04 +01:00
ITotalJustice
3e3ec71329 default to having boost mode enabled for progress bar 2025-05-26 01:46:28 +01:00
ITotalJustice
49abdc0590 hotkey R2 in filebrowser for when pressed along with L2, select all files. 2025-05-25 21:06:14 +01:00
ITotalJustice
4f931d2991 Merge branch 'master' into dev 2025-05-25 21:00:51 +01:00
ITotalJustice
f7c5ccfa87 huge optimisations (see below). Build with c++26 and c23.
- build with c++26 and c23, fixes warnings due to this change.
- use #embed over romfs where applicable.
- load all configs upfront in the app menu, massively improves boot time
- enable boost mode during load/scan time in all (slow loading) menus, huge load time improvement.
- enable boost mode when exiting the app, to speed up closing all the menus and saving the config.
- reduce the size of the nro nacp when loading to just the title + author + display version.
- add option to enable boost mode for all progress bar menus, huge perf improvement again.
- remove unused launch_count var from the playlog file.
- display full path when dumping.
- optimise appstore unzip code by iterating through the zip rather than finding a specific file, reduces retroarch extract time from 52s to 26s.

overall, this commit has reduced boot time from 0.4s to 0.3s and massively increased load times of other menus.
(it also reduced the binary size by 4kb, so yay)
2025-05-25 20:57:03 +01:00
ITotalJustice
5ce23f29fa GC add initial data and UID dumping (credit to nxdumptool). Fix gc bug where ns wasn't closed. Fix usb s2s being an option if dumping the bins. Always remount GC after storage has been mounted. 2025-05-25 13:24:03 +01:00
shadow2560
d13ad64099 Update french translation (#149)
Signed-off-by: shadow2560 <24191064+shadow2560@users.noreply.github.com>
2025-05-25 11:23:00 +01:00
ITotalJustice
be88bdb567 initial work towards support standard nsp spec.
see #147
2025-05-24 23:56:42 +01:00
ITotalJustice
1bff57f9c9 Merge branch 'master' into dev 2025-05-24 23:40:52 +01:00
Yorunokyujitsu
4172d5d5b6 Added new strings and update Korean, Japanese lang. (#146)
* Added new strings and update Korean, Japanese lang.

* Update ko.json, ja.json.
2025-05-24 23:32:38 +01:00
ITotalJustice
1cdea981de workaround for time / battery % x position changing every few seconds.
fixes #142
2025-05-24 23:13:53 +01:00
ITotalJustice
22ebfd4a82 fs display hdd at the top of mount list for quick access, change xci dump folder to Gamecard. 2025-05-24 22:31:24 +01:00
ITotalJustice
798ac47487 limit fs hash to a single file. 2025-05-24 22:17:12 +01:00
ITotalJustice
fba8051007 use feof and ferror to detect errors with stdio fs stream. 2025-05-24 22:14:06 +01:00
ITotalJustice
15721b8e8a split screen mode for fs. fix game dump. scrolling text for fs, progress, menu base. display icon when dumping. 2025-05-24 21:55:10 +01:00
ITotalJustice
d43ca37875 add file hashing to the file browser (crc32, md5, sha1, sha256) 2025-05-23 17:02:35 +01:00
ITotalJustice
1b5e7401f2 use stdio for minini to benefit from buffering (reduces startup by 300ms), reduce fatfs size by 60kb. 2025-05-23 14:51:49 +01:00
ITotalJustice
d8b2896bed fix native dump, fix xci dumps going to nsp folder.
native dump broke with prev commit due to not using the sd card, instead using an empty fs.
2025-05-23 13:07:00 +01:00
ITotalJustice
6475f4316a simplify stdio/native file paths by sharing the same code. 2025-05-23 12:44:56 +01:00
ITotalJustice
93c38da742 add support for mounting stdio (hdd) in the file browser. 2025-05-23 12:23:28 +01:00
ITotalJustice
8070268d2a add hdd dump support, cleanup dump code into a single generic file. 2025-05-22 15:50:50 +01:00
ITotalJustice
2e6d757852 update usbds_getspeed to match pr, fix appstore install returning a bool instead of Result. 2025-05-21 21:52:54 +01:00
ITotalJustice
a91550174a add dump options, allow for gamecard menu to be accessed without install enabled. 2025-05-21 19:52:22 +01:00
ITotalJustice
52b166932d Merge branch 'master' into dev 2025-05-21 17:42:07 +01:00
ITotalJustice
d50bcb650f fix crash if nro has corrupted asset entry, bump version for new release 0.10.2 -> 0.10.3
the nro that caused this was ClkrstQuery.nro

fixes #141
2025-05-21 17:29:18 +01:00
ITotalJustice
654f3a1446 replace progress box result from bool to Result. Display error box if progress box fails. 2025-05-21 16:50:18 +01:00
ITotalJustice
71415e5044 remove unused last_launch | prev_timestamp storing / fetching. 2025-05-21 15:32:42 +01:00
ITotalJustice
da33b9a6b9 add usbds speed / max packet detection, add zlt support for usbds, fix game usb transfer bug (see below).
due to the previous commit, i broke dumping multiple games via usb as the stream offset wasn't reset.
because of this, the first transfer would complete, but the 2nd one would fail.
2025-05-21 14:43:18 +01:00
ITotalJustice
fe2a1a3a80 add support for streamed usb upload, multi thread usb uploads / dumps.
Some notes i made when adding stream support:
The tinfoil API makes it hard / impossible to multi thread the file upload because each data transfer passes the the file name and offset, meaning that it can and will change files and offset in the middle of a transfer
So that prevents the read thread from running freely in the background and the pull thread pulling data when requested.
The extension adds a flag to the usb header which if set, enables stream mode (same as ftp installs). This removes random access, but allows for multi threading as the data will be requested in order.
2025-05-21 13:19:46 +01:00
ITotalJustice
a67171e2b8 disable sleep whilst downloading, uploading, using usb, inside ftp menu. 2025-05-20 23:33:51 +01:00
ITotalJustice
cf908d63b9 backport controller led flashing from ftpsrv (only flash 1 controller). 2025-05-20 23:13:20 +01:00
ITotalJustice
ef25c3edc7 multi thread game dumps and file uploads.
previous uploads were all single threaded, which meant that it only uploaded as fast as the slowest source.
usb transfer is still single threaded due it being random access for both files and data, making it
hard for the read thread to run freely.
2025-05-20 22:50:05 +01:00
ITotalJustice
f956adabc3 add xci dumping, add xci cert and id set dump.
still missing uid and initial dumping.
2025-05-20 17:56:55 +01:00
⭐️NINIKA⭐️
4244be9592 Fix handling of unicode filenames in usb_install_pc.py (#143) 2025-05-20 12:45:59 +01:00
Ny'hrarr
a7fc19e28a Pt patches (#140)
* Update pt.json

* Translate new keys

* Translate FTP, USB and GameCard menus

* Update pt.json

* Update pt.json

* Add missing comma and translate more entries

* Fix case

* Update pt.json

* Update pt.json
2025-05-20 09:00:39 +01:00
ITotalJustice
cf192fca85 fix hbmenu not being updated due to faulty string compare, bump version 0.10.1 -> 0.10.2 2025-05-19 20:34:22 +01:00
xHR
041bb2bbe5 Added Ukranian language (#139) 2025-05-19 19:57:15 +01:00
ITotalJustice
df558d5dcc bump version for new release 0.10.0 -> 0.10.1 2025-05-19 17:06:49 +01:00
ITotalJustice
33de03a923 fix sd card dumps due to the folder not being created. 2025-05-19 17:04:49 +01:00
ITotalJustice
1000b9c8ec fix sphaira not detecting latest update as we went from 0.9 to 0.10. 2025-05-19 16:23:45 +01:00
100 changed files with 9465 additions and 4674 deletions

View File

@@ -96,4 +96,5 @@ The output will be found in `build/MinSizeRel/sphaira.nro`
- GBATemp
- hb-appstore
- haze
- nxdumptool (for gamecard bin dumping and rsa verify code)
- Everyone who has contributed to this project!

View File

Before

Width:  |  Height:  |  Size: 569 B

After

Width:  |  Height:  |  Size: 569 B

View File

Before

Width:  |  Height:  |  Size: 703 B

After

Width:  |  Height:  |  Size: 703 B

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 783 B

After

Width:  |  Height:  |  Size: 783 B

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,2 @@
[config]
supported_extensions=mp3|ogg|flac|wav|aac|ac3|aif|asf|mp4|mkv|m3u|m3u8|hls|vob|avi|dv|flv|m2ts|m2v|m4a|mov|mpeg|mpg|mts|swf|ts|vob|wma|wmv|png|jpg|jpeg|bmp|gif

View File

@@ -0,0 +1,2 @@
[config]
supported_extensions=mp3|ogg|flac|wav|aac|ac3|aif|asf|mp4|mkv|m3u|m3u8|hls|vob|avi|dv|flv|m2ts|m2v|m4a|mov|mpeg|mpg|mts|swf|ts|vob|wma|wmv|png|jpg|jpeg|bmp|gif

View File

@@ -1,2 +0,0 @@
[config]
ext=nro

View File

@@ -1,72 +1,24 @@
{
"[Applet Mode]": " | Applet Modus |",
"No Internet": "Kein Internet",
"Files": "Dateien",
"Apps": "hb-Apps",
"Store": "hb-Store",
"Menu": "Menü",
"Options": "Optionen",
"OK": "OK",
"Back": "Zurück",
"Select": "Auswählen",
"Open": "Öffne",
"Launch": "Starte",
"Info": "Info",
"Install": "Installieren",
"Delete": "Löschen",
"Restart": "Neustart",
"Changelog": "Neuerungen",
"Details": "Details",
"Update": "Update",
"Remove": "Entfernen",
"Restore": "Wiederherstellen",
"Download": "Download",
"Next Page": "Nächste Seite",
"Prev Page": "Vorherige Seite",
"Unstar": "Kein Favorit",
"Star": "Favorit",
"System memory": "NAND Systemspeicher",
"microSD card": "SD-Karte",
"Sd": "SD-Karte | Root-Verzeichnis",
"Image System memory": "Album | NAND Systemspeicher",
"Image microSD card": "Album | SD-Karte",
"Slow": "Niedrig",
"Normal": "Mittel",
"Fast": "Hoch",
"Yes": "Ja",
"No": "Nein",
"Enabled": "An",
"Disabled": "Aus",
"Sort By": "Sortierung",
"Sort Options": " Sortierung | Optionen",
"Filter": "Rubrik",
"Sort": "Sortiert nach",
"Order": "Anordnung",
"Search": "Suchen",
"Updated": "zuletzt aktualisiert",
"Updated (Star)": "Favorit | zuletzt aktualisiert",
"Downloads": "Downloads",
"Size": "Größe",
"Size (Star)": "Favorit | Größe",
"Alphabetical": "Name",
"Alphabetical (Star)": "Favorit | Name",
"Likes": "Beliebtheit",
"ID": "Theme | Paket ID",
"Descending": "Absteigend ↓",
"Descending (down)": "Absteigend ↓",
"Desc": " ↓",
"Ascending": "Aufsteigend ↑",
"Ascending (Up)": "Aufsteigend ↑",
"Asc": " ↑",
"Switch-Handheld!": "Handheld!",
"Switch-Docked!": "Angedockt!",
"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": "",
"If this message appears repeatedly, please open an issue.": "Bei wiederholtem Auftreten bitte Issue erstellen.",
"Menu Options": " Menü | Optionen",
"Menu": "Menü",
"Theme": "Themes",
"Theme Options": " Themes | Optionen",
"Select Theme": "Theme wählen",
"Shuffle": "Zufällig",
"Music": "Musik",
"12 Hour Time": "12-Std Zeitformat",
"Download Default Music": "",
"Failed to download default_music.bfstm, please try again": "",
"Overwrite current default music?": "",
"Network": "Konnektivität",
"Network Options": "Konnektivität | Optionen",
"Ftp": "FTP",
@@ -75,8 +27,7 @@
"Nxlink Connected": "NXLink | Verbunden",
"Nxlink Upload": "NXLink | wird hochgeladen...",
"Nxlink Finished": "NXLink | Hochladen beendet",
"Switch-Handheld!": "Handheld!",
"Switch-Docked!": "Angedockt!",
"Language": "Sprache",
"Auto": "Systemsprache",
"English": "English",
@@ -92,87 +43,101 @@
"Russian": "Русский",
"Swedish": "Svenska",
"Vietnamese": "tiếng Việt",
"Logging": "Protokollieren",
"Replace hbmenu on exit": "hbmenu durch sphaira ersetzen",
"Ukrainian": "Українська",
"Misc": "Extras",
"Misc Options": " Extras | Optionen",
"Web": "WEB Browser",
"Install forwarders": "Forwarder installieren",
"Install location": "Einhängepunkt",
"Show install warning": "Warnungen anzeigen",
"Text scroll speed": "Laufschrift Tempo",
"FileBrowser": "Datei-Manager",
"%zd files": "%zd Dateien",
"%zd dirs": "%zd Ordner",
"File Options": "Datei - Ordner | Optionen",
"Show Hidden": "Versteckte zeigen",
"Folders First": "Ordner zuerst",
"Hidden Last": "Versteckte zuletzt",
"Cut": "Ausschneiden",
"Copy": "Kopieren",
"Paste": "Einfügen",
"Paste ": "Einfügen von: ",
" file(s)?": " Datei/en?",
"Rename": "Umbenennen",
"Set New File Name": "Neuen Dateinamen festlegen",
"Advanced": "Erweitert...",
"Advanced Options": " Erweitert | Optionen",
"Create File": "Neue Datei",
"Set File Name": "Dateiname festlegen",
"Create Folder": "Neuer Ordner",
"Set Folder Name": "Ordner umbenennen",
"View as text (unfinished)": "Als Text anzeigen",
"Ignore read only": "Schreibschutz umgehen?",
"Mount": "Einhängen",
"Empty...": "Keine Daten...",
"Open with DayBreak?": "Mit Daybreak öffnen?",
"Launch ": "Starte ",
"Launch option for: ": "Start Option für: ",
"Select launcher for: ": "Wähle Launcher für: ",
"Homebrew": "hbmenu",
"Homebrew Options": " hbmenu | Optionen",
"Hide Sphaira": "Verstecke sphaira",
"Install Forwarder": "Forwarder installieren",
"WARNING: Installing forwarders will lead to a ban!": "Installiere Forwarder-NSP´s mit VORSICHT.\nEs erhöht das Risiko eines Konsolen-Banns!",
"Installing Forwarder": "Installiere Forwarder",
"Creating Program": "Erstelle Programm",
"Creating Control": "Erstelle Control",
"Creating Meta": "Erstelle Meta",
"Writing Nca": "Schreibe NCA",
"Updating ncm databse": "Aktualisiere NCM-Datenbank",
"Pushing application record": "Übertrage Anwendungsdaten",
"Installed!": "Installiert!",
"Failed to install forwarder": "Fehler beim installieren des Forwarders",
"Unstarred ": "Favorit entfernt ",
"Starred ": "Favorit ",
"AppStore": "hb-AppStore",
"Filter: %s | Sort: %s | Order: %s": "Rubrik: %s | Sort.nach.: %s | Ordnung: %s",
"AppStore Options": " hb-AppStore | Optionen",
"All": "Alles anzeigen",
"Games": "Spiele",
"Emulators": "Emulatoren",
"Tools": "Tools",
"Themes": "Themes",
"Legacy": "Älteres",
"version: %s": "Version: %s",
"updated: %s": "Letztes Update am: %s",
"category: %s": "Rubrik: %s",
"extracted: %.2f MiB": "Größe: %.2f MiB",
"app_dls: %s": "Anzahl Downloads: %s",
"More by Author": "Weitere Apps des Entwicklers",
"Leave Feedback": "Feedback hinterlassen",
"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!": "",
"Themezer": "Themezer | NX Themes",
"Themezer Options": " Themezer | Optionen",
"Nsfw": "NSFW",
"Page": "Seiten Nr. wählen ",
"Page %zu / %zu": " %zu / %zu",
"Enter Page Number": "Zu Seite Nr.: ___",
"Bad Page": "Seite nicht gefunden",
"Download theme?": "Theme herunterladen?",
"GitHub": "GitHub",
"Downloading json": "Lade JSON-File",
"Select asset to download for ": "Wähle Asset für den Download von ",
"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!": "",
"IRS (Infrared Joycon Camera)": "",
"IRS": "",
"Irs": "IR-Sensor",
"Ambient Noise Level: ": "Umgebungsrauschen: ",
"Controller": "Controller",
"Pad ": "Pad ",
"HandHeld": "Handheld",
" (Available)": " (Verfügbar)",
" (Unsupported)": " (Nicht unterstützt)",
" (Unconnected)": " (Nicht verbunden)",
"HandHeld": "Handheld",
"Rotation": "Rotation",
"0 (Sideways)": "0° (Seitlich)",
"90 (Flat)": "90° (Flach)",
@@ -194,64 +159,234 @@
"Normal image": "Normal-Bild",
"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",
"Trimming Format": "Beschnitt-Format",
"External Light Filter": "Externes Lichtfilter",
"Load Default": "Standard laden",
"Themezer": "Themezer | NX Themes",
"Themezer Options": " Themezer | Optionen",
"Nsfw": "NSFW",
"Page": "Seiten Nr. wählen ",
"Page %zu / %zu": " %zu / %zu",
"Enter Page Number": "Zu Seite Nr.: ___",
"Bad Page": "Seite nicht gefunden",
"Download theme?": "Theme herunterladen?",
"GitHub": "GitHub",
"Downloading json": "Lade JSON-File",
"Select asset to download for ": "Wähle Asset für den Download von ",
"Installing ": "Installiert wird: ",
"Uninstalling ": "Deinstalliert wird: ",
"Deleting ": "Gelöscht wird: ",
"Deleting": "Gelöscht wurde:",
"Pasting ": "Eingefügt wird: ",
"Pasting": "Eingefügt wurde:",
"Removing ": "Entfernt wird: ",
"Scanning ": "Gescannt wird: ",
"Creating ": "Erstellt wird: ",
"Copying ": "Kopiert wird: ",
"Trying to load ": "Versucht zu laden wird: ",
"Downloading ": "Heruntergeladen wird: ",
"Downloaded ": "Heruntergeladen wurde: ",
"Removed ": "Entfernt wurde: ",
"Checking MD5": "Checke MD5 Prüfsumme",
"Loading...": "Wird geladen...",
"Loading": "Wird geladen",
"Empty!": "Keine Daten!",
"Not Ready...": "Nicht bereit...",
"Error loading page!": "Ladefehler!",
"Update avaliable: ": "Update verfügbar: ",
"Download update: ": " Herunterladen des Updates: ",
"Updated to ": "Aktualisiert auf: ",
"Press OK to restart Sphaira": "Drücke OK um sphaira erneut zustarten",
"Restart Sphaira?": "sphaira erneut starten?",
"Failed to download update": "Herunterladen des Updates fehlgeschlagen!",
"Advanced": "Erweitert...",
"Advanced Options": " Erweitert | Optionen",
"Logging": "Protokollieren",
"Replace hbmenu on exit": "hbmenu durch sphaira ersetzen",
"Restore hbmenu?": "hbmenu wiederherstellen?",
"Restore": "Wiederherstellen",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Die Datei /switch/hbmenu.nro fehlt.\nInstalliere hbmenu über den hb-AppStore.",
"Failed to restore hbmenu, please re-download hbmenu": "Fehler, hbmenu nicht wiederhergrstellt!\nInstalliere hbmenu über den hb-AppStore.",
"Failed to restore hbmenu, using sphaira instead": "Fehler, hbmenu nicht wiederhergrstellt!\nVerwende weiter sphaira",
"Restored hbmenu, closing sphaira": "hbmenu wurde wiederhergestellt, schließe sphaira",
"Restored hbmenu": "hbmenu wurde wiederhergestellt",
"Restart Sphaira?": "sphaira erneut starten?",
"Press OK to restart Sphaira": "Drücke OK um sphaira erneut zustarten",
"Text scroll speed": "Laufschrift Tempo",
"Slow": "Niedrig",
"Normal": "Mittel",
"Fast": "Hoch",
"Set right-side menu": "",
"Install options": "",
"Install Options": "",
"Enable sysmmc": "",
"Enable emummc": "",
"Show install warning": "Warnungen anzeigen",
"Install location": "Einhängepunkt",
"System memory": "NAND Systemspeicher",
"microSD card": "SD-Karte",
"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": "",
"Homebrew": "hbmenu",
"Apps": "hb-Apps",
"Homebrew Options": " hbmenu | Optionen",
"Hide Sphaira": "Verstecke sphaira",
"Install Forwarder": "Forwarder installieren",
"WARNING: Installing forwarders will lead to a ban!": "Installiere Forwarder-NSP´s mit VORSICHT.\nEs erhöht das Risiko eines Konsolen-Banns!",
"Installing Forwarder": "Installiere Forwarder",
"Creating Program": "Erstelle Programm",
"Creating Control": "Erstelle Control",
"Creating Meta": "Erstelle Meta",
"Writing Nca": "Schreibe NCA",
"Updating ncm databse": "Aktualisiere NCM-Datenbank",
"Pushing application record": "Übertrage Anwendungsdaten",
"Failed to install forwarder": "Fehler beim installieren des Forwarders",
"Unstarred ": "Favorit entfernt ",
"Starred ": "Favorit ",
"Failed to remove old forwarder, please manually remove it!": "",
"AppStore": "hb-AppStore",
"Appstore": "",
"Store": "hb-Store",
"Filter: %s | Sort: %s | Order: %s": "Rubrik: %s | Sort.nach.: %s | Ordnung: %s",
"AppStore Options": " hb-AppStore | Optionen",
"Info": "Info",
"Changelog": "Neuerungen",
"Details": "Details",
"version: %s": "Version: %s",
"updated: %s": "Letztes Update am: %s",
"category: %s": "Rubrik: %s",
"extracted: %.2f MiB": "Größe: %.2f MiB",
"app_dls: %s": "Anzahl Downloads: %s",
"More by Author": "Weitere Apps des Entwicklers",
"Leave Feedback": "Feedback hinterlassen",
"FileBrowser": "Datei-Manager",
"Files": "Dateien",
"%zd files": "%zd Dateien",
"%zd dirs": "%zd Ordner",
"File Options": "Datei - Ordner | Optionen",
"Show Hidden": "Versteckte zeigen",
"Folders First": "Ordner zuerst",
"Hidden Last": "Versteckte zuletzt",
"Cut": "Ausschneiden",
"Copy": "Kopieren",
"Copying ": "Kopiert wird: ",
"Paste": "Einfügen",
"Paste ": "Einfügen von: ",
" file(s)?": " Datei/en?",
"Pasting ": "Eingefügt wird: ",
"Pasting": "Eingefügt wurde:",
"Rename": "Umbenennen",
"Set New File Name": "Neuen Dateinamen festlegen",
"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!": "",
"Create File": "Neue Datei",
"Set File Name": "Dateiname festlegen",
"Create Folder": "Neuer Ordner",
"Set Folder Name": "Ordner umbenennen",
"Creating ": "Erstellt wird: ",
"Upload": "",
"Select upload location": "",
"No upload locations set!": "",
"Uploading": "",
"Upload successfull!": "",
"Upload failed!": "",
"View as text (unfinished)": "Als Text anzeigen",
"Ignore read only": "Schreibschutz umgehen?",
"Mount": "Einhängen",
"Sd": "SD-Karte | Root-Verzeichnis",
"Image System memory": "Album | NAND Systemspeicher",
"Image microSD card": "Album | SD-Karte",
"Empty...": "Keine Daten...",
"Open with DayBreak?": "Mit Daybreak öffnen?",
"Launch ": "Starte ",
"Launch option for: ": "Start Option für: ",
"Select launcher for: ": "Wähle Launcher für: ",
"Sort By": "Sortierung",
"Sort Options": " Sortierung | Optionen",
"Filter": "Rubrik",
"All": "Alles anzeigen",
"Emulators": "Emulatoren",
"Tools": "Tools",
"Themes": "Themes",
"Legacy": "Älteres",
"Sort": "Sortiert nach",
"Size": "Größe",
"Size (Star)": "Favorit | Größe",
"Alphabetical": "Name",
"Alphabetical (Star)": "Favorit | Name",
"Updated": "zuletzt aktualisiert",
"Updated (Star)": "Favorit | zuletzt aktualisiert",
"Downloads": "Downloads",
"Likes": "Beliebtheit",
"ID": "Theme | Paket ID",
"Order": "Anordnung",
"Descending": "Absteigend ↓",
"Descending (down)": "Absteigend ↓",
"Desc": " ↓",
"Ascending": "Aufsteigend ↑",
"Ascending (Up)": "Aufsteigend ↑",
"Asc": " ↑",
"Layout": "",
"List": "",
"Icon": "",
"Grid": "",
"Search": "Suchen",
"Options": "Optionen",
"OK": "OK",
"Back": "Zurück",
"Select": "Auswählen",
"Open": "Öffne",
"Launch": "Starte",
"Restart": "Neustart",
"Next": "",
"Prev": "",
"Unstar": "Kein Favorit",
"Star": "Favorit",
"Yes": "Ja",
"No": "Nein",
"On": "",
"Off": "",
"Install": "Installieren",
"Install Selected files?": "",
"Installing ": "Installiert wird: ",
"Installed ": "",
"Installed!": "Installiert!",
"Trying to load ": "Versucht zu laden wird: ",
"Checking MD5": "Checke MD5 Prüfsumme",
"Delete": "Löschen",
"Delete Selected files?": "Ausgewähle Dateien löschen?",
"Completely remove ": "Komplett gelöscht wird: ",
"Are you sure you want to delete ": "Bist du sicher zu löschen? Bestätige Löschung von: ",
"Are you sure you wish to cancel?": "Bist du sicher dass du abbrechen willst?",
"Audio disabled due to suspended game": "Audio deaktivert wegen Spielabbruch",
"If this message appears repeatedly, please open an issue.": "Bei wiederholtem Auftreten bitte Issue erstellen."
}
"Scanning ": "Gescannt wird: ",
"Deleting ": "Gelöscht wird: ",
"Deleting": "Gelöscht wurde:",
"Remove": "Entfernen",
"Completely remove ": "Komplett gelöscht wird: ",
"Removing ": "Entfernt wird: ",
"Removed ": "Entfernt wurde: ",
"Uninstalling ": "Deinstalliert wird: ",
"Download": "Download",
"Downloading ": "Heruntergeladen wird: ",
"Downloaded ": "Heruntergeladen wurde: ",
"Update": "Update",
"Update avaliable: ": "Update verfügbar: ",
"Download update: ": " Herunterladen des Updates: ",
"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": "",
"Loading...": "Wird geladen...",
"Loading": "Wird geladen",
"Empty!": "Keine Daten!",
"Not Ready...": "Nicht bereit...",
"Error loading page!": "Ladefehler!"
}

View File

@@ -1,72 +1,24 @@
{
"[Applet Mode]": "[Applet Mode]",
"No Internet": "No Internet",
"Files": "Files",
"Apps": "Apps",
"Store": "Store",
"Menu": "Menu",
"Options": "Options",
"OK": "OK",
"Back": "Back",
"Select": "Select",
"Open": "Open",
"Launch": "Launch",
"Info": "Info",
"Install": "Install",
"Delete": "Delete",
"Restart": "Restart",
"Changelog": "Changelog",
"Details": "Details",
"Update": "Update",
"Remove": "Remove",
"Restore": "Restore",
"Download": "Download",
"Next Page": "Next Page",
"Prev Page": "Prev Page",
"Unstar": "Unstar",
"Star": "Star",
"System memory": "System memory",
"microSD card": "microSD card",
"Sd": "Sd",
"Image System memory": "Image System memory",
"Image microSD card": "Image microSD card",
"Slow": "Slow",
"Normal": "Normal",
"Fast": "Fast",
"Yes": "Yes",
"No": "No",
"Enabled": "Enabled",
"Disabled": "Disabled",
"Sort By": "Sort By",
"Sort Options": "Sort Options",
"Filter": "Filter",
"Sort": "Sort",
"Order": "Order",
"Search": "Search",
"Updated": "Updated",
"Updated (Star)": "Updated (Star)",
"Downloads": "Downloads",
"Size": "Size",
"Size (Star)": "Size (Star)",
"Alphabetical": "Alphabetical",
"Alphabetical (Star)": "Alphabetical (Star)",
"Likes": "Likes",
"ID": "ID",
"Descending": "Descending",
"Descending (down)": "Descending (down)",
"Desc": "Desc",
"Ascending": "Ascending",
"Ascending (Up)": "Ascending (Up)",
"Asc": "Asc",
"Switch-Handheld!": "Switch-Handheld!",
"Switch-Docked!": "Switch-Docked!",
"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",
"If this message appears repeatedly, please open an issue.": "If this message appears repeatedly, please open an issue.",
"Menu Options": "Menu Options",
"Menu": "Menu",
"Theme": "Theme",
"Theme Options": "Theme Options",
"Select Theme": "Select Theme",
"Shuffle": "Shuffle",
"Music": "Music",
"12 Hour Time": "12 Hour Time",
"Download Default Music": "Download Default Music",
"Failed to download default_music.bfstm, please try again": "Failed to download default_music.bfstm, please try again",
"Overwrite current default music?": "Overwrite current default music?",
"Network": "Network",
"Network Options": "Network Options",
"Ftp": "FTP",
@@ -75,8 +27,7 @@
"Nxlink Connected": "Nxlink Connected",
"Nxlink Upload": "Nxlink Upload",
"Nxlink Finished": "Nxlink Finished",
"Switch-Handheld!": "Switch-Handheld!",
"Switch-Docked!": "Switch-Docked!",
"Language": "Language",
"Auto": "Auto",
"English": "English",
@@ -92,87 +43,101 @@
"Russian": "Русский",
"Swedish": "Svenska",
"Vietnamese": "Vietnamese",
"Logging": "Logging",
"Replace hbmenu on exit": "Replace hbmenu on exit",
"Ukrainian": "Українська",
"Misc": "Misc",
"Misc Options": "Misc Options",
"Web": "Web",
"Install forwarders": "Install forwarders",
"Install location": "Install location",
"Show install warning": "Show install warning",
"Text scroll speed": "Text scroll speed",
"FileBrowser": "FileBrowser",
"%zd files": "%zd files",
"%zd dirs": "%zd dirs",
"File Options": "File Options",
"Show Hidden": "Show Hidden",
"Folders First": "Folders First",
"Hidden Last": "Hidden Last",
"Cut": "Cut",
"Copy": "Copy",
"Paste": "Paste",
"Paste ": "Paste ",
" file(s)?": " file(s)?",
"Rename": "Rename",
"Set New File Name": "Set New File Name",
"Advanced": "Advanced",
"Advanced Options": "Advanced Options",
"Create File": "Create File",
"Set File Name": "Set File Name",
"Create Folder": "Create Folder",
"Set Folder Name": "Set Folder Name",
"View as text (unfinished)": "View as text (unfinished)",
"Ignore read only": "Ignore read only",
"Mount": "Mount",
"Empty...": "Empty...",
"Open with DayBreak?": "Open with DayBreak?",
"Launch ": "Launch ",
"Launch option for: ": "Launch option for: ",
"Select launcher for: ": "Select launcher for: ",
"Homebrew": "Homebrew",
"Homebrew Options": "Homebrew Options",
"Hide Sphaira": "Hide Sphaira",
"Install Forwarder": "Install Forwarder",
"WARNING: Installing forwarders will lead to a ban!": "WARNING: Installing forwarders will lead to a ban!",
"Installing Forwarder": "Installing Forwarder",
"Creating Program": "Creating Program",
"Creating Control": "Creating Control",
"Creating Meta": "Creating Meta",
"Writing Nca": "Writing Nca",
"Updating ncm databse": "Updating ncm databse",
"Pushing application record": "Pushing application record",
"Installed!": "Installed!",
"Failed to install forwarder": "Failed to install forwarder",
"Unstarred ": "Unstarred ",
"Starred ": "Starred ",
"AppStore": "AppStore",
"Filter: %s | Sort: %s | Order: %s": "Filter: %s | Sort: %s | Order: %s",
"AppStore Options": "AppStore Options",
"All": "All",
"Games": "Games",
"Emulators": "Emulators",
"Tools": "Tools",
"Themes": "Themes",
"Legacy": "Legacy",
"version: %s": "version: %s",
"updated: %s": "updated: %s",
"category: %s": "category: %s",
"extracted: %.2f MiB": "extracted: %.2f MiB",
"app_dls: %s": "app_dls: %s",
"More by Author": "More by Author",
"Leave Feedback": "Leave Feedback",
"Game Options": "Game Options",
"Hide forwarders": "Hide forwarders",
"Launch random game": "Launch random game",
"List meta records": "List meta records",
"Entries": "Entries",
"Failed to list application meta entries": "Failed to list application meta entries",
"No meta entries found...\n": "No meta entries found...\n",
"Updating application record list": "Updating application record list",
"Dump": "Dump",
"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",
"Select dump location": "Select dump location",
"microSD card (/dumps/NSP/)": "microSD card (/dumps/NSP/)",
"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!",
"Themezer": "Themezer",
"Themezer Options": "Themezer Options",
"Nsfw": "Nsfw",
"Page": "Page",
"Page %zu / %zu": "Page %zu / %zu",
"Enter Page Number": "Enter Page Number",
"Bad Page": "Bad Page",
"Download theme?": "Download theme?",
"GitHub": "GitHub",
"Downloading json": "Downloading json",
"Select asset to download for ": "Select asset to download for ",
"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",
"Host:": "Host:",
"Port:": "Port:",
"Username:": "Username:",
"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!",
"USB Install": "USB Install",
"USB": "USB",
"Connected, waiting for file list...": "Connected, waiting for file list...",
"Connected, starting transfer...": "Connected, starting transfer...",
"Failed to init usb, press B to exit...": "Failed to init usb, press  to exit...",
"Waiting for connection...": "Waiting for connection...",
"Transferring data...": "Transferring data...",
"USB connected, sending file list": "USB connected, sending file list",
"Sent file list, waiting for command...": "Sent file list, waiting for command...",
"waiting for usb connection...": "waiting for usb connection...",
"Disable MTP for usb install": "Disable MTP for usb install",
"Re-enabled MTP": "Re-enabled MTP",
"Installed via usb": "Installed via usb",
"Usb install success!": "Usb install success!",
"Usb install failed!": "Usb install failed!",
"Press B to exit...": "Press  to exit...",
"GameCard Install": "GameCard Install",
"GameCard": "GameCard",
"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",
"Gc install success!": "Gc install success!",
"Gc install failed!": "Gc install failed!",
"IRS (Infrared Joycon Camera)": "IRS (Infrared Joycon Camera)",
"IRS": "IRS",
"Irs": "Irs",
"Ambient Noise Level: ": "Ambient Noise Level: ",
"Controller": "Controller",
"Pad ": "Pad ",
"HandHeld": "HandHeld",
" (Available)": " (Available)",
" (Unsupported)": " (Unsupported)",
" (Unconnected)": " (Unconnected)",
"HandHeld": "HandHeld",
"Rotation": "Rotation",
"0 (Sideways)": "0 (Sideways)",
"90 (Flat)": "90 (Flat)",
@@ -194,64 +159,234 @@
"Normal image": "Normal image",
"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",
"Trimming Format": "Trimming Format",
"External Light Filter": "External Light Filter",
"Load Default": "Load Default",
"Themezer": "Themezer",
"Themezer Options": "Themezer Options",
"Nsfw": "Nsfw",
"Page": "Page",
"Page %zu / %zu": "Page %zu / %zu",
"Enter Page Number": "Enter Page Number",
"Bad Page": "Bad Page",
"Download theme?": "Download theme?",
"GitHub": "GitHub",
"Downloading json": "Downloading json",
"Select asset to download for ": "Select asset to download for ",
"Installing ": "Installing ",
"Uninstalling ": "Uninstalling ",
"Deleting ": "Deleting ",
"Deleting": "Deleting",
"Pasting ": "Pasting ",
"Pasting": "Pasting",
"Removing ": "Removing ",
"Scanning ": "Scanning ",
"Creating ": "Creating ",
"Copying ": "Copying ",
"Trying to load ": "Trying to load ",
"Downloading ": "Downloading ",
"Downloaded ": "Downloaded ",
"Removed ": "Removed ",
"Checking MD5": "Checking MD5",
"Loading...": "Loading...",
"Loading": "Loading",
"Empty!": "Empty!",
"Not Ready...": "Not Ready...",
"Error loading page!": "Error loading page!",
"Update avaliable: ": "Update avaliable: ",
"Download update: ": "Download update: ",
"Updated to ": "Updated to ",
"Press OK to restart Sphaira": "Press OK to restart Sphaira",
"Restart Sphaira?": "Restart Sphaira?",
"Failed to download update": "Failed to download update",
"Advanced": "Advanced",
"Advanced Options": "Advanced Options",
"Logging": "Logging",
"Replace hbmenu on exit": "Replace hbmenu on exit",
"Restore hbmenu?": "Restore hbmenu?",
"Restore": "Restore",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu",
"Failed to restore hbmenu, please re-download hbmenu": "Failed to restore hbmenu, please re-download hbmenu",
"Failed to restore hbmenu, using sphaira instead": "Failed to restore hbmenu, using sphaira instead",
"Restored hbmenu, closing sphaira": "Restored hbmenu, closing sphaira",
"Restored hbmenu": "Restored hbmenu",
"Restart Sphaira?": "Restart Sphaira?",
"Press OK to restart Sphaira": "Press OK to restart Sphaira",
"Text scroll speed": "Text scroll speed",
"Slow": "Slow",
"Normal": "Normal",
"Fast": "Fast",
"Set right-side menu": "Set right-side menu",
"Install options": "Install options",
"Install Options": "Install Options",
"Enable sysmmc": "Enable sysmmc",
"Enable emummc": "Enable emummc",
"Show install warning": "Show install warning",
"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",
"Skip base": "Skip base",
"Skip patch": "Skip patch",
"Skip dlc": "Skip dlc",
"Skip data patch": "Skip data patch",
"Skip ticket": "Skip ticket",
"Skip NCA hash verify": "Skip NCA hash verify",
"Skip RSA header verify": "Skip RSA header verify",
"Skip RSA NPDM verify": "Skip RSA NPDM verify",
"Ignore distribution bit": "Ignore distribution bit",
"Convert to standard crypto": "Convert to standard crypto",
"Lower master key": "Lower master key",
"Lower system version": "Lower system version",
"Homebrew": "Homebrew",
"Apps": "Apps",
"Homebrew Options": "Homebrew Options",
"Hide Sphaira": "Hide Sphaira",
"Install Forwarder": "Install Forwarder",
"WARNING: Installing forwarders will lead to a ban!": "WARNING: Installing forwarders will lead to a ban!",
"Installing Forwarder": "Installing Forwarder",
"Creating Program": "Creating Program",
"Creating Control": "Creating Control",
"Creating Meta": "Creating Meta",
"Writing Nca": "Writing Nca",
"Updating ncm databse": "Updating ncm databse",
"Pushing application record": "Pushing application record",
"Failed to install forwarder": "Failed to install forwarder",
"Unstarred ": "Unstarred ",
"Starred ": "Starred ",
"Failed to remove old forwarder, please manually remove it!": "Failed to remove old forwarder, please manually remove it!",
"AppStore": "AppStore",
"Appstore": "AppStore",
"Store": "Store",
"Filter: %s | Sort: %s | Order: %s": "Filter: %s | Sort: %s | Order: %s",
"AppStore Options": "AppStore Options",
"Info": "Info",
"Changelog": "Changelog",
"Details": "Details",
"version: %s": "version: %s",
"updated: %s": "updated: %s",
"category: %s": "category: %s",
"extracted: %.2f MiB": "extracted: %.2f MiB",
"app_dls: %s": "app_dls: %s",
"More by Author": "More by Author",
"Leave Feedback": "Leave Feedback",
"FileBrowser": "FileBrowser",
"Files": "Files",
"%zd files": "%zd files",
"%zd dirs": "%zd dirs",
"File Options": "File Options",
"Show Hidden": "Show Hidden",
"Folders First": "Folders First",
"Hidden Last": "Hidden Last",
"Cut": "Cut",
"Copy": "Copy",
"Copying ": "Copying ",
"Paste": "Paste",
"Paste ": "Paste ",
" file(s)?": " file(s)?",
"Pasting ": "Pasting ",
"Pasting": "Pasting",
"Rename": "Rename",
"Set New File Name": "Set New File Name",
"Extract zip": "Extract zip",
"Extract Options": "Extract Options",
"Extract here": "Extract here",
"Extract to root": "Extract to root",
"Are you sure you want to extract to root?": "Are you sure you want to extract to root?",
"Extract to...": "Extract to...",
"Enter the path to the folder to extract into": "Enter the path to the folder to extract into",
"Extracting ": "Extracting ",
"Extract success!": "Extract success!",
"Extract failed!": "Extract failed!",
"Compress to zip": "Compress to zip",
"Compress Options": "Compress Options",
"Compress": "Compress",
"Compress to...": "Compress to...",
"Compressing ": "Compressing ",
"Compress success!": "Compress success!",
"Compress failed!": "Compress failed!",
"Create File": "Create File",
"Set File Name": "Set File Name",
"Create Folder": "Create Folder",
"Set Folder Name": "Set Folder Name",
"Creating ": "Creating ",
"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)",
"Ignore read only": "Ignore read only",
"Mount": "Mount",
"Sd": "Sd",
"Image System memory": "Image System memory",
"Image microSD card": "Image microSD card",
"Empty...": "Empty...",
"Open with DayBreak?": "Open with DayBreak?",
"Launch ": "Launch ",
"Launch option for: ": "Launch option for: ",
"Select launcher for: ": "Select launcher for: ",
"Sort By": "Sort By",
"Sort Options": "Sort Options",
"Filter": "Filter",
"All": "All",
"Emulators": "Emulators",
"Tools": "Tools",
"Themes": "Themes",
"Legacy": "Legacy",
"Sort": "Sort",
"Size": "Size",
"Size (Star)": "Size (Star)",
"Alphabetical": "Alphabetical",
"Alphabetical (Star)": "Alphabetical (Star)",
"Updated": "Updated",
"Updated (Star)": "Updated (Star)",
"Downloads": "Downloads",
"Likes": "Likes",
"ID": "ID",
"Order": "Order",
"Descending": "Descending",
"Descending (down)": "Descending (down)",
"Desc": "Desc",
"Ascending": "Ascending",
"Ascending (Up)": "Ascending (Up)",
"Asc": "Asc",
"Layout": "Layout",
"List": "List",
"Icon": "Icon",
"Grid": "Grid",
"Search": "Search",
"Options": "Options",
"OK": "OK",
"Back": "Back",
"Select": "Select",
"Open": "Open",
"Launch": "Launch",
"Restart": "Restart",
"Next": "Next",
"Prev": "Prev",
"Unstar": "Unstar",
"Star": "Star",
"Yes": "Yes",
"No": "No",
"On": "On",
"Off": "Off",
"Install": "Install",
"Install Selected files?": "Install Selected files?",
"Installing ": "Installing ",
"Installed ": "Installed ",
"Installed!": "Installed!",
"Trying to load ": "Trying to load ",
"Checking MD5": "Checking MD5",
"Delete": "Delete",
"Delete Selected files?": "Delete Selected files?",
"Completely remove ": "Completely remove ",
"Are you sure you want to delete ": "Are you sure you want to delete ",
"Are you sure you wish to cancel?": "Are you sure you wish to cancel?",
"Audio disabled due to suspended game": "Audio disabled due to suspended game",
"If this message appears repeatedly, please open an issue.": "If this message appears repeatedly, please open an issue."
"Scanning ": "Scanning ",
"Deleting ": "Deleting ",
"Deleting": "Deleting",
"Remove": "Remove",
"Completely remove ": "Completely remove ",
"Removing ": "Removing ",
"Removed ": "Removed ",
"Uninstalling ": "Uninstalling ",
"Download": "Download",
"Downloading ": "Downloading ",
"Downloaded ": "Downloaded ",
"Update": "Update",
"Update avaliable: ": "Update avaliable: ",
"Download update: ": "Download update: ",
"Updated to ": "Updated to ",
"Failed to download update": "Failed to download update",
"%zu hours %zu minutes remaining": "%zu hours %zu minutes remaining",
"%zu minutes %zu seconds remaining": "%zu minutes %zu seconds remaining",
"%zu seconds remaining": "%zu seconds remaining",
"Loading...": "Loading...",
"Loading": "Loading",
"Empty!": "Empty!",
"Not Ready...": "Not Ready...",
"Error loading page!": "Error loading page!"
}

View File

@@ -1,72 +1,24 @@
{
"[Applet Mode]": "[Modo Applet]",
"No Internet": "Sin Internet",
"Files": "Archivos",
"Apps": "Apps",
"Store": "Tienda",
"Menu": "Menú",
"Options": "Opciones",
"OK": "OK",
"Back": "Atrás",
"Select": "Seleccionar",
"Open": "Abrir",
"Launch": "Ejecutar",
"Info": "Información",
"Install": "Instalar",
"Delete": "Borrar",
"Restart": "Reiniciar",
"Changelog": "Log de cambios",
"Details": "Detalles",
"Update": "Actualizar",
"Remove": "Borrar",
"Restore": "Restaurar",
"Download": "Descargar",
"Next Page": "Página siguiente",
"Prev Page": "Página anterior",
"Unstar": "Quitar favorito",
"Star": "Favorito",
"System memory": "Memoria de sistema",
"microSD card": "microSD",
"Sd": "SD",
"Image System memory": "Imagen memoria interna",
"Image microSD card": "Imagen tarjeta microSD",
"Slow": "Lento",
"Normal": "Normal",
"Fast": "Rápido",
"Yes": "Sí",
"No": "No",
"Enabled": "Activado",
"Disabled": "Desactivado",
"Sort By": "Ordenar por",
"Sort Options": "Opciones de clasificación",
"Filter": "Filtrar",
"Sort": "Clasificar",
"Order": "Orden",
"Search": "Buscar",
"Updated": "Actualizado",
"Updated (Star)": "Actualizado (favorito)",
"Downloads": "Descargas",
"Size": "Tamaño",
"Size (Star)": "Tamaño (favorito)",
"Alphabetical": "Alfabético",
"Alphabetical (Star)": "Alfabético (favorito)",
"Likes": "Me Gusta",
"ID": "ID",
"Descending": "Descendente",
"Descending (down)": "Descendente (abajo)",
"Desc": "Descendente",
"Ascending": "Ascendente",
"Ascending (Up)": "Ascendente (arriba)",
"Asc": "Ascendente",
"Switch-Handheld!": "¡Switch-Modo-Portátil!",
"Switch-Docked!": "¡Switch-Modo-TV!",
"Audio disabled due to suspended game": "",
"Are you sure you wish to cancel?": "¿Estás seguro que deseas cancelar?",
"An error occurred": "",
"If this message appears repeatedly, please open an issue.": "Si este mensaje aparece repetidamente, por favor abrir un 'issue'.",
"Menu Options": "Opciones de menú",
"Menu": "Menú",
"Theme": "Tema",
"Theme Options": "Opciones de tema",
"Select Theme": "Seleccionar tema",
"Shuffle": "Barajar",
"Music": "Música",
"12 Hour Time": "",
"Download Default Music": "",
"Failed to download default_music.bfstm, please try again": "",
"Overwrite current default music?": "",
"Network": "Red",
"Network Options": "Opciones de red",
"Ftp": "FTP",
@@ -75,8 +27,7 @@
"Nxlink Connected": "NXlink conectado",
"Nxlink Upload": "NXlink subida",
"Nxlink Finished": "NXlink finalizado",
"Switch-Handheld!": "¡Switch-Modo-Portátil!",
"Switch-Docked!": "¡Switch-Modo-TV!",
"Language": "Idioma",
"Auto": "Automático",
"English": "English",
@@ -92,87 +43,101 @@
"Russian": "Русский",
"Swedish": "Svenska",
"Vietnamese": "Vietnamese",
"Logging": "Registro",
"Replace hbmenu on exit": "Reemplazar hbmenu",
"Ukrainian": "Українська",
"Misc": "Varios",
"Misc Options": "Opciones varias",
"Web": "Web",
"Install forwarders": "Instalar forwarders",
"Install location": "Dispositivo de instalación",
"Show install warning": "Precaución de instalación",
"Text scroll speed": "Velocidad de scroll",
"FileBrowser": "Explorador de archivos",
"%zd files": "%zd archivos",
"%zd dirs": "%zd carpetas",
"File Options": "Opciones de archivo",
"Show Hidden": "Mostrar archivos ocultos",
"Folders First": "Carpetas primero",
"Hidden Last": "Ocultos al final",
"Cut": "Cortar",
"Copy": "Copiar",
"Paste": "Pegar",
"Paste ": "Pegar ",
" file(s)?": " ¿archivo(s)?",
"Rename": "Renombrar",
"Set New File Name": "Establecer nuevo nombre de archivo",
"Advanced": "Avanzado",
"Advanced Options": "Opciones avanzadas",
"Create File": "Crear archivo",
"Set File Name": "Establecer nombre de archivo",
"Create Folder": "Crear carpeta",
"Set Folder Name": "Establecer nombre de carpeta",
"View as text (unfinished)": "Ver como texto (sin terminar)",
"Ignore read only": "Ignorar sólo lectura",
"Mount": "Montar",
"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: ",
"Homebrew": "Homebrew",
"Homebrew Options": "Opciones de Homebrew",
"Hide Sphaira": "Ocultar Sphaira",
"Install Forwarder": "Instalar Forwarder",
"WARNING: Installing forwarders will lead to a ban!": "ADVERTENCIA: ¡La instalación de fordwarders podría producir un baneo de la consola!",
"Installing Forwarder": "Instalando Forwarder",
"Creating Program": "Creando Program",
"Creating Control": "Creando Control",
"Creating Meta": "Creando Meta",
"Writing Nca": "Creando NCA",
"Updating ncm databse": "Actualizando base de datos ncm",
"Pushing application record": "Registro de aplicación",
"Installed!": "¡Instalado!",
"Failed to install forwarder": "Fallo al instalar forwarder",
"Unstarred ": "Quitar Favorito",
"Starred ": "Favorito",
"AppStore": "Tienda",
"Filter: %s | Sort: %s | Order: %s": "Filtrar: %s | Clasificar: %s | Orden: %s",
"AppStore Options": "Opciones de la Tienda",
"All": "Todo",
"Games": "Juegos",
"Emulators": "Emuladores",
"Tools": "Herramientas",
"Themes": "Temas",
"Legacy": "Legado",
"version: %s": "version: %s",
"updated: %s": "actualizado: %s",
"category: %s": "categoría: %s",
"extracted: %.2f MiB": "extraído: %.2f MiB",
"app_dls: %s": "app_dls: %s",
"More by Author": "Mostrar mas del Autor",
"Leave Feedback": "Dejar Mensaje",
"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!": "",
"Themezer": "Themezer",
"Themezer Options": "Opciones de Themezer",
"Nsfw": "NSFW",
"Page": "Página",
"Page %zu / %zu": "Pág. %zu / %zu",
"Enter Page Number": "Ingresar Número de Página",
"Bad Page": "Página Errónea",
"Download theme?": "¿Descargar Tema?",
"GitHub": "GitHub",
"Downloading json": "Descargando json",
"Select asset to download for ": "Seleccionar recurso a descargar para ",
"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!": "",
"IRS (Infrared Joycon Camera)": "",
"IRS": "",
"Irs": "IRS",
"Ambient Noise Level: ": "Nivel de Ruido Ambiente",
"Controller": "Control",
"Pad ": "GamePad ",
"HandHeld": "Portátil",
" (Available)": " (Disponible)",
" (Unsupported)": "(No Compatible)",
" (Unconnected)": " (Desconectado)",
"HandHeld": "Portátil",
"Rotation": "Rotación",
"0 (Sideways)": "0° (De lado)",
"90 (Flat)": "90° (Plano)",
@@ -194,64 +159,234 @@
"Normal image": "Imagen normal",
"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",
"Trimming Format": "Formato de recorte",
"External Light Filter": "Filtro de luz externa",
"Load Default": "Cargar predeterminado",
"Themezer": "Themezer",
"Themezer Options": "Opciones de Themezer",
"Nsfw": "NSFW",
"Page": "Página",
"Page %zu / %zu": "Pág. %zu / %zu",
"Enter Page Number": "Ingresar Número de Página",
"Bad Page": "Página Errónea",
"Download theme?": "¿Descargar Tema?",
"GitHub": "GitHub",
"Downloading json": "Descargando json",
"Select asset to download for ": "Seleccionar recurso a descargar para ",
"Installing ": "Instalando ",
"Uninstalling ": "Desinstalando ",
"Deleting ": "Borrando ",
"Deleting": "Borrando",
"Pasting ": "Pegando ",
"Pasting": "Pegando",
"Removing ": "Removiendo ",
"Scanning ": "Escaneando ",
"Creating ": "Creando ",
"Copying ": "Copiando ",
"Trying to load ": "Intentando cargar ",
"Downloading ": "Descargando ",
"Downloaded ": "Descargado ",
"Removed ": "Removido ",
"Checking MD5": "Chequeando MD5",
"Loading...": "Cargando...",
"Loading": "Cargando",
"Empty!": "¡Vacío!",
"Not Ready...": "No listo aún...",
"Error loading page!": "¡Error cargando la página!",
"Update avaliable: ": "Actualización disponible: ",
"Download update: ": "Descargar actualización: ",
"Updated to ": "Actualizado a ",
"Press OK to restart Sphaira": "Presiona OK para reiniciar sphaira",
"Restart Sphaira?": "¿Reiniciar sphaira?",
"Failed to download update": "Fallo al descargar actualización",
"Advanced": "Avanzado",
"Advanced Options": "Opciones avanzadas",
"Logging": "Registro",
"Replace hbmenu on exit": "Reemplazar hbmenu",
"Restore hbmenu?": "¿Restaurar hbmenu?",
"Restore": "Restaurar",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Fallo al encontrar /switch/hbmenu.nro\nUsar la Tienda para reinstalar hbmenu",
"Failed to restore hbmenu, please re-download hbmenu": "Fallo al restaurar hbmenu, por favor volver a descargar hbmenu",
"Failed to restore hbmenu, using sphaira instead": "Fallo al restaurar hbmenu, se usará sphaira",
"Restored hbmenu, closing sphaira": "hbmenu restaurado, cerrando sphaira",
"Restored hbmenu": "hbmenu restaurado",
"Restart Sphaira?": "¿Reiniciar sphaira?",
"Press OK to restart Sphaira": "Presiona OK para reiniciar sphaira",
"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": "",
"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": "",
"Homebrew": "Homebrew",
"Apps": "Apps",
"Homebrew Options": "Opciones de Homebrew",
"Hide Sphaira": "Ocultar Sphaira",
"Install Forwarder": "Instalar Forwarder",
"WARNING: Installing forwarders will lead to a ban!": "ADVERTENCIA: ¡La instalación de fordwarders podría producir un baneo de la consola!",
"Installing Forwarder": "Instalando Forwarder",
"Creating Program": "Creando Program",
"Creating Control": "Creando Control",
"Creating Meta": "Creando Meta",
"Writing Nca": "Creando NCA",
"Updating ncm databse": "Actualizando base de datos ncm",
"Pushing application record": "Registro de aplicación",
"Failed to install forwarder": "Fallo al instalar forwarder",
"Unstarred ": "Quitar Favorito",
"Starred ": "Favorito",
"Failed to remove old forwarder, please manually remove it!": "",
"AppStore": "Tienda",
"Appstore": "",
"Store": "Tienda",
"Filter: %s | Sort: %s | Order: %s": "Filtrar: %s | Clasificar: %s | Orden: %s",
"AppStore Options": "Opciones de la Tienda",
"Info": "Información",
"Changelog": "Log de cambios",
"Details": "Detalles",
"version: %s": "version: %s",
"updated: %s": "actualizado: %s",
"category: %s": "categoría: %s",
"extracted: %.2f MiB": "extraído: %.2f MiB",
"app_dls: %s": "app_dls: %s",
"More by Author": "Mostrar mas del Autor",
"Leave Feedback": "Dejar Mensaje",
"FileBrowser": "Explorador de archivos",
"Files": "Archivos",
"%zd files": "%zd archivos",
"%zd dirs": "%zd carpetas",
"File Options": "Opciones de archivo",
"Show Hidden": "Mostrar archivos ocultos",
"Folders First": "Carpetas primero",
"Hidden Last": "Ocultos al final",
"Cut": "Cortar",
"Copy": "Copiar",
"Copying ": "Copiando ",
"Paste": "Pegar",
"Paste ": "Pegar ",
" file(s)?": " ¿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!": "",
"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)",
"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...",
"Open with DayBreak?": "¿Abrir con DayBreak?",
"Launch ": "Abrir ",
"Launch option for: ": "Opción de abrir con: ",
"Select launcher for: ": "Seleccionar abrir con: ",
"Sort By": "Ordenar por",
"Sort Options": "Opciones de clasificación",
"Filter": "Filtrar",
"All": "Todo",
"Emulators": "Emuladores",
"Tools": "Herramientas",
"Themes": "Temas",
"Legacy": "Legado",
"Sort": "Clasificar",
"Size": "Tamaño",
"Size (Star)": "Tamaño (favorito)",
"Alphabetical": "Alfabético",
"Alphabetical (Star)": "Alfabético (favorito)",
"Updated": "Actualizado",
"Updated (Star)": "Actualizado (favorito)",
"Downloads": "Descargas",
"Likes": "Me Gusta",
"ID": "ID",
"Order": "Orden",
"Descending": "Descendente",
"Descending (down)": "Descendente (abajo)",
"Desc": "Descendente",
"Ascending": "Ascendente",
"Ascending (Up)": "Ascendente (arriba)",
"Asc": "Ascendente",
"Layout": "",
"List": "",
"Icon": "",
"Grid": "",
"Search": "Buscar",
"Options": "Opciones",
"OK": "OK",
"Back": "Atrás",
"Select": "Seleccionar",
"Open": "Abrir",
"Launch": "Ejecutar",
"Restart": "Reiniciar",
"Next": "",
"Prev": "",
"Unstar": "Quitar favorito",
"Star": "Favorito",
"Yes": "Sí",
"No": "No",
"On": "",
"Off": "",
"Install": "Instalar",
"Install Selected files?": "",
"Installing ": "Instalando ",
"Installed ": "",
"Installed!": "¡Instalado!",
"Trying to load ": "Intentando cargar ",
"Checking MD5": "Chequeando MD5",
"Delete": "Borrar",
"Delete Selected files?": "¿Eliminar archivos seleccionados?",
"Completely remove ": "Eliminar completamente",
"Are you sure you want to delete ": "¿Estás seguro que quieres eliminar? ",
"Are you sure you wish to cancel?": "¿Estás seguro que deseas cancelar?",
"Audio disabled due to suspended game": "",
"If this message appears repeatedly, please open an issue.": "Si este mensaje aparece repetidamente, por favor abrir un 'issue'."
"Scanning ": "Escaneando ",
"Deleting ": "Borrando ",
"Deleting": "Borrando",
"Remove": "Borrar",
"Completely remove ": "Eliminar completamente",
"Removing ": "Removiendo ",
"Removed ": "Removido ",
"Uninstalling ": "Desinstalando ",
"Download": "Descargar",
"Downloading ": "Descargando ",
"Downloaded ": "Descargado ",
"Update": "Actualizar",
"Update avaliable: ": "Actualización disponible: ",
"Download update: ": "Descargar actualización: ",
"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": "",
"Loading...": "Cargando...",
"Loading": "Cargando",
"Empty!": "¡Vacío!",
"Not Ready...": "No listo aún...",
"Error loading page!": "¡Error cargando la página!"
}

View File

@@ -1,72 +1,24 @@
{
"[Applet Mode]": "[Mode Applet]",
"No Internet": "Pas d'Internet",
"Files": "Fichiers",
"Apps": "Applications",
"Store": "Magasin",
"Menu": "Menu",
"Options": "Options",
"OK": "OK",
"Back": "Retour",
"Select": "Sélectionner",
"Open": "Ouvrir",
"Launch": "Exécuter",
"Info": "Info.",
"Install": "Installer",
"Delete": "Supprimer",
"Restart": "Redémarrer",
"Changelog": "Changelog",
"Details": "Détails",
"Update": "Mise à jour",
"Remove": "Supprimer",
"Restore": "Restaurer",
"Download": "Télécharger",
"Next Page": "Page Suiv.",
"Prev Page": "Page Préc.",
"Unstar": "Retirer des favories",
"Star": "Ajouter aux favories",
"System memory": "Mémoire système",
"microSD card": "Carte microSD",
"Sd": "Sd",
"Image System memory": "Image de la mémoire System",
"Image microSD card": "Image de la Carte microSD",
"Slow": "Lent",
"Normal": "Normal",
"Fast": "Rapide",
"Yes": "Oui",
"No": "Non",
"Enabled": "Activé(e)",
"Disabled": "Désactivé(e)",
"Sort By": "Tri Par",
"Sort Options": "Options de Tri",
"Filter": "Filtre",
"Sort": "Tri",
"Order": "Ordre",
"Search": "Recherche",
"Updated": "Mis à jour",
"Updated (Star)": "Mis à jour (Favories)",
"Downloads": "Téléchargements",
"Size": "Taille",
"Size (Star)": "Taille (Favories)",
"Alphabetical": "Alphabétique",
"Alphabetical (Star)": "Alphabétique (Favories)",
"Likes": "Likes",
"ID": "ID",
"Descending": "Décroissant",
"Descending (down)": "Décroissant",
"Desc": "Décroissant",
"Ascending": "Croissant",
"Ascending (Up)": "Croissant",
"Asc": "Croissant",
"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?",
"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.",
"Menu Options": "Options des Menus",
"Menu": "Menu",
"Theme": "Thème",
"Theme Options": "Options de Thème",
"Select Theme": "Choisir un Thème",
"Shuffle": "Aléatoire",
"Music": "Musique",
"12 Hour Time": "Temps sur 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?",
"Network": "Réseau",
"Network Options": "Options Réseau",
"Ftp": "FTP",
@@ -75,13 +27,12 @@
"Nxlink Connected": "Nxlink Connecté",
"Nxlink Upload": "Nxlink téléversement",
"Nxlink Finished": "Nxlink terminé",
"Switch-Handheld!": "Switch-Portable",
"Switch-Docked!": "Switch-Dockée",
"Language": "Langue",
"Auto": "Auto",
"English": "English",
"Japanese": "日本語",
"French": "Français",
"French": "Français",
"German": "Deutsch",
"Italian": "Italiano",
"Spanish": "Español",
@@ -92,87 +43,101 @@
"Russian": "Русский",
"Swedish": "Svenska",
"Vietnamese": "Vietnamese",
"Logging": "Journalisation",
"Replace hbmenu on exit": "Remplacer hbmenu quand quitté",
"Ukrainian": "Українська",
"Misc": "Divers",
"Misc Options": "Options Diverses",
"Web": "Web",
"Install forwarders": "Installer les Forwarders",
"Install location": "Emplacement d'installation",
"Show install warning": "Afficher l'avertissement d'installation",
"Text scroll speed": "Vitesse de défilement du texte",
"FileBrowser": "Explorateur de Fichiers",
"%zd files": "%zd fichiers",
"%zd dirs": "%zd dossiers",
"File Options": "Options de Fichier",
"Show Hidden": "Afficher Masqués",
"Folders First": "Dossiers en Premier",
"Hidden Last": "Masqués en Dernier",
"Cut": "Couper",
"Copy": "Copier",
"Paste": "Coller",
"Paste ": "Coller ",
" file(s)?": " fichier(s)?",
"Rename": "Renommer",
"Set New File Name": "Nouveau Nom Du Fichier",
"Advanced": "Avancé",
"Advanced Options": "Options Avancées",
"Create File": "Créer un Fichier",
"Set File Name": "Nommer Le Fichier",
"Create Folder": "Créer un Dossier",
"Set Folder Name": "Nommer Le Dossier",
"View as text (unfinished)": "Afficher sous forme de texte (inachevé)",
"Ignore read only": "Ignorer lecture seule",
"Mount": "Monter",
"Empty...": "Vide...",
"Open with DayBreak?": "Ouvrir avec DayBreak?",
"Launch ": "Lancer ",
"Launch option for: ": "Option de lancement pour: ",
"Select launcher for: ": "Sélectionner le lanceur pour: ",
"Homebrew": "Homebrew",
"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",
"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",
"Pushing application record": "Ajout de l'enregistrement de l'application",
"Installed!": "Installé!",
"Failed to install forwarder": "Echec de l'installation du forwarder",
"Unstarred ": "Retiré des favories ",
"Starred ": "Ajouté aux favories ",
"AppStore": "AppStore",
"Filter: %s | Sort: %s | Order: %s": "Filtre: %s | Tri: %s | Ordre: %s",
"AppStore Options": "Options de l'AppStore",
"All": "Tous",
"Games": "Jeux",
"Emulators": "Émulateurs",
"Tools": "Outils",
"Themes": "Thèmes",
"Legacy": "Legacy",
"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",
"Leave Feedback": "Laisser un avis",
"Game Options": "Options de jeu",
"Hide forwarders": "Masquer les forwarders",
"Launch random game": "Lancer un jeu au hasard",
"List meta records": "Lister les enregistrements meta",
"Entries": "Entrées",
"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 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/)",
"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é!",
"Success": "Succès",
"Delete successfull!": "Suprression réussie!",
"Delete failed!": "Suprression échouée!",
"Themezer": "Themezer",
"Themezer Options": "Options Themezer",
"Nsfw": "Nsfw",
"Page": "Page",
"Page %zu / %zu": "Page %zu / %zu",
"Enter Page Number": "Entrez Un Numéro De Page",
"Bad Page": "Page inexistante",
"Download theme?": "Télécharger le thème?",
"GitHub": "GitHub",
"Downloading json": "Téléchargement du json",
"Select asset to download for ": "Sélectionner l'asset à télécharger pour ",
"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",
"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...",
"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",
"Sent file list, waiting for command...": "Liste des fichiers envoyée, attente d'une commande...",
"waiting for usb connection...": "En attente d'une connexion USB...",
"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!",
"Press B to exit...": "Appuyer sur B pour quitter...",
"GameCard Install": "Installation de la cartouche",
"GameCard": "Cartouche",
"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!",
"IRS (Infrared Joycon Camera)": "IRS (infrarouge de la caméra du Joycon",
"IRS": "IRS",
"Irs": "Irs",
"Ambient Noise Level: ": "Niveau De Bruit Ambiant: ",
"Controller": "Contrôleur",
"Pad ": "Manette ",
"HandHeld": "Portable",
" (Available)": " (Disponible)",
" (Unsupported)": "Non supporté",
" (Unconnected)": " (Non connectée)",
"HandHeld": "Portable",
"Rotation": "Rotation",
"0 (Sideways)": "0 (Paysage)",
"90 (Flat)": "90 (Portrait)",
@@ -190,68 +155,238 @@
"Dim group": "Groupe sombre",
"None": "Aucun",
"Gain": "Gain",
"Negative Image": "Négatif",
"Negative Image": "Image Négative",
"Normal image": "Image normale",
"Negative image": "Négatif",
"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",
"Trimming Format": "Format de Découpe",
"External Light Filter": "Filtre de Lumière Externe",
"Load Default": "Charger par Défaut",
"Themezer": "Themezer",
"Themezer Options": "Options Themezer",
"Nsfw": "Nsfw",
"Page": "Page",
"Page %zu / %zu": "Page %zu / %zu",
"Enter Page Number": "Entrez Un Numéro De Page",
"Bad Page": "Page inexistante",
"Download theme?": "Télécharger le thème?",
"GitHub": "GitHub",
"Downloading json": "Téléchargement du json",
"Select asset to download for ": "Sélectionner l'asset pour télécharger ",
"Installing ": "Installation ",
"Uninstalling ": "Désinstallation ",
"Deleting ": "Suppression ",
"Deleting": "Suppression",
"Pasting ": "Coller ",
"Pasting": "Coller",
"Removing ": "Suppression ",
"Scanning ": "Scan ",
"Creating ": "Création ",
"Copying ": "Copie ",
"Trying to load ": "Tente de charger ",
"Downloading ": "Téléchargement ",
"Downloaded ": "Téléchargé",
"Removed ": "Supprimé ",
"Checking MD5": "Vérification MD5",
"Loading...": "Chargement...",
"Loading": "Chargement",
"Empty!": "Vide!",
"Not Ready...": "Pas prêt",
"Error loading page!": "Erreur du chargement de la page!",
"Update avaliable: ": "Mise à jour disponible: ",
"Download update: ": "Télécharger la mise à jour: ",
"Updated to ": "Mis à jour vers ",
"Press OK to restart Sphaira": "Appuyez sur OK pour redémarrer Sphaira",
"Restart Sphaira?": "Redémarrer Sphaira?",
"Failed to download update": "Echec du téléchargement de la mise à jour",
"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?",
"Press OK to restart Sphaira": "Appuyez sur OK pour redémarrer Sphaira",
"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",
"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",
"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 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",
"Ignore distribution bit": "Ignorer le bit de distribution",
"Convert to standard crypto": "Convertir vers la crypto standard",
"Lower master key": "Abaisser la master key",
"Lower system version": "Abaisser la version du système",
"Homebrew": "Homebrew",
"Apps": "Applications",
"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",
"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",
"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!",
"AppStore": "AppStore",
"Appstore": "Magasin d'applications",
"Store": "Magasin",
"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",
"extracted: %.2f MiB": "Extrait: %.2f MiB",
"app_dls: %s": "app_dls: %s",
"More by Author": "Plus de cet Auteur",
"Leave Feedback": "Laisser un avis",
"FileBrowser": "Explorateur de Fichiers",
"Files": "Fichiers",
"%zd files": "%zd fichiers",
"%zd dirs": "%zd dossiers",
"File Options": "Options de Fichier",
"Show Hidden": "Afficher Masqués",
"Folders First": "Dossiers en Premier",
"Hidden Last": "Masqués en Dernier",
"Cut": "Couper",
"Copy": "Copier",
"Copying ": "Copie en cours ",
"Paste": "Coller",
"Paste ": "Coller ",
" file(s)?": " fichier(s)?",
"Pasting ": "Collage en cours ",
"Pasting": "Collage en cours",
"Rename": "Renommer",
"Set New File Name": "Nouveau Nom 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?",
"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!",
"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",
"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é)",
"Ignore read only": "Ignorer lecture seule",
"Mount": "Monter",
"Sd": "Sd",
"Image System memory": "Image de la mémoire System",
"Image microSD card": "Image de la Carte microSD",
"Empty...": "Vide...",
"Open with DayBreak?": "Ouvrir avec DayBreak?",
"Launch ": "Lancer ",
"Launch option for: ": "Option de lancement pour: ",
"Select launcher for: ": "Sélectionner le lanceur pour: ",
"Sort By": "Tri Par",
"Sort Options": "Options de Tri",
"Filter": "Filtre",
"All": "Tous",
"Emulators": "Émulateurs",
"Tools": "Outils",
"Themes": "Thèmes",
"Legacy": "Legacy",
"Sort": "Tri",
"Size": "Taille",
"Size (Star)": "Taille (Favories)",
"Alphabetical": "Alphabétique",
"Alphabetical (Star)": "Alphabétique (Favories)",
"Updated": "Mis à jour",
"Updated (Star)": "Mis à jour (Favories)",
"Downloads": "Téléchargements",
"Likes": "Likes",
"ID": "ID",
"Order": "Ordre",
"Descending": "Décroissant",
"Descending (down)": "Décroissant (vers le bas)",
"Desc": "Décroissant",
"Ascending": "Croissant",
"Ascending (Up)": "Croissant (vers le haut)",
"Asc": "Croissant",
"Layout": "Type d'affichage",
"List": "Liste",
"Icon": "Icône",
"Grid": "Grille",
"Search": "Recherche",
"Options": "Options",
"OK": "OK",
"Back": "Retour",
"Select": "Sélectionner",
"Open": "Ouvrir",
"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 ",
"Installed ": "Installé ",
"Installed!": "Installé!",
"Trying to load ": "Tente de charger ",
"Checking MD5": "Vérification MD5",
"Delete": "Supprimer",
"Delete Selected files?": "Supprimer les fichiers sélectionnés?",
"Completely remove ": "Supprimer totalement ",
"Are you sure you want to delete ": "Êtes-vous sûr de vouloir supprimer ",
"Are you sure you wish to cancel?": "Souhaitez-vous vraiment annuler?",
"Audio disabled due to suspended game": "Audio désactivé à cause d'un jeu suspendu",
"If this message appears repeatedly, please open an issue.": "Si ce message apparait en boucle veuillez ouvrir une issue."
"Scanning ": "Scan en cours ",
"Deleting ": "Suppression en cours ",
"Deleting": "Suppression en cours",
"Remove": "Supprimer",
"Completely remove ": "Supprimer totalement ",
"Removing ": "Suppression en cours ",
"Removed ": "Supprimé ",
"Uninstalling ": "Désinstallation en cours ",
"Download": "Télécharger",
"Downloading ": "Téléchargement en cours ",
"Downloaded ": "Téléchargé",
"Update": "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",
"%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",
"Error loading page!": "Erreur du chargement de la page!"
}

View File

@@ -1,72 +1,24 @@
{
"[Applet Mode]": "[Modalità applet]",
"No Internet": "Niente Internet",
"Files": "File",
"Apps": "App",
"Store": "Store",
"Menu": "Menu",
"Options": "Opzioni",
"OK": "OK",
"Back": "Indietro",
"Select": "Seleziona",
"Open": "Apri",
"Launch": "Lancia",
"Info": "Informazioni",
"Install": "Installa",
"Delete": "Elimina",
"Restart": "Riavvia",
"Changelog": "Patch notes",
"Details": "Dettagli",
"Update": "Aggiorna",
"Remove": "Rimuovi",
"Restore": "Ripristina",
"Download": "Download",
"Next Page": "Pagina successiva",
"Prev Page": "Pagina precedente",
"Unstar": "Rimuovi dai preferiti",
"Star": "Aggiungi ai preferiti",
"System memory": "Memoria di sistema",
"microSD card": "Scheda microSD",
"Sd": "SD",
"Image System memory": "Immagine memoria di sistema",
"Image microSD card": "Immagine scheda microSD",
"Slow": "",
"Normal": "",
"Fast": "",
"Yes": "Sì",
"No": "No",
"Enabled": "Abilitato",
"Disabled": "Disabilitato",
"Sort By": "Ordina per",
"Sort Options": "Opzioni filtro",
"Filter": "Filtro",
"Sort": "Riordina",
"Order": "Ordina",
"Search": "Ricerca",
"Updated": "Aggiornato",
"Updated (Star)": "",
"Downloads": "Download",
"Size": "Dimensione",
"Size (Star)": "Dimensione (Preferiti)",
"Alphabetical": "Alfabetico",
"Alphabetical (Star)": "Alfabetico (Preferiti)",
"Likes": "Mi Piace",
"ID": "ID",
"Descending": "Decrescente",
"Descending (down)": "Decrescente",
"Desc": "Decrescente",
"Ascending": "Crescente",
"Ascending (Up)": "Crescente",
"Asc": "Crescente",
"Switch-Handheld!": "Switch Portatile",
"Switch-Docked!": "Switch Dock",
"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": "",
"If this message appears repeatedly, please open an issue.": "Se questo messaggio appare frequentemente, segnala il bug.",
"Menu Options": "Opzioni menu",
"Menu": "Menu",
"Theme": "Tema",
"Theme Options": "Opzioni tema",
"Select Theme": "Seleziona tema",
"Shuffle": "Mescola",
"Music": "Musica",
"12 Hour Time": "",
"Download Default Music": "",
"Failed to download default_music.bfstm, please try again": "",
"Overwrite current default music?": "",
"Network": "Rete",
"Network Options": "Opzioni di rete",
"Ftp": "FTP",
@@ -75,8 +27,7 @@
"Nxlink Connected": "Nxlink connesso",
"Nxlink Upload": "Nxlink upload",
"Nxlink Finished": "Nxlink finito",
"Switch-Handheld!": "Switch Portatile",
"Switch-Docked!": "Switch Dock",
"Language": "Lingua",
"Auto": "Auto",
"English": "English",
@@ -92,87 +43,101 @@
"Russian": "Русский",
"Swedish": "Svenska",
"Vietnamese": "Vietnamese",
"Logging": "Logging",
"Replace hbmenu on exit": "Sostituisci hbmenu all'uscita",
"Ukrainian": "Українська",
"Misc": "Varie",
"Misc Options": "Opzioni varie",
"Web": "Rete",
"Install forwarders": "Installa forwarder",
"Install location": "Installa posizione",
"Show install warning": "Mostra avvertimento installazione",
"Text scroll speed": "",
"FileBrowser": "FileBrowser",
"%zd files": "%zd files",
"%zd dirs": "%zd dirs",
"File Options": "Opzioni file",
"Show Hidden": "Mostra nascosto",
"Folders First": "Prima le cartelle",
"Hidden Last": "Ultimo nascosto",
"Cut": "Taglia",
"Copy": "Copia",
"Paste": "Incolla",
"Paste ": "Incolla ",
" file(s)?": "(i)file?",
"Rename": "Rinomina",
"Set New File Name": "Imposta nuovo nome",
"Advanced": "Avanzato",
"Advanced Options": "Opzioni avanzate",
"Create File": "Crea file",
"Set File Name": "Imposta nome",
"Create Folder": "Crea cartella",
"Set Folder Name": "Imposta nome",
"View as text (unfinished)": "Visualizza come testo (non finito)",
"Ignore read only": "Ignora read only",
"Mount": "Monta",
"Empty...": "Vuoto...",
"Open with DayBreak?": "Vuoi aprire con Daybreak?",
"Launch ": "Lancia",
"Launch option for: ": "Lancia opzione per",
"Select launcher for: ": "Scegli launcher per",
"Homebrew": "Homebrew",
"Homebrew Options": "Opzioni Homebrew",
"Hide Sphaira": "Nascondi Sphaira",
"Install Forwarder": "Installa forwarder",
"WARNING: Installing forwarders will lead to a ban!": "ATTENZIONE: l'installazione di forwarder porterà al ban!",
"Installing Forwarder": "",
"Creating Program": "",
"Creating Control": "",
"Creating Meta": "",
"Writing Nca": "",
"Updating ncm databse": "",
"Pushing application record": "",
"Installed!": "",
"Failed to install forwarder": "",
"Unstarred ": "",
"Starred ": "",
"AppStore": "",
"Filter: %s | Sort: %s | Order: %s": "Filtro: %s | Riordina: %s | Ordina: %s",
"AppStore Options": "Opzioni dell'App Store",
"All": "Tutto",
"Games": "Giochi",
"Emulators": "Emulatori",
"Tools": "Strumenti",
"Themes": "Temi",
"Legacy": "Legacy",
"version: %s": "version: %s",
"updated: %s": "updated: %s",
"category: %s": "category: %s",
"extracted: %.2f MiB": "extracted: %.2f MiB",
"app_dls: %s": "app_dls: %s",
"More by Author": "",
"Leave Feedback": "",
"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!": "",
"Themezer": "Themezer",
"Themezer Options": "Impostazioni Themezer",
"Nsfw": "NSFW",
"Page": "Pagina",
"Page %zu / %zu": "Pagina %zu / %zu",
"Enter Page Number": "Inserisci il numero della pagina",
"Bad Page": "Pagina invalida",
"Download theme?": "Vuoi scaricare il tema?",
"GitHub": "GitHub",
"Downloading json": "Scaricamento json",
"Select asset to download for ": "",
"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!": "",
"IRS (Infrared Joycon Camera)": "",
"IRS": "",
"Irs": "Irs",
"Ambient Noise Level: ": "",
"Controller": "Controller",
"Pad ": "Pad ",
"HandHeld": "HandHeld",
" (Available)": " (Disponibile)",
" (Unsupported)": "",
" (Unconnected)": " (Non connesso)",
"HandHeld": "HandHeld",
"Rotation": "Rotazione",
"0 (Sideways)": "0 (Di lato)",
"90 (Flat)": "90 (Piatto)",
@@ -194,64 +159,234 @@
"Normal image": "Immagine normale",
"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",
"Trimming Format": "Formato di ritaglio",
"External Light Filter": "Filtro luce esterno",
"Load Default": "Carica predefinito",
"Themezer": "Themezer",
"Themezer Options": "Impostazioni Themezer",
"Nsfw": "NSFW",
"Page": "Pagina",
"Page %zu / %zu": "Pagina %zu / %zu",
"Enter Page Number": "Inserisci il numero della pagina",
"Bad Page": "Pagina invalida",
"Download theme?": "Vuoi scaricare il tema?",
"GitHub": "GitHub",
"Downloading json": "Scaricamento json",
"Select asset to download for": "Scegli l'asset da scaricare per",
"Installing ": "Installazione",
"Uninstalling ": "Disinstallazione",
"Deleting ": "Eliminazione",
"Deleting": "Eliminazione",
"Pasting ": "Incollo",
"Pasting": "Incollo",
"Removing ": "Rimozione",
"Scanning ": "Scan",
"Creating ": "Creazione",
"Copying ": "Copio",
"Trying to load ": "Cercando di caricare",
"Downloading ": "Scaricando",
"Downloaded ": "Scaricato",
"Removed ": ""Rimosso,
"Checking MD5": "Controllo MD5",
"Loading...": "Caricamento...",
"Loading": "Caricamento",
"Empty!": "Vuoto!",
"Not Ready...": "Non pronto...",
"Error loading page!": "Errore nel caricare la pagina!",
"Update avaliable: ": "Aggiornamento disponibile",
"Download update: ": "Scarica aggiornamento",
"Updated to ": "Aggiornato a",
"Press OK to restart Sphaira": "Premi OK per riavviare Sphaira",
"Restart Sphaira?": "Vuoi riavviare Sphaira?",
"Failed to download update": "Download aggiornamento fallito",
"Advanced": "Avanzato",
"Advanced Options": "Opzioni avanzate",
"Logging": "Logging",
"Replace hbmenu on exit": "Sostituisci hbmenu all'uscita",
"Restore hbmenu?": "Vuoi ripristinare hbmenu?",
"Restore": "Ripristina",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Impossibile trovare /switch/hbmenu.nro\nUsa l'Appstore per reinstallare hbmenu",
"Failed to restore hbmenu, please re-download hbmenu": "Impossibile ripristinare hbmenu, per favore riscaricalo",
"Failed to restore hbmenu, using sphaira instead": "Impossibile ripristinare hbmenu, uso Sphaira invece",
"Restored hbmenu, closing sphaira": "hbmenu ripristinato, chiudo Sphaira",
"Restored hbmenu": "hbmenu ripristinato",
"Restart Sphaira?": "Vuoi riavviare Sphaira?",
"Press OK to restart Sphaira": "Premi OK per riavviare Sphaira",
"Text scroll speed": "",
"Slow": "",
"Normal": "",
"Fast": "",
"Set right-side menu": "",
"Install options": "",
"Install Options": "",
"Enable sysmmc": "",
"Enable emummc": "",
"Show install warning": "Mostra avvertimento installazione",
"Install location": "Installa posizione",
"System memory": "Memoria di sistema",
"microSD card": "Scheda 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": "",
"Homebrew": "Homebrew",
"Apps": "App",
"Homebrew Options": "Opzioni Homebrew",
"Hide Sphaira": "Nascondi Sphaira",
"Install Forwarder": "Installa forwarder",
"WARNING: Installing forwarders will lead to a ban!": "ATTENZIONE: l'installazione di forwarder porterà al ban!",
"Installing Forwarder": "",
"Creating Program": "",
"Creating Control": "",
"Creating Meta": "",
"Writing Nca": "",
"Updating ncm databse": "",
"Pushing application record": "",
"Failed to install forwarder": "",
"Unstarred ": "",
"Starred ": "",
"Failed to remove old forwarder, please manually remove it!": "",
"AppStore": "",
"Appstore": "",
"Store": "Store",
"Filter: %s | Sort: %s | Order: %s": "Filtro: %s | Riordina: %s | Ordina: %s",
"AppStore Options": "Opzioni dell'App Store",
"Info": "Informazioni",
"Changelog": "Patch notes",
"Details": "Dettagli",
"version: %s": "version: %s",
"updated: %s": "updated: %s",
"category: %s": "category: %s",
"extracted: %.2f MiB": "extracted: %.2f MiB",
"app_dls: %s": "app_dls: %s",
"More by Author": "",
"Leave Feedback": "",
"FileBrowser": "FileBrowser",
"Files": "File",
"%zd files": "%zd files",
"%zd dirs": "%zd dirs",
"File Options": "Opzioni file",
"Show Hidden": "Mostra nascosto",
"Folders First": "Prima le cartelle",
"Hidden Last": "Ultimo nascosto",
"Cut": "Taglia",
"Copy": "Copia",
"Copying ": "Copio",
"Paste": "Incolla",
"Paste ": "Incolla ",
" file(s)?": "(i)file?",
"Pasting ": "Incollo",
"Pasting": "Incollo",
"Rename": "Rinomina",
"Set New File Name": "Imposta nuovo nome",
"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!": "",
"Create File": "Crea file",
"Set File Name": "Imposta nome",
"Create Folder": "Crea cartella",
"Set Folder Name": "Imposta nome",
"Creating ": "Creazione",
"Upload": "",
"Select upload location": "",
"No upload locations set!": "",
"Uploading": "",
"Upload successfull!": "",
"Upload failed!": "",
"View as text (unfinished)": "Visualizza come testo (non finito)",
"Ignore read only": "Ignora read only",
"Mount": "Monta",
"Sd": "SD",
"Image System memory": "Immagine memoria di sistema",
"Image microSD card": "Immagine scheda microSD",
"Empty...": "Vuoto...",
"Open with DayBreak?": "Vuoi aprire con Daybreak?",
"Launch ": "Lancia",
"Launch option for: ": "Lancia opzione per",
"Select launcher for: ": "Scegli launcher per",
"Sort By": "Ordina per",
"Sort Options": "Opzioni filtro",
"Filter": "Filtro",
"All": "Tutto",
"Emulators": "Emulatori",
"Tools": "Strumenti",
"Themes": "Temi",
"Legacy": "Legacy",
"Sort": "Riordina",
"Size": "Dimensione",
"Size (Star)": "Dimensione (Preferiti)",
"Alphabetical": "Alfabetico",
"Alphabetical (Star)": "Alfabetico (Preferiti)",
"Updated": "Aggiornato",
"Updated (Star)": "",
"Downloads": "Download",
"Likes": "Mi Piace",
"ID": "ID",
"Order": "Ordina",
"Descending": "Decrescente",
"Descending (down)": "Decrescente",
"Desc": "Decrescente",
"Ascending": "Crescente",
"Ascending (Up)": "Crescente",
"Asc": "Crescente",
"Layout": "",
"List": "",
"Icon": "",
"Grid": "",
"Search": "Ricerca",
"Options": "Opzioni",
"OK": "OK",
"Back": "Indietro",
"Select": "Seleziona",
"Open": "Apri",
"Launch": "Lancia",
"Restart": "Riavvia",
"Next": "",
"Prev": "",
"Unstar": "Rimuovi dai preferiti",
"Star": "Aggiungi ai preferiti",
"Yes": "Sì",
"No": "No",
"On": "",
"Off": "",
"Install": "Installa",
"Install Selected files?": "",
"Installing ": "Installazione",
"Installed ": "",
"Installed!": "",
"Trying to load ": "Cercando di caricare",
"Checking MD5": "Controllo MD5",
"Delete": "Elimina",
"Delete Selected files?": "Vuoi rimuovere i file selezionati?",
"Completely remove ": "Elimina definitivamente",
"Are you sure you want to delete ": "Sei sicuro di voler eliminare? ",
"Are you sure you wish to cancel?": "Sei sicuro di voler annullare?",
"Audio disabled due to suspended game": "Audio disabilitato poichè un app è in pausa",
"If this message appears repeatedly, please open an issue.": "Se questo messaggio appare frequentemente, segnala il bug."
}
"Scanning ": "Scan",
"Deleting ": "Eliminazione",
"Deleting": "Eliminazione",
"Remove": "Rimuovi",
"Completely remove ": "Elimina definitivamente",
"Removing ": "Rimozione",
"Removed ": "Rimosso",
"Uninstalling ": "Disinstallazione",
"Download": "Download",
"Downloading ": "Scaricando",
"Downloaded ": "Scaricato",
"Update": "Aggiorna",
"Update avaliable: ": "Aggiornamento disponibile",
"Download update: ": "Scarica aggiornamento",
"Updated to ": "Aggiornato a",
"Failed to download update": "Download aggiornamento fallito",
"%zu hours %zu minutes remaining": "",
"%zu minutes %zu seconds remaining": "",
"%zu seconds remaining": "",
"Loading...": "Caricamento...",
"Loading": "Caricamento",
"Empty!": "Vuoto!",
"Not Ready...": "Non pronto...",
"Error loading page!": "Errore nel caricare la pagina!"
}

View File

@@ -1,72 +1,24 @@
{
"[Applet Mode]": "Appletモード",
"No Internet": "インターネットなし",
"Files": "ファイル",
"Apps": "アプリ",
"Store": "AppStore",
"Menu": "メニュー",
"Options": "設定",
"OK": "確認",
"Back": "戻る",
"Select": "選択",
"Open": "開く",
"Launch": "起動",
"Info": "情報",
"Install": "インストール",
"Delete": "削除",
"Restart": "再起動",
"Changelog": "リリースノート",
"Details": "詳細",
"Update": "アップデート",
"Remove": "除去",
"Restore": "復元",
"Download": "ダウンロード",
"Next Page": "次のページ",
"Prev Page": "前のページ",
"Unstar": "お気に入り解除",
"Star": "お気に入り",
"System memory": "システムメモリ",
"microSD card": "SDメモリーカード",
"Sd": "SDメモリーカード",
"Image System memory": "システムメモリイメージ",
"Image microSD card": "SDイメージ",
"Slow": "遅い",
"Normal": "普通",
"Fast": "速い",
"Yes": "はい",
"No": "いいえ",
"Enabled": "",
"Disabled": "",
"Sort By": "並べ替え",
"Sort Options": "並べ替え設定",
"Filter": "フィルター",
"Sort": "並べ替え",
"Order": "順番",
"Search": "検索",
"Updated": "アップデート順",
"Updated (Star)": "アップデート順(お気に入り)",
"Downloads": "ダウンロード順",
"Size": "ファイルサイズ",
"Size (Star)": "ファイルサイズ(お気に入り)",
"Alphabetical": "アルファベット順",
"Alphabetical (Star)": "アルファベット順(お気に入り)",
"Likes": "いいね順",
"ID": "デベロッパー順",
"Descending": "降順",
"Descending (down)": "降順",
"Desc": "降順",
"Ascending": "上昇",
"Ascending (Up)": "上昇",
"Asc": "上昇",
"Switch-Handheld!": "ハンドヘルド!",
"Switch-Docked!": "ドック接続!",
"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.": "このメッセージが繰り返し表示される場合は、問題を開いてください",
"Menu Options": "メニュー設定",
"Menu": "メニュー",
"Theme": "テーマ",
"Theme Options": "テーマ設定",
"Select Theme": "テーマを選ぶ",
"Shuffle": "シャッフル",
"Music": "BGM",
"12 Hour Time": "",
"12 Hour Time": "12時間表示",
"Download Default Music": "基本BGMダウンロード",
"Failed to download default_music.bfstm, please try again": "基本BGMのダウンロードに失敗しました、もう一度お試しください",
"Overwrite current default music?": "BGMを書き換えますか?",
"Network": "ネットワーク",
"Network Options": "ネットワーク設定",
"Ftp": "FTP",
@@ -75,8 +27,7 @@
"Nxlink Connected": "Nxlink 接続",
"Nxlink Upload": "Nxlink アップロード",
"Nxlink Finished": "Nxlink 終了",
"Switch-Handheld!": "ハンドヘルド!",
"Switch-Docked!": "ドック接続!",
"Language": "言語",
"Auto": "自動",
"English": "English",
@@ -92,87 +43,101 @@
"Russian": "Русский",
"Swedish": "Svenska",
"Vietnamese": "Vietnamese",
"Logging": "ログの取得",
"Replace hbmenu on exit": "終了時に hbmenu を置き換える",
"Ukrainian": "Українська",
"Misc": "その他",
"Misc Options": "その他",
"Web": "ウェブブラウザ",
"Install forwarders": "Forwarderのインストール機能",
"Install location": "インストール経路",
"Show install warning": "警告文を示す",
"Text scroll speed": "流れる文字の速さ",
"FileBrowser": "ファイルブラウザ",
"%zd files": "%zd個のファイル",
"%zd dirs": "%zd個のフォルダー",
"File Options": "ファイル設定",
"Show Hidden": "非表示ファイルを表示",
"Folders First": "フォルダーを優先",
"Hidden Last": "非表示ファイルを劣後",
"Cut": "切り取り",
"Copy": "コピー",
"Paste": "ペースト",
"Paste ": " ",
" file(s)?": "個のファイルをペーストしますか?",
"Rename": "名前の変更",
"Set New File Name": "新しい名前を入力",
"Advanced": "高度な",
"Advanced Options": "高度設定",
"Create File": "ファイルの作成",
"Set File Name": "名前を入力",
"Create Folder": "フォルダーの作成",
"Set Folder Name": "名前を入力",
"View as text (unfinished)": "テキストとして表示 (未完成)",
"Ignore read only": "読み取り専用を無視する",
"Mount": "マウント",
"Empty...": "このフォルダーは空です",
"Open with DayBreak?": "DayBreakで開きますか?",
"Launch ": "起動しますか",
"Launch option for: ": "起動設定: ",
"Select launcher for: ": "起動ランチャーを選ぶ: ",
"Homebrew": "Homebrew",
"Homebrew Options": "Homebrew設定",
"Hide Sphaira": "Sphairaを非表示",
"Install Forwarder": "Forwarderのインストール",
"WARNING: Installing forwarders will lead to a ban!": "警告: ForwarderをインストールするとBANされます。",
"Installing Forwarder": "Forwarderのインストール中",
"Creating Program": "プログラム作成中",
"Creating Control": "コントロール作成中",
"Creating Meta": "メター作成中",
"Writing Nca": "Nca書き取り中",
"Updating ncm databse": "ncmのDBをアップデート中",
"Pushing application record": "アプリの記録をプッシュ中",
"Installed!": "インストール完了",
"Failed to install forwarder": "Forwarderのインストール失敗",
"Unstarred ": "お気に入り解除: ",
"Starred ": "お気に入りに登録: ",
"AppStore": "AppStore",
"Filter: %s | Sort: %s | Order: %s": "フィルター: %s | 並べ替え: %s | 順番: %s",
"AppStore Options": "AppStoreの設定",
"All": "全て",
"Games": "ゲーム",
"Emulators": "エミュレータ",
"Tools": "ツール",
"Themes": "テーマ",
"Legacy": "レガシー",
"version: %s": "バージョン: %s",
"updated: %s": "更新日: %s",
"category: %s": "カテゴリー: %s",
"extracted: %.2f MiB": "容量: %.2f MiB",
"app_dls: %s": "ダウンロード: %s",
"More by Author": "ディベロッパーの他のアプリを見る",
"Leave Feedback": "意見を残す",
"Game Options": "ゲーム設定",
"Hide forwarders": "Forwarderを隠す",
"Launch random game": "ゲームをランダム起動",
"List meta records": "メタレコード一覧",
"Entries": "エントリ",
"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": "ゲームパッチのみ",
"Dump AddOnContent": "DLCのみ",
"Dump DataPatch": "DLCパッチのみ",
"Select dump location": "吸出し位置を選択",
"microSD card (/dumps/NSP/)": "SDカード (/dumps/NSP/)",
"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!": "削除失敗!",
"Themezer": "Themezer",
"Themezer Options": "Themezer設定",
"Nsfw": "アダルトテーマ",
"Page": "ページ",
"Page %zu / %zu": "ページ %zu / %zu",
"Enter Page Number": "ページの番号を入力",
"Bad Page": "ページが見つかりません",
"Download theme?": "テーマをインストールしますか?",
"GitHub": "GitHub",
"Downloading json": "JSONからダウンロード",
"Select asset to download for ": "ダウンロードアイテムを選択 ",
"FTP Install": "FTPでインストール",
"FTP Install (EXPERIMENTAL)": "FTPでインストール(実験機能)",
"Connection Type: WiFi | Strength: ": "接続: WiFi | 強度: ",
"Connection Type: Ethernet": "接続: イーサネット",
"Connection Type: None": "接続: なし",
"Host:": "ホースと:",
"Port:": "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インストール失敗!",
"USB Install": "USBインストール",
"USB": "USBインストール",
"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接続待機中",
"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...": "を押して終了します",
"GameCard Install": "ゲームカードインストール",
"GameCard": "ゲームカードインストール",
"GC": "ゲームカード",
"System memory %.1f GB": "本体保存メモリー %.1f GB",
"microSD card %.1f GB": "SDカード %.1f GB",
"Nand Install": "本体保存メモリーにインストール",
"SD Card Install": "SDカードにインストール",
"Exit": "もどる",
"Gc install success!": "ゲームカードインストール完了!",
"Gc install failed!": "ゲームカードインストール失敗!",
"IRS (Infrared Joycon Camera)": "Joy-Con IRカメラ",
"IRS": "Joy-Con IRカメラ",
"Irs": "Joy-Con IRカメラ",
"Ambient Noise Level: ": "ノイズレベル: ",
"Controller": "コントローラー",
"Pad ": "Joy-Con ",
"HandHeld": "ハンドヘルド",
" (Available)": " (利用可能)",
" (Unsupported)": " (未対応)",
" (Unconnected)": " (未接続)",
"HandHeld": "ハンドヘルド",
"Rotation": "回転",
"0 (Sideways)": "0 (横)",
"90 (Flat)": "90 (フラット)",
@@ -194,64 +159,234 @@
"Normal image": "通常画像",
"Negative image": "ネガティブなイメージ",
"Format": "解像度",
"Trimming Format": "トリミングされた解像度",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "トリミングされた解像度",
"External Light Filter": "外光フィルター",
"Load Default": "基本設定に戻す",
"Themezer": "Themezer",
"Themezer Options": "Themezer設定",
"Nsfw": "アダルトテーマ",
"Page": "ページ",
"Page %zu / %zu": "ページ %zu / %zu",
"Enter Page Number": "ページの番号を入力",
"Bad Page": "ページが見つかりません",
"Download theme?": "テーマをインストールしますか?",
"GitHub": "GitHub",
"Downloading json": "JSONからダウンロード",
"Select asset to download for ": "ダウンロードアイテムを選択 ",
"Installing ": "インストール中 ",
"Uninstalling ": "アンインストール中 ",
"Deleting ": "削除中 ",
"Deleting": "削除中",
"Pasting ": "ペースト中 ",
"Pasting": "ペースト中",
"Removing ": "除去中 ",
"Scanning ": "スキャン中 ",
"Creating ": "作成中 ",
"Copying ": "コピー中 ",
"Trying to load ": "サムネイルを取得中 ",
"Downloading ": "ダウンロード中 ",
"Downloaded ": "ダウンロード完了 ",
"Removed ": "除去完了 ",
"Checking MD5": "MD5を確認中 ",
"Loading...": "ロード中",
"Loading": "ロード中",
"Empty!": "何も見つかりません",
"Not Ready...": "準備ができていません",
"Error loading page!": "ページのロードエラー",
"Update avaliable: ": "アップデート可能: ",
"Download update: ": "アップデートをダウンロード: ",
"Updated to ": "アップデート: ",
"Press OK to restart Sphaira": "確認ボタンを押してSphairaを再起動",
"Restart Sphaira?": "Sphairaを再起動しますか?",
"Failed to download update": "アップデートのダウンロード失敗",
"Advanced": "高度な",
"Advanced Options": "高度設定",
"Logging": "ログの取得",
"Replace hbmenu on exit": "終了時に hbmenu を置き換える",
"Restore hbmenu?": "hbmenuに戻しますか?",
"Restore": "復元",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "/switch/hbmemu.nroが見つかりません\nAppstoreから再インストールしてください",
"Failed to restore hbmenu, please re-download hbmenu": "hbmenuを復元できませんでした、再インストールしてください",
"Failed to restore hbmenu, using sphaira instead": "hbmenuを復元できませんでした、sphairaを引き続き使います",
"Restored hbmenu, closing sphaira": "hbmenuに復元されました、sphairaを終了します",
"Restored hbmenu": "hbmenuに復元されました",
"Restart Sphaira?": "Sphairaを再起動しますか?",
"Press OK to restart Sphaira": "確認ボタンを押してSphairaを再起動",
"Text scroll speed": "流れる文字の速さ",
"Slow": "遅い",
"Normal": "普通",
"Fast": "速い",
"Set right-side menu": "右側メニュー設定",
"Install options": "インストール設定",
"Install Options": "インストール設定",
"Enable sysmmc": "SYSNANDを有効化",
"Enable emummc": "EMUNANDを有効化",
"Show install warning": "警告文を示す",
"Install location": "インストール経路",
"System memory": "本体保存メモリー",
"microSD card": "SDカード",
"Boost CPU clock": "CPUクロックをブースト",
"Allow downgrade": "ダウングレード許可",
"Skip if already installed": "既にインストールされている場合はスキップします",
"Ticket only": "チケットのみ設置",
"Skip base": "ゲームをスキップ",
"Skip patch": "ゲームパッチをスキップ",
"Skip dlc": "DLCをスキップ",
"Skip data patch": "DLCパッチをスキップ",
"Skip ticket": "チケットをスキップ",
"Skip NCA hash verify": "NCAハッシュ検証をスキップ",
"Skip RSA header verify": "RSAヘッダー検証をスキップ",
"Skip RSA NPDM verify": "RSA NPDM検証をスキップ",
"Ignore distribution bit": "分散ビットを無視",
"Convert to standard crypto": "標準暗号に変換",
"Lower master key": "下位マスターキーを許容",
"Lower system version": "下位システムバージョンを許容",
"Homebrew": "Homebrew",
"Apps": "アプリ",
"Homebrew Options": "Homebrew設定",
"Hide Sphaira": "Sphairaを非表示",
"Install Forwarder": "Forwarderのインストール",
"WARNING: Installing forwarders will lead to a ban!": "警告: ForwarderをインストールするとBANされます",
"Installing Forwarder": "Forwarderのインストール中",
"Creating Program": "プログラム作成中",
"Creating Control": "コントロール作成中",
"Creating Meta": "メター作成中",
"Writing Nca": "Nca書き取り中",
"Updating ncm databse": "ncmのDBをアップデート中",
"Pushing application record": "アプリの記録をプッシュ中",
"Failed to install forwarder": "Forwarderのインストール失敗",
"Unstarred ": "お気に入り解除: ",
"Starred ": "お気に入りに登録: ",
"Failed to remove old forwarder, please manually remove it!": "古いForwarderを削除できませんでした、手動で削除してください!",
"AppStore": "AppStore",
"Appstore": "AppStore",
"Store": "AppStore",
"Filter: %s | Sort: %s | Order: %s": "フィルター: %s | 並べ替え: %s | 順番: %s",
"AppStore Options": "AppStoreの設定",
"Info": "情報",
"Changelog": "リリースノート",
"Details": "詳細",
"version: %s": "バージョン: %s",
"updated: %s": "更新日: %s",
"category: %s": "カテゴリー: %s",
"extracted: %.2f MiB": "容量: %.2f MiB",
"app_dls: %s": "ダウンロード: %s",
"More by Author": "ディベロッパーの他のアプリを見る",
"Leave Feedback": "意見を残す",
"FileBrowser": "ファイルブラウザ",
"Files": "ファイル",
"%zd files": "%zd個のファイル",
"%zd dirs": "%zd個のフォルダー",
"File Options": "ファイル設定",
"Show Hidden": "非表示ファイルを表示",
"Folders First": "フォルダーを優先",
"Hidden Last": "非表示ファイルを劣後",
"Cut": "切り取り",
"Copy": "コピー",
"Copying ": "コピー中 ",
"Paste": "ペースト",
"Paste ": " ",
" file(s)?": "個のファイルをペーストしますか?",
"Pasting ": "ペースト中 ",
"Pasting": "ペースト中",
"Rename": "名前の変更",
"Set New File Name": "新しい名前を入力",
"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": "ZIPファイルに圧縮",
"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)": "テキストとして表示 (未完成)",
"Ignore read only": "読み取り専用を無視する",
"Mount": "マウント",
"Sd": "SDメモリーカード",
"Image System memory": "システムメモリイメージ",
"Image microSD card": "SDイメージ",
"Empty...": "このフォルダーは空です",
"Open with DayBreak?": "DayBreakで開きますか?",
"Launch ": "起動しますか",
"Launch option for: ": "起動設定: ",
"Select launcher for: ": "起動ランチャーを選ぶ: ",
"Sort By": "並べ替え",
"Sort Options": "並べ替え設定",
"Filter": "フィルター",
"All": "全て",
"Emulators": "エミュレータ",
"Tools": "ツール",
"Themes": "テーマ",
"Legacy": "レガシー",
"Sort": "並べ替え",
"Size": "ファイルサイズ",
"Size (Star)": "ファイルサイズ(お気に入り)",
"Alphabetical": "アルファベット順",
"Alphabetical (Star)": "アルファベット順(お気に入り)",
"Updated": "アップデート順",
"Updated (Star)": "アップデート順(お気に入り)",
"Downloads": "ダウンロード順",
"Likes": "いいね順",
"ID": "デベロッパー順",
"Order": "順番",
"Descending": "降順",
"Descending (down)": "降順",
"Desc": "降順",
"Ascending": "上昇",
"Ascending (Up)": "上昇",
"Asc": "上昇",
"Layout": "レイアウト",
"List": "リスト",
"Icon": "アイコン",
"Grid": "グリッド",
"Search": "検索",
"Options": "設定",
"OK": "確認",
"Back": "戻る",
"Select": "選択",
"Open": "開く",
"Launch": "起動",
"Restart": "再起動",
"Next": "次へ",
"Prev": "前へ",
"Unstar": "お気に入り解除",
"Star": "お気に入り",
"Yes": "はい",
"No": "いいえ",
"On": "オン",
"Off": "オフ",
"Install": "インストール",
"Install Selected files?": "選択したファイルをインストールしますか?",
"Installing ": "インストール中 ",
"Installed ": "インストールしました ",
"Installed!": "インストール完了",
"Trying to load ": "サムネイルを取得中 ",
"Checking MD5": "MD5を確認中 ",
"Delete": "削除",
"Delete Selected files?": "本当に削除しますか?",
"Completely remove ": "除去しますか ",
"Are you sure you want to delete ": "消去してもよろしいですか ",
"Are you sure you wish to cancel?": "本当に取り消しますか?",
"Audio disabled due to suspended game": "ゲームが一時停止状態の場合、オーディオは無効になります",
"If this message appears repeatedly, please open an issue.": "このメッセージが繰り返し表示される場合は、問題を開いてください。"
"Scanning ": "スキャン中 ",
"Deleting ": "削除中 ",
"Deleting": "削除中",
"Remove": "除去",
"Completely remove ": "除去しますか ",
"Removing ": "除去中 ",
"Removed ": "除去完了 ",
"Uninstalling ": "アンインストール中 ",
"Download": "ダウンロード",
"Downloading ": "ダウンロード中 ",
"Downloaded ": "ダウンロード完了 ",
"Update": "アップデート",
"Update avaliable: ": "アップデート可能: ",
"Download update: ": "アップデートをダウンロード: ",
"Updated to ": "アップデート: ",
"Failed to download update": "アップデートのダウンロード失敗",
"%zu hours %zu minutes remaining": "残り %zu 時間 %zu 分",
"%zu minutes %zu seconds remaining": "残り %zu 分",
"%zu seconds remaining": "残り %zu 秒",
"Loading...": "ロード中",
"Loading": "ロード中",
"Empty!": "何も見つかりません",
"Not Ready...": "準備ができていません",
"Error loading page!": "ページのロードエラー"
}

View File

@@ -1,72 +1,24 @@
{
"[Applet Mode]": "[ 애플릿 모드 ]",
"No Internet": "인터넷 연결 없음",
"Files": "파일 탐색기",
"Apps": "홈브류",
"Store": "앱스토어",
"Menu": "메뉴",
"Options": "설정",
"OK": "확인",
"Back": "뒤로",
"Select": "선택",
"Open": "열기",
"Launch": "실행",
"Info": "정보",
"Install": "설치",
"Delete": "삭제",
"Restart": "재시작",
"Changelog": "변경 내역",
"Details": "상세",
"Update": "업데이트",
"Remove": "제거",
"Restore": "복원",
"Download": "다운로드",
"Next Page": "다음 페이지",
"Prev Page": "이전 페이지",
"Unstar": "즐겨찾기 해제",
"Star": "즐겨찾기",
"System memory": "낸드 저장소",
"microSD card": "SD 카드",
"Sd": "SD 카드",
"Image System memory": "낸드 이미지",
"Image microSD card": "SD 이미지",
"Slow": "느림",
"Normal": "보통",
"Fast": "빠름",
"Yes": "예",
"No": "아니요",
"Enabled": "",
"Disabled": "",
"Sort By": "정렬",
"Sort Options": "정렬 옵션",
"Filter": "필터",
"Sort": "분류",
"Order": "정렬",
"Search": "검색",
"Updated": "업데이트순",
"Updated (Star)": "업데이트순 (즐겨찾기)",
"Downloads": "다운로드순",
"Size": "크기순",
"Size (Star)": "크기순 (즐겨찾기)",
"Alphabetical": "알파벳순",
"Alphabetical (Star)": "알파벳순 (즐겨찾기)",
"Likes": "좋아요순",
"ID": "ID순",
"Descending": "내림차순",
"Descending (down)": "내림차순",
"Desc": "내림차순",
"Ascending": "오름차순",
"Ascending (Up)": "오름차순",
"Asc": "오름차순",
"Switch-Handheld!": "휴대모드로 전환됨!",
"Switch-Docked!": "독 모드로 전환됨!",
"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.": "해당 메시지가 반복해서 나타나는 경우, 이슈를 등록하세요.",
"Menu Options": "메뉴",
"Menu": "메뉴",
"Theme": "테마",
"Theme Options": "테마 옵션",
"Select Theme": "테마 선택",
"Shuffle": "셔플",
"Music": "BGM",
"12 Hour Time": "",
"12 Hour Time": "12 시간제",
"Download Default Music": "기본 BGM 다운로드",
"Failed to download default_music.bfstm, please try again": "BGM 파일 다운로드에 실패했습니다, 다시 시도하세요",
"Overwrite current default music?": "BGM 파일을 덮어쓸까요?",
"Network": "네트워크",
"Network Options": "네트워크 옵션",
"Ftp": "FTP (무선)",
@@ -75,8 +27,7 @@
"Nxlink Connected": "Nxlink 연결됨",
"Nxlink Upload": "Nxlink 업로드",
"Nxlink Finished": "Nxlink 종료됨",
"Switch-Handheld!": "휴대모드로 전환됨!",
"Switch-Docked!": "독 모드로 전환됨!",
"Language": "언어",
"Auto": "자동",
"English": "English",
@@ -92,98 +43,112 @@
"Russian": "Русский",
"Swedish": "Svenska",
"Vietnamese": "Vietnamese",
"Logging": "로깅",
"Replace hbmenu on exit": "종료 시 hbmenu 교체",
"Ukrainian": "Українська",
"Misc": "기타",
"Misc Options": "기타 옵션",
"Web": "웹 브라우저",
"Install forwarders": "바로가기 설치",
"Install location": "설치 위치",
"Show install warning": "경고 메시지",
"Text scroll speed": "흐르는 텍스트 속도",
"FileBrowser": "파일 탐색기",
"%zd files": "%zd 개 파일",
"%zd dirs": "%zd 개 폴더",
"File Options": "파일 옵션",
"Show Hidden": "숨겨진 항목 표시",
"Folders First": "폴더 우선 정렬",
"Hidden Last": "숨겨진 항목 후순 정렬",
"Cut": "잘라내기",
"Copy": "복사",
"Paste": "붙여넣기",
"Paste ": " ",
" file(s)?": "개 항목을 붙여넣을까요?",
"Rename": "이름 바꾸기",
"Set New File Name": "새 파일명 입력",
"Advanced": "고급",
"Advanced Options": "고급 옵션",
"Create File": "새 파일",
"Set File Name": "파일명 입력",
"Create Folder": "새 폴더",
"Set Folder Name": "폴더명 입력",
"View as text (unfinished)": "텍스트로 보기 (미완성)",
"Ignore read only": "읽기 전용 설정 무시",
"Mount": "마운트",
"Empty...": "비어있음...",
"Open with DayBreak?": "DayBreak로 열까요?",
"Launch ": "실행할까요 ",
"Launch option for: ": "실행 옵션: ",
"Select launcher for: ": "실행 런처: ",
"Homebrew": "홈브류",
"Homebrew Options": "홈브류 옵션",
"Hide Sphaira": "Sphaira 숨기기",
"Install Forwarder": "바로가기 설치",
"WARNING: Installing forwarders will lead to a ban!": "경고: 시스낸드에 바로가기 설치 시, 밴 위험이 있습니다!",
"Installing Forwarder": "바로가기 설치",
"Creating Program": "프로그램 생성",
"Creating Control": "컨트롤 생성",
"Creating Meta": "메타 생성",
"Writing Nca": "Nca 쓰기",
"Updating ncm databse": "Ncm 데이터베이스 업데이트",
"Pushing application record": "응용 프로그램 기록 푸싱",
"Installed!": "설치 완료!",
"Failed to install forwarder": "바로가기 설치 실패",
"Unstarred ": "즐겨찾기 해제: ",
"Starred ": "즐겨찾기 적용: ",
"AppStore": "앱스토어",
"Filter: %s | Sort: %s | Order: %s": "필터: %s | 분류: %s | 정렬: %s",
"AppStore Options": "앱스토어 옵션",
"All": "모두",
"Games": "게임",
"Emulators": "에뮬레이터",
"Tools": "도구",
"Themes": "테마",
"Legacy": "레거시",
"version: %s": "버전: %s",
"updated: %s": "업데이트: %s",
"category: %s": "카테고리: %s",
"extracted: %.2f MiB": "용량: %.2f MiB",
"app_dls: %s": "다운로드 횟수: %s",
"More by Author": "개발자의 다른 앱 더 보기",
"Leave Feedback": "피드백 남기기",
"Game Options": "게임 옵션",
"Hide forwarders": "바로가기 앱 숨기기",
"Launch random game": "게임 랜덤 실행",
"List meta records": "메타 기록",
"Entries": "목록",
"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": "게임 패치",
"Dump AddOnContent": "DLC",
"Dump DataPatch": "DLC 패치",
"Select dump location": "덤프 위치",
"microSD card (/dumps/NSP/)": "SD 카드 (sdmc:/dumps/NSP/)",
"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!": "삭제 실패!",
"Themezer": "Themezer",
"Themezer Options": "Themezer 옵션",
"Nsfw": "선정성 테마",
"Page": "페이지",
"Page %zu / %zu": "페이지 %zu / %zu",
"Enter Page Number": "페이지 번호 입력",
"Bad Page": "잘못된 페이지 입력됨",
"Download theme?": "테마를 다운로드할까요?",
"GitHub": "GitHub",
"Downloading json": "JSON에서 다운로드",
"Select asset to download for ": "다운로드 아이템: ",
"FTP Install": "FTP 설치",
"FTP Install (EXPERIMENTAL)": "FTP 설치 (실험실 기능)",
"Connection Type: WiFi | Strength: ": "상태: WiFi | 신호 세기: ",
"Connection Type: Ethernet": "상태: 이더넷",
"Connection Type: None": "상태: 연결 없음",
"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 설치 실패!",
"USB Install": "USB 설치",
"USB": "USB 설치",
"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": "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...": " 버튼으로 나가기",
"GameCard Install": "카트리지 설치",
"GameCard": "카트리지 설치",
"GC": "카트리지",
"System memory %.1f GB": "본체 저장 메모리 %.1f GB",
"microSD card %.1f GB": "SD 카드 %.1f GB",
"Nand Install": "본체 저장 메모리에 설치",
"SD Card Install": "SD 카드에 설치",
"Exit": "나가기",
"Gc install success!": "카트리지 설치 완료!",
"Gc install failed!": "카트리지 설치 실패!",
"IRS (Infrared Joycon Camera)": "조이콘 적외선 카메라",
"IRS": "조이콘 적외선 카메라",
"Irs": "조이콘 적외선 카메라",
"Ambient Noise Level: ": "주변 노이즈 레벨: ",
"Controller": "컨트롤러",
"Pad ": "조이콘 ",
"HandHeld": "본체 연결",
" (Available)": " (사용 가능)",
" (Unsupported)": " (지원 안됨)",
" (Unconnected)": " (연결 없음)",
"HandHeld": "본체 연결",
"Rotation": "화면 회전",
"0 (Sideways)": "반시계방향 90° 회전",
"90 (Flat)": "정방향",
"180 (-Sideways)": "시계방향 90° 회전",
"270 (Upside down)": "상하반전",
"Colour": "색상",
"Grey": "회색",
"Colour": "색상 팔레트",
"Grey": "그레이스케일",
"Ironbow": "아이언보우",
"Green": "초록색",
"Red": "빨간색",
"Blue": "파란색",
"Green": "그린",
"Red": "레드",
"Blue": "블루",
"Light Target": "반사 표적",
"All leds": "모든 LED 켜기",
"Bright group": "Bright LED 켜기",
@@ -194,64 +159,234 @@
"Normal image": "일반",
"Negative image": "반전",
"Format": "해상도",
"Trimming Format": "트리밍 해상도",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "트리밍 해상도",
"External Light Filter": "외부 조명 필터",
"Load Default": "기본값으로 설정",
"Themezer": "Themezer",
"Themezer Options": "Themezer 옵션",
"Nsfw": "선정성 테마",
"Page": "페이지",
"Page %zu / %zu": "페이지 %zu / %zu",
"Enter Page Number": "페이지 번호 입력",
"Bad Page": "잘못된 페이지",
"Download theme?": "테마를 다운로드할까요?",
"Advanced": "고급",
"Advanced Options": "고급 옵션",
"Logging": "로깅",
"Replace hbmenu on exit": "종료 시 hbmenu 교체",
"Restore hbmenu?": "hbmenu로 복원할까요?",
"Restore": "복원",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "/switch/hbmemu.nro를 찾을 수 없습니다\n앱스토어에서 다시 설치하세요",
"Failed to restore hbmenu, please re-download hbmenu": "hbmenu 복원 실패함, hbmenu를 다시 다운로드하세요",
"Failed to restore hbmenu, using sphaira instead": "hbmenu 복원 실패함, sphaira를 계속 사용합니다",
"Restored hbmenu, closing sphaira": "hbmenu 복원됨, sphaira를 종료합니다",
"Restored hbmenu": "hbmenu 복원됨",
"Restart Sphaira?": "Sphaira를 재시작할까요?",
"Press OK to restart Sphaira": "확인 버튼 입력하여 Sphaira 재시작",
"Text scroll speed": "긴 텍스트 표시 속도",
"Slow": "천천히",
"Normal": "보통",
"Fast": "빠르게",
"Set right-side menu": "우측 메뉴 설정",
"Install options": "설치 옵션",
"Install Options": "설치 옵션",
"Enable sysmmc": "시스낸드 활성화",
"Enable emummc": "에뮤낸드 활성화",
"Show install warning": "설치 경고 표시",
"Install location": "설치 위치",
"System memory": "본체 저장 메모리",
"microSD card": "SD 카드",
"Boost CPU clock": "CPU 클럭 향상",
"Allow downgrade": "다운그레이드 허용",
"Skip if already installed": "설치된 항목 건너뛰기",
"Ticket only": "티켓만 설치",
"Skip base": "게임 건너뛰기",
"Skip patch": "게임 패치 건너뛰기",
"Skip dlc": "DLC 건너뛰기",
"Skip data patch": "DLC 패치 건너뛰기",
"Skip ticket": "티켓 건너뛰기",
"Skip NCA hash verify": "NCA 해시 확인 건너뛰기",
"Skip RSA header verify": "RSA 헤더 확인 건너뛰기",
"Skip RSA NPDM verify": "RSA NPDM 확인 건너뛰기",
"Ignore distribution bit": "배포 비트 무시",
"Convert to standard crypto": "표준 암호화로 변환",
"Lower master key": "하위 마스터 키 지원",
"Lower system version": "하위 시스템 버전 지원",
"GitHub": "GitHub",
"Downloading json": "JSON에서 다운로드",
"Select asset to download for ": "다운로드 아이템: ",
"Homebrew": "홈브류",
"Apps": "홈브류",
"Homebrew Options": "홈브류 옵션",
"Hide Sphaira": "Sphaira 숨기기",
"Install Forwarder": "바로가기 설치",
"WARNING: Installing forwarders will lead to a ban!": "경고: 시스낸드에 설치 시, 밴 위험이 있습니다!",
"Installing Forwarder": "바로가기 설치",
"Creating Program": "프로그램 생성",
"Creating Control": "컨트롤 생성",
"Creating Meta": "메타 생성",
"Writing Nca": "Nca 쓰기",
"Updating ncm databse": "Ncm 데이터베이스 업데이트",
"Pushing application record": "응용 프로그램 기록 푸싱",
"Failed to install forwarder": "바로가기 설치 실패함",
"Unstarred ": "즐겨찾기 해제: ",
"Starred ": "즐겨찾기 등록: ",
"Failed to remove old forwarder, please manually remove it!": "바로가기 제거 실패함, 직접 제거해주세요!",
"Installing ": "설치 ",
"Uninstalling ": "설치 제거 ",
"Deleting ": "삭제 ",
"Deleting": "삭제",
"Pasting ": "붙여넣기 ",
"AppStore": "앱스토어",
"Appstore": "앱스토어",
"Store": "앱스토어",
"Filter: %s | Sort: %s | Order: %s": "필터: %s | 분류: %s | 정렬: %s",
"AppStore Options": "앱스토어 옵션",
"Info": "정보",
"Changelog": "변경사항",
"Details": "상세",
"version: %s": "버전: %s",
"updated: %s": "업데이트: %s",
"category: %s": "카테고리: %s",
"extracted: %.2f MiB": "용량: %.2f MiB",
"app_dls: %s": "다운로드 횟수: %s",
"More by Author": "개발자의 다른 앱 더 보기",
"Leave Feedback": "피드백 남기기",
"FileBrowser": "파일 탐색기",
"Files": "파일 탐색기",
"%zd files": "%zd 개 파일",
"%zd dirs": "%zd 개 폴더",
"File Options": "파일 옵션",
"Show Hidden": "숨겨진 항목 표시",
"Folders First": "폴더 우선 정렬",
"Hidden Last": "숨겨진 항목 후순 정렬",
"Cut": "잘라내기",
"Copy": "복사",
"Copying ": "복사 중 ",
"Paste": "붙여넣기",
"Paste ": " ",
" file(s)?": "개 항목을 붙여넣을까요?",
"Pasting ": "붙여넣는 중 ",
"Pasting": "붙여넣기",
"Removing ": "제거 ",
"Scanning ": "스캔 ",
"Creating ": "생성 ",
"Copying ": "복사 ",
"Trying to load ": "썸네일 받아오는 중... ",
"Downloading ": "다운로드 ",
"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!": "압축 해제 실패!",
"Compress to zip": "압축",
"Compress Options": "압축 옵션",
"Compress": "ZIP 파일로 압축",
"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)": "텍스트로 보기 (미완성)",
"Ignore read only": "읽기 전용 설정 무시",
"Mount": "마운트",
"Sd": "SD 카드",
"Image System memory": "시스낸드 앨범",
"Image microSD card": "에뮤낸드 앨범",
"Empty...": "비어있음...",
"Open with DayBreak?": "DayBreak로 열까요?",
"Launch ": "실행할까요 ",
"Launch option for: ": "실행 옵션: ",
"Select launcher for: ": "실행 런처: ",
"Sort By": "정렬",
"Sort Options": "정렬 옵션",
"Filter": "필터",
"All": "전체",
"Emulators": "에뮬레이터",
"Tools": "도구",
"Themes": "테마",
"Legacy": "레거시",
"Sort": "분류",
"Size": "크기순",
"Size (Star)": "크기순 (즐겨찾기)",
"Alphabetical": "알파벳순",
"Alphabetical (Star)": "알파벳순 (즐겨찾기)",
"Updated": "업데이트순",
"Updated (Star)": "업데이트순 (즐겨찾기)",
"Downloads": "다운로드순",
"Likes": "좋아요순",
"ID": "ID순",
"Order": "정렬",
"Descending": "내림차순",
"Descending (down)": "내림차순",
"Desc": "내림차순",
"Ascending": "오름차순",
"Ascending (Up)": "오름차순",
"Asc": "오름차순",
"Layout": "레이아웃",
"List": "목록",
"Icon": "아이콘",
"Grid": "격자",
"Search": "검색",
"Options": "설정",
"OK": "확인",
"Back": "뒤로",
"Select": "선택",
"Open": "열기",
"Launch": "실행",
"Restart": "재시작",
"Next": "다음",
"Prev": "이전",
"Unstar": "즐겨찾기 해제",
"Star": "즐겨찾기",
"Yes": "예",
"No": "아니요",
"On": "켬",
"Off": "끔",
"Install": "설치",
"Install Selected files?": "선택한 파일을 설치할까요?",
"Installing ": "설치 중 ",
"Installed ": "설치됨 ",
"Installed!": "설치 완료!",
"Trying to load ": "정보 취득 중 ",
"Checking MD5": "MD5 확인 중",
"Delete": "삭제",
"Delete Selected files?": "선택한 파일을 삭제할까요?",
"Are you sure you want to delete ": "정말 삭제할까요 ",
"Scanning ": "스캔 중 ",
"Deleting ": "삭제 중 ",
"Deleting": "삭제 중",
"Remove": "제거",
"Completely remove ": "정말 삭제할까요 ",
"Removing ": "제거 중 ",
"Removed ": "제거됨: ",
"Uninstalling ": "설치 제거됨: ",
"Download": "다운로드",
"Downloading ": "다운로드 중 ",
"Downloaded ": "다운로드 완료: ",
"Removed ": "제거 됨: ",
"Checking MD5": "MD5 확인",
"Update": "업데이트",
"Update avaliable: ": "업데이트 가능: ",
"Download update: ": "업데이트 다운로드: ",
"Updated to ": "업데이트됨: ",
"Failed to download update": "업데이트 다운로드 실패함",
"%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!": "페이지 로딩 오류!",
"Update avaliable: ": "업데이트 가능: ",
"Download update: ": "업데이트 다운로드: ",
"Updated to ": "업데이트: ",
"Press OK to restart Sphaira": "확인 버튼 입력하여 Sphaira 재시작",
"Restart Sphaira?": "Sphaira를 재시작할까요?",
"Failed to download update": "업데이트 다운로드 실패함",
"Restore hbmenu?": "hbmenu로 교체할까요?",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "/switch/hbmemu.nro를 찾을 수 없습니다\n앱스토어에서 다시 설치하세요",
"Failed to restore hbmenu, please re-download hbmenu": "hbmenu 교체 실패함, hbmenu를 다시 다운로드하세요",
"Failed to restore hbmenu, using sphaira instead": "hbmenu 교체 실패함, sphaira를 계속 사용합니다",
"Restored hbmenu, closing sphaira": "hbmenu로 교체됨, sphaira를 종료합니다",
"Restored hbmenu": "hbmenu로 교체됨",
"Delete Selected files?": "선택한 파일을 삭제할까요?",
"Completely remove ": "정말 삭제할까요 ",
"Are you sure you want to delete ": "정말 삭제할까요 ",
"Are you sure you wish to cancel?": "정말 취소할까요?",
"Audio disabled due to suspended game": "게임 실행 중에는 BGM이 비활성화 됩니다",
"If this message appears repeatedly, please open an issue.": "해당 메시지가 반복해서 나타나는 경우, 이슈를 등록하세요."
"Error loading page!": "페이지 로딩 오류!"
}

View File

@@ -1,72 +1,24 @@
{
"[Applet Mode]": "[Applet-modus]",
"No Internet": "Geen internet",
"Files": "",
"Apps": "",
"Store": "",
"Menu": "Menu",
"Options": "Opties",
"OK": "",
"Back": "Terug",
"Select": "",
"Open": "Open",
"Launch": "Launch",
"Info": "Info",
"Install": "Installeren",
"Delete": "Verwijderen",
"Restart": "",
"Changelog": "",
"Details": "",
"Update": "",
"Remove": "",
"Restore": "",
"Download": "Downloaden",
"Next Page": "Volgende pagina",
"Prev Page": "Vorige pagina",
"Unstar": "",
"Star": "",
"System memory": "",
"microSD card": "",
"Sd": "",
"Image System memory": "",
"Image microSD card": "",
"Slow": "",
"Normal": "",
"Fast": "",
"Yes": "Ja",
"No": "Nee",
"Enabled": "Ingeschakeld",
"Disabled": "Gehandicapt",
"Sort By": "Sorteer op",
"Sort Options": "Sorteeropties",
"Filter": "Filter",
"Sort": "Soort",
"Order": "Volgorde",
"Search": "Zoekopdracht",
"Updated": "Bijgewerkt",
"Updated (Star)": "",
"Downloads": "Downloads",
"Size": "Maat",
"Size (Star)": "",
"Alphabetical": "Alfabetisch",
"Alphabetical (Star)": "",
"Likes": "",
"ID": "",
"Descending": "Aflopend",
"Descending (down)": "Aflopend",
"Desc": "Aflopend",
"Ascending": "Oplopend",
"Ascending (Up)": "Oplopend",
"Asc": "Oplopend",
"Switch-Handheld!": "",
"Switch-Docked!": "",
"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.": "",
"Menu Options": "Menu-opties",
"Menu": "Menu",
"Theme": "Thema",
"Theme Options": "Thema Opties",
"Select Theme": "Selecteer Thema",
"Shuffle": "Schudden",
"Music": "Muziek",
"12 Hour Time": "",
"Download Default Music": "",
"Failed to download default_music.bfstm, please try again": "",
"Overwrite current default music?": "",
"Network": "Netwerk",
"Network Options": "Netwerkopties",
"Ftp": "FTP",
@@ -75,8 +27,7 @@
"Nxlink Connected": "",
"Nxlink Upload": "",
"Nxlink Finished": "",
"Switch-Handheld!": "",
"Switch-Docked!": "",
"Language": "Taal",
"Auto": "",
"English": "English",
@@ -92,87 +43,101 @@
"Russian": "Русский",
"Swedish": "Svenska",
"Vietnamese": "Vietnamese",
"Logging": "Loggen",
"Replace hbmenu on exit": "Vervang hbmenu bij afsluiten",
"Ukrainian": "Українська",
"Misc": "Diversen",
"Misc Options": "Diverse opties",
"Web": "Web",
"Install forwarders": "",
"Install location": "",
"Show install warning": "",
"Text scroll speed": "",
"FileBrowser": "Bestandsbrowser",
"%zd files": "%zd files",
"%zd dirs": "%zd dirs",
"File Options": "Bestandsopties",
"Show Hidden": "Toon verborgen",
"Folders First": "Mappen eerst",
"Hidden Last": "Verborgen laatste",
"Cut": "Snee",
"Copy": "Kopiëren",
"Paste": "",
"Paste ": "",
" file(s)?": "",
"Rename": "Hernoemen",
"Set New File Name": "",
"Advanced": "Geavanceerd",
"Advanced Options": "Bestand maken",
"Create File": "Bestand maken",
"Set File Name": "",
"Create Folder": "Map maken",
"Set Folder Name": "",
"View as text (unfinished)": "Bekijk als tekst (onvoltooid)",
"Ignore read only": "",
"Mount": "",
"Empty...": "",
"Open with DayBreak?": "",
"Launch ": "",
"Launch option for: ": "",
"Select launcher for: ": "",
"Homebrew": "Zelf brouwen",
"Homebrew Options": "Homebrew-opties",
"Hide Sphaira": "Verberg Sphaira",
"Install Forwarder": "Forwarder installeren",
"WARNING: Installing forwarders will lead to a ban!": "WAARSCHUWING: Het installeren van forwarders leidt tot een ban!",
"Installing Forwarder": "",
"Creating Program": "",
"Creating Control": "",
"Creating Meta": "",
"Writing Nca": "",
"Updating ncm databse": "",
"Pushing application record": "",
"Installed!": "",
"Failed to install forwarder": "",
"Unstarred ": "",
"Starred ": "",
"AppStore": "",
"Filter: %s | Sort: %s | Order: %s": "Filter: %s | Soort: %s | Volgorde: %s",
"AppStore Options": "AppStore-opties",
"All": "Alle",
"Games": "Spellen",
"Emulators": "Emulators",
"Tools": "Hulpmiddelen",
"Themes": "Thema's",
"Legacy": "Nalatenschap",
"version: %s": "version: %s",
"updated: %s": "updated: %s",
"category: %s": "category: %s",
"extracted: %.2f MiB": "extracted: %.2f MiB",
"app_dls: %s": "app_dls: %s",
"More by Author": "",
"Leave Feedback": "",
"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!": "",
"Themezer": "Themamaker",
"Themezer Options": "",
"Nsfw": "",
"Page": "",
"Page %zu / %zu": "Page %zu / %zu",
"Enter Page Number": "",
"Bad Page": "",
"Download theme?": "",
"GitHub": "",
"Downloading json": "",
"Select asset to download for ": "",
"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!": "",
"IRS (Infrared Joycon Camera)": "",
"IRS": "",
"Irs": "Ir",
"Ambient Noise Level: ": "",
"Controller": "Controleur",
"Pad ": "Pad ",
"HandHeld": "Handbediende",
" (Available)": " (Beschikbaar)",
" (Unsupported)": "",
" (Unconnected)": " (Niet verbonden)",
"HandHeld": "Handbediende",
"Rotation": "Rotatie",
"0 (Sideways)": "0 (zijwaarts)",
"90 (Flat)": "90 (plat)",
@@ -194,64 +159,234 @@
"Normal image": "Normaal beeld",
"Negative image": "Negatief beeld",
"Format": "Formaat",
"Trimming Format": "Trimformaat",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "Trimformaat",
"External Light Filter": "Extern lichtfilter",
"Load Default": "Standaard laden",
"Themezer": "Themamaker",
"Themezer Options": "",
"Nsfw": "",
"Page": "",
"Page %zu / %zu": "Page %zu / %zu",
"Enter Page Number": "",
"Bad Page": "",
"Download theme?": "",
"GitHub": "",
"Downloading json": "",
"Select asset to download for ": "",
"Installing ": "",
"Uninstalling ": "",
"Deleting ": "",
"Deleting": "",
"Pasting ": "",
"Pasting": "",
"Removing ": "",
"Scanning ": "",
"Creating ": "",
"Copying ": "",
"Trying to load ": "",
"Downloading ": "",
"Downloaded ": "",
"Removed ": "",
"Checking MD5": "",
"Loading...": "",
"Loading": "",
"Empty!": "",
"Not Ready...": "",
"Error loading page!": "",
"Update avaliable: ": "",
"Download update: ": "",
"Updated to ": "",
"Press OK to restart Sphaira": "",
"Restart Sphaira?": "",
"Failed to download update": "",
"Advanced": "Geavanceerd",
"Advanced Options": "Bestand maken",
"Logging": "Loggen",
"Replace hbmenu on exit": "Vervang hbmenu bij afsluiten",
"Restore hbmenu?": "",
"Restore": "",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "",
"Failed to restore hbmenu, please re-download hbmenu": "",
"Failed to restore hbmenu, using sphaira instead": "",
"Restored hbmenu, closing sphaira": "",
"Restored hbmenu": "",
"Restart Sphaira?": "",
"Press OK to restart Sphaira": "",
"Text scroll speed": "",
"Slow": "",
"Normal": "",
"Fast": "",
"Set right-side menu": "",
"Install options": "",
"Install Options": "",
"Enable sysmmc": "",
"Enable emummc": "",
"Show install warning": "",
"Install location": "",
"System memory": "",
"microSD card": "",
"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": "",
"Homebrew": "Zelf brouwen",
"Apps": "",
"Homebrew Options": "Homebrew-opties",
"Hide Sphaira": "Verberg Sphaira",
"Install Forwarder": "Forwarder installeren",
"WARNING: Installing forwarders will lead to a ban!": "WAARSCHUWING: Het installeren van forwarders leidt tot een ban!",
"Installing Forwarder": "",
"Creating Program": "",
"Creating Control": "",
"Creating Meta": "",
"Writing Nca": "",
"Updating ncm databse": "",
"Pushing application record": "",
"Failed to install forwarder": "",
"Unstarred ": "",
"Starred ": "",
"Failed to remove old forwarder, please manually remove it!": "",
"AppStore": "",
"Appstore": "",
"Store": "",
"Filter: %s | Sort: %s | Order: %s": "Filter: %s | Soort: %s | Volgorde: %s",
"AppStore Options": "AppStore-opties",
"Info": "Info",
"Changelog": "",
"Details": "",
"version: %s": "version: %s",
"updated: %s": "updated: %s",
"category: %s": "category: %s",
"extracted: %.2f MiB": "extracted: %.2f MiB",
"app_dls: %s": "app_dls: %s",
"More by Author": "",
"Leave Feedback": "",
"FileBrowser": "Bestandsbrowser",
"Files": "",
"%zd files": "%zd files",
"%zd dirs": "%zd dirs",
"File Options": "Bestandsopties",
"Show Hidden": "Toon verborgen",
"Folders First": "Mappen eerst",
"Hidden Last": "Verborgen laatste",
"Cut": "Snee",
"Copy": "Kopiëren",
"Copying ": "",
"Paste": "",
"Paste ": "",
" file(s)?": "",
"Pasting ": "",
"Pasting": "",
"Rename": "Hernoemen",
"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!": "",
"Compress to zip": "",
"Compress Options": "",
"Compress": "",
"Compress to...": "",
"Compressing ": "",
"Compress success!": "",
"Compress failed!": "",
"Create File": "Bestand maken",
"Set File Name": "",
"Create Folder": "Map maken",
"Set Folder Name": "",
"Creating ": "",
"Upload": "",
"Select upload location": "",
"No upload locations set!": "",
"Uploading": "",
"Upload successfull!": "",
"Upload failed!": "",
"View as text (unfinished)": "Bekijk als tekst (onvoltooid)",
"Ignore read only": "",
"Mount": "",
"Sd": "",
"Image System memory": "",
"Image microSD card": "",
"Empty...": "",
"Open with DayBreak?": "",
"Launch ": "",
"Launch option for: ": "",
"Select launcher for: ": "",
"Sort By": "Sorteer op",
"Sort Options": "Sorteeropties",
"Filter": "Filter",
"All": "Alle",
"Emulators": "Emulators",
"Tools": "Hulpmiddelen",
"Themes": "Thema's",
"Legacy": "Nalatenschap",
"Sort": "Soort",
"Size": "Maat",
"Size (Star)": "",
"Alphabetical": "Alfabetisch",
"Alphabetical (Star)": "",
"Updated": "Bijgewerkt",
"Updated (Star)": "",
"Downloads": "Downloads",
"Likes": "",
"ID": "",
"Order": "Volgorde",
"Descending": "Aflopend",
"Descending (down)": "Aflopend",
"Desc": "Aflopend",
"Ascending": "Oplopend",
"Ascending (Up)": "Oplopend",
"Asc": "Oplopend",
"Layout": "",
"List": "",
"Icon": "",
"Grid": "",
"Search": "Zoekopdracht",
"Options": "Opties",
"OK": "",
"Back": "Terug",
"Select": "",
"Open": "Open",
"Launch": "Launch",
"Restart": "",
"Next": "",
"Prev": "",
"Unstar": "",
"Star": "",
"Yes": "Ja",
"No": "Nee",
"On": "",
"Off": "",
"Install": "Installeren",
"Install Selected files?": "",
"Installing ": "",
"Installed ": "",
"Installed!": "",
"Trying to load ": "",
"Checking MD5": "",
"Delete": "Verwijderen",
"Delete Selected files?": "",
"Completely remove ": "",
"Are you sure you want to delete ": "Weet u zeker dat u wilt verwijderen ",
"Are you sure you wish to cancel?": "",
"Audio disabled due to suspended game": "",
"If this message appears repeatedly, please open an issue.": ""
"Scanning ": "",
"Deleting ": "",
"Deleting": "",
"Remove": "",
"Completely remove ": "",
"Removing ": "",
"Removed ": "",
"Uninstalling ": "",
"Download": "Downloaden",
"Downloading ": "",
"Downloaded ": "",
"Update": "",
"Update avaliable: ": "",
"Download update: ": "",
"Updated to ": "",
"Failed to download update": "",
"%zu hours %zu minutes remaining": "",
"%zu minutes %zu seconds remaining": "",
"%zu seconds remaining": "",
"Loading...": "",
"Loading": "",
"Empty!": "",
"Not Ready...": "",
"Error loading page!": ""
}

View File

@@ -1,78 +1,24 @@
{
"[Applet Mode]": "[Modo Applet]",
"No Internet": "Sem Internet",
"Files": "Arquivos",
"Apps": "Aplicativos",
"Store": "Loja",
"Menu": "Menu",
"Options": "Opções",
"OK": "OK",
"Back": "Voltar",
"Select": "Selecionar",
"Open": "Abrir",
"Launch": "Iniciar",
"Info": "Informações",
"Install": "Instalar",
"Delete": "Excluir",
"Restart": "Reiniciar",
"Changelog": "Alterações",
"Details": "Detalhes",
"Update": "Atualizar",
"Remove": "Remover",
"Restore": "Restaurar",
"Download": "Baixar",
"Next": "Prómixo",
"Prev": "Anterior",
"Next Page": "Próxima página",
"Prev Page": "Página anterior",
"Unstar": "Desfavoritar",
"Star": "Favoritar",
"System memory": "Memória do console",
"microSD card": "Cartão microSD",
"Sd": "SD",
"Image System memory": "Imagem (memória do console)",
"Image microSD card": "Imagem (cartão microSD)",
"Slow": "Lenta",
"Normal": "Normal",
"Fast": "Rápida",
"Yes": "Sim",
"No": "Não",
"On": "Sim",
"Off": "Não",
"Enable": "Habilitar",
"Enabled": "Sim",
"Disabled": "Não",
"Sort By": "Ordernar/Organizar",
"Sort Options": "Ordernar/Organizar",
"Filter": "Filtro",
"Sort": "Organizar por",
"Order": "Ordem",
"Search": "Buscar",
"Updated": "Atualizado",
"Updated (Star)": "Atualizado (favoritos)",
"Downloads": "Nº de downloads",
"Size": "Tamanho",
"Size (Star)": "Tamanho (favoritos)",
"Alphabetical": "Ordem alfabética",
"Alphabetical (Star)": "Ordem alfabética (favoritos)",
"Likes": "Nº de curtidas",
"ID": "ID",
"Descending": "Decrescente",
"Descending (down)": "Decrescente (baixo)",
"Desc": "Decr.",
"Ascending": "Ascendente",
"Ascending (Up)": "Ascendente (cima)",
"Asc": "Asc.",
"[Applet Mode]": "[Modo applet]",
"No Internet": "Sem internet",
"Switch-Handheld!": "Switch-Portátil",
"Switch-Docked!": "Switch-Docado",
"Audio disabled due to suspended game": "Áudio desativado devido ao software suspenso.",
"Are you sure you wish to cancel?": "Você tem certeza que quer cancelar?",
"An error occurred": "",
"If this message appears repeatedly, please open an issue.": "Se esta mensagem aparecer repetidamente, abra um issue.",
"Menu Options": "Opções do menu",
"Menu": "Menu",
"Theme": "Tema",
"Theme Options": "Opções de tema",
"Select Theme": "Tema atual",
"Shuffle": "Embaralhar temas",
"Music": "Música",
"12 Hour Time": "Relógio de 12 horas",
"Download Default Music": "Baixar música padrão",
"Failed to download default_music.bfstm, please try again": "",
"Overwrite current default music?": "",
"Network": "Rede",
"Network Options": "Opções de rede",
"Ftp": "Servidor FTP",
@@ -81,8 +27,7 @@
"Nxlink Connected": "Nxlink conectado",
"Nxlink Upload": "Envio Nxlink",
"Nxlink Finished": "Nxlink finalizado",
"Switch-Handheld!": "Switch-Portátil",
"Switch-Docked!": "Switch-Docado",
"Language": "Idioma",
"Auto": "Automático",
"English": "English",
@@ -98,97 +43,101 @@
"Russian": "Русский",
"Swedish": "Svenska",
"Vietnamese": "Vietnamese",
"Logging": "Registro de depuração",
"Replace hbmenu on exit": "Substituir hbmenu ao sair",
"Ukrainian": "Українська",
"Misc": "Diversos",
"Misc Options": "Opções diversas",
"Web": "Navegador de internet",
"Install forwarders": "Instalar atalhos (forwarders)",
"Install location": "Local de instalação",
"Show install warning": "Mostrar aviso de instalação",
"Text scroll speed": "Rolagem do texto",
"Set right-side menu": "Menu direito (R)",
"FileBrowser": "Arquivos",
"%zd files": "%zd arquivo(s)",
"%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",
"Cut": "Recortar",
"Copy": "Copiar",
"Paste": "Colar",
"Paste ": "Colar ",
" file(s)?": " arquivo(s)?",
"Rename": "Renomear",
"Compress to zip": "Comprimir em zip",
"Set New File Name": "Defina o nome do novo arquivo",
"Advanced": "Avançado",
"Advanced Options": "Opções avançadas",
"Create File": "Criar arquivo",
"Set File Name": "Defina o nome do arquivo",
"Create Folder": "Criar pasta",
"Set Folder Name": "Defina o nome da pasta",
"View as text (unfinished)": "Ver como texto (inacabado)",
"Ignore read only": "Ignorar somente leitura",
"Mount": "Montar",
"Empty...": "Vazio...",
"Open with DayBreak?": "Abrir com DayBreak?",
"Launch ": "Iniciar ",
"Launch option for: ": "Opções de inicialização para: ",
"Select launcher for: ": "Selecionar launcher para: ",
"Homebrew": "Aplicativos",
"Homebrew Options": "Opções de aplicativo",
"Hide Sphaira": "Esconder sphaira",
"Install Forwarder": "Instalar atalho (forwarder)",
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Instalar atalhos pode\nresultar em um banimento!",
"Installing Forwarder": "Instalando forwarder",
"Creating Program": "Criando Program",
"Creating Control": "Criando Control",
"Creating Meta": "Criando Meta",
"Writing Nca": "Escrevendo NCA",
"Updating ncm databse": "Atualizando base de dados NCM",
"Pushing application record": "Aplicando registro do aplicativo",
"Installed!": "Instalado!",
"Failed to install forwarder": "Falha ao instalar forwarder",
"Unstarred ": "Desfavoritado ",
"Starred ": "Favoritado ",
"AppStore": "Loja",
"Filter: %s | Sort: %s | Order: %s": "Filtro: %s | Por: %s | Ordem: %s",
"AppStore Options": "Opções da loja",
"All": "Todos",
"Games": "Jogos",
"Emulators": "Emuladores",
"Tools": "Ferramentas",
"Themes": "Temas",
"Legacy": "Legado",
"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",
"More by Author": "Mais deste autor",
"Leave Feedback": "Deixar um feedback",
"Game Options": "Opções de jogo",
"Launch random game": "Iniciar jogo aleatório",
"List meta records": "Listar registro de metadados",
"Entries": "Entradas",
"Delete entity": "Excluir entidade",
"Games": "Softwares",
"Game Options": "Opções de software",
"Hide forwarders": "Ocultar atalhos (forwarders)",
"Launch random game": "Iniciar um software aleatório",
"List meta records": "Registro de conteúdos",
"Entries": "Entradas",
"Failed to list application meta entries": "",
"No meta entries found...\n": "",
"Updating application record list": "",
"Dump": "Exportar",
"Select content to dump": "Exportação de conteúdo",
"Dump All": "Exportar tudo",
"Dump Application": "Exportar software base",
"Dump Patch": "Exportar atualização",
"Dump AddOnContent": "Exportar DLCs",
"Dump DataPatch": "Exportar patch de dados",
"Select dump location": "Selecione o local de exportação",
"microSD card (/dumps/NSP/)": "Cartão microSD (/dump/NSP/)",
"USB transfer (Switch 2 Switch)": "Transferência via USB (Switch 2 Switch)",
"/dev/null (Speed Test)": "Teste de velocidade (/dev/null)",
"Dumping": "Extraindo...",
"Dump successfull!": "Extração foi concluída com sucesso.",
"Dump failed!": "Extração falhou.",
"Success": "",
"Delete successfull!": "Exclusão foi concluída com sucesso.",
"Delete failed!": "Exclusão falhou",
"Themezer": "Themezer",
"Themezer Options": "Opções do Themezer",
"Nsfw": "Temas 18+ (NSFW)",
"Page": "Ir para página",
"Page %zu / %zu": "Página %zu / %zu",
"Enter Page Number": "Número da página",
"Bad Page": "Página inválida",
"Download theme?": "Baixar tema?",
"GitHub": "GitHub",
"Downloading json": "Baixando JSON",
"Select asset to download for ": "Selecione o recurso para baixar de ",
"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: None": "Sem conexão",
"Host:": "Host:",
"Port:": "Porta:",
"Username:": "Nome de usuário:",
"Password:": "Senha:",
"SSID:": "SSID:",
"Passphrase:": "Senha:",
"Failed to install via FTP, press B to exit...": "Falha ao instalar via FTP,\naperte B para sair.",
"Ftp install success!": "Instalação via FTP concluída com sucesso.",
"Ftp install failed!": "Instalação via FTP falhou.",
"USB Install": "Instalação via USB",
"USB": "USB",
"Connected, waiting for file list...": "",
"Connected, starting transfer...": "",
"Failed to init usb, press B to exit...": "Falha ao instalar via USB,\naperte B para sair.",
"Waiting for connection...": "Aguardando conexão...",
"Transferring data...": "Transferindo dados...",
"USB connected, sending file list": "",
"Sent file list, waiting for command...": "",
"waiting for usb connection...": "",
"Disable MTP for usb install": "Escuta MTP desabilitada temporáriamente.",
"Re-enabled MTP": "Escuta MTP reabilitada.",
"Installed via usb": "Instalado via USB",
"Usb install success!": "Instalação via USB concluída com sucesso.",
"Usb install failed!": "Instalação via USB falhou.",
"Press B to exit...": "Aperte B para sair.",
"GameCard Install": "Instalação de cartão de jogo",
"GameCard": "Cartão de jogo",
"GC": "",
"System memory %.1f GB": "",
"microSD card %.1f GB": "",
"Nand Install": "",
"SD Card Install": "",
"Exit": "",
"Gc install success!": "Instalação de cartão de jogo concluída com sucesso.",
"Gc install failed!": "Instalação de cartão de jogo falhou.",
"Irs": "Câmera de movimento IR",
"IRS (Infrared Joycon Camera)": "Câmera de movimento IR",
"IRS": "Câmera de movimento IR",
"Irs": "Câmera de movimento IR",
"Ambient Noise Level: ": "Nível de ruído ambiente: ",
"Controller": "Controle",
"Pad ": "Pad ",
"HandHeld": "Portátil",
" (Available)": " (disponível)",
" (Unsupported)": "(não suportado)",
" (Unconnected)": " (desconectado)",
"HandHeld": "Portátil",
"Rotation": "Rotação",
"0 (Sideways)": "0 (lateralmente)",
"90 (Flat)": "90 (plano)",
@@ -210,117 +159,234 @@
"Normal image": "Imagem normal",
"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",
"Trimming Format": "Formato do recorte",
"External Light Filter": "Filtro de luz externa",
"Load Default": "Restaurar padrão",
"Themezer": "Themezer",
"Themezer Options": "Opções do Themezer",
"Nsfw": "Temas 18+ (NSFW)",
"Page": "Ir para página",
"Page %zu / %zu": "Página %zu / %zu",
"Enter Page Number": "Número da página",
"Bad Page": "Página inválida",
"Download theme?": "Baixar tema?",
"GitHub": "GitHub",
"Downloading json": "Baixando JSON",
"Select asset to download for ": "Selecione o recurso para baixar de ",
"Install Options": "Opções de instalação",
"Advanced": "Avançado",
"Advanced Options": "Opções avançadas",
"Logging": "Registro 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, baixe o hbmenu novamente.",
"Failed to restore hbmenu, using sphaira instead": "Falha ao restaurar hbmenu, usando sphaira.",
"Restored hbmenu, closing sphaira": "hbmenu restaurado, fechando sphaira.",
"Restored hbmenu": "hbmenu restaurado.",
"Restart Sphaira?": "Reiniciar sphaira?",
"Press OK to restart Sphaira": "Selecione OK para reiniciar o sphaira.",
"Text scroll speed": "Rolagem do texto",
"Slow": "Lenta",
"Normal": "Normal",
"Fast": "Rápida",
"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",
"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": "Aumentar clock da CPU",
"Allow downgrade": "Permitir downgrade",
"Skip if already installed": "Pular se já instalado",
"Ticket only": "Instalar apenas ticket",
"Patch ticket": "Fazer patch de ticket",
"Skip base": "Pular base",
"Skip patch": "Pular patch",
"Skip dlc": "Pular DLC",
"Skip base": "Pular software base",
"Skip patch": "Pular atualizações",
"Skip dlc": "Pular DLCs",
"Skip data patch": "Pular patch de dados",
"Skip ticket": "Pular ticket",
"skip NCA hash verify": "Pular verificação de hash NCA",
"Skip RSA header verify": "Pular verificação de header RSA",
"Skip RSA NPDM verify": "Pular verificação de NPDM RSA",
"Skip NCA hash verify": "",
"Skip RSA header verify": "Pular checagem de header RSA",
"Skip RSA NPDM verify": "Pular checagem de NPDM RSA",
"Ignore distribution bit": "Ignorar bit de distribuição",
"Convert to standard crypto": "Convertr para crypto padrão",
"Lower master key": "Reduzir a master key",
"Lower system version": "Reduzir versão do sistema",
"Install Selected files?": "Instalar os arquivos selecionados?",
"Installed ": "Instalado ",
"FTP Install": "Instalação via FTP",
"USB Install": "Instalação via USB",
"GameCard Install": "Instalação de cartão de jogo",
"FTP Install (EXPERIMENTAL)": "Instalação via FTP (EXPERIMENTAL)",
"USB": "USB",
"GameCard": "Cartão de jogo",
"Disable MTP for usb install": "Escuta MTP desabilitada temporáriamente",
"Re-enabled MTP": "Escuta MTP reabilitada",
"Waiting for connection...": "Aguardando conexão...",
"Transferring data...": "Transferindo dados...",
"Ftp install success!": "Instalação via FTP concluída com sucesso.",
"Ftp install failed!": "Instalação via FTP falhou.",
"Usb install success!": "Instalação via USB concluída com sucesso.",
"Usb install failed!": "Instalação via USB falhou.",
"Gc install success!": "Instalação de cartão de jogo concluída com sucesso.",
"Gc install failed!": "Instalação de cartão de jogo falhou.",
"Installed via usb": "Instalado via USB",
"Failed to install via FTP, press B to exit...": "Falha ao instalar via FTP,\naperte B para sair.",
"Failed to init usb, press B to exit...": "Falha ao instalar via USB,\naperte B para sair.",
"Press B to exit...": "Aperte B para sair.",
"Connection Type: WiFi | Strength: ": "Conexão por rede Wi-Fi | Intensidade do sinal: ",
"Connection Type: Ethernet": "Conexão por cabo (ethernet)",
"Connection Type: None": "Sem conexão",
"Host:": "Host:",
"Port:": "Porta:",
"Username:": "Nome de usuário:",
"Password:": "Senha:",
"SSID:": "SSID:",
"Passphrase:": "Senha:",
"Homebrew": "Aplicativos",
"Apps": "Aplicativos",
"Homebrew Options": "Opções de aplicativo",
"Hide Sphaira": "Esconder sphaira",
"Install Forwarder": "Instalar atalho (forwarder)",
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Instalar atalhos pode resultar em um banimento!",
"Installing Forwarder": "Instalando forwarder...",
"Creating Program": "Criando Program",
"Creating Control": "Criando Control",
"Creating Meta": "Criando Meta",
"Writing Nca": "Escrevendo NCA",
"Updating ncm databse": "Atualizando base de dados NCM",
"Pushing application record": "Aplicando registro do aplicativo",
"Failed to install forwarder": "Falha ao instalar forwarder",
"Unstarred ": "Desfavoritado ",
"Starred ": "Favoritado ",
"Failed to remove old forwarder, please manually remove it!": "",
"Installing ": "Instalando ",
"Uninstalling ": "Desinstalando ",
"Deleting ": "Excluindo ",
"Deleting": "Excluindo",
"AppStore": "Loja",
"Appstore": "Loja",
"Store": "Loja",
"Filter: %s | Sort: %s | Order: %s": "Filtro: %s | Por: %s | Ordem: %s",
"AppStore Options": "Opções da loja",
"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",
"More by Author": "Mais deste autor",
"Leave Feedback": "Deixar um feedback",
"FileBrowser": "Arquivos",
"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",
"Cut": "Recortar",
"Copy": "Copiar",
"Copying ": "Copiando ",
"Paste": "Colar",
"Paste ": "Colar ",
" file(s)?": " arquivo(s)?",
"Pasting ": "Colando ",
"Pasting": "Colando ",
"Removing ": "Removendo ",
"Scanning ": "Analisando ",
"Rename": "Renomear",
"Set New File Name": "Defina o nome do novo arquivo",
"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": "Comprimir em zip",
"Compress Options": "",
"Compress": "",
"Compress to...": "",
"Compressing ": "",
"Compress success!": "",
"Compress failed!": "",
"Create File": "Criar arquivo",
"Set File Name": "Defina o nome do arquivo",
"Create Folder": "Criar pasta",
"Set Folder Name": "Defina o nome da pasta",
"Creating ": "Criando ",
"Copying ": "Copiando ",
"Upload": "",
"Select upload location": "",
"No upload locations set!": "",
"Uploading": "",
"Upload successfull!": "",
"Upload failed!": "",
"View as text (unfinished)": "Ver como texto (inacabado)",
"Ignore read only": "Ignorar somente leitura",
"Mount": "Montar",
"Sd": "SD",
"Image System memory": "Imagem (memória do console)",
"Image microSD card": "Imagem (cartão microSD)",
"Empty...": "Vazio",
"Open with DayBreak?": "Abrir com DayBreak?",
"Launch ": "Iniciar ",
"Launch option for: ": "Opções de inicialização para: ",
"Select launcher for: ": "Selecionar launcher para: ",
"Sort By": "Ordernar/Organizar",
"Sort Options": "Ordernar/Organizar",
"Filter": "Filtro",
"All": "Todos",
"Emulators": "Emuladores",
"Tools": "Ferramentas",
"Themes": "Temas",
"Legacy": "Legado",
"Sort": "Organizar por",
"Size": "Tamanho",
"Size (Star)": "Tamanho (favoritos)",
"Alphabetical": "Ordem alfabética",
"Alphabetical (Star)": "Ordem alfabética (favoritos)",
"Updated": "Atualizado",
"Updated (Star)": "Atualizado (favoritos)",
"Downloads": "Nº de downloads",
"Likes": "Nº de curtidas",
"ID": "ID",
"Order": "Ordem",
"Descending": "Decrescente",
"Descending (down)": "Decrescente (baixo)",
"Desc": "Decr.",
"Ascending": "Ascendente",
"Ascending (Up)": "Ascendente (cima)",
"Asc": "Asc.",
"Layout": "Exibição",
"List": "Lista",
"Icon": "Ícones",
"Grid": "Grade",
"Search": "Buscar",
"Options": "Opções",
"OK": "OK",
"Back": "Voltar",
"Select": "Selecionar",
"Open": "Abrir",
"Launch": "Iniciar",
"Restart": "Reiniciar",
"Next": "Prómixo",
"Prev": "Anterior",
"Unstar": "Desfavoritar",
"Star": "Favoritar",
"Yes": "Sim",
"No": "Não",
"On": "Sim",
"Off": "Não",
"Install": "Instalar",
"Install Selected files?": "Instalar os arquivos selecionados?",
"Installing ": "Instalando ",
"Installed ": "Instalado ",
"Installed!": "Instalado!",
"Trying to load ": "Tentando carregar ",
"Checking MD5": "Checando MD5",
"Delete": "Excluir",
"Delete Selected files?": "Excluir os arquivos selecionados?",
"Are you sure you want to delete ": "Você tem certeza que quer excluir ",
"Scanning ": "Analisando ",
"Deleting ": "Excluindo ",
"Deleting": "Excluindo...",
"Remove": "Remover",
"Completely remove ": "Remover completamente ",
"Removing ": "Removendo ",
"Removed ": "Removido ",
"Uninstalling ": "Desinstalando ",
"Download": "Baixar",
"Downloading ": "Baixando ",
"Downloaded ": "Baixado ",
"Removed ": "Removido ",
"Checking MD5": "Checando MD5",
"Update": "Atualizar",
"Update avaliable: ": "Atualização disponível: ",
"Download update: ": "Baixar autalização: ",
"Updated to ": "Atualizado para ",
"Failed to download update": "Falha ao baixar a atualização.",
"%zu hours %zu minutes remaining": "",
"%zu minutes %zu seconds remaining": "",
"%zu seconds remaining": "",
"Loading...": "Carregando...",
"Loading": "Carregando",
"Empty!": "Vazio",
"Not Ready...": "Não está pronto...",
"Error loading page!": "Erro ao carregar página",
"Update avaliable: ": "Atualização disponível: ",
"Download update: ": "Baixar autalização: ",
"Updated to ": "Atualizado para ",
"Press OK to restart Sphaira": "Selecione OK para reiniciar o sphaira",
"Restart Sphaira?": "Reiniciar sphaira?",
"Failed to download update": "Falha ao baixar a atualização",
"Restore hbmenu?": "Restaurar hbmenu?",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Falha ao buscar /switch/hbmenu.nro\nUse a AppStore para reinstalar o hbmenu",
"Failed to restore hbmenu, please re-download hbmenu": "Falha ao restaurar o hbmenu, baixe o hbmenu novamente",
"Failed to restore hbmenu, using sphaira instead": "Falha ao restaurar hbmenu, usando sphaira",
"Restored hbmenu, closing sphaira": "hbmenu restaurado, fechando sphaira",
"Restored hbmenu": "hbmenu restaurado",
"Delete Selected files?": "Excluir os arquivos selecionados?",
"Completely remove ": "Remover completamente ",
"Are you sure you want to delete ": "Você tem certeza que quer excluir ",
"Are you sure you wish to cancel?": "Você tem certeza que quer cancelar?",
"Audio disabled due to suspended game": "Áudio desativado devido ao software suspenso",
"If this message appears repeatedly, please open an issue.": "Se esta mensagem aparecer repetidamente, abra um issue."
}
"Error loading page!": "Erro ao carregar página."
}

View File

@@ -1,72 +1,24 @@
{
"[Applet Mode]": "[Режим апплета]",
"No Internet": "Нет Интернета",
"Files": "",
"Apps": "",
"Store": "",
"Menu": "Меню",
"Options": "Параметры темы",
"OK": "",
"Back": "Назад",
"Select": "",
"Open": "Открыть",
"Launch": "Запуск",
"Info": "Информация",
"Install": "Установить",
"Delete": "Удалить",
"Restart": "",
"Changelog": "",
"Details": "",
"Update": "",
"Remove": "",
"Restore": "",
"Download": "Скачать",
"Next Page": "Следующая страница",
"Prev Page": "Предыдущая страница",
"Unstar": "",
"Star": "",
"System memory": "",
"microSD card": "",
"Sd": "",
"Image System memory": "",
"Image microSD card": "",
"Slow": "",
"Normal": "",
"Fast": "",
"Yes": "Да",
"No": "Нет",
"Enabled": "Включено",
"Disabled": "Отключено",
"Sort By": "Сортировать по",
"Sort Options": "Параметры сортировки",
"Filter": "Фильтр",
"Sort": "Сортировать",
"Order": "Порядок",
"Search": "Поиск",
"Updated": "Обновлено",
"Updated (Star)": "",
"Downloads": "Загрузки",
"Size": "Размер",
"Size (Star)": "",
"Alphabetical": "По наименованию",
"Alphabetical (Star)": "",
"Likes": "",
"ID": "",
"Descending": "По убыванию",
"Descending (down)": "По убыванию",
"Desc": "По убыванию",
"Ascending": "По возрастанию",
"Ascending (Up)": "По возрастанию",
"Asc": "По возрастанию",
"Switch-Handheld!": "",
"Switch-Docked!": "",
"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.": "",
"Menu Options": "Параметры меню",
"Menu": "Меню",
"Theme": "Тема",
"Theme Options": "Параметры темы",
"Select Theme": "Выберите тему",
"Shuffle": "Перетасовать",
"Music": "Музыка",
"12 Hour Time": "",
"Download Default Music": "",
"Failed to download default_music.bfstm, please try again": "",
"Overwrite current default music?": "",
"Network": "Сеть",
"Network Options": "Параметры сети",
"Ftp": "FTP",
@@ -75,8 +27,7 @@
"Nxlink Connected": "",
"Nxlink Upload": "",
"Nxlink Finished": "",
"Switch-Handheld!": "",
"Switch-Docked!": "",
"Language": "Язык",
"Auto": "",
"English": "English",
@@ -92,87 +43,101 @@
"Russian": "Русский",
"Swedish": "Svenska",
"Vietnamese": "Vietnamese",
"Logging": "Журналирование",
"Replace hbmenu on exit": "Заменить hbmenu при выходе",
"Ukrainian": "Українська",
"Misc": "Прочее",
"Misc Options": "Прочие параметры",
"Web": "Интернет",
"Install forwarders": "",
"Install location": "",
"Show install warning": "",
"Text scroll speed": "",
"FileBrowser": "Файловый менеджер",
"%zd files": "%zd files",
"%zd dirs": "%zd dirs",
"File Options": "Параметры файла",
"Show Hidden": "Показать скрытые",
"Folders First": "Папки в первую очередь",
"Hidden Last": "Скрытые в последнюю очередь",
"Cut": "Вырезать",
"Copy": "Копировать",
"Paste": "",
"Paste ": "",
" file(s)?": "",
"Rename": "Переименовать",
"Set New File Name": "",
"Advanced": "Продвинутые",
"Advanced Options": "Расширенные параметры",
"Create File": "Создать файл",
"Set File Name": "",
"Create Folder": "Создать папку",
"Set Folder Name": "",
"View as text (unfinished)": "Посмотреть как текст (незакончено)",
"Ignore read only": "",
"Mount": "",
"Empty...": "",
"Open with DayBreak?": "",
"Launch ": "",
"Launch option for: ": "",
"Select launcher for: ": "",
"Homebrew": "Homebrew",
"Homebrew Options": "Параметры Homebrew",
"Hide Sphaira": "Скрыть Sphaira",
"Install Forwarder": "Установить форвардер",
"WARNING: Installing forwarders will lead to a ban!": "ВНИМАНИЕ: Установка форвардеров приведет к бану!",
"Installing Forwarder": "Установить форвардер",
"Creating Program": "",
"Creating Control": "",
"Creating Meta": "",
"Writing Nca": "",
"Updating ncm databse": "",
"Pushing application record": "",
"Installed!": "",
"Failed to install forwarder": "",
"Unstarred ": "",
"Starred ": "",
"AppStore": "",
"Filter: %s | Sort: %s | Order: %s": "Фильтр: %s | Сортировать: %s | Порядок: %s",
"AppStore Options": "Параметры магазина приложений",
"All": "Все",
"Games": "Игры",
"Emulators": "Эмуляторы",
"Tools": "Инструменты",
"Themes": "Темы",
"Legacy": "Легаси",
"version: %s": "version: %s",
"updated: %s": "updated: %s",
"category: %s": "category: %s",
"extracted: %.2f MiB": "extracted: %.2f MiB",
"app_dls: %s": "app_dls: %s",
"More by Author": "",
"Leave Feedback": "",
"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!": "",
"Themezer": "Themezer",
"Themezer Options": "",
"Nsfw": "",
"Page": "",
"Page %zu / %zu": "Page %zu / %zu",
"Enter Page Number": "",
"Bad Page": "",
"Download theme?": "",
"GitHub": "",
"Downloading json": "",
"Select asset to download for ": "",
"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!": "",
"IRS (Infrared Joycon Camera)": "",
"IRS": "",
"Irs": "Irs",
"Ambient Noise Level: ": "",
"Controller": "Контроллер",
"Pad ": "Pad ",
"HandHeld": "Портативный",
" (Available)": " (Доступно)",
" (Unsupported)": "",
" (Unconnected)": " (Не подключено)",
"HandHeld": "Портативный",
"Rotation": "Вращение",
"0 (Sideways)": "0 (набок)",
"90 (Flat)": "90 (ровно)",
@@ -194,64 +159,234 @@
"Normal image": "Обычное изображение",
"Negative image": "Негативное изображение",
"Format": "Формат",
"Trimming Format": "Формат обрезки",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "Формат обрезки",
"External Light Filter": "Внешний светофильтр",
"Load Default": "Загрузить умолчания",
"Themezer": "Themezer",
"Themezer Options": "",
"Nsfw": "",
"Page": "",
"Page %zu / %zu": "Page %zu / %zu",
"Enter Page Number": "",
"Bad Page": "",
"Download theme?": "",
"GitHub": "",
"Downloading json": "",
"Select asset to download for ": "",
"Installing ": "",
"Uninstalling ": "",
"Deleting ": "",
"Deleting": "",
"Pasting ": "",
"Pasting": "",
"Removing ": "",
"Scanning ": "",
"Creating ": "",
"Copying ": "",
"Trying to load ": "",
"Downloading ": "",
"Downloaded ": "",
"Removed ": "",
"Checking MD5": "",
"Loading...": "",
"Loading": "",
"Empty!": "",
"Not Ready...": "",
"Error loading page!": "",
"Update avaliable: ": "",
"Download update: ": "",
"Updated to ": "",
"Press OK to restart Sphaira": "",
"Restart Sphaira?": "",
"Failed to download update": "",
"Advanced": "Продвинутые",
"Advanced Options": "Расширенные параметры",
"Logging": "Журналирование",
"Replace hbmenu on exit": "Заменить hbmenu при выходе",
"Restore hbmenu?": "",
"Restore": "",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "",
"Failed to restore hbmenu, please re-download hbmenu": "",
"Failed to restore hbmenu, using sphaira instead": "",
"Restored hbmenu, closing sphaira": "",
"Restored hbmenu": "",
"Restart Sphaira?": "",
"Press OK to restart Sphaira": "",
"Text scroll speed": "",
"Slow": "",
"Normal": "",
"Fast": "",
"Set right-side menu": "",
"Install options": "",
"Install Options": "",
"Enable sysmmc": "",
"Enable emummc": "",
"Show install warning": "",
"Install location": "",
"System memory": "",
"microSD card": "",
"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": "",
"Homebrew": "Homebrew",
"Apps": "",
"Homebrew Options": "Параметры Homebrew",
"Hide Sphaira": "Скрыть Sphaira",
"Install Forwarder": "Установить форвардер",
"WARNING: Installing forwarders will lead to a ban!": "ВНИМАНИЕ: Установка форвардеров приведет к бану!",
"Installing Forwarder": "Установить форвардер",
"Creating Program": "",
"Creating Control": "",
"Creating Meta": "",
"Writing Nca": "",
"Updating ncm databse": "",
"Pushing application record": "",
"Failed to install forwarder": "",
"Unstarred ": "",
"Starred ": "",
"Failed to remove old forwarder, please manually remove it!": "",
"AppStore": "",
"Appstore": "",
"Store": "",
"Filter: %s | Sort: %s | Order: %s": "Фильтр: %s | Сортировать: %s | Порядок: %s",
"AppStore Options": "Параметры магазина приложений",
"Info": "Информация",
"Changelog": "",
"Details": "",
"version: %s": "version: %s",
"updated: %s": "updated: %s",
"category: %s": "category: %s",
"extracted: %.2f MiB": "extracted: %.2f MiB",
"app_dls: %s": "app_dls: %s",
"More by Author": "",
"Leave Feedback": "",
"FileBrowser": "Файловый менеджер",
"Files": "",
"%zd files": "%zd files",
"%zd dirs": "%zd dirs",
"File Options": "Параметры файла",
"Show Hidden": "Показать скрытые",
"Folders First": "Папки в первую очередь",
"Hidden Last": "Скрытые в последнюю очередь",
"Cut": "Вырезать",
"Copy": "Копировать",
"Copying ": "",
"Paste": "",
"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!": "",
"Compress to zip": "",
"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)": "Посмотреть как текст (незакончено)",
"Ignore read only": "",
"Mount": "",
"Sd": "",
"Image System memory": "",
"Image microSD card": "",
"Empty...": "",
"Open with DayBreak?": "",
"Launch ": "",
"Launch option for: ": "",
"Select launcher for: ": "",
"Sort By": "Сортировать по",
"Sort Options": "Параметры сортировки",
"Filter": "Фильтр",
"All": "Все",
"Emulators": "Эмуляторы",
"Tools": "Инструменты",
"Themes": "Темы",
"Legacy": "Легаси",
"Sort": "Сортировать",
"Size": "Размер",
"Size (Star)": "",
"Alphabetical": "По наименованию",
"Alphabetical (Star)": "",
"Updated": "Обновлено",
"Updated (Star)": "",
"Downloads": "Загрузки",
"Likes": "",
"ID": "",
"Order": "Порядок",
"Descending": "По убыванию",
"Descending (down)": "По убыванию",
"Desc": "По убыванию",
"Ascending": "По возрастанию",
"Ascending (Up)": "По возрастанию",
"Asc": "По возрастанию",
"Layout": "",
"List": "",
"Icon": "",
"Grid": "",
"Search": "Поиск",
"Options": "Параметры темы",
"OK": "",
"Back": "Назад",
"Select": "",
"Open": "Открыть",
"Launch": "Запуск",
"Restart": "",
"Next": "",
"Prev": "",
"Unstar": "",
"Star": "",
"Yes": "Да",
"No": "Нет",
"On": "",
"Off": "",
"Install": "Установить",
"Install Selected files?": "",
"Installing ": "",
"Installed ": "",
"Installed!": "",
"Trying to load ": "",
"Checking MD5": "",
"Delete": "Удалить",
"Delete Selected files?": "",
"Completely remove ": "",
"Are you sure you want to delete ": "Вы уверены, что хотите удалить ",
"Are you sure you wish to cancel?": "",
"Audio disabled due to suspended game": "",
"If this message appears repeatedly, please open an issue.": ""
"Scanning ": "",
"Deleting ": "",
"Deleting": "",
"Remove": "",
"Completely remove ": "",
"Removing ": "",
"Removed ": "",
"Uninstalling ": "",
"Download": "Скачать",
"Downloading ": "",
"Downloaded ": "",
"Update": "",
"Update avaliable: ": "",
"Download update: ": "",
"Updated to ": "",
"Failed to download update": "",
"%zu hours %zu minutes remaining": "",
"%zu minutes %zu seconds remaining": "",
"%zu seconds remaining": "",
"Loading...": "",
"Loading": "",
"Empty!": "",
"Not Ready...": "",
"Error loading page!": ""
}

View File

@@ -1,72 +1,24 @@
{
"[Applet Mode]": "[Applet-läge]",
"No Internet": "Ingen Internetanslutning",
"Files": "Filer",
"Apps": "Appar",
"Store": "Butik",
"Menu": "Meny",
"Options": "Alternativ",
"OK": "OK",
"Back": "Tillbaka",
"Select": "Välj",
"Open": "Öppna",
"Launch": "Starta",
"Info": "Info",
"Install": "Installera",
"Delete": "Radera",
"Restart": "Starta om",
"Changelog": "Ändringslogg",
"Details": "Detaljer",
"Update": "Uppdatera",
"Remove": "Ta bort",
"Restore": "Återställ",
"Download": "Ladda ner",
"Next Page": "Nästa sida",
"Prev Page": "Föregående sida",
"Unstar": "Avmarkera stjärna",
"Star": "Markera stjärna",
"System memory": "Systemminne",
"microSD card": "microSD-kort",
"Sd": "Sd",
"Image System memory": "Avbild Systemminne",
"Image microSD card": "Avbild microSD-kort",
"Slow": "",
"Normal": "",
"Fast": "",
"Yes": "Ja",
"No": "Nej",
"Enabled": "Aktiverad",
"Disabled": "Inaktiverad",
"Sort By": "Sortera efter",
"Sort Options": "Sorteringsalternativ",
"Filter": "Filtrera",
"Sort": "Sortera",
"Order": "Ordning",
"Search": "Sök",
"Updated": "Uppdaterad",
"Updated (Star)": "Uppdaterad (Stjärna)",
"Downloads": "Nedladdningar",
"Size": "Storlek",
"Size (Star)": "Storlek (Stjärna)",
"Alphabetical": "Alfabetisk",
"Alphabetical (Star)": "Alfabetisk (Stjärna)",
"Likes": "Gillar",
"ID": "ID",
"Descending": "Fallande",
"Descending (down)": "Fallande (nedåt)",
"Desc": "Fall",
"Ascending": "Stigande",
"Ascending (Up)": "Stigande (uppåt)",
"Asc": "Stig",
"Switch-Handheld!": "Switch Handhållen!",
"Switch-Docked!": "Switch Dockad!",
"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": "",
"If this message appears repeatedly, please open an issue.": "Om detta meddelande visas upprepade gånger, vänligen öppna en felanmälan.",
"Menu Options": "Menyalternativ",
"Menu": "Meny",
"Theme": "Tema",
"Theme Options": "Temaalternativ",
"Select Theme": "Välj tema",
"Shuffle": "Blanda",
"Music": "Musik",
"12 Hour Time": "",
"Download Default Music": "",
"Failed to download default_music.bfstm, please try again": "",
"Overwrite current default music?": "",
"Network": "Nätverk",
"Network Options": "Nätverksalternativ",
"Ftp": "FTP",
@@ -75,8 +27,7 @@
"Nxlink Connected": "Nxlink ansluten",
"Nxlink Upload": "Nxlink överför",
"Nxlink Finished": "Nxlink klar",
"Switch-Handheld!": "Switch Handhållen!",
"Switch-Docked!": "Switch Dockad!",
"Language": "Språk",
"Auto": "Auto",
"English": "Engelska",
@@ -92,87 +43,101 @@
"Russian": "Ryska",
"Swedish": "Svenska",
"Vietnamese": "Vietnamese",
"Logging": "Loggning",
"Replace hbmenu on exit": "Ersätt hbmenu vid avslut",
"Ukrainian": "Українська",
"Misc": "Övrigt",
"Misc Options": "Övriga alternativ",
"Web": "Webb",
"Install forwarders": "Installera genvägar",
"Install location": "Installationsplats",
"Show install warning": "Visa installationsvarning",
"Text scroll speed": "",
"FileBrowser": "Filbläddrare",
"%zd files": "%zd filer",
"%zd dirs": "%zd kataloger",
"File Options": "Filalternativ",
"Show Hidden": "Visa dolda",
"Folders First": "Mappar först",
"Hidden Last": "Dolda sist",
"Cut": "Klipp ut",
"Copy": "Kopiera",
"Paste": "Klistra in",
"Paste ": "Klistra in ",
" file(s)?": " fil(er)?",
"Rename": "Byt namn",
"Set New File Name": "Ange nytt filnamn",
"Advanced": "Avancerat",
"Advanced Options": "Avancerade alternativ",
"Create File": "Skapa fil",
"Set File Name": "Ange filnamn",
"Create Folder": "Skapa mapp",
"Set Folder Name": "Ange mappnamn",
"View as text (unfinished)": "Visa som text (ofärdig)",
"Ignore read only": "Ignorera skrivskydd",
"Mount": "Montera",
"Empty...": "Tom...",
"Open with DayBreak?": "Öppna med DayBreak?",
"Launch ": "Starta ",
"Launch option for: ": "Startalternativ för: ",
"Select launcher for: ": "Välj startprogram för: ",
"Homebrew": "Homebrew",
"Homebrew Options": "Homebrew-alternativ",
"Hide Sphaira": "Dölj Sphaira",
"Install Forwarder": "Installera genväg",
"WARNING: Installing forwarders will lead to a ban!": "VARNING: Att installera genvägar kan leda till avstängning!",
"Installing Forwarder": "Installerar genväg",
"Creating Program": "Skapar program",
"Creating Control": "Skapar kontroll",
"Creating Meta": "Skapar metadata",
"Writing Nca": "Skriver Nca",
"Updating ncm databse": "Uppdaterar ncm-databas",
"Pushing application record": "Skickar programpost",
"Installed!": "Installerad!",
"Failed to install forwarder": "Misslyckades att installera genväg",
"Unstarred ": "Avmarkerad ",
"Starred ": "Markerad ",
"AppStore": "AppStore",
"Filter: %s | Sort: %s | Order: %s": "Filter: %s | Sortering: %s | Ordning: %s",
"AppStore Options": "AppStore-alternativ",
"All": "Alla",
"Games": "Spel",
"Emulators": "Emulatorer",
"Tools": "Verktyg",
"Themes": "Teman",
"Legacy": "Legacy",
"version: %s": "version: %s",
"updated: %s": "uppdaterad: %s",
"category: %s": "kategori: %s",
"extracted: %.2f MiB": "extraherad: %.2f MiB",
"app_dls: %s": "app_nedladdningar: %s",
"More by Author": "Mer av författaren",
"Leave Feedback": "Lämna feedback",
"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!": "",
"Themezer": "Themezer",
"Themezer Options": "Themezer-alternativ",
"Nsfw": "Nsfw",
"Page": "Sida",
"Page %zu / %zu": "Sida %zu / %zu",
"Enter Page Number": "Ange sidnummer",
"Bad Page": "Ogiltig sida",
"Download theme?": "Ladda ner tema?",
"GitHub": "GitHub",
"Downloading json": "Laddar ner JSON",
"Select asset to download for ": "Välj tillgång att ladda ner för ",
"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!": "",
"IRS (Infrared Joycon Camera)": "",
"IRS": "",
"Irs": "Irs",
"Ambient Noise Level: ": "Omgivningsljudnivå: ",
"Controller": "Kontroller",
"Pad ": "Handkontroll ",
"HandHeld": "Handhållen",
" (Available)": " (Tillgänglig)",
" (Unsupported)": " (Ej stödd)",
" (Unconnected)": " (Ej ansluten)",
"HandHeld": "Handhållen",
"Rotation": "Rotation",
"0 (Sideways)": "0 (Sidan)",
"90 (Flat)": "90 (Platt)",
@@ -194,64 +159,234 @@
"Normal image": "Normal bild",
"Negative image": "Negativ bild",
"Format": "Format",
"Trimming Format": "Trimningsformat",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "Trimningsformat",
"External Light Filter": "Externt ljusfilter",
"Load Default": "Ladda standard",
"Themezer": "Themezer",
"Themezer Options": "Themezer-alternativ",
"Nsfw": "Nsfw",
"Page": "Sida",
"Page %zu / %zu": "Sida %zu / %zu",
"Enter Page Number": "Ange sidnummer",
"Bad Page": "Ogiltig sida",
"Download theme?": "Ladda ner tema?",
"GitHub": "GitHub",
"Downloading json": "Laddar ner JSON",
"Select asset to download for ": "Välj tillgång att ladda ner för ",
"Installing ": "Installerar ",
"Uninstalling ": "Avinstallerar ",
"Deleting ": "Raderar ",
"Deleting": "Raderar",
"Pasting ": "Klistrar in ",
"Pasting": "Klistrar in",
"Removing ": "Tar bort ",
"Scanning ": "Skannar ",
"Creating ": "Skapar ",
"Copying ": "Kopierar ",
"Trying to load ": "Försöker ladda ",
"Downloading ": "Laddar ner ",
"Downloaded ": "Nedladdad ",
"Removed ": "Borttagen ",
"Checking MD5": "Kontrollerar MD5",
"Loading...": "Laddar...",
"Loading": "Laddar",
"Empty!": "Tomt!",
"Not Ready...": "Inte redo...",
"Error loading page!": "Fel vid laddning av sida!",
"Update avaliable: ": "Uppdatering tillgänglig: ",
"Download update: ": "Ladda ner uppdatering: ",
"Updated to ": "Uppdaterad till ",
"Press OK to restart Sphaira": "",
"Restart Sphaira?": "Starta om Sphaira?",
"Failed to download update": "Misslyckades att ladda ner uppdatering",
"Advanced": "Avancerat",
"Advanced Options": "Avancerade alternativ",
"Logging": "Loggning",
"Replace hbmenu on exit": "Ersätt hbmenu vid avslut",
"Restore hbmenu?": "Återställ hbmenu?",
"Restore": "Återställ",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Kunde inte hitta /switch/hbmenu.nro\nInstallera om hbmenu från Appstore ",
"Failed to restore hbmenu, please re-download hbmenu": "Misslyckades med att återställa hbmenu, vänligen ladda ner hbmenu igen.",
"Failed to restore hbmenu, using sphaira instead": "Misslyckades med att återställa hbmenu, använder istället sphaira.",
"Restored hbmenu, closing sphaira": "Återställde hbmenu, stänger sphaira.",
"Restored hbmenu": "Återställde hbmenu.",
"Restart Sphaira?": "Starta om Sphaira?",
"Press OK to restart Sphaira": "",
"Text scroll speed": "",
"Slow": "",
"Normal": "",
"Fast": "",
"Set right-side menu": "",
"Install options": "",
"Install Options": "",
"Enable sysmmc": "",
"Enable emummc": "",
"Show install warning": "Visa installationsvarning",
"Install location": "Installationsplats",
"System memory": "Systemminne",
"microSD card": "microSD-kort",
"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": "",
"Homebrew": "Homebrew",
"Apps": "Appar",
"Homebrew Options": "Homebrew-alternativ",
"Hide Sphaira": "Dölj Sphaira",
"Install Forwarder": "Installera genväg",
"WARNING: Installing forwarders will lead to a ban!": "VARNING: Att installera genvägar kan leda till avstängning!",
"Installing Forwarder": "Installerar genväg",
"Creating Program": "Skapar program",
"Creating Control": "Skapar kontroll",
"Creating Meta": "Skapar metadata",
"Writing Nca": "Skriver Nca",
"Updating ncm databse": "Uppdaterar ncm-databas",
"Pushing application record": "Skickar programpost",
"Failed to install forwarder": "Misslyckades att installera genväg",
"Unstarred ": "Avmarkerad ",
"Starred ": "Markerad ",
"Failed to remove old forwarder, please manually remove it!": "",
"AppStore": "AppStore",
"Appstore": "",
"Store": "Butik",
"Filter: %s | Sort: %s | Order: %s": "Filter: %s | Sortering: %s | Ordning: %s",
"AppStore Options": "AppStore-alternativ",
"Info": "Info",
"Changelog": "Ändringslogg",
"Details": "Detaljer",
"version: %s": "version: %s",
"updated: %s": "uppdaterad: %s",
"category: %s": "kategori: %s",
"extracted: %.2f MiB": "extraherad: %.2f MiB",
"app_dls: %s": "app_nedladdningar: %s",
"More by Author": "Mer av författaren",
"Leave Feedback": "Lämna feedback",
"FileBrowser": "Filbläddrare",
"Files": "Filer",
"%zd files": "%zd filer",
"%zd dirs": "%zd kataloger",
"File Options": "Filalternativ",
"Show Hidden": "Visa dolda",
"Folders First": "Mappar först",
"Hidden Last": "Dolda sist",
"Cut": "Klipp ut",
"Copy": "Kopiera",
"Copying ": "Kopierar ",
"Paste": "Klistra in",
"Paste ": "Klistra in ",
" file(s)?": " fil(er)?",
"Pasting ": "Klistrar in ",
"Pasting": "Klistrar in",
"Rename": "Byt namn",
"Set New File Name": "Ange nytt filnamn",
"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!": "",
"Create File": "Skapa fil",
"Set File Name": "Ange filnamn",
"Create Folder": "Skapa mapp",
"Set Folder Name": "Ange mappnamn",
"Creating ": "Skapar ",
"Upload": "",
"Select upload location": "",
"No upload locations set!": "",
"Uploading": "",
"Upload successfull!": "",
"Upload failed!": "",
"View as text (unfinished)": "Visa som text (ofärdig)",
"Ignore read only": "Ignorera skrivskydd",
"Mount": "Montera",
"Sd": "Sd",
"Image System memory": "Avbild Systemminne",
"Image microSD card": "Avbild microSD-kort",
"Empty...": "Tom...",
"Open with DayBreak?": "Öppna med DayBreak?",
"Launch ": "Starta ",
"Launch option for: ": "Startalternativ för: ",
"Select launcher for: ": "Välj startprogram för: ",
"Sort By": "Sortera efter",
"Sort Options": "Sorteringsalternativ",
"Filter": "Filtrera",
"All": "Alla",
"Emulators": "Emulatorer",
"Tools": "Verktyg",
"Themes": "Teman",
"Legacy": "Legacy",
"Sort": "Sortera",
"Size": "Storlek",
"Size (Star)": "Storlek (Stjärna)",
"Alphabetical": "Alfabetisk",
"Alphabetical (Star)": "Alfabetisk (Stjärna)",
"Updated": "Uppdaterad",
"Updated (Star)": "Uppdaterad (Stjärna)",
"Downloads": "Nedladdningar",
"Likes": "Gillar",
"ID": "ID",
"Order": "Ordning",
"Descending": "Fallande",
"Descending (down)": "Fallande (nedåt)",
"Desc": "Fall",
"Ascending": "Stigande",
"Ascending (Up)": "Stigande (uppåt)",
"Asc": "Stig",
"Layout": "",
"List": "",
"Icon": "",
"Grid": "",
"Search": "Sök",
"Options": "Alternativ",
"OK": "OK",
"Back": "Tillbaka",
"Select": "Välj",
"Open": "Öppna",
"Launch": "Starta",
"Restart": "Starta om",
"Next": "",
"Prev": "",
"Unstar": "Avmarkera stjärna",
"Star": "Markera stjärna",
"Yes": "Ja",
"No": "Nej",
"On": "",
"Off": "",
"Install": "Installera",
"Install Selected files?": "",
"Installing ": "Installerar ",
"Installed ": "",
"Installed!": "Installerad!",
"Trying to load ": "Försöker ladda ",
"Checking MD5": "Kontrollerar MD5",
"Delete": "Radera",
"Delete Selected files?": "Radera valda filer?",
"Completely remove ": "Ta bort helt ",
"Are you sure you want to delete ": "Är du säker på att du vill radera ",
"Are you sure you wish to cancel?": "Är du säker på att du vill avbryta?",
"Audio disabled due to suspended game": "Ljud är avstängt på grund av bakgrundsprogram",
"If this message appears repeatedly, please open an issue.": "Om detta meddelande visas upprepade gånger, vänligen öppna en felanmälan.",
"Scanning ": "Skannar ",
"Deleting ": "Raderar ",
"Deleting": "Raderar",
"Remove": "Ta bort",
"Completely remove ": "Ta bort helt ",
"Removing ": "Tar bort ",
"Removed ": "Borttagen ",
"Uninstalling ": "Avinstallerar ",
"Download": "Ladda ner",
"Downloading ": "Laddar ner ",
"Downloaded ": "Nedladdad ",
"Update": "Uppdatera",
"Update avaliable: ": "Uppdatering tillgänglig: ",
"Download update: ": "Ladda ner uppdatering: ",
"Updated to ": "Uppdaterad till ",
"Failed to download update": "Misslyckades att ladda ner uppdatering",
"%zu hours %zu minutes remaining": "",
"%zu minutes %zu seconds remaining": "",
"%zu seconds remaining": "",
"Loading...": "Laddar...",
"Loading": "Laddar",
"Empty!": "Tomt!",
"Not Ready...": "Inte redo...",
"Error loading page!": "Fel vid laddning av sida!"
}

392
assets/romfs/i18n/uk.json Normal file
View File

@@ -0,0 +1,392 @@
{
"[Applet Mode]": "[Режим Аплету]",
"No Internet": "Без інтернету",
"Switch-Handheld!": "Switch - Портатив!",
"Switch-Docked!": "Switch - Докований!",
"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.": "Якщо це повідомлення з'являється повторно, будь ласка, повідомте про проблему.",
"Menu Options": "Опції меню",
"Menu": "Меню",
"Theme": "Тема",
"Theme Options": "Опції теми",
"Select Theme": "Вибрати тему",
"Music": "Музика",
"12 Hour Time": "12-годинний формат часу",
"Download Default Music": "Завантажити музику за замовчуванням",
"Failed to download default_music.bfstm, please try again": "",
"Overwrite current default music?": "",
"Network": "Мережа",
"Network Options": "Опції мережі",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink підключено",
"Nxlink Upload": "Nxlink | Завантаження",
"Nxlink Finished": "Nxlink | Завершено",
"Language": "Мова",
"Auto": "Автоматично",
"English": "English",
"Japanese": "日本語",
"French": "Français",
"German": "Deutsch",
"Italian": "Italiano",
"Spanish": "Español",
"Chinese": "中文",
"Korean": "한국어",
"Dutch": "Nederlands",
"Portuguese": "Português",
"Russian": "Русский",
"Swedish": "Svenska",
"Vietnamese": "Tiếng Việt",
"Ukrainian": "Українська",
"Misc": "Різне",
"Misc Options": "Опції різного",
"Games": "Ігри",
"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!": "",
"Themezer": "Themezer",
"Themezer Options": "Опції Themezer",
"Nsfw": "NSFW",
"Page": "Сторінка",
"Page %zu / %zu": "Сторінка %zu / %zu",
"Enter Page Number": "Введіть номер сторінки",
"Bad Page": "Неправильна сторінка",
"Download theme?": "Завантажити тему?",
"GitHub": "GitHub",
"Downloading json": "Завантаження JSON",
"Select asset to download for ": "Виберіть ресурс для завантаження для ",
"FTP Install": "Встановлення через FTP",
"FTP Install (EXPERIMENTAL)": "Встановлення через FTP (ЕКСПЕРИМЕНТАЛЬНО)",
"Connection Type: WiFi | Strength: ": "Тип підключення: WiFi | Сила сигналу: ",
"Connection Type: Ethernet": "Тип підключення: Ethernet",
"Connection Type: None": "Тип підключення: Немає",
"Host:": "Хост:",
"Port:": "Порт:",
"Username:": "Ім'я користувача:",
"Password:": "Пароль:",
"SSID:": "SSID:",
"Passphrase:": "Кодова фраза:",
"Failed to install via FTP, press B to exit...": "Не вдалося встановити через FTP, натисніть B для виходу...",
"Ftp install success!": "Встановлення через FTP успішно завершено.",
"Ftp install failed!": "Встановлення через FTP не вдалося.",
"USB Install": "Встановлення через USB",
"USB": "USB",
"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...": "",
"Disable MTP for usb install": "Вимкнути MTP для встановлення через USB",
"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": "",
"Gc install success!": "Встановлення з картриджа успішно завершено.",
"Gc install failed!": "Встановлення з картриджа не вдалося.",
"IRS (Infrared Joycon Camera)": "ІЧ (Інфрачервона камера Joycon)",
"IRS": "",
"Irs": "ІЧ-сенсор",
"Ambient Noise Level: ": "Рівень навколишнього шуму: ",
"Controller": "Контролер",
"Pad ": "Геймпад ",
"HandHeld": "Портативний режим",
" (Available)": " (Доступно)",
" (Unsupported)": " (Не підтримується)",
" (Unconnected)": " (Не підключено)",
"Rotation": "Обертання",
"0 (Sideways)": "0° (Збоку)",
"90 (Flat)": "90° (Плоско)",
"180 (-Sideways)": "180° (-Збоку)",
"270 (Upside down)": "270° (Догори дном)",
"Colour": "Колір",
"Grey": "Сірий",
"Ironbow": "Ironbow",
"Green": "Зелений",
"Red": "Червоний",
"Blue": "Синій",
"Light Target": "Ціль освітлення",
"All leds": "Всі світлодіоди",
"Bright group": "Яскрава група",
"Dim group": "Тьмяна група",
"None": "Немає",
"Gain": "Підсилення",
"Negative Image": "Негативне зображення",
"Normal image": "Нормальне зображення",
"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": "Завантажити типові",
"Advanced": "Додатково",
"Advanced Options": "Додаткові опції",
"Logging": "Логування",
"Replace hbmenu on exit": "Заміна hbmenu при виході",
"Restore hbmenu?": "Відновити hbmenu?",
"Restore": "Відновити",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Не вдалося знайти /switch/hbmenu.nro\nВикористовуйте Магазин програм для перевстановлення hbmenu",
"Failed to restore hbmenu, please re-download hbmenu": "Не вдалося відновити hbmenu, будь ласка, завантажте hbmenu знову",
"Failed to restore hbmenu, using sphaira instead": "Не вдалося відновити hbmenu, замість цього використовується Sphaira",
"Restored hbmenu, closing sphaira": "hbmenu відновлено, закриття Sphaira",
"Restored hbmenu": "hbmenu відновлено",
"Restart Sphaira?": "Перезапустити Sphaira?",
"Press OK to restart Sphaira": "Натисніть OK для перезапуску Sphaira",
"Text scroll speed": "Швидк. прокрутки",
"Slow": "Повільно",
"Normal": "Нормально",
"Fast": "Швидко",
"Set right-side menu": "Праве меню",
"Install options": "Опції встановлення",
"Install Options": "Опції встановлення",
"Enable sysmmc": "",
"Enable emummc": "",
"Show install warning": "Попередж. при встанов.",
"Install location": "Місце встановлення",
"System memory": "Пам'ять консолі",
"microSD card": "SD-карта",
"Boost CPU clock": "Розігнати CPU",
"Allow downgrade": "Дозволити відкат",
"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": "Конвертувати у стандартне шифрування",
"Lower master key": "Знизити майстер-ключ",
"Lower system version": "Знизити версію системи",
"Homebrew": "Домашні програми",
"Apps": "Програми",
"Homebrew Options": "Опції домашніх програм",
"Hide Sphaira": "Приховати Sphaira",
"Install Forwarder": "Встановити форвардер",
"WARNING: Installing forwarders will lead to a ban!": "УВАГА: Встановлення форвардерів може призвести до бану!",
"Installing Forwarder": "Встановлення форвардера",
"Creating Program": "Створення програми",
"Creating Control": "Створення контролера",
"Creating Meta": "Створення метаданих",
"Writing Nca": "Запис NCA",
"Updating ncm databse": "Оновлення бази даних NCM",
"Pushing application record": "Запис даних програми",
"Failed to install forwarder": "Не вдалося встановити форвардер",
"Unstarred ": "Знято зірку з ",
"Starred ": "Позначено зіркою ",
"Failed to remove old forwarder, please manually remove it!": "",
"AppStore": "Магазин програм",
"Appstore": "",
"Store": "Магазин",
"Filter: %s | Sort: %s | Order: %s": "Фільтр: %s | Сорт.: %s | Порядок: %s",
"AppStore Options": "Опції магазину програм",
"Info": "Інфо",
"Changelog": "Журнал змін",
"Details": "Деталі",
"version: %s": "версія: %s",
"updated: %s": "оновлено: %s",
"category: %s": "категорія: %s",
"extracted: %.2f MiB": "розмір: %.2f MiB",
"app_dls: %s": "завантажень: %s",
"More by Author": "Більше від автора",
"Leave Feedback": "Залишити відгук",
"FileBrowser": "Файловий менеджер",
"Files": "Файли",
"%zd files": "%zd файл(и)",
"%zd dirs": "%zd тек(и)",
"File Options": "Опції файлів",
"Show Hidden": "Показати приховані",
"Folders First": "Теки спочатку",
"Hidden Last": "Приховані в кінці",
"Cut": "Вирізати",
"Copy": "Копіювати",
"Copying ": "Копіювання ",
"Paste": "Вставити",
"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!": "",
"Compress to zip": "Стиснути в zip",
"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)": "Переглянути як текст (незавершено)",
"Ignore read only": "Ігнорувати лише читання",
"Mount": "Монтувати",
"Sd": "SD-карта",
"Image System memory": "Фото | Пам'ять консолі",
"Image microSD card": "Фото | SD-карта",
"Empty...": "Пусто...",
"Open with DayBreak?": "Відкрити за допомогою DayBreak?",
"Launch ": "Запустити ",
"Launch option for: ": "Опція запуску для: ",
"Select launcher for: ": "Виберіть лаунчер для: ",
"Sort By": "Сортувати за",
"Sort Options": "Опції сортування",
"Filter": "Фільтр",
"All": "Всі",
"Emulators": "Емулятори",
"Tools": "Інструменти",
"Themes": "Теми",
"Legacy": "Доступні оновлення",
"Sort": "Сортування",
"Size": "Розмір",
"Size (Star)": "Розмір (Зірка)",
"Alphabetical": "За алфавітом",
"Alphabetical (Star)": "За алфавітом (Зірка)",
"Updated": "Оновлено",
"Updated (Star)": "Оновлено (Зірка)",
"Downloads": "Завантаження",
"Likes": "Вподобання",
"ID": "ID",
"Order": "Порядок",
"Descending": "За спаданням",
"Descending (down)": "За спаданням (вниз)",
"Desc": "Спад.",
"Ascending": "За зростанням",
"Ascending (Up)": "За зростанням (вгору)",
"Asc": "Зрост.",
"Layout": "",
"List": "",
"Icon": "",
"Grid": "",
"Search": "Пошук",
"Options": "Налаштування",
"OK": "ОК",
"Back": "Назад",
"Select": "Вибрати",
"Open": "Відкрити",
"Launch": "Запустити",
"Restart": "Перезапустити",
"Next": "Наступний",
"Prev": "Попередній",
"Unstar": "Прибрати з обраного",
"Star": "Позначити зіркою",
"Yes": "Так",
"No": "Ні",
"On": "Увімк.",
"Off": "Вимк.",
"Install": "Встановити",
"Install Selected files?": "Встановити вибрані файли?",
"Installing ": "Встановлення ",
"Installed ": "Встановлено ",
"Installed!": "Встановлено!",
"Trying to load ": "Спроба завантажити ",
"Checking MD5": "Перевірка MD5",
"Delete": "Видалити",
"Delete Selected files?": "Видалити вибрані файли?",
"Are you sure you want to delete ": "Ви впевнені, що хочете видалити ",
"Scanning ": "Сканування ",
"Deleting ": "Видалення ",
"Deleting": "Видалення",
"Remove": "Видалити",
"Completely remove ": "Повністю видалити ",
"Removing ": "Видалення ",
"Removed ": "Видалено ",
"Uninstalling ": "Видалення ",
"Download": "Завантажити",
"Downloading ": "Завантаження ",
"Downloaded ": "Завантажено ",
"Update": "Оновити",
"Update avaliable: ": "Доступне оновлення: ",
"Download update: ": "Завантажити оновлення: ",
"Updated to ": "Оновлено до ",
"Failed to download update": "Не вдалося завантажити оновлення",
"%zu hours %zu minutes remaining": "",
"%zu minutes %zu seconds remaining": "",
"%zu seconds remaining": "",
"Loading...": "Завантаження...",
"Loading": "Завантаження",
"Empty!": "Пусто!",
"Not Ready...": "Не готово...",
"Error loading page!": "Помилка завантаження сторінки!"
}

View File

@@ -1,72 +1,24 @@
{
"[Applet Mode]": "[Applet Mode]",
"No Internet": "Không có Internet",
"Files": "Tập tin",
"Apps": "Ứng dụng",
"Store": "Cửa hàng",
"Menu": "Menu",
"Options": "Tuỳ chọn",
"OK": "OK",
"Back": "Trở về",
"Select": "Chọn",
"Open": "Mở",
"Launch": "Chạy",
"Info": "Thông tin",
"Install": "Cài đặt",
"Delete": "Xoá",
"Restart": "Khởi động lại",
"Changelog": "Thay đổi",
"Details": "Chi tiết",
"Update": "Cập nhật",
"Remove": "Gỡ",
"Restore": "Khôi phục",
"Download": "Tải về",
"Next Page": "Trang kế",
"Prev Page": "Trang trước",
"Unstar": "Xoá yêu thích",
"Star": "Yêu thích",
"System memory": "Bộ nhớ máy",
"microSD card": "Thẻ nhớ",
"Sd": "Sd",
"Image System memory": "Bộ nhớ hệ thống hình ảnh",
"Image microSD card": "Thẻ nhớ hệ thống hình ảnh",
"Slow": "",
"Normal": "",
"Fast": "",
"Yes": "Có",
"No": "Không",
"Enabled": "Bật",
"Disabled": "Tắt",
"Sort By": "Sắp xếp bởi",
"Sort Options": "Tuỳ chọn sắp xếp",
"Filter": "Lọc",
"Sort": "Sắp xếp",
"Order": "Thứ tự",
"Search": "Tìm kiếm",
"Updated": "Updated",
"Updated (Star)": "Đã cập nhật (Yêu thích)",
"Downloads": "Danh sách tải về",
"Size": "Kích thước",
"Size (Star)": "Kích thước (Yêu thích)",
"Alphabetical": "A-Z",
"Alphabetical (Star)": "A-Z (Yêu thích)",
"Likes": "Thích",
"ID": "ID",
"Descending": "Giảm dần",
"Descending (down)": "Giảm dần (xuống)",
"Desc": "Giảm",
"Ascending": "Tăng dần",
"Ascending (Up)": "Tăng dần (lên)",
"Asc": "Tăng",
"Switch-Handheld!": "Switch-Handheld!",
"Switch-Docked!": "Switch-Docked!",
"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": "",
"If this message appears repeatedly, please open an issue.": "Nếu thấy tin nhắn này, hãy báo lỗi.",
"Menu Options": "Menu tuỳ chọn",
"Menu": "Menu",
"Theme": "Theme",
"Theme Options": "Theme tuỳ chọn",
"Select Theme": "Chọn Theme",
"Shuffle": "Trộn",
"Music": "Âm nhạc",
"12 Hour Time": "",
"Download Default Music": "",
"Failed to download default_music.bfstm, please try again": "",
"Overwrite current default music?": "",
"Network": "Mạng",
"Network Options": "Tuỳ chọn mạng",
"Ftp": "FTP",
@@ -75,8 +27,7 @@
"Nxlink Connected": "Nxlink Kết Nối",
"Nxlink Upload": "Nxlink Đăng Tải",
"Nxlink Finished": "Nxlink Hoàn Thành",
"Switch-Handheld!": "Switch-Handheld!",
"Switch-Docked!": "Switch-Docked!",
"Language": "Ngôn ngữ",
"Auto": "Tự động",
"English": "English",
@@ -92,87 +43,101 @@
"Russian": "Русский",
"Swedish": "Svenska",
"Vietnamese": "Việt Nam",
"Logging": "Logging",
"Replace hbmenu on exit": "Thay thế hbmenu khi thoát",
"Ukrainian": "Українська",
"Misc": "Tiện ích",
"Misc Options": "Tiện ích mở rộng",
"Web": "Web",
"Install forwarders": "Cài ra màn hình",
"Install location": "Vị trí cài đặt",
"Show install warning": "Hiển thị cảnh báo cài đặt",
"Text scroll speed": "",
"FileBrowser": "Duyệt tập tin",
"%zd files": "%zd tập tin",
"%zd dirs": "%zd thư mục",
"File Options": "Tuỳ chọn tập tin",
"Show Hidden": "Hiển thị tập tin ẩn",
"Folders First": "Thư mục đầu tiên",
"Hidden Last": "Ẩn cuối",
"Cut": "Cắt",
"Copy": "Sao chép",
"Paste": "Dán",
"Paste ": "Paste ",
" file(s)?": " tập tin(nhiều)?",
"Rename": "Đổi tên",
"Set New File Name": "Đặt tên mới cho tập tin",
"Advanced": "Mở rộng",
"Advanced Options": "Tuỳ chọn mở rộng",
"Create File": "Tạo tập tin",
"Set File Name": "Đặt tên cho tập tin",
"Create Folder": "Tạo thư mục",
"Set Folder Name": "Đặt tên thư mục",
"View as text (unfinished)": "Xem dạng văn bản (chưa xong)",
"Ignore read only": "Bỏ qua chỉ đọc",
"Mount": "Gắn",
"Empty...": "Rỗng...",
"Open with DayBreak?": "Mở với DayBreak?",
"Launch ": "Chạy ",
"Launch option for: ": "Chạy với tuỳ chọn cho: ",
"Select launcher for: ": "Chọn trình chạy cho: ",
"Homebrew": "Homebrew",
"Homebrew Options": "Tuỳ chọn Homebrew",
"Hide Sphaira": "Ẩn Sphaira",
"Install Forwarder": "Cài ra ngoài màn hình",
"WARNING: Installing forwarders will lead to a ban!": "CẢNH BÁO: Bạn có chắn muốn cài ra ngoài màn hình!",
"Installing Forwarder": "Đang cài đặt ra ngoài màn hình",
"Creating Program": "Tạo chương trình",
"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",
"Pushing application record": "Đẩy ứng dụng",
"Installed!": "Đã cài xong!",
"Failed to install forwarder": "Cài đặt ra ngoài màn hình thất bại",
"Unstarred ": "Bỏ yêu thích ",
"Starred ": "Đã yêu thích ",
"AppStore": "AppStore",
"Filter: %s | Sort: %s | Order: %s": "Lọc: %s | Sắp xếp: %s | Thứ tự: %s",
"AppStore Options": "Tuỳ chọn AppStore",
"All": "Tất cả",
"Games": "Games",
"Emulators": "Emulators",
"Tools": "Tools",
"Themes": "Themes",
"Legacy": "Legacy",
"version: %s": "version: %s",
"updated: %s": "updated: %s",
"category: %s": "category: %s",
"extracted: %.2f MiB": "extracted: %.2f MiB",
"app_dls: %s": "app_dls: %s",
"More by Author": "Xem thêm tác giả",
"Leave Feedback": "Để lại phản hồi",
"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!": "",
"Themezer": "Themezer",
"Themezer Options": "Tuỳ chọn Themezer",
"Nsfw": "18+",
"Page": "Trang",
"Page %zu / %zu": "Trang %zu / %zu",
"Enter Page Number": "Nhập số trang",
"Bad Page": "Trang không tồn tại",
"Download theme?": "Tải theme?",
"GitHub": "GitHub",
"Downloading json": "Đang tải json",
"Select asset to download for ": "Chọn nội dung để tải xuống cho ",
"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!": "",
"IRS (Infrared Joycon Camera)": "",
"IRS": "",
"Irs": "Irs",
"Ambient Noise Level: ": "Mức ồn xung quanh: ",
"Controller": "Điều khiển",
"Pad ": "Pad ",
"HandHeld": "Cầm tay",
" (Available)": " (Có sẵn)",
" (Unsupported)": " (Không hỗ trợ)",
" (Unconnected)": " (Không kết nối)",
"HandHeld": "Cầm tay",
"Rotation": "Xoay",
"0 (Sideways)": "0 (Đi ngang)",
"90 (Flat)": "90 (Phẳng)",
@@ -194,64 +159,234 @@
"Normal image": "Ảnh bình thường",
"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",
"Trimming Format": "Định dạng cắt tỉa",
"External Light Filter": "Bộ lộc ánh sáng bên ngoài",
"Load Default": "Tải mặc định",
"Themezer": "Themezer",
"Themezer Options": "Tuỳ chọn Themezer",
"Nsfw": "18+",
"Page": "Trang",
"Page %zu / %zu": "Trang %zu / %zu",
"Enter Page Number": "Nhập số trang",
"Bad Page": "Trang không tồn tại",
"Download theme?": "Tải theme?",
"GitHub": "GitHub",
"Downloading json": "Đang tải json",
"Select asset to download for ": "Chọn nội dung để tải xuống cho ",
"Installing ": "Đang cài đặt ",
"Uninstalling ": "Đang gỡ cài đặt ",
"Deleting ": "Đang xoá ",
"Deleting": "Đang xoá",
"Pasting ": "Đang dán ",
"Pasting": "Đang dán",
"Removing ": "Đang gỡ ",
"Scanning ": "Đang quét ",
"Creating ": "Đang tạo ",
"Copying ": "Đang sao chép ",
"Trying to load ": "Đang cố gắn mở ",
"Downloading ": "Đang tải xuống ",
"Downloaded ": "Đã tải xong ",
"Removed ": "Đã gỡ ",
"Checking MD5": "Kiểm tra MD5",
"Loading...": "Đang tải...",
"Loading": "Đang tải",
"Empty!": "Trống!",
"Not Ready...": "Chưa sẵn sàng...",
"Error loading page!": "Lỗi tải trang!",
"Update avaliable: ": "Cập nhậc có sẵn: ",
"Download update: ": "Tải cập nhật: ",
"Updated to ": "Đã cập nhật ",
"Press OK to restart Sphaira": "",
"Restart Sphaira?": "Khởi động lại Sphaira?",
"Failed to download update": "Cập nhật thất bại",
"Advanced": "Mở rộng",
"Advanced Options": "Tuỳ chọn mở rộng",
"Logging": "Logging",
"Replace hbmenu on exit": "Thay thế hbmenu khi thoát",
"Restore hbmenu?": "Khôi phục hbmenu?",
"Restore": "Khôi phục",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Không tìm thấy /switch/hbmenu.nro\nSử dụng AppStore để cài lại hbmenu",
"Failed to restore hbmenu, please re-download hbmenu": "Không thể khôi phục hbmenu, Vui lòng tải lại hbmenu",
"Failed to restore hbmenu, using sphaira instead": "Không thể khôi phục hbmenu, thay vào đó sử dụng Sphira",
"Restored hbmenu, closing sphaira": "Khôi mục hbmenu thành công, đóng sphaira",
"Restored hbmenu": "Đã khôi phục hbmenu",
"Restart Sphaira?": "Khởi động lại Sphaira?",
"Press OK to restart Sphaira": "",
"Text scroll speed": "",
"Slow": "",
"Normal": "",
"Fast": "",
"Set right-side menu": "",
"Install options": "",
"Install Options": "",
"Enable sysmmc": "",
"Enable emummc": "",
"Show install warning": "Hiển thị cảnh báo cài đặt",
"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": "",
"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": "",
"Homebrew": "Homebrew",
"Apps": "Ứng dụng",
"Homebrew Options": "Tuỳ chọn Homebrew",
"Hide Sphaira": "Ẩn Sphaira",
"Install Forwarder": "Cài ra ngoài màn hình",
"WARNING: Installing forwarders will lead to a ban!": "CẢNH BÁO: Bạn có chắn muốn cài ra ngoài màn hình!",
"Installing Forwarder": "Đang cài đặt ra ngoài màn hình",
"Creating Program": "Tạo chương trình",
"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",
"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",
"Unstarred ": "Bỏ yêu thích ",
"Starred ": "Đã yêu thích ",
"Failed to remove old forwarder, please manually remove it!": "",
"AppStore": "AppStore",
"Appstore": "",
"Store": "Cửa hàng",
"Filter: %s | Sort: %s | Order: %s": "Lọc: %s | Sắp xếp: %s | Thứ tự: %s",
"AppStore Options": "Tuỳ chọn AppStore",
"Info": "Thông tin",
"Changelog": "Thay đổi",
"Details": "Chi tiết",
"version: %s": "version: %s",
"updated: %s": "updated: %s",
"category: %s": "category: %s",
"extracted: %.2f MiB": "extracted: %.2f MiB",
"app_dls: %s": "app_dls: %s",
"More by Author": "Xem thêm tác giả",
"Leave Feedback": "Để lại phản hồi",
"FileBrowser": "Duyệt tập tin",
"Files": "Tập tin",
"%zd files": "%zd tập tin",
"%zd dirs": "%zd thư mục",
"File Options": "Tuỳ chọn tập tin",
"Show Hidden": "Hiển thị tập tin ẩn",
"Folders First": "Thư mục đầu tiên",
"Hidden Last": "Ẩn cuối",
"Cut": "Cắt",
"Copy": "Sao chép",
"Copying ": "Đang sao chép ",
"Paste": "Dán",
"Paste ": "Paste ",
" file(s)?": " tập tin(nhiều)?",
"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",
"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!": "",
"Create File": "Tạo tập tin",
"Set File Name": "Đặt tên cho tập tin",
"Create Folder": "Tạo thư mục",
"Set Folder Name": "Đặt tên thư mục",
"Creating ": "Đang tạo ",
"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)",
"Ignore read only": "Bỏ qua chỉ đọc",
"Mount": "Gắn",
"Sd": "Sd",
"Image System memory": "Bộ nhớ hệ thống hình ảnh",
"Image microSD card": "Thẻ nhớ hệ thống hình ảnh",
"Empty...": "Rỗng...",
"Open with DayBreak?": "Mở với DayBreak?",
"Launch ": "Chạy ",
"Launch option for: ": "Chạy với tuỳ chọn cho: ",
"Select launcher for: ": "Chọn trình chạy cho: ",
"Sort By": "Sắp xếp bởi",
"Sort Options": "Tuỳ chọn sắp xếp",
"Filter": "Lọc",
"All": "Tất cả",
"Emulators": "Emulators",
"Tools": "Tools",
"Themes": "Themes",
"Legacy": "Legacy",
"Sort": "Sắp xếp",
"Size": "Kích thước",
"Size (Star)": "Kích thước (Yêu thích)",
"Alphabetical": "A-Z",
"Alphabetical (Star)": "A-Z (Yêu thích)",
"Updated": "Updated",
"Updated (Star)": "Đã cập nhật (Yêu thích)",
"Downloads": "Danh sách tải về",
"Likes": "Thích",
"ID": "ID",
"Order": "Thứ tự",
"Descending": "Giảm dần",
"Descending (down)": "Giảm dần (xuống)",
"Desc": "Giảm",
"Ascending": "Tăng dần",
"Ascending (Up)": "Tăng dần (lên)",
"Asc": "Tăng",
"Layout": "",
"List": "",
"Icon": "",
"Grid": "",
"Search": "Tìm kiếm",
"Options": "Tuỳ chọn",
"OK": "OK",
"Back": "Trở về",
"Select": "Chọn",
"Open": "Mở",
"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": "",
"Off": "",
"Install": "Cài đặt",
"Install Selected files?": "",
"Installing ": "Đang cài đặt ",
"Installed ": "",
"Installed!": "Đã cài xong!",
"Trying to load ": "Đang cố gắn mở ",
"Checking MD5": "Kiểm tra MD5",
"Delete": "Xoá",
"Delete Selected files?": "Xoá những tập tin được chọn?",
"Completely remove ": "Đã gỡ thành công ",
"Are you sure you want to delete ": "Bạn có muốn xoá ",
"Are you sure you wish to cancel?": "Bạn có chắn muốn huỷ không?",
"Audio disabled due to suspended game": "",
"If this message appears repeatedly, please open an issue.": "Nếu thấy tin nhắn này, hãy báo lỗi."
"Scanning ": "Đang quét ",
"Deleting ": "Đang xoá ",
"Deleting": "Đang xoá",
"Remove": "Gỡ",
"Completely remove ": "Đã gỡ thành công ",
"Removing ": "Đang gỡ ",
"Removed ": "Đã gỡ ",
"Uninstalling ": "Đang gỡ cài đặt ",
"Download": "Tải về",
"Downloading ": "Đang tải xuống ",
"Downloaded ": "Đã tải xong ",
"Update": "Cập nhật",
"Update avaliable: ": "Cập nhậc có sẵn: ",
"Download update: ": "Tải cập nhật: ",
"Updated to ": "Đã cập nhật ",
"Failed to download update": "Cập nhật thất bại",
"%zu hours %zu minutes remaining": "",
"%zu minutes %zu seconds remaining": "",
"%zu seconds remaining": "",
"Loading...": "Đang tải...",
"Loading": "Đang tải",
"Empty!": "Trống!",
"Not Ready...": "Chưa sẵn sàng...",
"Error loading page!": "Lỗi tải trang!"
}

View File

@@ -1,75 +1,24 @@
{
"[Applet Mode]": "[小程序模式]",
"No Internet": "网络未连接",
"Files": "文件",
"Apps": "应用",
"Store": "商店",
"Menu": "菜单",
"Options": "选项",
"OK": "确定",
"Back": "返回",
"Select": "选择",
"Open": "打开",
"Launch": "启动",
"Info": "信息",
"Install": "安装",
"Delete": "删除",
"Restart": "重启",
"Changelog": "更新日志",
"Details": "详情",
"Update": "更新",
"Remove": "删除",
"Restore": "恢复",
"Download": "下载",
"Next": "下一项",
"Prev": "上一项",
"Next Page": "下一页",
"Prev Page": "上一页",
"Unstar": "取消星标",
"Star": "星标",
"System memory": "主机内存",
"microSD card": "SD卡",
"Sd": "SD卡",
"Image System memory": "主机内存图像",
"Image microSD card": "SD卡图像",
"Slow": "慢",
"Normal": "正常",
"Fast": "快",
"Yes": "是",
"No": "否",
"Enabled": "启用",
"Disabled": "禁用",
"Sort By": "排序方式",
"Sort Options": "排序选项",
"Filter": "筛选",
"Sort": "排序",
"Order": "顺序",
"Search": "搜索",
"Updated": "最近使用",
"Updated (Star)": "最近更新(星标优先)",
"Downloads": "下载",
"Size": "按大小",
"Size (Star)": "按大小(星标优先)",
"Alphabetical": "按字母顺序",
"Alphabetical (Star)": "按字母顺序(星标优先)",
"Likes": "点赞量",
"ID": "ID",
"Descending": "降序",
"Descending (down)": "降序",
"Desc": "降序",
"Ascending": "升序",
"Ascending (Up)": "升序",
"Asc": "升序",
"Switch-Handheld!": "切换至掌机模式!",
"Switch-Docked!": "切换至底座模式!",
"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.": "若此消息反复出现,请提交问题报告。",
"Menu Options": "菜单选项",
"Menu": "菜单",
"Theme": "主题",
"Theme Options": "主题选项",
"Select Theme": "选择主题",
"Shuffle": "随机播放",
"Music": "音乐",
"12 Hour Time": "12小时制时间",
"Download Default Music": "下载默认音乐",
"Failed to download default_music.bfstm, please try again": "",
"Overwrite current default music?": "",
"Network": "网络",
"Network Options": "网络选项",
"Ftp": "FTP",
@@ -78,8 +27,7 @@
"Nxlink Connected": "Nxlink 已连接",
"Nxlink Upload": "Nxlink 上传中",
"Nxlink Finished": "Nxlink 已结束",
"Switch-Handheld!": "切换至掌机模式!",
"Switch-Docked!": "切换至底座模式!",
"Language": "语言",
"Auto": "自动",
"English": "English",
@@ -95,95 +43,101 @@
"Russian": "Русский",
"Swedish": "Svenska",
"Vietnamese": "Vietnamese",
"Logging": "日志",
"Replace hbmenu on exit": "退出后用Sphaira替换hbmenu",
"Ukrainian": "Українська",
"Misc": "拓展",
"Misc Options": "拓展设置",
"Web": "网页浏览器",
"Install forwarders": "允许安装前端应用",
"Install location": "安装位置",
"Show install warning": "显示安装警告",
"Text scroll speed": "文本滚动速度",
"FileBrowser": "文件浏览",
"%zd files": "%zd 个文件",
"%zd dirs": "%zd 个文件夹",
"File Options": "文件选项",
"Show Hidden": "显示隐藏项目",
"Folders First": "文件夹靠前",
"Hidden Last": "隐藏项目置后",
"Cut": "剪切",
"Copy": "复制",
"Paste": "粘贴",
"Paste ": "粘贴 ",
" file(s)?": "个文件(夹)",
"Rename": "重命名",
"Compress to zip": "压缩到zip",
"Set New File Name": "输入新命名",
"Advanced": "高级",
"Advanced Options": "高级选项",
"Create File": "新建文件",
"Set File Name": "输入文件名",
"Create Folder": "新建文件夹",
"Set Folder Name": "输入文件夹名",
"View as text (unfinished)": "以文本形式查看(未完善)",
"Ignore read only": "忽略只读",
"Mount": "挂载",
"Empty...": "空...",
"Open with DayBreak?": "使用DayBreak打开",
"Launch ": "启动 ",
"Launch option for: ": "启动选项:",
"Select launcher for: ": "选择启动器用于:",
"Homebrew": "应用列表",
"Homebrew Options": "应用选项",
"Hide Sphaira": "在应用列表中隐藏Sphaira",
"Install Forwarder": "安装前端应用",
"WARNING: Installing forwarders will lead to a ban!": "警告安装前端应用可能导致ban机",
"Installing Forwarder": "正在生成前端应用",
"Creating Program": "正在创建程序",
"Creating Control": "正在创建控制器",
"Creating Meta": "正在创建元数据",
"Writing Nca": "正在写入Nca",
"Updating ncm databse": "正在更新ncm数据库",
"Pushing application record": "正在推送应用记录",
"Installed!": "安装完成!",
"Failed to install forwarder": "前端应用安装失败",
"Unstarred ": "取消星标 ",
"Starred ": "已星标 ",
"AppStore": "应用商店",
"Filter: %s | Sort: %s | Order: %s": "筛选: %s | 排序: %s | 顺序: %s",
"AppStore Options": "应用商店选项",
"All": "全部",
"Games": "游戏",
"Emulators": "模拟器",
"Tools": "工具",
"Themes": "主题",
"Legacy": "可更新",
"version: %s": "版本: %s",
"updated: %s": "更新时间: %s",
"category: %s": "分类: %s",
"extracted: %.2f MiB": "应用大小: %.2f MiB",
"app_dls: %s": "下载量: %s",
"More by Author": "作者更多作品",
"Leave Feedback": "留言反馈",
"Game Options": "游戏选项",
"Hide forwarders": "隐藏前端启动",
"Launch random game": "开启随机游戏",
"List meta records": "列出元数据记录",
"Entries": "条目",
"Delete entity": "删除整体",
"Hide forwarders": "隐藏前端启动",
"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!": "",
"Themezer": "在线主题",
"Themezer Options": "在线主题选项",
"Nsfw": "公共场合不宜的主题",
"Page": "页面",
"Page %zu / %zu": "页面 %zu / %zu",
"Enter Page Number": "输入跳转的页码",
"Bad Page": "错误的页面",
"Download theme?": "下载该主题?",
"GitHub": "GitHub",
"Downloading json": "正在下载 json",
"Select asset to download for ": "选择要下载的资源用于 ",
"FTP Install": "通过 FTP 安装",
"FTP Install (EXPERIMENTAL)": "通过 FTP 安装(实验性)",
"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 安装失败,按 B 键退出...",
"Ftp install success!": "通过 FTP 安装成功。",
"Ftp install failed!": "通过 FTP 安装失败。",
"USB Install": "通过 USB 安装",
"USB": "USB",
"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...": "",
"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": "",
"Gc install success!": "游戏安装成功。",
"Gc install failed!": "游戏安装失败。",
"IRS (Infrared Joycon Camera)": "",
"IRS": "",
"Irs": "红外成像",
"Ambient Noise Level: ": "环境噪声等级:",
"Controller": "控制器",
"Pad ": "手柄 ",
"HandHeld": "掌机模式",
" (Available)": " (可用的)",
" (Unsupported)": " (不支持的)",
" (Unconnected)": " (未连接)",
"HandHeld": "掌机模式",
"Rotation": "旋转",
"0 (Sideways)": "0度",
"90 (Flat)": "90度",
@@ -205,116 +159,234 @@
"Normal image": "正常图像",
"Negative image": "负片图像",
"Format": "格式",
"Trimming Format": "裁剪格式",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "裁剪格式",
"External Light Filter": "外部光滤镜",
"Load Default": "加载默认值",
"Themezer": "在线主题",
"Themezer Options": "在线主题选项",
"Nsfw": "公共场合不宜的主题",
"Page": "页面",
"Page %zu / %zu": "页面 %zu / %zu",
"Enter Page Number": "输入跳转的页码",
"Bad Page": "错误的页面",
"Download theme?": "下载该主题?",
"GitHub": "GitHub",
"Downloading json": "正在下载 json",
"Select asset to download for ": "选择要下载的资源用于 ",
"Install Options": "安装选项",
"Advanced": "高级",
"Advanced Options": "高级选项",
"Logging": "日志",
"Replace hbmenu on exit": "退出后用Sphaira替换hbmenu",
"Restore hbmenu?": "恢复 hbmenu",
"Restore": "恢复",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "未能找到 /switch/hbmenu.nro\n请使用应用商店重新安装 hbmenu",
"Failed to restore hbmenu, please re-download hbmenu": "恢复 hbmenu 失败,请重新下载 hbmenu",
"Failed to restore hbmenu, using sphaira instead": "恢复 hbmenu 失败,改用 Sphaira",
"Restored hbmenu, closing sphaira": "已恢复 hbmenu正在关闭 Sphaira",
"Restored hbmenu": "已恢复 hbmenu",
"Restart Sphaira?": "重启 Sphaira",
"Press OK to restart Sphaira": "按OK键以重启shphaira菜单",
"Text scroll speed": "文本滚动速度",
"Slow": "慢",
"Normal": "正常",
"Fast": "快",
"Set right-side menu": "",
"Install options": "安装选项",
"Install Options": "安装选项",
"Enable sysmmc": "",
"Enable emummc": "",
"Show install warning": "显示安装警告",
"Install location": "安装位置",
"System memory": "主机内存",
"microSD card": "SD卡",
"Boost CPU clock": "提升 CPU 频率",
"Allow downgrade": "允许降级",
"Skip if already installed": "若已安装则跳过",
"Ticket only": "仅安装票据",
"Patch ticket": "修补票据",
"Skip base": "跳过基础部分",
"Skip patch": "跳过补丁",
"Skip dlc": "跳过 DLC可下载内容",
"Skip data patch": "跳过数据补丁",
"Skip ticket": "跳过票据",
"skip NCA hash verify": "跳过 NCA 哈希验证",
"Skip NCA hash verify": "",
"Skip RSA header verify": "跳过 RSA 头部验证",
"Skip RSA NPDM verify": "跳过 RSA NPDM 验证",
"Ignore distribution bit": "忽略分布位",
"Convert to standard crypto": "转换为标准加密方式",
"Lower master key": "降低主密钥",
"Lower system version": "降低系统版本",
"Install Selected files?": "安装所选文件?",
"Installed": "已安装",
"FTP Install": "通过 FTP 安装",
"USB Install": "通过 USB 安装",
"GameCard Install": "卡带安装",
"FTP Install (EXPERIMENTAL)": "通过 FTP 安装(实验性)",
"USB": "USB",
"GameCard": "卡带",
"Disable MTP for usb install": "暂时禁用 USB 安装的 MTP 功能",
"Re-enabled MTP": "重新启用 MTP",
"Waiting for connection...": "等待连接中...",
"Transferring data...": "正在传输数据...",
"Ftp install success!": "通过 FTP 安装成功。",
"Ftp install failed!": "通过 FTP 安装失败。",
"Usb install success!": "通过 USB 安装成功。",
"Usb install failed!": "通过 USB 安装失败。",
"Gc install success!": "游戏安装成功。",
"Gc install failed!": "游戏安装失败。",
"Installed via usb": "通过 USB 安装",
"Failed to install via FTP, press B to exit...": "通过 FTP 安装失败,按 B 键退出...",
"Failed to init usb, press B to exit...": "USB 初始化失败,按 B 键退出...",
"Press B to exit...": "按 B 键退出...",
"Connection Type: WiFi | Strength:": "连接类型WiFi | 信号强度:",
"Connection Type: Ethernet": "连接类型:以太网",
"Connection Type: None": "连接类型:无",
"Host:": "主机:",
"Port:": "端口:",
"Username:": "用户名:",
"Password:": "密码:",
"SSID:": "网络名称:",
"Passphrase:": "密码:",
"Homebrew": "应用列表",
"Apps": "应用",
"Homebrew Options": "应用选项",
"Hide Sphaira": "在应用列表中隐藏Sphaira",
"Install Forwarder": "安装前端应用",
"WARNING: Installing forwarders will lead to a ban!": "警告安装前端应用可能导致ban机",
"Installing Forwarder": "正在生成前端应用",
"Creating Program": "正在创建程序",
"Creating Control": "正在创建控制器",
"Creating Meta": "正在创建元数据",
"Writing Nca": "正在写入Nca",
"Updating ncm databse": "正在更新ncm数据库",
"Pushing application record": "正在推送应用记录",
"Failed to install forwarder": "前端应用安装失败",
"Unstarred ": "取消星标 ",
"Starred ": "已星标 ",
"Failed to remove old forwarder, please manually remove it!": "",
"Installing ": "正在安装 ",
"Uninstalling ": "正在卸载 ",
"Deleting ": "正在删除 ",
"Deleting": "正在删除",
"AppStore": "应用商店",
"Appstore": "",
"Store": "商店",
"Filter: %s | Sort: %s | Order: %s": "筛选: %s | 排序: %s | 顺序: %s",
"AppStore Options": "应用商店选项",
"Info": "信息",
"Changelog": "更新日志",
"Details": "详情",
"version: %s": "版本: %s",
"updated: %s": "更新时间: %s",
"category: %s": "分类: %s",
"extracted: %.2f MiB": "应用大小: %.2f MiB",
"app_dls: %s": "下载量: %s",
"More by Author": "作者更多作品",
"Leave Feedback": "留言反馈",
"FileBrowser": "文件浏览",
"Files": "文件",
"%zd files": "%zd 个文件",
"%zd dirs": "%zd 个文件夹",
"File Options": "文件选项",
"Show Hidden": "显示隐藏项目",
"Folders First": "文件夹靠前",
"Hidden Last": "隐藏项目置后",
"Cut": "剪切",
"Copy": "复制",
"Copying ": "正在复制 ",
"Paste": "粘贴",
"Paste ": "粘贴 ",
" file(s)?": "个文件(夹)",
"Pasting ": "正在粘贴 ",
"Pasting": "正在粘贴",
"Removing ": "正在移除 ",
"Scanning ": "正在扫描 ",
"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!": "",
"Compress to zip": "压缩到zip",
"Compress Options": "",
"Compress": "",
"Compress to...": "",
"Compressing ": "",
"Compress success!": "",
"Compress failed!": "",
"Create File": "新建文件",
"Set File Name": "输入文件名",
"Create Folder": "新建文件夹",
"Set Folder Name": "输入文件夹名",
"Creating ": "正在创建 ",
"Copying ": "正在复制 ",
"Upload": "",
"Select upload location": "",
"No upload locations set!": "",
"Uploading": "",
"Upload successfull!": "",
"Upload failed!": "",
"View as text (unfinished)": "以文本形式查看(未完善)",
"Ignore read only": "忽略只读",
"Mount": "挂载",
"Sd": "SD卡",
"Image System memory": "主机内存图像",
"Image microSD card": "SD卡图像",
"Empty...": "空...",
"Open with DayBreak?": "使用DayBreak打开",
"Launch ": "启动 ",
"Launch option for: ": "启动选项:",
"Select launcher for: ": "选择启动器用于:",
"Sort By": "排序方式",
"Sort Options": "排序选项",
"Filter": "筛选",
"All": "全部",
"Emulators": "模拟器",
"Tools": "工具",
"Themes": "主题",
"Legacy": "可更新",
"Sort": "排序",
"Size": "按大小",
"Size (Star)": "按大小(星标优先)",
"Alphabetical": "按字母顺序",
"Alphabetical (Star)": "按字母顺序(星标优先)",
"Updated": "最近使用",
"Updated (Star)": "最近更新(星标优先)",
"Downloads": "下载",
"Likes": "点赞量",
"ID": "ID",
"Order": "顺序",
"Descending": "降序",
"Descending (down)": "降序",
"Desc": "降序",
"Ascending": "升序",
"Ascending (Up)": "升序",
"Asc": "升序",
"Layout": "",
"List": "",
"Icon": "",
"Grid": "",
"Search": "搜索",
"Options": "选项",
"OK": "确定",
"Back": "返回",
"Select": "选择",
"Open": "打开",
"Launch": "启动",
"Restart": "重启",
"Next": "下一项",
"Prev": "上一项",
"Unstar": "取消星标",
"Star": "星标",
"Yes": "是",
"No": "否",
"On": "",
"Off": "",
"Install": "安装",
"Install Selected files?": "安装所选文件?",
"Installing ": "正在安装 ",
"Installed ": "",
"Installed!": "安装完成!",
"Trying to load ": "尝试加载 ",
"Checking MD5": "正在校验 MD5",
"Delete": "删除",
"Delete Selected files?": "删除选中的文件?",
"Are you sure you want to delete ": "您确定要删除吗 ",
"Scanning ": "正在扫描 ",
"Deleting ": "正在删除 ",
"Deleting": "正在删除",
"Remove": "删除",
"Completely remove ": "彻底删除 ",
"Removing ": "正在移除 ",
"Removed ": "已移除 ",
"Uninstalling ": "正在卸载 ",
"Download": "下载",
"Downloading ": "正在下载 ",
"Downloaded ": "已下载 ",
"Removed ": "已移除 ",
"Checking MD5": "正在校验 MD5",
"Update": "更新",
"Update avaliable: ": "有可用更新!",
"Download update: ": "下载更新:",
"Updated to ": "更新至 ",
"Failed to download update": "更新下载失败",
"%zu hours %zu minutes remaining": "",
"%zu minutes %zu seconds remaining": "",
"%zu seconds remaining": "",
"Loading...": "加载中...",
"Loading": "加载中",
"Empty!": "空空如野!",
"Not Ready...": "尚未准备好...",
"Error loading page!": "页面加载失败!",
"Update avaliable: ": "有可用更新!",
"Download update: ": "下载更新:",
"Updated to ": "更新至 ",
"Press OK to restart Sphaira": "按OK键以重启shphaira菜单",
"Restart Sphaira?": "重启 Sphaira",
"Failed to download update": "更新下载失败",
"Restore hbmenu?": "恢复 hbmenu",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "未能找到 /switch/hbmenu.nro\n请使用应用商店重新安装 hbmenu",
"Failed to restore hbmenu, please re-download hbmenu": "恢复 hbmenu 失败,请重新下载 hbmenu",
"Failed to restore hbmenu, using sphaira instead": "恢复 hbmenu 失败,改用 Sphaira",
"Restored hbmenu, closing sphaira": "已恢复 hbmenu正在关闭 Sphaira",
"Restored hbmenu": "已恢复 hbmenu",
"Delete Selected files?": "删除选中的文件?",
"Completely remove ": "彻底删除 ",
"Are you sure you want to delete ": "您确定要删除吗 ",
"Are you sure you wish to cancel?": "您确定要取消吗?",
"Audio disabled due to suspended game": "由于游戏暂停,音频已禁用",
"If this message appears repeatedly, please open an issue.": "若此消息反复出现,请提交问题报告。"
}
"Error loading page!": "页面加载失败!"
}

View File

@@ -78,11 +78,13 @@ function(nx_create_npdm target config)
dkp_set_target_file(${outtarget} "${NX_NPDM_OUTPUT}")
endfunction()
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/exefs)
nx_create_nso(hbl
OUTPUT main
OUTPUT exefs/main
)
nx_create_npdm(hbl
OUTPUT main.npdm
OUTPUT exefs/main.npdm
CONFIG ${CMAKE_CURRENT_SOURCE_DIR}/hbl.json
)

View File

@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.13)
set(sphaira_VERSION 0.10.0)
set(sphaira_VERSION 0.11.0)
project(sphaira
VERSION ${sphaira_VERSION}
@@ -66,6 +66,7 @@ add_executable(sphaira
source/app.cpp
source/download.cpp
source/dumper.cpp
source/option.cpp
source/evman.cpp
source/fs.cpp
@@ -77,8 +78,10 @@ add_executable(sphaira
source/nxlink.cpp
source/owo.cpp
source/swkbd.cpp
source/hasher.cpp
source/i18n.cpp
source/ftpsrv_helper.cpp
source/threaded_file_transfer.cpp
source/usb/base.cpp
source/usb/usbds.cpp
@@ -89,7 +92,6 @@ add_executable(sphaira
source/yati/container/nsp.cpp
source/yati/container/xci.cpp
source/yati/source/file.cpp
source/yati/source/stdio.cpp
source/yati/source/usb.cpp
source/yati/source/stream.cpp
source/yati/source/stream_file.cpp
@@ -145,8 +147,6 @@ target_compile_options(sphaira PRIVATE
-Wimplicit-fallthrough=5
-Wsuggest-final-types
-Wuninitialized
-fimplicit-constexpr
-Wmissing-requires
)
include(FetchContent)
@@ -195,6 +195,11 @@ FetchContent_Declare(zstd
SOURCE_SUBDIR build/cmake
)
FetchContent_Declare(libusbhsfs
GIT_REPOSITORY https://github.com/ITotalJustice/libusbhsfs.git
GIT_TAG db2bf2a
)
set(USE_NEW_ZSTD ON)
set(ZSTD_BUILD_STATIC ON)
@@ -209,7 +214,7 @@ set(ZSTD_BUILD_TESTS OFF)
set(MININI_LIB_NAME minIni)
set(MININI_USE_STDIO ON)
set(MININI_USE_NX ON)
set(MININI_USE_NX OFF)
set(MININI_USE_FLOAT OFF)
if (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
@@ -233,6 +238,10 @@ set(YYJSON_DISABLE_NON_STANDARD ON)
set(YYJSON_DISABLE_UTF8_VALIDATION ON)
set(YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS OFF)
# enable this if you want ntfs and ext4 support, at the cost of a huge final binary size.
set(USBHSFS_GPL OFF)
set(USBHSFS_SXOS_DISABLE ON)
FetchContent_MakeAvailable(
ftpsrv
libhaze
@@ -242,6 +251,7 @@ FetchContent_MakeAvailable(
minIni
yyjson
zstd
libusbhsfs
)
set(FTPSRV_LIB_BUILD TRUE)
@@ -302,9 +312,9 @@ if (NOT USE_NEW_ZSTD)
endif()
set_target_properties(sphaira PROPERTIES
C_STANDARD 11
C_STANDARD 23
C_EXTENSIONS ON
CXX_STANDARD 23
CXX_STANDARD 26
CXX_EXTENSIONS ON
)
@@ -316,6 +326,7 @@ target_link_libraries(sphaira PRIVATE
nanovg
stb
yyjson
libusbhsfs
${minizip_lib}
ZLIB::ZLIB
@@ -344,12 +355,13 @@ file(COPY ${CMAKE_SOURCE_DIR}/assets/romfs DESTINATION ${CMAKE_CURRENT_BINARY_DI
# create assets target
dkp_add_asset_target(sphaira_romfs ${CMAKE_CURRENT_BINARY_DIR}/romfs)
# add hbl exefs to romfs, used for forwarders
dkp_install_assets(sphaira_romfs
DESTINATION exefs
TARGETS
hbl_nso
hbl_npdm
# wait until hbl is built first as we need the exefs to embed
add_dependencies(sphaira hbl_nso hbl_npdm)
# set the embed path for assets and hbl
target_compile_options(sphaira PRIVATE
--embed-dir=${CMAKE_SOURCE_DIR}/assets/embed
--embed-dir=${CMAKE_BINARY_DIR}/hbl
)
# add nanovg shaders to romfs

View File

@@ -61,11 +61,15 @@ public:
static void NotifyClear(ui::NotifEntry::Side side = ui::NotifEntry::Side::RIGHT);
static void NotifyFlashLed();
// if R_FAILED(rc), pushes error box. returns rc passed in.
static Result PushErrorBox(Result rc, const std::string& message);
static auto GetThemeMetaList() -> std::span<ThemeMeta>;
static void SetTheme(s64 theme_index);
static auto GetThemeIndex() -> s64;
static auto GetDefaultImage() -> int;
static auto GetDefaultImageData() -> std::span<const u8>;
// returns argv[0]
static auto GetExePath() -> fs::FsPath;
@@ -75,6 +79,8 @@ public:
static auto GetMtpEnable() -> bool;
static auto GetFtpEnable() -> bool;
static auto GetNxlinkEnable() -> bool;
static auto GetHddEnable() -> bool;
static auto GetWriteProtect() -> bool;
static auto GetLogEnable() -> bool;
static auto GetReplaceHbmenuEnable() -> bool;
static auto GetInstallEnable() -> bool;
@@ -90,6 +96,8 @@ public:
static void SetMtpEnable(bool enable);
static void SetFtpEnable(bool enable);
static void SetNxlinkEnable(bool enable);
static void SetHddEnable(bool enable);
static void SetWriteProtect(bool enable);
static void SetLogEnable(bool enable);
static void SetReplaceHbmenuEnable(bool enable);
static void SetInstallSysmmcEnable(bool enable);
@@ -112,6 +120,7 @@ public:
static void DisplayMiscOptions(bool left_side = true);
static void DisplayAdvancedOptions(bool left_side = true);
static void DisplayInstallOptions(bool left_side = true);
static void DisplayDumpOptions(bool left_side = true);
void Draw();
void Update();
@@ -127,6 +136,10 @@ public:
void ScanThemes(const std::string& path);
void ScanThemeEntries();
// helper that converts 1.2.3 to a u32 used for comparisons.
static auto GetVersionFromString(const char* str) -> u32;
static auto IsVersionNewer(const char* current, const char* new_version) -> u32;
static auto IsApplication() -> bool {
const auto type = appletGetAppletType();
return type == AppletType_Application || type == AppletType_SystemApplication;
@@ -161,16 +174,35 @@ public:
return (paths.unk[0] != '\0') || (paths.nintendo[0] != '\0');
}
static void SetAutoSleepDisabled(bool enable) {
static Mutex mutex{};
static int ref_count{};
mutexLock(&mutex);
ON_SCOPE_EXIT(mutexUnlock(&mutex));
if (enable) {
appletSetAutoSleepDisabled(true);
ref_count++;
} else {
if (ref_count) {
ref_count--;
}
if (!ref_count) {
appletSetAutoSleepDisabled(false);
}
}
}
// 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";
fs::FsPath m_app_path;
u64 m_start_timestamp{};
u64 m_prev_timestamp{};
fs::FsPath m_prev_last_launch{};
int m_default_image{};
bool m_is_launched_via_sphaira_forwader{};
@@ -195,22 +227,28 @@ public:
bool m_quit{};
// network
option::OptionBool m_nxlink_enabled{INI_SECTION, "nxlink_enabled", true};
option::OptionBool m_mtp_enabled{INI_SECTION, "mtp_enabled", false};
option::OptionBool m_ftp_enabled{INI_SECTION, "ftp_enabled", false};
option::OptionBool m_hdd_enabled{INI_SECTION, "hdd_enabled", true};
option::OptionBool m_hdd_write_protect{INI_SECTION, "hdd_write_protect", false};
option::OptionBool m_log_enabled{INI_SECTION, "log_enabled", false};
option::OptionBool m_replace_hbmenu{INI_SECTION, "replace_hbmenu", false};
option::OptionString m_theme_path{INI_SECTION, "theme", DEFAULT_THEME_PATH};
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
option::OptionBool m_12hour_time{INI_SECTION, "12hour_time", false};
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto
option::OptionString m_right_side_menu{INI_SECTION, "right_side_menu", "Appstore"};
option::OptionString m_left_menu{INI_SECTION, "left_side_menu", "FileBrowser"};
option::OptionString m_right_menu{INI_SECTION, "right_side_menu", "Appstore"};
option::OptionBool m_progress_boost_mode{INI_SECTION, "progress_boost_mode", true};
// install options
option::OptionBool m_install_sysmmc{INI_SECTION, "install_sysmmc", false};
option::OptionBool m_install_emummc{INI_SECTION, "install_emummc", false};
option::OptionBool m_install_sd{INI_SECTION, "install_sd", true};
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
option::OptionLong m_boost_mode{INI_SECTION, "boost_mode", false};
option::OptionBool m_allow_downgrade{INI_SECTION, "allow_downgrade", false};
option::OptionBool m_skip_if_already_installed{INI_SECTION, "skip_if_already_installed", true};
option::OptionBool m_ticket_only{INI_SECTION, "ticket_only", false};
@@ -227,6 +265,13 @@ public:
option::OptionBool m_lower_master_key{INI_SECTION, "lower_master_key", false};
option::OptionBool m_lower_system_version{INI_SECTION, "lower_system_version", false};
// dump options
option::OptionBool m_dump_app_folder{"dump", "app_folder", true};
option::OptionBool m_dump_append_folder_with_xci{"dump", "append_folder_with_xci", true};
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};
// todo: move this into it's own menu
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal

View File

@@ -0,0 +1,46 @@
#pragma once
#include "fs.hpp"
#include <switch.h>
#include <vector>
#include <memory>
#include <functional>
namespace sphaira::dump {
enum DumpLocationType {
// dump using native fs.
DumpLocationType_SdCard,
// dump to usb using tinfoil protocol.
DumpLocationType_UsbS2S,
// speed test, only reads the data, doesn't write anything.
DumpLocationType_DevNull,
// dump to stdio, ideal for custom mount points using devoptab, such as hdd.
DumpLocationType_Stdio,
// dump to custom locations found in locations.ini.
DumpLocationType_Network,
};
enum DumpLocationFlag {
DumpLocationFlag_SdCard = 1 << DumpLocationType_SdCard,
DumpLocationFlag_UsbS2S = 1 << DumpLocationType_UsbS2S,
DumpLocationFlag_DevNull = 1 << DumpLocationType_DevNull,
DumpLocationFlag_Stdio = 1 << DumpLocationType_Stdio,
DumpLocationFlag_Network = 1 << DumpLocationType_Network,
DumpLocationFlag_All = DumpLocationFlag_SdCard | DumpLocationFlag_UsbS2S | DumpLocationFlag_DevNull | DumpLocationFlag_Stdio | DumpLocationFlag_Network,
};
struct BaseSource {
virtual ~BaseSource() = default;
virtual Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) = 0;
virtual auto GetName(const std::string& path) const -> std::string = 0;
virtual auto GetSize(const std::string& path) const -> s64 = 0;
virtual auto GetIcon(const std::string& path) const -> int { return 0; }
};
// called after dump has finished.
using OnExit = std::function<void(Result rc)>;
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

View File

@@ -1,6 +1,7 @@
#pragma once
#include <switch.h>
#include <dirent.h>
#include <cstring>
#include <vector>
#include <string>
@@ -169,6 +170,40 @@ static_assert(FsPath::TestFrom(std::string_view{"abc"}));
static_assert(FsPath::TestFrom(std::string{"abc"}));
static_assert(FsPath::TestFrom(FsPath{"abc"}));
// fwd
struct Fs;
struct File {
~File();
Result Read(s64 off, void* buf, u64 read_size, u32 option, u64* bytes_read);
Result Write(s64 off, const void* buf, u64 write_size, u32 option);
Result SetSize(s64 sz);
Result GetSize(s64* out);
void Close();
fs::Fs* m_fs{};
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{};
};
struct Dir {
~Dir();
Result GetEntryCount(s64* out);
Result ReadAll(std::vector<FsDirectoryEntry>& buf);
void Close();
fs::Fs* m_fs{};
FsDir m_native{};
DIR* m_stdio{};
u32 m_mode{};
};
FsPath AppendPath(const fs::FsPath& root_path, const fs::FsPath& file_path);
Result CreateFile(FsFileSystem* fs, const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = true);
@@ -182,6 +217,7 @@ Result RenameFile(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool i
Result RenameDirectory(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only = true);
Result GetEntryType(FsFileSystem* fs, const FsPath& path, FsDirEntryType* out);
Result GetFileTimeStampRaw(FsFileSystem* fs, const FsPath& path, FsTimeStampRaw *out);
Result SetTimestamp(FsFileSystem* fs, const FsPath& path, const FsTimeStampRaw* ts);
bool FileExists(FsFileSystem* fs, const FsPath& path);
bool DirExists(FsFileSystem* fs, const FsPath& path);
Result read_entire_file(FsFileSystem* fs, const FsPath& path, std::vector<u8>& out);
@@ -199,12 +235,31 @@ Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only =
Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = true);
Result GetEntryType(const FsPath& path, FsDirEntryType* out);
Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out);
Result SetTimestamp(const FsPath& path, const FsTimeStampRaw* ts);
bool FileExists(const FsPath& path);
bool DirExists(const FsPath& path);
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 = true);
Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only = true);
Result OpenFile(fs::Fs* fs, const fs::FsPath& path, u32 mode, File* f);
Result OpenDirectory(fs::Fs* fs, const fs::FsPath& path, u32 mode, Dir* d);
// opens dir, fetches count for all entries.
// NOTE: this function will be slow on non-native fs, due to multiple
// readdir() functions being needed!
Result DirGetEntryCount(fs::Fs* fs, const fs::FsPath& path, s64* count, u32 mode);
// same as the above, but fetches file and folder count in a single pass
// this is faster when using native, and *much* faster for stdio.
Result DirGetEntryCount(fs::Fs* fs, const fs::FsPath& path, s64* file_count, s64* dir_count, u32 mode = FsDirOpenMode_ReadDirs|FsDirOpenMode_ReadFiles);
// optimised for stdio calls as stat returns size and timestamp in a single call.
// whereas for native, this is 2 function calls.
// however if you need both, you will need 2 calls for native anyway,
// but can avoid the second (expensive) stat call.
Result FileGetSizeAndTimestamp(fs::Fs* fs, const FsPath& path, FsTimeStampRaw* ts, s64* size);
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);
@@ -236,12 +291,34 @@ struct Fs {
virtual Result RenameDirectory(const FsPath& src, const FsPath& dst) = 0;
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 bool FileExists(const FsPath& path) = 0;
virtual bool DirExists(const FsPath& path) = 0;
virtual bool IsNative() const = 0;
virtual FsPath Root() const { return "/"; }
virtual Result read_entire_file(const FsPath& path, std::vector<u8>& out) = 0;
virtual Result write_entire_file(const FsPath& path, const std::vector<u8>& in) = 0;
virtual Result copy_entire_file(const FsPath& dst, const FsPath& src) = 0;
Result OpenFile(const fs::FsPath& path, u32 mode, File* f) {
return fs::OpenFile(this, path, mode, f);
}
Result OpenDirectory(const fs::FsPath& path, u32 mode, Dir* d) {
return fs::OpenDirectory(this, path, mode, d);
}
Result DirGetEntryCount(const fs::FsPath& path, s64* count, u32 mode) {
return fs::DirGetEntryCount(this, path, count, mode);
}
Result DirGetEntryCount(const fs::FsPath& path, s64* file_count, s64* dir_count, u32 mode = FsDirOpenMode_ReadDirs|FsDirOpenMode_ReadFiles) {
return fs::DirGetEntryCount(this, path, file_count, dir_count, mode);
}
Result FileGetSizeAndTimestamp(const FsPath& path, FsTimeStampRaw* ts, s64* size) {
return fs::FileGetSizeAndTimestamp(this, path, ts, size);
}
Result IsDirEmpty(const fs::FsPath& path, bool* out) {
return fs::IsDirEmpty(this, path, out);
}
void SetIgnoreReadOnly(bool enable) {
m_ignore_read_only = enable;
}
@@ -251,7 +328,7 @@ protected:
};
struct FsStdio : Fs {
FsStdio(bool ignore_read_only = true) : Fs{ignore_read_only} {}
FsStdio(bool ignore_read_only = true, const FsPath& root = "/") : Fs{ignore_read_only}, m_root{root} {}
virtual ~FsStdio() = default;
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0) override {
@@ -287,12 +364,21 @@ struct FsStdio : Fs {
Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out) override {
return fs::GetFileTimeStampRaw(path, out);
}
Result SetTimestamp(const FsPath& path, const FsTimeStampRaw *ts) override {
return fs::SetTimestamp(path, ts);
}
bool FileExists(const FsPath& path) override {
return fs::FileExists(path);
}
bool DirExists(const FsPath& path) override {
return fs::DirExists(path);
}
bool IsNative() const override {
return false;
}
FsPath Root() const override {
return m_root;
}
Result read_entire_file(const FsPath& path, std::vector<u8>& out) override {
return fs::read_entire_file(path, out);
}
@@ -302,6 +388,8 @@ struct FsStdio : Fs {
Result copy_entire_file(const FsPath& dst, const FsPath& src) override {
return fs::copy_entire_file(dst, src, m_ignore_read_only);
}
const FsPath m_root;
};
struct FsNative : Fs {
@@ -326,39 +414,35 @@ struct FsNative : Fs {
return fsFsGetTotalSpace(&m_fs, path, out);
}
Result OpenFile(const FsPath& path, u32 mode, FsFile *out) {
return fsFsOpenFile(&m_fs, path, mode, out);
}
// Result OpenDirectory(const FsPath& path, u32 mode, FsDir *out) {
// return fsFsOpenDirectory(&m_fs, path, mode, out);
// }
Result OpenDirectory(const FsPath& path, u32 mode, FsDir *out) {
return fsFsOpenDirectory(&m_fs, path, mode, out);
}
// void DirClose(FsDir *d) {
// fsDirClose(d);
// }
void DirClose(FsDir *d) {
fsDirClose(d);
}
// Result DirGetEntryCount(FsDir *d, s64* out) {
// return fsDirGetEntryCount(d, out);
// }
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 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(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);
}
// 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);
@@ -401,12 +485,18 @@ struct FsNative : Fs {
Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out) override {
return fs::GetFileTimeStampRaw(&m_fs, path, out);
}
Result SetTimestamp(const FsPath& path, const FsTimeStampRaw *ts) override {
return fs::SetTimestamp(&m_fs, path, ts);
}
bool FileExists(const FsPath& path) override {
return fs::FileExists(&m_fs, path);
}
bool DirExists(const FsPath& path) override {
return fs::DirExists(&m_fs, path);
}
bool IsNative() const override {
return true;
}
Result read_entire_file(const FsPath& path, std::vector<u8>& out) override {
return fs::read_entire_file(&m_fs, path, out);
}

View File

@@ -0,0 +1,30 @@
#pragma once
#include "fs.hpp"
#include "ui/progress_box.hpp"
#include <string>
#include <memory>
#include <switch.h>
namespace sphaira::hash {
enum class Type {
Crc32,
Md5,
Sha1,
Sha256,
};
struct BaseSource {
virtual ~BaseSource() = default;
virtual Result Size(s64* out) = 0;
virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
};
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);
} // namespace sphaira::hash

View File

@@ -21,4 +21,21 @@ using Entries = std::vector<Entry>;
auto Load() -> Entries;
void Add(const Entry& e);
// helper for hdd devices.
// this doesn't really belong in this header, however
// locations likely will be renamed to something more generic soon.
struct StdioEntry {
// mount point (ums0:)
std::string mount{};
// ums0: (USB Flash Disk)
std::string name{};
// set if read-only.
bool write_protect;
};
using StdioEntries = std::vector<StdioEntry>;
// set write=true to filter out write protected devices.
auto GetStdio(bool write) -> StdioEntries;
} // namespace sphaira::location

View File

@@ -11,13 +11,17 @@ namespace sphaira {
struct Hbini {
u64 timestamp{}; // timestamp of last launch
u32 launch_count{}; //
};
struct MiniNacp {
NacpLanguageEntry lang;
char display_version[0x10];
};
struct NroEntry {
fs::FsPath path{};
s64 size{};
NacpStruct nacp{};
MiniNacp nacp{};
u64 icon_size{};
u64 icon_offset{};
@@ -31,11 +35,11 @@ struct NroEntry {
std::optional<bool> has_star{std::nullopt};
auto GetName() const -> const char* {
return nacp.lang[0].name;
return nacp.lang.name;
}
auto GetAuthor() const -> const char* {
return nacp.lang[0].author;
return nacp.lang.author;
}
auto GetDisplayVersion() const -> const char* {

View File

@@ -17,6 +17,11 @@ struct OptionBase {
auto GetOr(const char* name) -> T;
void Set(T value);
// returns true if loaded.
auto LoadFrom(const char* section, const char* name, const char* value) -> bool;
// same as above, but only checks the name.
auto LoadFrom(const char* name, const char* value) -> bool;
private:
auto GetInternal(const char* name) -> T;

View File

@@ -4,7 +4,6 @@
#include <string>
#include <vector>
#include "ui/progress_box.hpp"
// #include <optional>
namespace sphaira {
@@ -15,12 +14,9 @@ struct OwoConfig {
std::string author{};
NacpStruct nacp;
std::vector<u8> icon;
std::vector<u8> main;
std::vector<u8> npdm;
std::vector<u8> logo;
std::vector<u8> gif;
// std::optional<u64> tid;
std::vector<u8> program_nca{};
};

View File

@@ -0,0 +1,32 @@
#pragma once
#include "ui/progress_box.hpp"
#include <functional>
#include <switch.h>
namespace sphaira::thread {
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)>;
// used for pull api
using PullCallback = std::function<Result(void* data, s64 size, u64* bytes_read)>;
using StartThreadCallback = std::function<Result(void)>;
// called when threads are started.
// call pull() to receive data.
using StartCallback = std::function<Result(PullCallback pull)>;
// same as above, but the callee must call start() in order to start threads.
// this is for convenience as there may be race conditions otherwise, such as the read thread
// trying to read from the pull callback before it is set.
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);
// 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);
} // namespace sphaira::thread

View File

@@ -60,6 +60,14 @@ struct List final : Object {
return m_page;
}
auto SetPageJump(bool enable) {
m_page_jump = enable;
}
auto GetPageJump() const {
return m_page_jump;
}
private:
auto Draw(NVGcontext* vg, Theme* theme) -> void override {}
auto ClampX(float x, s64 count) const -> float;
@@ -85,6 +93,7 @@ private:
float m_y_prog{};
Layout m_layout{Layout::GRID};
bool m_page_jump{true};
};
} // namespace sphaira::ui

View File

@@ -139,7 +139,7 @@ enum OrderType {
using LayoutType = grid::LayoutType;
struct Menu final : grid::Menu {
Menu();
Menu(u32 flags);
~Menu();
auto GetShortTitle() const -> const char* override { return "Store"; };

View File

@@ -8,7 +8,6 @@ namespace sphaira::ui::menu::fileview {
struct Menu final : MenuBase {
Menu(const fs::FsPath& path);
~Menu();
auto GetShortTitle() const -> const char* override { return "File"; };
void Update(Controller* controller, TouchInfo* touch) override;
@@ -18,7 +17,7 @@ struct Menu final : MenuBase {
private:
const fs::FsPath m_path;
fs::FsNativeSd m_fs{};
FsFile m_file{};
fs::File m_file{};
s64 m_file_size{};
s64 m_file_offset{};

View File

@@ -1,19 +1,29 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "ui/scrolling_text.hpp"
#include "ui/list.hpp"
#include "nro.hpp"
#include "fs.hpp"
#include "option.hpp"
#include "hasher.hpp"
// #include <optional>
#include <span>
namespace sphaira::ui::menu::filebrowser {
enum FsEntryFlag {
FsEntryFlag_None,
// write protected.
FsEntryFlag_ReadOnly = 1 << 0,
// supports file assoc.
FsEntryFlag_Assoc = 1 << 1,
};
enum class FsType {
Sd,
ImageNand,
ImageSd,
Stdio,
};
enum class SelectedType {
@@ -23,6 +33,11 @@ enum class SelectedType {
Delete,
};
enum class ViewSide {
Left,
Right,
};
enum SortType {
SortType_Size,
SortType_Alphabetical,
@@ -33,6 +48,25 @@ enum OrderType {
OrderType_Ascending,
};
struct FsEntry {
fs::FsPath name{};
fs::FsPath root{};
FsType type{};
u32 flags{FsEntryFlag_None};
auto IsReadOnly() const -> bool {
return flags & FsEntryFlag_ReadOnly;
}
auto IsAssoc() const -> bool {
return flags & FsEntryFlag_Assoc;
}
auto IsSame(const FsEntry& e) const {
return root == e.root && type == e.type;
}
};
// roughly 1kib in size per entry
struct FileEntry : FsDirectoryEntry {
std::string extension{}; // if any
@@ -120,11 +154,15 @@ struct FsDirCollection {
using FsDirCollections = std::vector<FsDirCollection>;
struct Menu final : MenuBase {
Menu(const std::vector<NroEntry>& nro_entries);
~Menu();
struct Menu;
struct FsView final : Widget {
friend class Menu;
FsView(Menu* menu, ViewSide side);
FsView(Menu* menu, const fs::FsPath& path, const FsEntry& entry, ViewSide side);
~FsView();
auto GetShortTitle() const -> const char* override { return "Files"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
@@ -133,6 +171,16 @@ struct Menu final : MenuBase {
return fs::AppendPath(root_path, file_path);
}
auto GetFs() {
return m_fs.get();
}
auto& GetFsEntry() const {
return m_fs_entry;
}
void SetSide(ViewSide side);
private:
void SetIndex(s64 index);
void InstallForwarder();
@@ -144,10 +192,6 @@ private:
auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result;
void LoadAssocEntriesPath(const fs::FsPath& path);
void LoadAssocEntries();
auto FindFileAssocFor() -> std::vector<FileAssocEntry>;
auto GetNewPath(const FileEntry& entry) const -> fs::FsPath {
return GetNewPath(m_path, entry.name);
}
@@ -176,39 +220,6 @@ private:
return out;
}
void AddSelectedEntries(SelectedType type) {
auto entries = GetSelectedEntries();
if (entries.empty()) {
// log_write("%s with no selected files\n", __PRETTY_FUNCTION__);
return;
}
m_selected_type = type;
m_selected_files = entries;
m_selected_path = m_path;
}
void ResetSelection() {
m_selected_files.clear();
m_selected_count = 0;
m_selected_type = SelectedType::None;
m_selected_path = {};
}
auto HasTypeInSelectedEntries(FsDirEntryType type) const -> bool {
if (!m_selected_count) {
return GetEntry().type == type;
} else {
for (auto&p : m_selected_files) {
if (p.type == type) {
return true;
}
}
return false;
}
}
auto GetEntry(u32 index) -> FileEntry& {
return m_entries[m_entries_current[index]];
}
@@ -225,27 +236,42 @@ private:
return GetEntry(m_index);
}
auto IsSd() const -> bool {
return m_fs_entry.type == FsType::Sd;
}
void Sort();
void SortAndFindLastFile();
void SetIndexFromLastFile(const LastFile& last_file);
void UpdateSubheading();
void OnDeleteCallback();
void OnPasteCallback();
void OnRenameCallback();
auto CheckIfUpdateFolder() -> 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) -> 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;
void SetFs(const fs::FsPath& new_path, u32 new_type);
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;
void SetFs(const fs::FsPath& new_path, const FsEntry& new_entry);
auto GetNative() -> fs::FsNative* {
return (fs::FsNative*)m_fs.get();
}
void DisplayHash(hash::Type type);
void DisplayOptions();
void DisplayAdvancedOptions();
private:
static constexpr inline const char* INI_SECTION = "filebrowser";
Menu* m_menu{};
ViewSide m_side{};
const std::vector<NroEntry>& m_nro_entries;
std::unique_ptr<fs::FsNative> m_fs{};
FsType m_fs_type{};
std::unique_ptr<fs::Fs> m_fs{};
FsEntry m_fs_entry{};
fs::FsPath m_path{};
std::vector<FileEntry> m_entries{};
std::vector<u32> m_entries_index{}; // files not including hidden
@@ -256,23 +282,123 @@ private:
std::unique_ptr<List> m_list{};
std::optional<fs::FsPath> m_daybreak_path{};
// search options
// show files [X]
// show folders [X]
// recursive (slow) [ ]
// this keeps track of the highlighted file before opening a folder
// if the user presses B to go back to the previous dir
// this vector is popped, then, that entry is checked if it still exists
// if it does, the index becomes that file.
std::vector<LastFile> m_previous_highlighted_file{};
s64 m_index{};
s64 m_selected_count{};
ScrollingText m_scroll_name{};
bool m_is_update_folder{};
};
// contains all selected files for a command, such as copy, delete, cut etc.
struct SelectedStash {
void Add(std::shared_ptr<FsView> view, SelectedType type, const std::vector<FileEntry>& files, const fs::FsPath& path) {
if (files.empty()) {
Reset();
} else {
m_view = view;
m_type = type;
m_files = files;
m_path = path;
}
}
auto SameFs(FsView* view) -> bool {
if (m_view && view->GetFsEntry().IsSame(m_view->GetFsEntry())) {
return true;
} else {
return false;
}
}
auto Type() const -> SelectedType {
return m_type;
}
auto Empty() const -> bool {
return m_files.empty();
}
void Reset() {
m_view = {};
m_type = {};
m_files = {};
m_path = {};
}
// private:
std::shared_ptr<FsView> m_view{};
std::vector<FileEntry> m_files{};
fs::FsPath m_path{};
SelectedType m_type{SelectedType::None};
};
struct Menu final : MenuBase {
friend class FsView;
Menu(u32 flags);
~Menu();
auto GetShortTitle() const -> const char* override { return "Files"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
static auto GetNewPath(const fs::FsPath& root_path, const fs::FsPath& file_path) -> fs::FsPath {
return fs::AppendPath(root_path, file_path);
}
private:
auto IsSplitScreen() const {
return m_split_screen;
}
void SetSplitScreen(bool enable);
void RefreshViews();
void LoadAssocEntriesPath(const fs::FsPath& path);
void LoadAssocEntries();
auto FindFileAssocFor() -> std::vector<FileAssocEntry>;
void AddSelectedEntries(SelectedType type) {
auto entries = view->GetSelectedEntries();
if (entries.empty()) {
return;
}
m_selected.Add(view, type, entries, view->m_path);
}
void ResetSelection() {
m_selected.Reset();
}
void UpdateSubheading();
void PromptIfShouldExit();
private:
static constexpr inline const char* INI_SECTION = "filebrowser";
std::shared_ptr<FsView> view{};
std::shared_ptr<FsView> view_left{};
std::shared_ptr<FsView> view_right{};
std::vector<FileAssocEntry> m_assoc_entries{};
std::vector<FileEntry> m_selected_files{};
SelectedStash m_selected{};
// this keeps track of the highlighted file before opening a folder
// if the user presses B to go back to the previous dir
// this vector is popped, then, that entry is checked if it still exists
// if it does, the index becomes that file.
std::vector<LastFile> m_previous_highlighted_file{};
fs::FsPath m_selected_path{};
s64 m_index{};
s64 m_selected_count{};
SelectedType m_selected_type{SelectedType::None};
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Alphabetical};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
@@ -280,10 +406,10 @@ private:
option::OptionBool m_folders_first{INI_SECTION, "folders_first", true};
option::OptionBool m_hidden_last{INI_SECTION, "hidden_last", false};
option::OptionBool m_ignore_read_only{INI_SECTION, "ignore_read_only", false};
option::OptionLong m_mount{INI_SECTION, "mount", 0};
bool m_loaded_assoc_entries{};
bool m_is_update_folder{};
bool m_split_screen{};
};
} // namespace sphaira::ui::menu::filebrowser

View File

@@ -35,7 +35,7 @@ struct StreamFtp final : yati::source::Stream {
};
struct Menu final : MenuBase {
Menu();
Menu(u32 flags);
~Menu();
auto GetShortTitle() const -> const char* override { return "FTP"; };

View File

@@ -88,7 +88,7 @@ enum OrderType {
using LayoutType = grid::LayoutType;
struct Menu final : grid::Menu {
Menu();
Menu(u32 flags);
~Menu();
auto GetShortTitle() const -> const char* override { return "Games"; };

View File

@@ -9,6 +9,129 @@
namespace sphaira::ui::menu::gc {
typedef enum {
FsGameCardPartitionRaw_None = -1,
FsGameCardPartitionRaw_Normal = 0,
FsGameCardPartitionRaw_Secure = 1,
} FsGameCardPartitionRaw;
////////////////////////////////////////////////
// The below structs are taken from nxdumptool./
////////////////////////////////////////////////
/// Located at offset 0x7000 in the gamecard image.
typedef struct {
u8 signature[0x100]; ///< RSA-2048-PKCS#1 v1.5 with SHA-256 signature over the rest of the data.
u32 magic; ///< "CERT".
u32 version;
u8 kek_index;
u8 reserved[0x7];
u8 t1_card_device_id[0x10];
u8 iv[0x10];
u8 hw_key[0x10]; ///< Encrypted.
u8 data[0xC0]; ///< Encrypted.
} FsGameCardCertificate;
static_assert(sizeof(FsGameCardCertificate) == 0x200);
typedef struct {
u8 maker_code; ///< FsCardId1MakerCode.
u8 memory_capacity; ///< Matches GameCardRomSize.
u8 reserved; ///< Known values: 0x00, 0x01, 0x02, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0C, 0x0D, 0x0E, 0x80.
u8 memory_type; ///< FsCardId1MemoryType.
} FsCardId1;
static_assert(sizeof(FsCardId1) == 0x4);
typedef struct {
u8 card_security_number; ///< FsCardId2CardSecurityNumber.
u8 card_type; ///< FsCardId2CardType.
u8 reserved[0x2]; ///< Usually filled with zeroes.
} FsCardId2;
static_assert(sizeof(FsCardId2) == 0x4);
typedef struct {
u8 reserved[0x4]; ///< Usually filled with zeroes.
} FsCardId3;
static_assert(sizeof(FsCardId3) == 0x4);
/// Returned by fsDeviceOperatorGetGameCardIdSet.
typedef struct {
FsCardId1 id1; ///< Specifies maker code, memory capacity and memory type.
FsCardId2 id2; ///< Specifies card security number and card type.
FsCardId3 id3; ///< Always zero (so far).
} FsGameCardIdSet;
/// Encrypted using AES-128-ECB with the common titlekek generator key (stored in the .rodata segment from the Lotus firmware).
typedef struct {
union {
u8 value[0x10];
struct {
u8 package_id[0x8]; ///< Matches package_id from GameCardHeader.
u8 reserved[0x8]; ///< Just zeroes.
};
};
} GameCardKeySource;
static_assert(sizeof(GameCardKeySource) == 0x10);
/// Plaintext area. Dumped from FS program memory.
typedef struct {
GameCardKeySource key_source;
u8 encrypted_titlekey[0x10]; ///< Encrypted using AES-128-CCM with the decrypted key_source and the nonce from this section.
u8 mac[0x10]; ///< Used to verify the validity of the decrypted titlekey.
u8 nonce[0xC]; ///< Used as the IV to decrypt encrypted_titlekey using AES-128-CCM.
u8 reserved[0x1C4];
} GameCardInitialData;
static_assert(sizeof(GameCardInitialData) == 0x200);
typedef struct {
u8 maker_code; ///< GameCardUidMakerCode.
u8 version; ///< TODO: determine whether this matches GameCardVersion or not.
u8 card_type; ///< GameCardUidCardType.
u8 unique_data[0x9];
u32 random;
u8 platform_flag;
u8 reserved[0xB];
FsCardId1 card_id_1_mirror; ///< This field mirrors bit 5 of FsCardId1MemoryType.
u8 mac[0x20];
} GameCardUid;
static_assert(sizeof(GameCardUid) == 0x40);
/// Plaintext area. Dumped from FS program memory.
/// Overall structure may change with each new LAFW version.
typedef struct {
u32 asic_security_mode; ///< Determines how the Lotus ASIC initialised the gamecard security mode. Usually 0xFFFFFFF9.
u32 asic_status; ///< Bitmask of the internal gamecard interface status. Usually 0x20000000.
FsCardId1 card_id1;
FsCardId2 card_id2;
GameCardUid card_uid;
u8 reserved[0x190];
u8 mac[0x20]; ///< Changes with each gamecard (re)insertion.
} GameCardSpecificData;
static_assert(sizeof(GameCardSpecificData) == 0x200);
/// Plaintext area. Dumped from FS program memory.
/// This struct is returned by Lotus command "ChangeToSecureMode" (0xF). This means it is only available *after* the gamecard secure area has been mounted.
/// A copy of the gamecard header without the RSA-2048 signature and a plaintext GameCardInfo precedes this struct in FS program memory.
typedef struct {
GameCardSpecificData specific_data;
FsGameCardCertificate certificate;
u8 reserved[0x200];
GameCardInitialData initial_data;
} GameCardSecurityInformation;
static_assert(sizeof(GameCardSecurityInformation) == 0x800);
///////////////////
// nxdumptool fin./
///////////////////
struct GcCollection : yati::container::CollectionEntry {
GcCollection(const char* _name, s64 _size, u8 _type, u8 _id_offset) {
name = _name;
@@ -28,6 +151,9 @@ struct ApplicationEntry {
u64 app_id{};
u32 version{};
u8 key_gen{};
std::unique_ptr<NsApplicationControlData> control{};
u64 control_size{};
NacpLanguageEntry lang_entry{};
std::vector<GcCollections> application{};
std::vector<GcCollections> patch{};
@@ -40,7 +166,7 @@ struct ApplicationEntry {
};
struct Menu final : MenuBase {
Menu();
Menu(u32 flags);
~Menu();
auto GetShortTitle() const -> const char* override { return "GC"; };
@@ -48,15 +174,31 @@ struct Menu final : MenuBase {
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
Result GcStorageRead(void* buf, s64 off, s64 size);
private:
Result GcPoll(bool* inserted);
Result GcOnEvent(bool force = false);
// GameCard FS api.
Result GcMount();
void GcUnmount();
Result GcPoll(bool* inserted);
Result GcOnEvent();
Result UpdateStorageSize();
// GameCard Storage api
Result GcMountStorage();
void GcUmountStorage();
Result GcMountPartition(FsGameCardPartitionRaw partition);
void GcUnmountPartition();
Result GcStorageReadInternal(void* buf, s64 off, s64 size, u64* bytes_read);
// taken from nxdumptool.
Result GcGetSecurityInfo(GameCardSecurityInformation& out);
Result LoadControlData(ApplicationEntry& e);
Result UpdateStorageSize();
void FreeImage();
void OnChangeIndex(s64 new_index);
Result DumpGames(u32 flags);
private:
FsDeviceOperator m_dev_op{};
@@ -74,9 +216,30 @@ private:
s64 m_size_total_sd{};
s64 m_size_free_nand{};
s64 m_size_total_nand{};
NacpLanguageEntry m_lang_entry{};
int m_icon{};
bool m_mounted{};
FsStorage m_storage{};
// size of normal partition.
s64 m_parition_normal_size{};
// size of secure partition.
s64 m_parition_secure_size{};
// used size reported in the xci header.
s64 m_storage_trimmed_size{};
// total size of m_parition_normal_size + m_parition_secure_size.
s64 m_storage_total_size{};
// reported size via rom_size in the xci header.
s64 m_storage_full_size{};
// found in xci header.
u64 m_package_id{};
// found in xci header.
u8 m_initial_data_hash[SHA256_HASH_SIZE]{};
// currently mounted storage partiton.
FsGameCardPartitionRaw m_partition{FsGameCardPartitionRaw_None};
bool m_storage_mounted{};
// set when the gc should be re-mounted, cleared when handled.
bool m_dirty{};
};
} // namespace sphaira::ui::menu::gc

View File

@@ -42,7 +42,7 @@ struct GhApiEntry {
};
struct Menu final : MenuBase {
Menu();
Menu(u32 flags);
~Menu();
auto GetShortTitle() const -> const char* override { return "GitHub"; };

View File

@@ -24,6 +24,8 @@ enum OrderType {
using LayoutType = grid::LayoutType;
auto GetNroEntries() -> std::span<const NroEntry>;
struct Menu final : grid::Menu {
Menu();
~Menu();
@@ -37,7 +39,7 @@ struct Menu final : grid::Menu {
return m_entries;
}
static Result InstallHomebrew(const fs::FsPath& path, const NacpStruct& nacp, const std::vector<u8>& icon);
static Result InstallHomebrew(const fs::FsPath& path, const std::vector<u8>& icon);
static Result InstallHomebrewFromPath(const fs::FsPath& path);
private:

View File

@@ -27,7 +27,7 @@ struct Entry {
};
struct Menu final : MenuBase {
Menu();
Menu(u32 flags);
~Menu();
auto GetShortTitle() const -> const char* override { return "IRS"; };

View File

@@ -1,8 +1,8 @@
#pragma once
#include "ui/widget.hpp"
#include "ui/menus/homebrew.hpp"
#include "ui/menus/filebrowser.hpp"
#include "ui/menus/menu_base.hpp"
#include <span>
namespace sphaira::ui::menu::main {
@@ -17,7 +17,7 @@ enum class UpdateState {
Error,
};
using MiscMenuFunction = std::function<std::shared_ptr<ui::menu::MenuBase>(void)>;
using MiscMenuFunction = std::function<std::shared_ptr<ui::menu::MenuBase>(u32 flags)>;
enum MiscMenuFlag : u8 {
// can be set as the rightside menu.
@@ -62,9 +62,9 @@ private:
void AddOnLRPress();
private:
std::shared_ptr<homebrew::Menu> m_homebrew_menu{};
std::shared_ptr<filebrowser::Menu> m_filebrowser_menu{};
std::shared_ptr<MenuBase> m_right_side_menu{};
std::shared_ptr<MenuBase> m_centre_menu{};
std::shared_ptr<MenuBase> m_left_menu{};
std::shared_ptr<MenuBase> m_right_menu{};
std::shared_ptr<MenuBase> m_current_menu{};
std::string m_update_url{};

View File

@@ -1,12 +1,28 @@
#pragma once
#include "ui/widget.hpp"
#include "ui/scrolling_text.hpp"
#include <string>
namespace sphaira::ui::menu {
enum MenuFlag {
MenuFlag_None = 0,
MenuFlag_Tab = 1 << 1,
};
struct PolledData {
struct tm tm{};
u32 battery_percetange{};
PsmChargerType charger_type{};
NifmInternetConnectionType type{};
NifmInternetConnectionStatus status{};
u32 strength{};
u32 ip{};
};
struct MenuBase : Widget {
MenuBase(std::string title);
MenuBase(const std::string& title, u32 flags);
virtual ~MenuBase();
virtual auto GetShortTitle() const -> const char* = 0;
@@ -25,23 +41,21 @@ struct MenuBase : Widget {
return m_title;
}
private:
void UpdateVars();
auto IsTab() const -> bool {
return m_flags & MenuFlag_Tab;
}
static auto GetPolledData(bool force_refresh = false) -> PolledData;
private:
std::string m_title{};
std::string m_title_sub_heading{};
std::string m_sub_heading{};
protected:
struct tm m_tm{};
TimeStamp m_poll_timestamp{};
u32 m_battery_percetange{};
PsmChargerType m_charger_type{};
NifmInternetConnectionType m_type{};
NifmInternetConnectionStatus m_status{};
u32 m_strength{};
u32 m_ip{};
ScrollingText m_scroll_title_sub_heading{};
ScrollingText m_scroll_sub_heading{};
u32 m_flags{};
};
} // namespace sphaira::ui::menu

View File

@@ -130,7 +130,7 @@ struct PageEntry {
};
struct Menu final : MenuBase {
Menu();
Menu(u32 flags);
~Menu();
auto GetShortTitle() const -> const char* override { return "Themezer"; };

View File

@@ -21,7 +21,7 @@ enum class State {
};
struct Menu final : MenuBase {
Menu();
Menu(u32 flags);
~Menu();
auto GetShortTitle() const -> const char* override { return "USB"; };

View File

@@ -1,6 +1,7 @@
#pragma once
#include "widget.hpp"
#include "ui/widget.hpp"
#include "ui/scrolling_text.hpp"
#include "fs.hpp"
#include <functional>
#include <span>
@@ -8,15 +9,15 @@
namespace sphaira::ui {
struct ProgressBox;
using ProgressBoxCallback = std::function<bool(ProgressBox*)>;
using ProgressBoxDoneCallback = std::function<void(bool success)>;
using ProgressBoxCallback = std::function<Result(ProgressBox*)>;
using ProgressBoxDoneCallback = std::function<void(Result rc)>;
struct ProgressBox final : Widget {
ProgressBox(
int image,
const std::string& action,
const std::string& title,
ProgressBoxCallback callback, ProgressBoxDoneCallback done = [](bool success){},
ProgressBoxCallback callback, ProgressBoxDoneCallback done = [](Result rc){},
int cpuid = 1, int prio = 0x2C, int stack_size = 1024*128
);
~ProgressBox();
@@ -28,15 +29,24 @@ struct ProgressBox final : Widget {
auto NewTransfer(const std::string& transfer) -> ProgressBox&;
auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
// not const in order to avoid copy by using std::swap
auto SetImage(int image) -> ProgressBox&;
auto SetImageData(std::vector<u8>& data) -> ProgressBox&;
auto SetImageDataConst(std::span<const u8> data) -> ProgressBox&;
void RequestExit();
auto ShouldExit() -> bool;
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;
void Yield();
auto GetCpuId() const {
return m_cpuid;
}
auto OnDownloadProgressCallback() {
return [this](s64 dltotal, s64 dlnow, s64 ultotal, s64 ulnow){
if (this->ShouldExit()) {
@@ -60,7 +70,7 @@ public:
struct ThreadData {
ProgressBox* pbox{};
ProgressBoxCallback callback{};
bool result{};
Result result{};
};
private:
@@ -79,8 +89,14 @@ private:
s64 m_speed{};
TimeStamp m_timestamp{};
std::vector<u8> m_image_data{};
int m_image_pending{};
bool m_is_image_pending{};
// shared data end.
ScrollingText m_scroll_title{};
ScrollingText m_scroll_transfer{};
int m_cpuid{};
int m_image{};
bool m_own_image{};
};

View File

@@ -299,6 +299,7 @@ enum class Button : u64 {
UP = static_cast<u64>(HidNpadButton_AnyUp),
DOWN = static_cast<u64>(HidNpadButton_AnyDown),
NONE = 0,
ANY_BUTTON = A | B | X | Y | L | R | L2 | R2 | L3 | R3 | START | SELECT,
ANY_HORIZONTAL = LEFT | RIGHT,
ANY_VERTICAL = UP | DOWN,

View File

@@ -15,6 +15,7 @@ struct Base {
};
Base(u64 transfer_timeout);
virtual ~Base();
// sets up usb.
virtual Result Init() = 0;
@@ -23,9 +24,9 @@ struct Base {
virtual Result IsUsbConnected(u64 timeout) = 0;
// transfers a chunk of data, check out_size_transferred for how much was transferred.
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout);
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred) {
return TransferPacketImpl(read, page, size, out_size_transferred, m_transfer_timeout);
Result TransferPacketImpl(bool read, void *page, u32 remaining, u32 size, u32 *out_size_transferred, u64 timeout);
Result TransferPacketImpl(bool read, void *page, u32 remaining, u32 size, u32 *out_size_transferred) {
return TransferPacketImpl(read, page, remaining, size, out_size_transferred, m_transfer_timeout);
}
// transfers all data.
@@ -89,7 +90,7 @@ protected:
virtual Event *GetCompletionEvent(UsbSessionEndpoint ep) = 0;
virtual Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) = 0;
virtual Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_xfer_id) = 0;
virtual Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 remaining, u32 size, u32 *out_xfer_id) = 0;
virtual Result GetTransferResult(UsbSessionEndpoint ep, u32 xfer_id, u32 *out_requested_size, u32 *out_transferred_size) = 0;
private:

View File

@@ -19,10 +19,20 @@ enum USBCmdId : u32 {
FILE_RANGE = 1
};
// extension flags for sphaira.
enum USBFlag : u8 {
USBFlag_NONE = 0,
// stream install, does not allow for random access.
// allows the upload to be multi threaded., do not modify!
// the order of the file list must be kept as-is.
USBFlag_STREAM = 1 << 0,
};
struct TUSHeader {
u32 magic; // TUL0 (Tinfoil Usb List 0)
u32 nspListSize;
u64 padding;
u8 flags;
u8 padding[0x7];
};
struct NX_PACKED USBCmdHeader {

View File

@@ -31,7 +31,7 @@ struct Usb {
}
// waits for connection and then sends file list.
Result WaitForConnection(u64 timeout, std::span<const std::string> names);
Result WaitForConnection(u64 timeout, u8 flags, std::span<const std::string> names);
// polls for command, executes transfer if possible.
// will return Result_Exit if exit command is recieved.

View File

@@ -11,17 +11,19 @@ struct UsbDs final : Base {
Result Init() override;
Result IsUsbConnected(u64 timeout) override;
Result GetSpeed(UsbDeviceSpeed* out);
Result GetSpeed(UsbDeviceSpeed* out, u16* max_packet_size);
private:
Result WaitUntilConfigured(u64 timeout);
Event *GetCompletionEvent(UsbSessionEndpoint ep) override;
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) override;
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) override;
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 remaining, u32 size, u32 *out_urb_id) override;
Result GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) override;
private:
UsbDsInterface* m_interface{};
UsbDsEndpoint* m_endpoints[2]{};
u16 m_max_packet_size{};
};
} // namespace sphaira::usb

View File

@@ -14,7 +14,7 @@ struct UsbHs final : Base {
private:
Event *GetCompletionEvent(UsbSessionEndpoint ep) override;
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) override;
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_xfer_id) override;
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 remaining, u32 size, u32 *out_xfer_id) override;
Result GetTransferResult(UsbSessionEndpoint ep, u32 xfer_id, u32 *out_requested_size, u32 *out_transferred_size) override;
Result Connect();

View File

@@ -3,17 +3,17 @@
#include "base.hpp"
#include "fs.hpp"
#include <switch.h>
#include <memory>
namespace sphaira::yati::source {
struct File final : Base {
File(FsFileSystem* fs, const fs::FsPath& path);
~File();
File(fs::Fs* fs, const fs::FsPath& path);
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
private:
FsFile m_file{};
fs::Fs* m_fs{};
fs::File m_file{};
};
} // namespace sphaira::yati::source

View File

@@ -1,20 +0,0 @@
#pragma once
#include "base.hpp"
#include "fs.hpp"
#include <cstdio>
#include <switch.h>
namespace sphaira::yati::source {
struct Stdio final : Base {
Stdio(const fs::FsPath& path);
~Stdio();
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
private:
std::FILE* m_file{};
};
} // namespace sphaira::yati::source

View File

@@ -18,6 +18,10 @@ struct Stream : Base {
return true;
}
void Reset() {
m_offset = 0;
}
protected:
Result m_open_result{};

View File

@@ -5,17 +5,17 @@
#include "stream.hpp"
#include "fs.hpp"
#include <switch.h>
#include <memory>
namespace sphaira::yati::source {
struct StreamFile final : Stream {
StreamFile(FsFileSystem* fs, const fs::FsPath& path);
~StreamFile();
StreamFile(fs::Fs* fs, const fs::FsPath& path);
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
private:
FsFile m_file{};
fs::Fs* m_fs{};
fs::File m_file{};
s64 m_offset{};
};

View File

@@ -24,6 +24,7 @@ struct Usb final : Base {
Usb(u64 transfer_timeout);
~Usb();
bool IsStream() const override;
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
Result Finished(u64 timeout);
@@ -45,6 +46,7 @@ private:
private:
std::unique_ptr<usb::UsbDs> m_usb;
std::string m_transfer_file_name{};
u8 m_flags{};
};
} // namespace sphaira::yati::source

View File

@@ -70,10 +70,6 @@ enum : Result {
struct Config {
bool sd_card_install{};
// sets the performance mode to FastLoad which boosts the CPU clock
// and lowers the GPU clock.
bool boost_mode{};
// enables downgrading patch / data patch (dlc) version.
bool allow_downgrade{};
@@ -129,8 +125,7 @@ struct ConfigOverride {
std::optional<bool> lower_system_version{};
};
Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path, const ConfigOverride& override = {});
Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path, const ConfigOverride& override = {});
Result InstallFromFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& path, const ConfigOverride& override = {});
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const fs::FsPath& path, const ConfigOverride& override = {});
Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr<container::Base> container, const ConfigOverride& override = {});
Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const container::Collections& collections, const ConfigOverride& override = {});

View File

@@ -25,11 +25,13 @@
#include <pulsar.h>
#include <haze.h>
#include <algorithm>
#include <ranges>
#include <cassert>
#include <cstring>
#include <ctime>
#include <span>
#include <dirent.h>
#include <usbhsfs.h>
extern "C" {
u32 __nx_applet_exit_mode = 0;
@@ -42,6 +44,10 @@ constexpr fs::FsPath DEFAULT_MUSIC_PATH = "/config/sphaira/themes/default_music.
constexpr const char* DEFAULT_MUSIC_URL = "https://files.catbox.moe/1ovji1.bfstm";
// constexpr const char* DEFAULT_MUSIC_URL = "https://raw.githubusercontent.com/ITotalJustice/sphaira/refs/heads/master/assets/default_music.bfstm";
constexpr const u8 DEFAULT_IMAGE_DATA[]{
#embed <icons/default.png>
};
void download_default_music() {
App::Push(std::make_shared<ui::ProgressBox>(0, "Downloading "_i18n, "default_music.bfstm", [](auto pbox){
const auto result = curl::Api().ToFile(
@@ -50,15 +56,17 @@ void download_default_music() {
curl::OnProgress{pbox->OnDownloadProgressCallback()}
);
return result.success;
}, [](bool success){
if (success) {
if (!result.success) {
R_THROW(0x1);
}
R_SUCCEED();
}, [](Result rc){
App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n);
if (R_SUCCEEDED(rc)) {
App::Notify("Downloaded "_i18n + "default_music.bfstm");
App::SetTheme(App::GetThemeIndex());
} else {
App::Push(std::make_shared<ui::ErrorBox>(
"Failed to download default_music.bfstm, please try again"_i18n
));
}
}));
}
@@ -439,15 +447,9 @@ void App::Loop() {
timeGetCurrentTime(TimeType_LocalSystemClock, &timestamp);
const auto nro_path = nro_normalise_path(arg.path);
ini_puts("paths", "last_launch_full", arg.argv.c_str(), App::CONFIG_PATH);
ini_puts("paths", "last_launch_path", nro_path.c_str(), App::CONFIG_PATH);
// update timestamp
ini_putl(nro_path.c_str(), "timestamp", timestamp, App::PLAYLOG_PATH);
// update launch_count
const long old_launch_count = ini_getl(nro_path.c_str(), "launch_count", 0, App::PLAYLOG_PATH);
ini_putl(nro_path.c_str(), "launch_count", old_launch_count + 1, App::PLAYLOG_PATH);
log_write("updating timestamp and launch count for: %s %lu %ld\n", nro_path.c_str(), timestamp, old_launch_count + 1);
log_write("updating timestamp for: %s %lu\n", nro_path.c_str(), timestamp);
// force disable pop-back to main menu.
__nx_applet_exit_mode = 0;
@@ -516,11 +518,11 @@ auto App::Push(std::shared_ptr<ui::Widget> widget) -> void {
}
auto App::PopToMenu() -> void {
for (auto it = g_app->m_widgets.rbegin(); it != g_app->m_widgets.rend(); it++) {
const auto& p = *it;
for (auto& p : std::ranges::views::reverse(g_app->m_widgets)) {
if (p->IsMenu()) {
break;
}
p->SetPop();
}
}
@@ -542,7 +544,7 @@ void App::NotifyClear(ui::NotifEntry::Side side) {
}
void App::NotifyFlashLed() {
static const HidsysNotificationLedPattern pattern = {
static constexpr HidsysNotificationLedPattern pattern = {
.baseMiniCycleDuration = 0x1, // 12.5ms.
.totalMiniCycles = 0x1, // 1 mini cycle(s).
.totalFullCycles = 0x1, // 1 full run(s).
@@ -554,15 +556,30 @@ void App::NotifyFlashLed() {
}}
};
Result rc;
s32 total;
HidsysUniquePadId unique_pad_ids[16] = {0};
if (R_SUCCEEDED(hidsysGetUniquePadIds(unique_pad_ids, 16, &total))) {
for (int i = 0; i < total; i++) {
hidsysSetNotificationLedPattern(&pattern, unique_pad_ids[i]);
HidsysUniquePadId unique_pad_id;
rc = hidsysGetUniquePadsFromNpad(HidNpadIdType_Handheld, &unique_pad_id, 1, &total);
if (R_SUCCEEDED(rc) && total) {
rc = hidsysSetNotificationLedPattern(&pattern, unique_pad_id);
}
if (R_FAILED(rc) || !total) {
rc = hidsysGetUniquePadsFromNpad(HidNpadIdType_No1, &unique_pad_id, 1, &total);
if (R_SUCCEEDED(rc) && total) {
hidsysSetNotificationLedPattern(&pattern, unique_pad_id);
}
}
}
Result App::PushErrorBox(Result rc, const std::string& message) {
if (R_FAILED(rc)) {
App::Push(std::make_shared<ui::ErrorBox>(rc, message));
}
return rc;
}
auto App::GetThemeMetaList() -> std::span<ThemeMeta> {
return g_app->m_theme_meta_entries;
}
@@ -580,6 +597,10 @@ auto App::GetDefaultImage() -> int {
return g_app->m_default_image;
}
auto App::GetDefaultImageData() -> std::span<const u8> {
return DEFAULT_IMAGE_DATA;
}
auto App::GetExePath() -> fs::FsPath {
return g_app->m_app_path;
}
@@ -592,6 +613,14 @@ auto App::GetNxlinkEnable() -> bool {
return g_app->m_nxlink_enabled.Get();
}
auto App::GetHddEnable() -> bool {
return g_app->m_hdd_enabled.Get();
}
auto App::GetWriteProtect() -> bool {
return g_app->m_hdd_write_protect.Get();
}
auto App::GetLogEnable() -> bool {
return g_app->m_log_enabled.Get();
}
@@ -659,6 +688,32 @@ void App::SetNxlinkEnable(bool enable) {
}
}
void App::SetHddEnable(bool enable) {
if (App::GetHddEnable() != enable) {
g_app->m_hdd_enabled.Set(enable);
if (enable) {
if (App::GetWriteProtect()) {
usbHsFsSetFileSystemMountFlags(UsbHsFsMountFlags_ReadOnly);
}
usbHsFsInitialize(1);
} else {
usbHsFsExit();
}
}
}
void App::SetWriteProtect(bool enable) {
if (App::GetWriteProtect() != enable) {
g_app->m_hdd_write_protect.Set(enable);
if (enable) {
usbHsFsSetFileSystemMountFlags(UsbHsFsMountFlags_ReadOnly);
} else {
usbHsFsSetFileSystemMountFlags(0);
}
}
}
void App::SetLogEnable(bool enable) {
if (App::GetLogEnable() != enable) {
g_app->m_log_enabled.Set(enable);
@@ -719,7 +774,7 @@ void App::SetReplaceHbmenuEnable(bool enable) {
}
if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
if (std::strcmp(sphaira_nacp.display_version, hbmenu_nacp.display_version) < 0) {
if (IsVersionNewer(sphaira_nacp.display_version, hbmenu_nacp.display_version)) {
if (R_FAILED(rc = fs.copy_entire_file(sphaira_path, "/hbmenu.nro"))) {
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path.s, rc, R_MODULE(rc), R_DESCRIPTION(rc));
} else {
@@ -737,9 +792,10 @@ void App::SetReplaceHbmenuEnable(bool enable) {
if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", "/switch/hbmenu.nro"))) {
// try and restore sphaira in a last ditch effort.
if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", sphaira_path))) {
App::Push(std::make_shared<ui::ErrorBox>(rc,
App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n);
App::PushErrorBox(rc,
"Failed to restore hbmenu, please re-download hbmenu"_i18n
));
);
} else {
App::Push(std::make_shared<ui::OptionBox>(
"Failed to restore hbmenu, using sphaira instead"_i18n,
@@ -837,18 +893,7 @@ void App::SetTextScrollSpeed(long index) {
}
auto App::Install(OwoConfig& config) -> Result {
R_TRY(romfsInit());
ON_SCOPE_EXIT(romfsExit());
std::vector<u8> main_data, npdm_data, logo_data, gif_data;
R_TRY(fs::read_entire_file("romfs:/exefs/main", main_data));
R_TRY(fs::read_entire_file("romfs:/exefs/main.npdm", npdm_data));
config.nro_path = nro_add_arg_file(config.nro_path);
config.main = main_data;
config.npdm = npdm_data;
config.logo = logo_data;
config.gif = gif_data;
if (!config.icon.empty()) {
config.icon = GetNroIcon(config.icon);
}
@@ -867,18 +912,7 @@ auto App::Install(OwoConfig& config) -> Result {
}
auto App::Install(ui::ProgressBox* pbox, OwoConfig& config) -> Result {
R_TRY(romfsInit());
ON_SCOPE_EXIT(romfsExit());
std::vector<u8> main_data, npdm_data, logo_data, gif_data;
R_TRY(fs::read_entire_file("romfs:/exefs/main", main_data));
R_TRY(fs::read_entire_file("romfs:/exefs/main.npdm", npdm_data));
config.nro_path = nro_add_arg_file(config.nro_path);
config.main = main_data;
config.npdm = npdm_data;
config.logo = logo_data;
config.gif = gif_data;
if (!config.icon.empty()) {
config.icon = GetNroIcon(config.icon);
}
@@ -1237,6 +1271,9 @@ void App::ScanThemeEntries() {
App::App(const char* argv0) {
TimeStamp ts;
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
g_app = this;
m_start_timestamp = armGetSystemTick();
if (!std::strncmp(argv0, "sdmc:/", 6)) {
@@ -1252,10 +1289,59 @@ App::App(const char* argv0) {
}
fs::FsNativeSd fs;
fs.CreateDirectoryRecursively("/config/sphaira/assoc");
fs.CreateDirectoryRecursively("/config/sphaira/themes");
fs.CreateDirectoryRecursively("/config/sphaira/github");
fs.CreateDirectoryRecursively("/config/sphaira/i18n");
fs.CreateDirectoryRecursively("/config/sphaira");
fs.CreateDirectory("/config/sphaira/assoc");
fs.CreateDirectory("/config/sphaira/themes");
fs.CreateDirectory("/config/sphaira/github");
fs.CreateDirectory("/config/sphaira/i18n");
auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
auto app = static_cast<App*>(UserData);
if (!std::strcmp(Section, INI_SECTION)) {
if (app->m_nxlink_enabled.LoadFrom(Key, Value)) {}
else if (app->m_mtp_enabled.LoadFrom(Key, Value)) {}
else if (app->m_ftp_enabled.LoadFrom(Key, Value)) {}
else if (app->m_hdd_enabled.LoadFrom(Key, Value)) {}
else if (app->m_hdd_write_protect.LoadFrom(Key, Value)) {}
else if (app->m_log_enabled.LoadFrom(Key, Value)) {}
else if (app->m_replace_hbmenu.LoadFrom(Key, Value)) {}
else if (app->m_theme_path.LoadFrom(Key, Value)) {}
else if (app->m_theme_music.LoadFrom(Key, Value)) {}
else if (app->m_12hour_time.LoadFrom(Key, Value)) {}
else if (app->m_language.LoadFrom(Key, Value)) {}
else if (app->m_left_menu.LoadFrom(Key, Value)) {}
else if (app->m_right_menu.LoadFrom(Key, Value)) {}
else if (app->m_install_sysmmc.LoadFrom(Key, Value)) {}
else if (app->m_install_emummc.LoadFrom(Key, Value)) {}
else if (app->m_install_sd.LoadFrom(Key, Value)) {}
else if (app->m_install_prompt.LoadFrom(Key, Value)) {}
else if (app->m_progress_boost_mode.LoadFrom(Key, Value)) {}
else if (app->m_allow_downgrade.LoadFrom(Key, Value)) {}
else if (app->m_skip_if_already_installed.LoadFrom(Key, Value)) {}
else if (app->m_ticket_only.LoadFrom(Key, Value)) {}
else if (app->m_skip_base.LoadFrom(Key, Value)) {}
else if (app->m_skip_patch.LoadFrom(Key, Value)) {}
else if (app->m_skip_addon.LoadFrom(Key, Value)) {}
else if (app->m_skip_data_patch.LoadFrom(Key, Value)) {}
else if (app->m_skip_ticket.LoadFrom(Key, Value)) {}
else if (app->m_skip_nca_hash_verify.LoadFrom(Key, Value)) {}
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_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)) {}
} else if (!std::strcmp(Section, "accessibility")) {
if (app->m_text_scroll_speed.LoadFrom(Key, Value)) {}
}
return 1;
};
// load all configs ahead of time, as this is actually faster than
// loading each config one by one as it avoids re-opening the file multiple times.
ini_browse(cb, this, CONFIG_PATH);
if (App::GetLogEnable()) {
log_file_init();
@@ -1275,6 +1361,14 @@ App::App(const char* argv0) {
nxlinkInitialize(nxlink_callback);
}
if (App::GetWriteProtect()) {
usbHsFsSetFileSystemMountFlags(UsbHsFsMountFlags_ReadOnly);
}
if (App::GetHddEnable()) {
usbHsFsInitialize(1);
}
curl::Init();
// get current size of the framebuffer
@@ -1374,17 +1468,14 @@ App::App(const char* argv0) {
ScanThemeEntries();
fs::FsPath theme_path{};
constexpr fs::FsPath default_theme_path{"romfs:/themes/abyss_theme.ini"};
ini_gets("config", "theme", default_theme_path, theme_path, sizeof(theme_path), CONFIG_PATH);
// try and load previous theme, default to previous version otherwise.
fs::FsPath theme_path = m_theme_path.Get();
ThemeMeta theme_meta;
if (R_SUCCEEDED(romfsInit())) {
ON_SCOPE_EXIT(romfsExit());
if (!LoadThemeMeta(theme_path, theme_meta)) {
log_write("failed to load meta using default\n");
theme_path = default_theme_path;
theme_path = DEFAULT_THEME_PATH;
LoadThemeMeta(theme_path, theme_meta);
}
}
@@ -1407,17 +1498,6 @@ App::App(const char* argv0) {
// padInitializeDefault(&m_pad);
padInitializeAny(&m_pad);
// usbHsFsSetFileSystemMountFlags(UsbHsFsMountFlags_ReadOnly);
// usbHsFsSetPopulateCallback();
// usbHsFsInitialize(0);
m_prev_timestamp = ini_getl("paths", "timestamp", 0, App::CONFIG_PATH);
const auto last_launch_path_size = ini_gets("paths", "last_launch_path", "", m_prev_last_launch, sizeof(m_prev_last_launch), App::CONFIG_PATH);
fs::FsPath last_launch_path;
if (last_launch_path_size) {
ini_gets("paths", "last_launch_path", "", last_launch_path, sizeof(last_launch_path), App::CONFIG_PATH);
}
ini_puts("paths", "last_launch_path", "", App::CONFIG_PATH);
const auto loader_info_size = envGetLoaderInfoSize();
if (loader_info_size) {
@@ -1432,20 +1512,12 @@ App::App(const char* argv0) {
}
ini_putl(GetExePath(), "timestamp", m_start_timestamp, App::PLAYLOG_PATH);
const long old_launch_count = ini_getl(GetExePath(), "launch_count", 0, App::PLAYLOG_PATH);
ini_putl(GetExePath(), "launch_count", old_launch_count + 1, App::PLAYLOG_PATH);
// load default image
if (R_SUCCEEDED(romfsInit())) {
ON_SCOPE_EXIT(romfsExit());
const auto image = ImageLoadFromFile("romfs:/default.png");
if (!image.data.empty()) {
m_default_image = nvgCreateImageRGBA(vg, image.w, image.h, 0, image.data.data());
}
}
m_default_image = nvgCreateImageMem(vg, 0, DEFAULT_IMAGE_DATA, std::size(DEFAULT_IMAGE_DATA));
App::Push(std::make_shared<ui::menu::main::MainMenu>());
log_write("finished app constructor, time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
log_write("\n\tfinished app constructor, time taken: %.2fs %zums\n\n", ts.GetSecondsD(), ts.GetMs());
}
void App::PlaySoundEffect(SoundEffect effect) {
@@ -1510,7 +1582,7 @@ void App::DisplayMiscOptions(bool left_side) {
ON_SCOPE_EXIT(App::Push(options));
for (auto& e : ui::menu::main::GetMiscMenuEntries()) {
if (e.name == g_app->m_right_side_menu.Get()) {
if (e.name == g_app->m_right_menu.Get()) {
continue;
}
@@ -1519,7 +1591,7 @@ void App::DisplayMiscOptions(bool left_side) {
}
options->Add(std::make_shared<ui::SidebarEntryCallback>(i18n::get(e.title), [e](){
App::Push(e.func());
App::Push(e.func(ui::menu::MenuFlag_None));
}));
}
}
@@ -1546,14 +1618,14 @@ void App::DisplayAdvancedOptions(bool left_side) {
menu_names.emplace_back(e.name);
}
ui::SidebarEntryArray::Items right_side_menu_items;
ui::SidebarEntryArray::Items side_menu_items;
for (auto& str : menu_names) {
right_side_menu_items.push_back(i18n::get(str));
side_menu_items.push_back(i18n::get(str));
}
const auto it = std::find(menu_names.cbegin(), menu_names.cend(), g_app->m_right_side_menu.Get());
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_side_menu.Set(menu_names[0]);
g_app->m_right_menu.Set(menu_names[0]);
}
options->Add(std::make_shared<ui::SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [](bool& enable){
@@ -1564,25 +1636,45 @@ void App::DisplayAdvancedOptions(bool left_side) {
App::SetReplaceHbmenuEnable(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Boost CPU during transfer"_i18n, App::GetApp()->m_progress_boost_mode.Get(), [](bool& enable){
App::GetApp()->m_progress_boost_mode.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [](s64& index_out){
App::SetTextScrollSpeed(index_out);
}, App::GetTextScrollSpeed()));
options->Add(std::make_shared<ui::SidebarEntryArray>("Set right-side menu"_i18n, right_side_menu_items, [menu_names](s64& index_out){
options->Add(std::make_shared<ui::SidebarEntryArray>("Set left-side menu"_i18n, side_menu_items, [menu_names](s64& index_out){
const auto e = menu_names[index_out];
if (g_app->m_right_side_menu.Get() != e) {
g_app->m_right_side_menu.Set(e);
if (g_app->m_left_menu.Get() != e) {
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();
}
));
}
}, i18n::get(g_app->m_right_side_menu.Get())));
}, 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){
const auto e = menu_names[index_out];
if (g_app->m_right_menu.Get() != e) {
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();
}
));
}
}, i18n::get(g_app->m_right_menu.Get())));
options->Add(std::make_shared<ui::SidebarEntryCallback>("Install options"_i18n, [left_side](){
App::DisplayInstallOptions(left_side);
}));
options->Add(std::make_shared<ui::SidebarEntryCallback>("Dump options"_i18n, [left_side](){
App::DisplayDumpOptions(left_side);
}));
}
void App::DisplayInstallOptions(bool left_side) {
@@ -1609,10 +1701,6 @@ void App::DisplayInstallOptions(bool left_side) {
App::SetInstallSdEnable(index_out);
}, (s64)App::GetInstallSdEnable()));
options->Add(std::make_shared<ui::SidebarEntryBool>("Boost CPU clock"_i18n, App::GetApp()->m_boost_mode.Get(), [](bool& enable){
App::GetApp()->m_boost_mode.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Allow downgrade"_i18n, App::GetApp()->m_allow_downgrade.Get(), [](bool& enable){
App::GetApp()->m_allow_downgrade.Set(enable);
}));
@@ -1645,7 +1733,7 @@ void App::DisplayInstallOptions(bool left_side) {
App::GetApp()->m_skip_ticket.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [](bool& enable){
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [](bool& enable){
App::GetApp()->m_skip_nca_hash_verify.Set(enable);
}));
@@ -1674,7 +1762,35 @@ void App::DisplayInstallOptions(bool left_side) {
}));
}
void App::DisplayDumpOptions(bool left_side) {
auto options = std::make_shared<ui::Sidebar>("Dump Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<ui::SidebarEntryBool>("Created nested folder"_i18n, App::GetApp()->m_dump_app_folder.Get(), [](bool& enable){
App::GetApp()->m_dump_app_folder.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Append folder with .xci"_i18n, App::GetApp()->m_dump_append_folder_with_xci.Get(), [](bool& enable){
App::GetApp()->m_dump_append_folder_with_xci.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Trim XCI"_i18n, App::GetApp()->m_dump_trim_xci.Get(), [](bool& enable){
App::GetApp()->m_dump_trim_xci.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Label trimmed XCI"_i18n, App::GetApp()->m_dump_label_trim_xci.Get(), [](bool& enable){
App::GetApp()->m_dump_label_trim_xci.Set(enable);
}));
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);
}));
}
App::~App() {
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
log_write("starting to exit\n");
i18n::exit();
@@ -1744,7 +1860,7 @@ App::~App() {
// found sphaira, now lets get compare version
if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
if (std::strcmp(hbmenu_nacp.display_version, sphaira_nacp.display_version) < 0) {
if (IsVersionNewer(hbmenu_nacp.display_version, sphaira_nacp.display_version)) {
if (R_FAILED(rc = fs.copy_entire_file(GetExePath(), sphaira_path))) {
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path.s, rc, R_MODULE(rc), R_DESCRIPTION(rc));
} else {
@@ -1772,14 +1888,25 @@ App::~App() {
nxlinkExit();
}
if (App::GetHddEnable()) {
log_write("closing hdd\n");
usbHsFsExit();
}
if (App::GetLogEnable()) {
log_write("closing log\n");
log_file_exit();
}
}
u64 timestamp;
timeGetCurrentTime(TimeType_LocalSystemClock, &timestamp);
ini_putl("paths", "timestamp", timestamp, App::CONFIG_PATH);
auto App::GetVersionFromString(const char* str) -> u32 {
u32 major{}, minor{}, macro{};
std::sscanf(str, "%u.%u.%u", &major, &minor, &macro);
return MAKEHOSVERSION(major, minor, macro);
}
auto App::IsVersionNewer(const char* current, const char* new_version) -> u32 {
return GetVersionFromString(current) < GetVersionFromString(new_version);
}
void App::createFramebufferResources() {

View File

@@ -3,6 +3,7 @@
#include "defines.hpp"
#include "evman.hpp"
#include "fs.hpp"
#include "app.hpp"
#include <switch.h>
#include <cstring>
@@ -45,13 +46,13 @@ struct UploadStruct {
std::span<const u8> data;
s64 offset{};
s64 size{};
FsFile f{};
fs::File f{};
};
struct DataStruct {
std::vector<u8> data;
s64 offset{};
FsFile f{};
fs::File f{};
s64 file_offset{};
};
@@ -438,7 +439,7 @@ auto ReadFileCallback(char *ptr, size_t size, size_t nmemb, void *userp) -> size
const auto realsize = size * nmemb;
u64 bytes_read;
if (R_FAILED(fsFileRead(&data_struct->f, data_struct->offset, ptr, realsize, FsReadOption_None, &bytes_read))) {
if (R_FAILED(data_struct->f.Read(data_struct->offset, ptr, realsize, FsReadOption_None, &bytes_read))) {
log_write("reading file error\n");
return 0;
}
@@ -508,7 +509,7 @@ auto WriteFileCallback(void *contents, size_t size, size_t num_files, void *user
// flush data if incomming data would overflow the buffer
if (data_struct->offset && data_struct->data.size() < data_struct->offset + realsize) {
if (R_FAILED(fsFileWrite(&data_struct->f, data_struct->file_offset, data_struct->data.data(), data_struct->offset, FsWriteOption_None))) {
if (R_FAILED(data_struct->f.Write(data_struct->file_offset, data_struct->data.data(), data_struct->offset, FsWriteOption_None))) {
return 0;
}
@@ -518,7 +519,7 @@ auto WriteFileCallback(void *contents, size_t size, size_t num_files, void *user
// we have a huge chunk! write it directly to file
if (data_struct->data.size() < realsize) {
if (R_FAILED(fsFileWrite(&data_struct->f, data_struct->file_offset, contents, realsize, FsWriteOption_None))) {
if (R_FAILED(data_struct->f.Write(data_struct->file_offset, contents, realsize, FsWriteOption_None))) {
return 0;
}
@@ -669,6 +670,9 @@ void SetCommonCurlOptions(CURL* curl, const Api& e) {
}
auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
App::SetAutoSleepDisabled(true);
ON_SCOPE_EXIT(App::SetAutoSleepDisabled(false));
// check if stop has been requested before starting download
if (e.GetToken().stop_requested()) {
return {};
@@ -758,10 +762,10 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
if (has_file) {
ON_SCOPE_EXIT( fs.DeleteFile(tmp_buf) );
if (res == CURLE_OK && chunk.offset) {
fsFileWrite(&chunk.f, chunk.file_offset, chunk.data.data(), chunk.offset, FsWriteOption_None);
chunk.f.Write(chunk.file_offset, chunk.data.data(), chunk.offset, FsWriteOption_None);
}
fsFileClose(&chunk.f);
chunk.f.Close();
if (res == CURLE_OK) {
if (http_code == 304) {
@@ -831,7 +835,7 @@ auto UploadInternal(CURL* curl, const Api& e) -> ApiResult {
return {};
}
fsFileGetSize(&chunk.f, &chunk.size);
chunk.f.GetSize(&chunk.size);
log_write("got chunk size: %zd\n", chunk.size);
} else {
if (info.m_callback) {
@@ -925,7 +929,7 @@ auto UploadInternal(CURL* curl, const Api& e) -> ApiResult {
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
if (has_file) {
fsFileClose(&chunk.f);
chunk.f.Close();
}
log_write("Uploaded %s code: %ld %s\n", url.c_str(), http_code, curl_easy_strerror(res));

381
sphaira/source/dumper.cpp Normal file
View File

@@ -0,0 +1,381 @@
#include "dumper.hpp"
#include "app.hpp"
#include "log.hpp"
#include "fs.hpp"
#include "download.hpp"
#include "defines.hpp"
#include "i18n.hpp"
#include "location.hpp"
#include "threaded_file_transfer.hpp"
#include "ui/sidebar.hpp"
#include "ui/error_box.hpp"
#include "ui/option_box.hpp"
#include "ui/progress_box.hpp"
#include "ui/popup_list.hpp"
#include "ui/nvg_util.hpp"
#include "yati/source/stream.hpp"
#include "usb/usb_uploader.hpp"
#include "usb/tinfoil.hpp"
namespace sphaira::dump {
namespace {
struct DumpEntry {
DumpLocationType type;
s32 index;
};
struct DumpLocation {
const DumpLocationType type;
const char* name;
};
constexpr DumpLocation DUMP_LOCATIONS[]{
{ DumpLocationType_SdCard, "microSD card (/dumps/)" },
{ DumpLocationType_UsbS2S, "USB transfer (Switch 2 Switch)" },
{ DumpLocationType_DevNull, "/dev/null (Speed Test)" },
};
struct UsbTest final : usb::upload::Usb, yati::source::Stream {
UsbTest(ui::ProgressBox* pbox, BaseSource* source) : Usb{UINT64_MAX} {
m_pbox = pbox;
m_source = source;
}
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override {
R_TRY(m_pull(buf, size, bytes_read));
m_pull_offset += *bytes_read;
R_SUCCEED();
}
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
if (m_pull) {
return Stream::Read(buf, off, size, bytes_read);
} else {
return ReadInternal(path, buf, off, size, bytes_read);
}
}
Result ReadInternal(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) {
if (m_path != path) {
m_path = path;
m_progress = 0;
m_pull_offset = 0;
Stream::Reset();
m_size = m_source->GetSize(path);
m_pbox->SetImage(m_source->GetIcon(path));
m_pbox->SetTitle(m_source->GetName(path));
m_pbox->NewTransfer(m_path);
}
R_TRY(m_source->Read(path, buf, off, size, bytes_read));
m_offset += *bytes_read;
m_progress += *bytes_read;
m_pbox->UpdateTransfer(m_progress, m_size);
R_SUCCEED();
}
void SetPullCallback(thread::PullCallback pull) {
m_pull = pull;
}
auto* GetSource() {
return m_source;
}
auto GetPullOffset() const {
return m_pull_offset;
}
private:
ui::ProgressBox* m_pbox{};
BaseSource* m_source{};
std::string m_path{};
thread::PullCallback m_pull{};
s64 m_offset{};
s64 m_size{};
s64 m_progress{};
s64 m_pull_offset{};
};
Result DumpToFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& root, BaseSource* source, std::span<const fs::FsPath> paths) {
constexpr s64 BIG_FILE_SIZE = 1024ULL*1024ULL*1024ULL*4ULL;
for (const auto& path : paths) {
const auto base_path = fs::AppendPath(root, path);
const auto file_size = source->GetSize(path);
pbox->SetImage(source->GetIcon(path));
pbox->SetTitle(source->GetName(path));
pbox->NewTransfer(base_path);
const auto temp_path = base_path + ".temp";
fs->CreateDirectoryRecursivelyWithPath(temp_path);
fs->DeleteFile(temp_path);
const auto flags = file_size >= BIG_FILE_SIZE ? FsCreateOption_BigFile : 0;
R_TRY(fs->CreateFile(temp_path, file_size, flags));
ON_SCOPE_EXIT(fs->DeleteFile(temp_path));
{
fs::File file;
R_TRY(fs->OpenFile(temp_path, FsOpenMode_Write, &file));
R_TRY(thread::Transfer(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return source->Read(path, data, off, size, bytes_read);
},
[&](const void* data, s64 off, s64 size) -> Result {
return file.Write(off, data, size, FsWriteOption_None);
}
));
}
fs->DeleteFile(base_path);
R_TRY(fs->RenameFile(temp_path, base_path));
}
R_SUCCEED();
}
Result DumpToFileNative(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths) {
fs::FsNativeSd fs{};
return DumpToFile(pbox, &fs, "/", source, paths);
}
Result DumpToStdio(ui::ProgressBox* pbox, const location::StdioEntry& loc, BaseSource* source, std::span<const fs::FsPath> paths) {
fs::FsStdio fs{};
return DumpToFile(pbox, &fs, loc.mount, source, paths);
}
Result DumpToUsbS2SStream(ui::ProgressBox* pbox, UsbTest* usb, std::span<const fs::FsPath> paths) {
auto source = usb->GetSource();
for (auto& path : paths) {
const auto file_size = source->GetSize(path);
R_TRY(thread::TransferPull(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return usb->ReadInternal(path, data, off, size, bytes_read);
},
[&](thread::StartThreadCallback start, thread::PullCallback pull) -> Result {
usb->SetPullCallback(pull);
R_TRY(start());
while (!pbox->ShouldExit()) {
R_TRY(usb->PollCommands());
if (usb->GetPullOffset() >= file_size) {
R_SUCCEED();
}
}
R_THROW(0xFFFF);
}
));
}
R_SUCCEED();
}
Result DumpToUsbS2SRandom(ui::ProgressBox* pbox, UsbTest* usb) {
while (!pbox->ShouldExit()) {
R_TRY(usb->PollCommands());
}
R_THROW(0xFFFF);
}
Result DumpToUsbS2S(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths) {
std::vector<std::string> file_list;
for (const auto& path : paths) {
file_list.emplace_back(path);
}
// auto usb = std::make_unique<UsbTest>(pbox, entries);
auto usb = std::make_unique<UsbTest>(pbox, source);
constexpr u64 timeout = 1e+9;
while (!pbox->ShouldExit()) {
if (R_SUCCEEDED(usb->IsUsbConnected(timeout))) {
pbox->NewTransfer("USB connected, sending file list");
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...");
Result rc;
if (flags & usb::tinfoil::USBFlag_STREAM) {
rc = DumpToUsbS2SStream(pbox, usb.get(), paths);
} else {
rc = DumpToUsbS2SRandom(pbox, usb.get());
}
// wait for exit command.
if (R_SUCCEEDED(rc)) {
log_write("waiting for exit command\n");
rc = usb->PollCommands();
log_write("finished polling for exit command\n");
} else {
log_write("skipped polling for exit command\n");
}
if (rc == usb->Result_Exit) {
log_write("got exit command\n");
R_SUCCEED();
}
return rc;
}
} else {
pbox->NewTransfer("waiting for usb connection...");
}
}
R_THROW(0xFFFF);
}
Result DumpToDevNull(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths) {
for (auto path : paths) {
R_TRY(pbox->ShouldExitResult());
const auto file_size = source->GetSize(path);
pbox->SetImage(source->GetIcon(path));
pbox->SetTitle(source->GetName(path));
pbox->NewTransfer(path);
R_TRY(thread::Transfer(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return source->Read(path, data, off, size, bytes_read);
},
[&](const void* data, s64 off, s64 size) -> Result {
R_SUCCEED();
}
));
}
R_SUCCEED();
}
Result DumpToNetwork(ui::ProgressBox* pbox, const location::Entry& loc, BaseSource* source, std::span<const fs::FsPath> paths) {
for (auto path : paths) {
R_TRY(pbox->ShouldExitResult());
const auto file_size = source->GetSize(path);
pbox->SetImage(source->GetIcon(path));
pbox->SetTitle(source->GetName(path));
pbox->NewTransfer(path);
R_TRY(thread::TransferPull(pbox, file_size,
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
return source->Read(path, data, off, size, bytes_read);
},
[&](thread::PullCallback pull) -> Result {
s64 offset{};
const auto result = curl::Api().FromMemory(
CURL_LOCATION_TO_API(loc),
curl::OnProgress{pbox->OnDownloadProgressCallback()},
curl::UploadInfo{
path, file_size,
[&](void *ptr, size_t size) -> size_t {
// curl will request past the size of the file, causing an error.
if (offset >= file_size) {
log_write("finished file upload\n");
return 0;
}
u64 bytes_read{};
if (R_FAILED(pull(ptr, size, &bytes_read))) {
log_write("failed to read in custom callback: %zd size: %zd\n", offset, size);
return 0;
}
offset += bytes_read;
return bytes_read;
}
}
);
R_UNLESS(result.success, 0x1);
R_SUCCEED();
}
));
}
R_SUCCEED();
}
} // namespace
void Dump(std::shared_ptr<BaseSource> source, const std::vector<fs::FsPath>& paths, OnExit on_exit, u32 location_flags) {
ui::PopupList::Items items;
std::vector<DumpEntry> dump_entries;
const auto network_locations = location::Load();
if (location_flags & (1 << DumpLocationType_Network)) {
for (s32 i = 0; i < std::size(network_locations); i++) {
dump_entries.emplace_back(DumpLocationType_Network, i);
items.emplace_back(network_locations[i].name);
}
}
const auto stdio_locations = location::GetStdio(true);
if (location_flags & (1 << DumpLocationType_Stdio)) {
for (s32 i = 0; i < std::size(stdio_locations); i++) {
dump_entries.emplace_back(DumpLocationType_Stdio, i);
items.emplace_back(stdio_locations[i].name);
}
}
for (s32 i = 0; i < std::size(DUMP_LOCATIONS); i++) {
if (location_flags & (1 << DUMP_LOCATIONS[i].type)) {
dump_entries.emplace_back(DUMP_LOCATIONS[i].type, i);
items.emplace_back(i18n::get(DUMP_LOCATIONS[i].name));
}
}
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);
}));
}
));
}
} // namespace sphaira::dump

View File

@@ -119,13 +119,20 @@ Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& _path, bool ig
R_UNLESS(ignore_read_only || !is_read_only_root(_path), Fs::ResultReadOnly);
auto path_view = std::string_view{_path};
// todo: fix this for sdmc: and ums0:
FsPath path{"/"};
if (auto s = std::strchr(_path.s, ':')) {
const int len = (s - _path.s) + 1;
std::snprintf(path, sizeof(path), "%.*s/", len, _path.s);
path_view = path_view.substr(len);
}
for (const auto dir : std::views::split(path_view, '/')) {
if (dir.empty()) {
continue;
}
std::strncat(path, dir.data(), dir.size());
log_write("[FS] dir creation path is now: %s\n", path.s);
Result rc;
if (fs) {
@@ -214,6 +221,11 @@ Result GetFileTimeStampRaw(FsFileSystem* fs, const FsPath& path, FsTimeStampRaw
return fsFsGetFileTimeStampRaw(fs, path, out);
}
Result SetTimestamp(FsFileSystem* fs, const FsPath& path, const FsTimeStampRaw* ts) {
// unsuported.
R_SUCCEED();
}
bool FileExists(FsFileSystem* fs, const FsPath& path) {
FsDirEntryType type;
R_TRY_RESULT(GetEntryType(fs, path, &type), false);
@@ -230,16 +242,16 @@ Result read_entire_file(FsFileSystem* _fs, const FsPath& path, std::vector<u8>&
FsNative fs{_fs, false};
R_TRY(fs.GetFsOpenResult());
FsFile f;
File f;
R_TRY(fs.OpenFile(path, FsOpenMode_Read, &f));
ON_SCOPE_EXIT(fsFileClose(&f));
ON_SCOPE_EXIT(f.Close());
s64 size;
R_TRY(fsFileGetSize(&f, &size));
R_TRY(f.GetSize(&size));
out.resize(size);
u64 bytes_read;
R_TRY(fsFileRead(&f, 0, out.data(), out.size(), FsReadOption_None, &bytes_read));
R_TRY(f.Read(0, out.data(), out.size(), FsReadOption_None, &bytes_read));
R_UNLESS(bytes_read == out.size(), 1);
R_SUCCEED();
@@ -255,12 +267,12 @@ Result write_entire_file(FsFileSystem* _fs, const FsPath& path, const std::vecto
return rc;
}
FsFile f;
File f;
R_TRY(fs.OpenFile(path, FsOpenMode_Write, &f));
ON_SCOPE_EXIT(fsFileClose(&f));
ON_SCOPE_EXIT(f.Close());
R_TRY(fsFileSetSize(&f, in.size()));
R_TRY(fsFileWrite(&f, 0, in.data(), in.size(), FsWriteOption_None));
R_TRY(f.SetSize(in.size()));
R_TRY(f.Write(0, in.data(), in.size(), FsWriteOption_None));
R_SUCCEED();
}
@@ -276,19 +288,32 @@ 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);
auto fd = open(path, O_CREAT | S_IRUSR | S_IWUSR);
auto fd = open(path, O_WRONLY | O_CREAT, DEFFILEMODE);
if (fd == -1) {
if (errno == EEXIST) {
return FsError_PathAlreadyExists;
}
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
close(fd);
ON_SCOPE_EXIT(close(fd));
if (size) {
R_UNLESS(!ftruncate(fd, size), Fs::ResultUnknownStdioError);
}
R_SUCCEED();
}
Result CreateDirectory(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only_root(path), Fs::ResultReadOnly);
if (mkdir(path, 0777)) {
if (mkdir(path, ACCESSPERMS)) {
if (errno == EEXIST) {
return FsError_PathAlreadyExists;
}
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
@@ -310,7 +335,7 @@ Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_o
Result DeleteFile(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
if (remove(path)) {
if (unlink(path)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
@@ -320,7 +345,11 @@ Result DeleteFile(const FsPath& path, bool ignore_read_only) {
Result DeleteDirectory(const FsPath& path, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
return DeleteFile(path, ignore_read_only);
if (rmdir(path)) {
R_TRY(fsdevGetLastResult());
return Fs::ResultUnknownStdioError;
}
R_SUCCEED();
}
// ftw / ntfw isn't found by linker...
@@ -386,6 +415,20 @@ Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out) {
R_SUCCEED();
}
Result SetTimestamp(const FsPath& path, const FsTimeStampRaw* ts) {
if (ts->is_valid) {
timeval val[2]{};
val[0].tv_sec = ts->accessed;
val[1].tv_sec = ts->modified;
if (utimes(path, val)) {
log_write("utimes() failed: %d %s\n", errno, strerror(errno));
}
}
R_SUCCEED();
}
bool FileExists(const FsPath& path) {
FsDirEntryType type;
R_TRY_RESULT(GetEntryType(path, &type), false);
@@ -438,4 +481,321 @@ Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_o
return write_entire_file(dst, data, ignore_read_only);
}
Result OpenFile(fs::Fs* fs, const fs::FsPath& path, u32 mode, File* f) {
f->m_fs = fs;
if (f->m_fs->IsNative()) {
auto fs = (fs::FsNative*)f->m_fs;
R_TRY(fsFsOpenFile(&fs->m_fs, path, mode, &f->m_native));
} else {
if ((mode & FsOpenMode_Read) && (mode & FsOpenMode_Write)) {
f->m_stdio = std::fopen(path, "rb+");
} else if (mode & FsOpenMode_Read) {
f->m_stdio = std::fopen(path, "rb");
} else if (mode & FsOpenMode_Write) {
// not possible to open file with just write and not append
// or create or truncate. So rw it is!
f->m_stdio = std::fopen(path, "rb+");
}
R_UNLESS(f->m_stdio, Fs::ResultUnknownStdioError);
std::strcpy(f->m_path, path);
}
R_SUCCEED();
}
File::~File() {
Close();
}
Result File::Read( s64 off, void* buf, u64 read_size, u32 option, u64* bytes_read) {
if (m_fs->IsNative()) {
R_TRY(fsFileRead(&m_native, off, buf, read_size, option, bytes_read));
} else {
if (m_stdio_off != off) {
m_stdio_off = off;
std::fseek(m_stdio, off, SEEK_SET);
}
*bytes_read = std::fread(buf, 1, read_size, m_stdio);
// 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);
}
}
m_stdio_off += *bytes_read;
}
R_SUCCEED();
}
Result File::Write(s64 off, const void* buf, u64 write_size, u32 option) {
if (m_fs->IsNative()) {
R_TRY(fsFileWrite(&m_native, off, buf, write_size, option));
} else {
if (m_stdio_off != off) {
log_write("[FS] diff seek\n");
m_stdio_off = off;
std::fseek(m_stdio, off, SEEK_SET);
}
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);
m_stdio_off += write_size;
}
R_SUCCEED();
}
Result File::SetSize(s64 sz) {
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_SUCCEED();
}
Result File::GetSize(s64* out) {
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);
}
*out = st.st_size;
}
R_SUCCEED();
}
void File::Close() {
if (m_fs->IsNative()) {
if (serviceIsActive(&m_native.s)) {
fsFileClose(&m_native);
m_native = {};
}
} else {
if (m_stdio) {
std::fclose(m_stdio);
m_stdio = {};
}
}
}
Result OpenDirectory(fs::Fs* fs, const fs::FsPath& path, u32 mode, Dir* d) {
d->m_fs = fs;
d->m_mode = mode;
if (d->m_fs->IsNative()) {
auto fs = (fs::FsNative*)d->m_fs;
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_SUCCEED();
}
Result DirGetEntryCount(fs::Fs* m_fs, const fs::FsPath& path, s64* count, u32 mode) {
s64 file_count, dir_count;
R_TRY(DirGetEntryCount(m_fs, path, &file_count, &dir_count, mode));
*count = file_count + dir_count;
R_SUCCEED();
}
Result DirGetEntryCount(fs::Fs* m_fs, const fs::FsPath& path, s64* file_count, s64* dir_count, u32 mode) {
*file_count = *dir_count = 0;
if (m_fs->IsNative()) {
if (mode & FsDirOpenMode_ReadDirs){
fs::Dir dir;
R_TRY(m_fs->OpenDirectory(path, FsDirOpenMode_ReadDirs|FsDirOpenMode_NoFileSize, &dir));
R_TRY(dir.GetEntryCount(file_count));
}
if (mode & FsDirOpenMode_ReadFiles){
fs::Dir dir;
R_TRY(m_fs->OpenDirectory(path, FsDirOpenMode_ReadFiles|FsDirOpenMode_NoFileSize, &dir));
R_TRY(dir.GetEntryCount(file_count));
}
} else {
fs::Dir dir;
R_TRY(m_fs->OpenDirectory(path, mode, &dir));
while (auto d = readdir(dir.m_stdio)) {
if (!std::strcmp(d->d_name, ".") || !std::strcmp(d->d_name, "..")) {
continue;
}
if (d->d_type == DT_DIR) {
if (!(mode & FsDirOpenMode_ReadDirs)) {
continue;
}
(*dir_count)++;
} else if (d->d_type == DT_REG) {
if (!(mode & FsDirOpenMode_ReadFiles)) {
continue;
}
(*file_count)++;
}
}
}
R_SUCCEED();
}
Dir::~Dir() {
Close();
}
Result Dir::GetEntryCount(s64* out) {
*out = 0;
if (m_fs->IsNative()) {
R_TRY(fsDirGetEntryCount(&m_native, out));
} else {
while (auto d = readdir(m_stdio)) {
if (!std::strcmp(d->d_name, ".") || !std::strcmp(d->d_name, "..")) {
continue;
}
(*out)++;
}
// NOTE: this will *not* work for native mounted folders!!!
rewinddir(m_stdio);
}
R_SUCCEED();
}
Result Dir::ReadAll(std::vector<FsDirectoryEntry>& buf) {
buf.clear();
if (m_fs->IsNative()) {
s64 count;
R_TRY(GetEntryCount(&count));
buf.resize(count);
R_TRY(fsDirRead(&m_native, &count, buf.size(), buf.data()));
buf.resize(count);
} else {
buf.reserve(1000);
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);
buf.emplace_back(entry);
}
}
R_SUCCEED();
}
void Dir::Close() {
if (m_fs->IsNative()) {
if (serviceIsActive(&m_native.s)) {
fsDirClose(&m_native);
m_native = {};
}
} else {
if (m_stdio) {
closedir(m_stdio);
m_stdio = {};
}
}
}
Result FileGetSizeAndTimestamp(fs::Fs* m_fs, const FsPath& path, FsTimeStampRaw* ts, s64* size) {
*ts = {};
*size = {};
if (m_fs->IsNative()) {
auto fs = (fs::FsNative*)m_fs;
R_TRY(fs->GetFileTimeStampRaw(path, ts));
File f;
R_TRY(m_fs->OpenFile(path, FsOpenMode_Read, &f));
ON_SCOPE_EXIT(f.Close());
R_TRY(f.GetSize(size));
} else {
struct stat st;
R_UNLESS(!lstat(path, &st), 0x1);
ts->is_valid = true;
ts->created = st.st_ctim.tv_sec;
ts->modified = st.st_mtim.tv_sec;
ts->accessed = st.st_atim.tv_sec;
*size = st.st_size;
}
R_SUCCEED();
}
Result IsDirEmpty(fs::Fs* m_fs, const fs::FsPath& path, bool* out) {
*out = true;
if (m_fs->IsNative()) {
auto fs = (fs::FsNative*)m_fs;
s64 count;
R_TRY(fs->DirGetEntryCount(path, &count, FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles));
*out = !count;
} else {
auto dir = opendir(path);
R_UNLESS(dir, 0x1);
ON_SCOPE_EXIT(closedir(dir));
while (auto d = readdir(dir)) {
if (!std::strcmp(d->d_name, ".") || !std::strcmp(d->d_name, "..")) {
continue;
}
*out = false;
break;
}
}
R_SUCCEED();
}
} // namespace fs

180
sphaira/source/hasher.cpp Normal file
View File

@@ -0,0 +1,180 @@
#include "hasher.hpp"
#include <mbedtls/md5.h>
namespace sphaira::hash {
namespace {
consteval auto CalculateHashStrLen(s64 buf_size) {
return buf_size * 2 + 1;
}
struct FileSource final : BaseSource {
FileSource(fs::Fs* fs, const fs::FsPath& path) : m_fs{fs} {
m_open_result = m_fs->OpenFile(path, FsOpenMode_Read, std::addressof(m_file));
}
Result Size(s64* out) {
return m_file.GetSize(out);
}
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) {
return m_file.Read(off, buf, size, 0, bytes_read);
}
private:
fs::Fs* m_fs{};
fs::File m_file{};
Result m_open_result{};
};
struct HashSource {
virtual ~HashSource() = default;
virtual void Update(const void* buf, s64 size) = 0;
virtual void Get(std::string& out) = 0;
};
struct HashCrc32 final : HashSource {
void Update(const void* buf, s64 size) {
m_seed = crc32CalculateWithSeed(m_seed, buf, size);
}
void Get(std::string& out) {
char str[CalculateHashStrLen(sizeof(m_seed))];
std::snprintf(str, sizeof(str), "%08x", m_seed);
out = str;
}
private:
u32 m_seed{};
};
struct HashMd5 final : HashSource {
HashMd5() {
mbedtls_md5_init(&m_ctx);
mbedtls_md5_starts_ret(&m_ctx);
}
~HashMd5() {
mbedtls_md5_free(&m_ctx);
}
void Update(const void* buf, s64 size) {
mbedtls_md5_update_ret(&m_ctx, (const u8*)buf, size);
}
void Get(std::string& out) {
u8 hash[16];
mbedtls_md5_finish_ret(&m_ctx, hash);
char str[CalculateHashStrLen(sizeof(hash))];
for (u32 i = 0; i < sizeof(hash); i++) {
std::sprintf(str + i * 2, "%02x", hash[i]);
}
out = str;
}
private:
mbedtls_md5_context m_ctx{};
};
struct HashSha1 final : HashSource {
HashSha1() {
sha1ContextCreate(&m_ctx);
}
void Update(const void* buf, s64 size) {
sha1ContextUpdate(&m_ctx, buf, size);
}
void Get(std::string& out) {
u8 hash[SHA1_HASH_SIZE];
sha1ContextGetHash(&m_ctx, hash);
char str[CalculateHashStrLen(sizeof(hash))];
for (u32 i = 0; i < sizeof(hash); i++) {
std::sprintf(str + i * 2, "%02x", hash[i]);
}
out = str;
}
private:
Sha1Context m_ctx{};
};
struct HashSha256 final : HashSource {
HashSha256() {
sha256ContextCreate(&m_ctx);
}
void Update(const void* buf, s64 size) {
sha256ContextUpdate(&m_ctx, buf, size);
}
void Get(std::string& out) {
u8 hash[SHA256_HASH_SIZE];
sha256ContextGetHash(&m_ctx, hash);
char str[CalculateHashStrLen(sizeof(hash))];
for (u32 i = 0; i < sizeof(hash); i++) {
std::sprintf(str + i * 2, "%02x", hash[i]);
}
out = str;
}
private:
Sha256Context m_ctx{};
};
Result Hash(ui::ProgressBox* pbox, std::unique_ptr<HashSource> hash, std::shared_ptr<BaseSource> source, std::string& out) {
s64 size;
R_TRY(source->Size(&size));
s64 offset{};
std::vector<u8> chunk(1024 * 512);
while (offset < size) {
R_TRY(pbox->ShouldExitResult());
const auto rsize = std::min<s64>(chunk.size(), size - offset);
u64 bytes_read;
R_TRY(source->Read(chunk.data(), offset, rsize, &bytes_read));
hash->Update(chunk.data(), bytes_read);
offset += bytes_read;
pbox->UpdateTransfer(offset, size);
}
hash->Get(out);
R_SUCCEED();
}
} // namespace
auto GetTypeStr(Type type) -> const char* {
switch (type) {
case Type::Crc32: return "CRC32";
case Type::Md5: return "MD5";
case Type::Sha1: return "SHA1";
case Type::Sha256: return "SHA256";
}
return "";
}
Result Hash(ui::ProgressBox* pbox, Type type, std::shared_ptr<BaseSource> source, std::string& out) {
switch (type) {
case Type::Crc32: return Hash(pbox, std::make_unique<HashCrc32>(), source, out);
case Type::Md5: return Hash(pbox, std::make_unique<HashMd5>(), source, out);
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);
}
Result Hash(ui::ProgressBox* pbox, Type type, fs::Fs* fs, const fs::FsPath& path, std::string& out) {
auto source = std::make_shared<FileSource>(fs, path);
return Hash(pbox, type, source, out);
}
} // namespace sphaira::has

View File

@@ -78,6 +78,7 @@ bool init(long index) {
case 11: setLanguage = SetLanguage_RU; break; // "Russian"
case 12: lang_name = "se"; break; // "Swedish"
case 13: lang_name = "vi"; break; // "Vietnamese"
case 14: lang_name = "uk"; break; // "Ukrainian"
}
switch (setLanguage) {
@@ -86,7 +87,7 @@ bool init(long index) {
case SetLanguage_DE: lang_name = "de"; break;
case SetLanguage_IT: lang_name = "it"; break;
case SetLanguage_ES: lang_name = "es"; break;
case SetLanguage_ZHCN: lang_name = "zh"; break;
case SetLanguage_ZHCN: lang_name = "zh"; break;
case SetLanguage_KO: lang_name = "ko"; break;
case SetLanguage_NL: lang_name = "nl"; break;
case SetLanguage_PT: lang_name = "pt"; break;

View File

@@ -1,8 +1,10 @@
#include "location.hpp"
#include "fs.hpp"
#include "app.hpp"
#include <cstring>
#include <minIni.h>
#include <usbhsfs.h>
namespace sphaira::location {
namespace {
@@ -72,4 +74,35 @@ auto Load() -> Entries {
return out;
}
auto GetStdio(bool write) -> StdioEntries {
if (!App::GetHddEnable()) {
log_write("[USBHSFS] not enabled\n");
return {};
}
static UsbHsFsDevice devices[0x20];
const auto count = usbHsFsListMountedDevices(devices, std::size(devices));
log_write("[USBHSFS] got connected: %u\n", usbHsFsGetPhysicalDeviceCount());
log_write("[USBHSFS] got count: %u\n", count);
StdioEntries out{};
for (s32 i = 0; i < count; i++) {
const auto& e = devices[i];
if (write && (e.write_protect || (e.flags & UsbHsFsMountFlags_ReadOnly))) {
log_write("[USBHSFS] skipping write protect\n");
continue;
}
char display_name[0x100];
std::snprintf(display_name, sizeof(display_name), "%s (%s - %s - %zu GB)", e.name, LIBUSBHSFS_FS_TYPE_STR(e.fs_type), e.product_name, e.capacity / 1024 / 1024 / 1024);
out.emplace_back(e.name, display_name, e.write_protect);
log_write("\t[USBHSFS] %s name: %s serial: %s man: %s\n", e.name, e.product_name, e.serial_number, e.manufacturer);
}
return out;
}
} // namespace sphaira::location

View File

@@ -27,49 +27,52 @@ struct NroData {
NroHeader header;
};
auto nro_parse_internal(fs::FsNative& fs, const fs::FsPath& path, NroEntry& entry) -> Result {
auto nro_parse_internal(fs::Fs* fs, const fs::FsPath& path, NroEntry& entry) -> Result {
entry.path = path;
// todo: special sorting for fw 2.0.0 to make it not look like shit
if (hosversionAtLeast(3,0,0)) {
// it doesn't matter if we fail
entry.timestamp.is_valid = false;
fs.GetFileTimeStampRaw(entry.path, &entry.timestamp);
fs->GetFileTimeStampRaw(entry.path, &entry.timestamp);
// if (R_FAILED(fsFsGetFileTimeStampRaw(fs, entry.path, &entry.timestamp))) {
// // log_write("failed to get timestamp for: %s\n", path);
// }
}
FsFile f;
R_TRY(fs.OpenFile(entry.path, FsOpenMode_Read, &f));
ON_SCOPE_EXIT(fsFileClose(&f));
R_TRY(fsFileGetSize(&f, &entry.size));
fs::File f;
R_TRY(fs->OpenFile(entry.path, FsOpenMode_Read, &f));
// todo: buffer reads to 16k to avoid 2 fs read calls per entry.
NroData data;
u64 bytes_read;
R_TRY(fsFileRead(&f, 0, &data, sizeof(data), FsReadOption_None, &bytes_read));
R_TRY(f.Read(0, &data, sizeof(data), FsReadOption_None, &bytes_read));
R_UNLESS(data.header.magic == NROHEADER_MAGIC, NroError_BadMagic);
NroAssetHeader asset;
R_TRY(fsFileRead(&f, data.header.size, &asset, sizeof(asset), FsReadOption_None, &bytes_read));
R_TRY(f.Read(data.header.size, &asset, sizeof(asset), FsReadOption_None, &bytes_read));
// R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, NroError_BadMagic);
// we can avoid a GetSize() call by calculating the size manually.
entry.size = data.header.size;
// some .nro (vgedit) have bad nacp, fake the nacp
if (asset.magic != NROASSETHEADER_MAGIC || asset.nacp.offset == 0 || asset.nacp.size != sizeof(entry.nacp)) {
std::memset(&entry.nacp, 0, sizeof(entry.nacp));
auto& nacp = entry.nacp;
if (asset.magic != NROASSETHEADER_MAGIC || asset.nacp.offset == 0 || asset.nacp.size != sizeof(NacpStruct)) {
std::memset(&asset, 0, sizeof(asset));
std::memset(&nacp, 0, sizeof(nacp));
// get the name without the .nro
const auto file_name = std::strrchr(path, '/') + 1;
const auto file_name_len = std::strlen(file_name);
for (auto& lang : entry.nacp.lang) {
std::strncpy(lang.name, file_name, file_name_len - 4);
std::strcpy(lang.author, "Unknown");
}
std::strcpy(entry.nacp.display_version, "Unknown");
std::strncpy(nacp.lang.name, file_name, file_name_len - 4);
std::strcpy(nacp.lang.author, "Unknown");
std::strcpy(nacp.display_version, "Unknown");
entry.is_nacp_valid = false;
} else {
R_TRY(fsFileRead(&f, data.header.size + asset.nacp.offset, &entry.nacp, sizeof(entry.nacp), FsReadOption_None, &bytes_read));
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));
entry.is_nacp_valid = true;
}
@@ -83,32 +86,19 @@ auto nro_parse_internal(fs::FsNative& fs, const fs::FsPath& path, NroEntry& entr
// this function is recursive by 1 level deep
// if the nro is in switch/folder/folder2/app.nro it will NOT be found
// switch/folder/app.nro for example will work fine.
auto nro_scan_internal(const fs::FsPath& path, std::vector<NroEntry>& nros, bool hide_sphaira, bool nested, bool scan_all_dir, bool root) -> Result {
fs::FsNativeSd fs;
R_TRY(fs.GetFsOpenResult());
auto nro_scan_internal(fs::Fs* fs, const fs::FsPath& path, std::vector<NroEntry>& nros, bool hide_sphaira, bool nested, bool scan_all_dir, bool root) -> Result {
// we don't need to scan for folders if we are not root
u32 dir_open_type = FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize;
if (root) {
dir_open_type |= FsDirOpenMode_ReadDirs;
}
FsDir d;
R_TRY(fs.OpenDirectory(path, dir_open_type, &d));
ON_SCOPE_EXIT(fs.DirClose(&d));
s64 count;
R_TRY(fs.DirGetEntryCount(&d, &count));
// return early if empty
R_UNLESS(count > 0, 0x0);
fs::Dir d;
R_TRY(fs->OpenDirectory(path, dir_open_type, &d));
// we won't run out of memory here
std::vector<FsDirectoryEntry> entries(count);
R_TRY(fs.DirRead(&d, &count, entries.size(), entries.data()));
// size may of changed
entries.resize(count);
std::vector<FsDirectoryEntry> entries;
R_TRY(d.ReadAll(entries));
for (const auto& e : entries) {
// skip hidden files / folders
@@ -134,7 +124,7 @@ auto nro_scan_internal(const fs::FsPath& path, std::vector<NroEntry>& nros, bool
} else {
// slow path...
std::snprintf(fullpath, sizeof(fullpath), "%s/%s", path.s, e.name);
nro_scan_internal(fullpath, nros, hide_sphaira, nested, scan_all_dir, false);
nro_scan_internal(fs, fullpath, nros, hide_sphaira, nested, scan_all_dir, false);
}
} else if (e.type == FsDirEntryType_File && std::string_view{e.name}.ends_with(".nro")) {
fs::FsPath fullpath;
@@ -156,12 +146,22 @@ auto nro_scan_internal(const fs::FsPath& path, std::vector<NroEntry>& nros, bool
R_SUCCEED();
}
auto nro_get_icon_internal(FsFile* f, u64 size, u64 offset) -> std::vector<u8> {
auto nro_scan_internal(const fs::FsPath& path, std::vector<NroEntry>& nros, bool hide_sphaira, bool nested, bool scan_all_dir, bool root) -> Result {
fs::FsNativeSd fs;
return nro_scan_internal(&fs, path, nros, hide_sphaira, nested, scan_all_dir, root);
}
auto nro_get_icon_internal(fs::File* f, u64 size, u64 offset) -> std::vector<u8> {
// protect again really messed up sizes.
if (size > 1024 * 1024) {
return {};
}
std::vector<u8> icon;
u64 bytes_read{};
icon.resize(size);
R_TRY_RESULT(fsFileRead(f, offset, icon.data(), icon.size(), FsReadOption_None, &bytes_read), {});
R_TRY_RESULT(f->Read(offset, icon.data(), icon.size(), FsReadOption_None, &bytes_read), {});
R_UNLESS(bytes_read == icon.size(), {});
return icon;
@@ -202,9 +202,7 @@ auto nro_verify(std::span<const u8> data) -> Result {
auto nro_parse(const fs::FsPath& path, NroEntry& entry) -> Result {
fs::FsNativeSd fs;
R_TRY(fs.GetFsOpenResult());
return nro_parse_internal(fs, path, entry);
return nro_parse_internal(&fs, path, entry);
}
auto nro_scan(const fs::FsPath& path, std::vector<NroEntry>& nros, bool hide_sphaira, bool nested, bool scan_all_dir) -> Result {
@@ -213,31 +211,23 @@ auto nro_scan(const fs::FsPath& path, std::vector<NroEntry>& nros, bool hide_sph
auto nro_get_icon(const fs::FsPath& path, u64 size, u64 offset) -> std::vector<u8> {
fs::FsNativeSd fs;
FsFile f;
R_TRY_RESULT(fs.GetFsOpenResult(), {});
fs::File f;
R_TRY_RESULT(fs.OpenFile(path, FsOpenMode_Read, &f), {});
ON_SCOPE_EXIT(fsFileClose(&f));
return nro_get_icon_internal(&f, size, offset);
}
auto nro_get_icon(const fs::FsPath& path) -> std::vector<u8> {
fs::FsNativeSd fs;
FsFile f;
NroData data;
NroAssetHeader asset;
u64 bytes_read;
R_TRY_RESULT(fs.GetFsOpenResult(), {});
fs::File f;
R_TRY_RESULT(fs.OpenFile(path, FsOpenMode_Read, &f), {});
ON_SCOPE_EXIT(fsFileClose(&f));
R_TRY_RESULT(fsFileRead(&f, 0, &data, sizeof(data), FsReadOption_None, &bytes_read), {});
R_TRY_RESULT(f.Read(0, &data, sizeof(data), FsReadOption_None, &bytes_read), {});
R_UNLESS(data.header.magic == NROHEADER_MAGIC, {});
R_TRY_RESULT(fsFileRead(&f, data.header.size, &asset, sizeof(asset), FsReadOption_None, &bytes_read), {});
R_TRY_RESULT(f.Read(data.header.size, &asset, sizeof(asset), FsReadOption_None, &bytes_read), {});
R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, {});
return nro_get_icon_internal(&f, asset.icon.size, data.header.size + asset.icon.offset);
@@ -245,21 +235,18 @@ auto nro_get_icon(const fs::FsPath& path) -> std::vector<u8> {
auto nro_get_nacp(const fs::FsPath& path, NacpStruct& nacp) -> Result {
fs::FsNativeSd fs;
FsFile f;
NroData data;
NroAssetHeader asset;
u64 bytes_read;
R_TRY_RESULT(fs.GetFsOpenResult(), {});
fs::File f;
R_TRY(fs.OpenFile(path, FsOpenMode_Read, &f));
ON_SCOPE_EXIT(fsFileClose(&f));
R_TRY(fsFileRead(&f, 0, &data, sizeof(data), FsReadOption_None, &bytes_read));
R_TRY(f.Read(0, &data, sizeof(data), FsReadOption_None, &bytes_read));
R_UNLESS(data.header.magic == NROHEADER_MAGIC, NroError_BadMagic);
R_TRY(fsFileRead(&f, data.header.size, &asset, sizeof(asset), FsReadOption_None, &bytes_read));
R_TRY(f.Read(data.header.size, &asset, sizeof(asset), FsReadOption_None, &bytes_read));
R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, NroError_BadMagic);
R_TRY(fsFileRead(&f, data.header.size + asset.nacp.offset, &nacp, sizeof(nacp), FsReadOption_None, &bytes_read));
R_TRY(f.Read(data.header.size + asset.nacp.offset, &nacp, sizeof(nacp), FsReadOption_None, &bytes_read));
R_SUCCEED();
}

View File

@@ -357,15 +357,14 @@ void loop(void* args) {
ON_SCOPE_EXIT(fs.DeleteFile(temp_path));
{
FsFile f;
fs::File f;
if (R_FAILED(rc = fs.OpenFile(temp_path, FsOpenMode_Write, &f))) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
log_write("failed to open file %X\n", rc);
continue;
}
ON_SCOPE_EXIT(fsFileClose(&f));
if (R_FAILED(rc = fsFileSetSize(&f, file_data.size()))) {
if (R_FAILED(rc = f.SetSize(file_data.size()))) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
log_write("failed to set file size: 0x%X\n", socketGetLastResult());
continue;
@@ -379,7 +378,7 @@ void loop(void* args) {
if (offset + chunk_size > file_data.size()) {
chunk_size = file_data.size() - offset;
}
if (R_FAILED(rc = fsFileWrite(&f, offset, file_data.data() + offset, chunk_size, FsWriteOption_None))) {
if (R_FAILED(rc = f.Write(offset, file_data.data() + offset, chunk_size, FsWriteOption_None))) {
break;
}
offset += chunk_size;

View File

@@ -3,7 +3,33 @@
#include "option.hpp"
#include "app.hpp"
#include <cctype>
#include <cstring>
#include <cstdlib>
namespace sphaira::option {
namespace {
// these are taken from minini in order to parse a value already loaded in memory.
long getl(const char* LocalBuffer, long def) {
const auto len = strlen(LocalBuffer);
return (len == 0) ? def
: ((len >= 2 && toupper((int)LocalBuffer[1]) == 'X') ? strtol(LocalBuffer, NULL, 16)
: strtol(LocalBuffer, NULL, 10));
}
bool getbool(const char* LocalBuffer, bool def) {
const auto c = toupper(LocalBuffer[0]);
if (c == 'Y' || c == '1' || c == 'T')
return true;
else if (c == 'N' || c == '0' || c == 'F')
return false;
else
return def;
}
} // namespace
template<typename T>
auto OptionBase<T>::GetInternal(const char* name) -> T {
@@ -47,6 +73,28 @@ void OptionBase<T>::Set(T value) {
}
}
template<typename T>
auto OptionBase<T>::LoadFrom(const char* section, const char* name, const char* value) -> bool {
return m_section == section && LoadFrom(name, value);
}
template<typename T>
auto OptionBase<T>::LoadFrom(const char* name, const char* value) -> bool {
if (m_name == name) {
if constexpr(std::is_same_v<T, bool>) {
m_value = getbool(value, m_default_value);
} else if constexpr(std::is_same_v<T, long>) {
m_value = getl(value, m_default_value);
} else if constexpr(std::is_same_v<T, std::string>) {
m_value = value;
}
return true;
}
return false;
}
template struct OptionBase<bool>;
template struct OptionBase<long>;
template struct OptionBase<std::string>;

View File

@@ -34,6 +34,14 @@ constexpr u32 PFS0_PADDING_SIZE = 0x200;
constexpr u32 ROMFS_ENTRY_EMPTY = 0xFFFFFFFF;
constexpr u32 ROMFS_FILEPARTITION_OFS = 0x200;
constexpr const u8 HBL_MAIN_DATA[]{
#embed <exefs/main>
};
constexpr const u8 HBL_NPDM_DATA[]{
#embed <exefs/main.npdm>
};
// stdio-like wrapper for std::vector
struct BufHelper {
BufHelper() = default;
@@ -831,6 +839,9 @@ auto create_meta_nca(u64 tid, const keys::Keys& keys, NcmStorageId storage_id, c
}
auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId storage_id) -> Result {
pbox->SetTitle(config.name);
pbox->SetImageDataConst(config.icon);
R_UNLESS(!config.nro_path.empty(), OwoError_BadArgs);
// R_UNLESS(!config.icon.empty(), OwoError_BadArgs);
@@ -864,13 +875,10 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
// create program
if (config.program_nca.empty()) {
R_UNLESS(!config.main.empty(), OwoError_BadArgs);
R_UNLESS(!config.npdm.empty(), OwoError_BadArgs);
pbox->NewTransfer("Creating Program"_i18n).UpdateTransfer(0, 8);
FileEntries exefs;
add_file_entry(exefs, "main", config.main);
add_file_entry(exefs, "main.npdm", config.npdm);
add_file_entry(exefs, "main", HBL_MAIN_DATA);
add_file_entry(exefs, "main.npdm", HBL_NPDM_DATA);
FileEntries romfs;
add_file_entry(romfs, "/nextArgv", config.args.data(), config.args.length());
@@ -984,7 +992,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
// remove old id for forwarders.
const auto rc = nsDeleteApplicationCompletely(old_tid);
if (R_FAILED(rc) && rc != 0x410) { // not found
App::Notify("Failed to remove old forwarder, please manually remove it!");
App::Notify("Failed to remove old forwarder, please manually remove it!"_i18n);
}
// remove previous application record
@@ -1012,8 +1020,8 @@ auto install_forwarder(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId st
}
auto install_forwarder(OwoConfig& config, NcmStorageId storage_id) -> Result {
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing Forwarder"_i18n, config.name, [config, storage_id](auto pbox) mutable -> bool {
return R_SUCCEEDED(install_forwarder(pbox, config, storage_id));
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing Forwarder"_i18n, config.name, [config, storage_id](auto pbox) mutable -> Result {
return install_forwarder(pbox, config, storage_id);
}));
R_SUCCEED();
}

View File

@@ -0,0 +1,388 @@
#include "threaded_file_transfer.hpp"
#include "log.hpp"
#include "defines.hpp"
#include "app.hpp"
#include <vector>
#include <algorithm>
#include <cstring>
namespace sphaira::thread {
namespace {
constexpr u64 READ_BUFFER_MAX = 1024*1024*4;
struct ThreadBuffer {
ThreadBuffer() {
buf.reserve(READ_BUFFER_MAX);
}
std::vector<u8> buf;
s64 off;
};
template<std::size_t Size>
struct RingBuf {
private:
ThreadBuffer buf[Size]{};
unsigned r_index{};
unsigned w_index{};
static_assert((sizeof(RingBuf::buf) & (sizeof(RingBuf::buf) - 1)) == 0, "Must be power of 2!");
public:
void ringbuf_reset() {
this->r_index = this->w_index;
}
unsigned ringbuf_capacity() const {
return sizeof(this->buf) / sizeof(this->buf[0]);
}
unsigned ringbuf_size() const {
return (this->w_index - this->r_index) % (ringbuf_capacity() * 2U);
}
unsigned ringbuf_free() const {
return ringbuf_capacity() - ringbuf_size();
}
void ringbuf_push(std::vector<u8>& buf_in, s64 off_in) {
auto& value = this->buf[this->w_index % ringbuf_capacity()];
value.off = off_in;
std::swap(value.buf, buf_in);
this->w_index = (this->w_index + 1U) % (ringbuf_capacity() * 2U);
}
void ringbuf_pop(std::vector<u8>& buf_out, s64& off_out) {
auto& value = this->buf[this->r_index % ringbuf_capacity()];
off_out = value.off;
std::swap(value.buf, buf_out);
this->r_index = (this->r_index + 1U) % (ringbuf_capacity() * 2U);
}
};
struct ThreadData {
ThreadData(ui::ProgressBox* _pbox, s64 size, ReadCallback _rfunc, WriteCallback _wfunc);
auto GetResults() -> Result;
void WakeAllThreads();
void SetReadResult(Result result) {
read_result = result;
}
void SetWriteResult(Result result) {
write_result = result;
}
void SetPullResult(Result result) {
pull_result = result;
}
auto GetWriteOffset() const {
return write_offset;
}
auto GetWriteSize() const {
return write_size;
}
Result Pull(void* data, s64 size, u64* bytes_read);
Result readFuncInternal();
Result writeFuncInternal();
private:
Result SetWriteBuf(std::vector<u8>& buf, s64 size);
Result GetWriteBuf(std::vector<u8>& buf_out, s64& off_out);
Result SetPullBuf(std::vector<u8>& buf, s64 size);
Result GetPullBuf(void* data, s64 size, u64* bytes_read);
Result Read(void* buf, s64 size, u64* bytes_read);
private:
// these need to be copied
ui::ProgressBox* pbox{};
ReadCallback rfunc{};
WriteCallback wfunc{};
// these need to be created
Mutex mutex{};
Mutex pull_mutex{};
CondVar can_read{};
CondVar can_write{};
CondVar can_pull{};
CondVar can_pull_write{};
RingBuf<2> write_buffers{};
std::vector<u8> pull_buffer{};
s64 pull_buffer_offset{};
u64 read_buffer_size{};
u64 max_buffer_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} {
mutexInit(std::addressof(mutex));
mutexInit(std::addressof(pull_mutex));
condvarInit(std::addressof(can_read));
condvarInit(std::addressof(can_write));
condvarInit(std::addressof(can_pull));
condvarInit(std::addressof(can_pull_write));
write_size = size;
read_buffer_size = READ_BUFFER_MAX;
max_buffer_size = READ_BUFFER_MAX;
}
auto ThreadData::GetResults() -> Result {
R_UNLESS(!pbox->ShouldExit(), 0x1);
R_TRY(read_result);
R_TRY(write_result);
R_TRY(pull_result);
R_SUCCEED();
}
void ThreadData::WakeAllThreads() {
condvarWakeAll(std::addressof(can_read));
condvarWakeAll(std::addressof(can_write));
condvarWakeAll(std::addressof(can_pull));
condvarWakeAll(std::addressof(can_pull_write));
mutexUnlock(std::addressof(mutex));
mutexUnlock(std::addressof(pull_mutex));
}
Result ThreadData::SetWriteBuf(std::vector<u8>& buf, s64 size) {
buf.resize(size);
mutexLock(std::addressof(mutex));
if (!write_buffers.ringbuf_free()) {
R_TRY(condvarWait(std::addressof(can_read), std::addressof(mutex)));
}
ON_SCOPE_EXIT(mutexUnlock(std::addressof(mutex)));
R_TRY(GetResults());
write_buffers.ringbuf_push(buf, 0);
return condvarWakeOne(std::addressof(can_write));
}
Result ThreadData::GetWriteBuf(std::vector<u8>& buf_out, s64& off_out) {
mutexLock(std::addressof(mutex));
if (!write_buffers.ringbuf_size()) {
R_TRY(condvarWait(std::addressof(can_write), std::addressof(mutex)));
}
ON_SCOPE_EXIT(mutexUnlock(std::addressof(mutex)));
R_TRY(GetResults());
write_buffers.ringbuf_pop(buf_out, off_out);
return condvarWakeOne(std::addressof(can_read));
}
Result ThreadData::SetPullBuf(std::vector<u8>& buf, s64 size) {
buf.resize(size);
mutexLock(std::addressof(pull_mutex));
if (!pull_buffer.empty()) {
R_TRY(condvarWait(std::addressof(can_pull_write), std::addressof(pull_mutex)));
}
ON_SCOPE_EXIT(mutexUnlock(std::addressof(pull_mutex)));
R_TRY(GetResults());
pull_buffer.swap(buf);
return condvarWakeOne(std::addressof(can_pull));
}
Result ThreadData::GetPullBuf(void* data, s64 size, u64* bytes_read) {
mutexLock(std::addressof(pull_mutex));
if (pull_buffer.empty()) {
R_TRY(condvarWait(std::addressof(can_pull), std::addressof(pull_mutex)));
}
ON_SCOPE_EXIT(mutexUnlock(std::addressof(pull_mutex)));
R_TRY(GetResults());
*bytes_read = size = std::min<s64>(size, pull_buffer.size() - pull_buffer_offset);
std::memcpy(data, pull_buffer.data() + pull_buffer_offset, size);
pull_buffer_offset += size;
if (pull_buffer_offset == pull_buffer.size()) {
pull_buffer_offset = 0;
pull_buffer.clear();
return condvarWakeOne(std::addressof(can_pull_write));
} else {
R_SUCCEED();
}
}
Result ThreadData::Read(void* buf, s64 size, u64* bytes_read) {
size = std::min<s64>(size, write_size - read_offset);
const auto rc = rfunc(buf, read_offset, size, bytes_read);
read_offset += *bytes_read;
return rc;
}
Result ThreadData::Pull(void* data, s64 size, u64* bytes_read) {
return GetPullBuf(data, size, bytes_read);
}
// read thread reads all data from the source
Result ThreadData::readFuncInternal() {
// the main buffer which data is read into.
std::vector<u8> buf;
buf.reserve(this->max_buffer_size);
while (this->read_offset < this->write_size && R_SUCCEEDED(this->GetResults())) {
// read more data
s64 read_size = this->read_buffer_size;
u64 bytes_read{};
buf.resize(read_size);
R_TRY(this->Read(buf.data(), read_size, std::addressof(bytes_read)));
auto buf_size = bytes_read;
R_TRY(this->SetWriteBuf(buf, buf_size));
}
log_write("read 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);
while (this->write_offset < this->write_size && R_SUCCEEDED(this->GetResults())) {
s64 dummy_off;
R_TRY(this->GetWriteBuf(buf, dummy_off));
const auto size = buf.size();
if (!this->wfunc) {
R_TRY(this->SetPullBuf(buf, buf.size()));
} else {
R_TRY(this->wfunc(buf.data(), this->write_offset, buf.size()));
}
this->write_offset += size;
}
log_write("finished write thread!\n");
R_SUCCEED();
}
void readFunc(void* d) {
auto t = static_cast<ThreadData*>(d);
t->SetReadResult(t->readFuncInternal());
log_write("read thread returned now\n");
}
void writeFunc(void* d) {
auto t = static_cast<ThreadData*>(d);
t->SetWriteResult(t->writeFuncInternal());
log_write("write thread returned now\n");
}
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));
const auto WRITE_THREAD_CORE = sfunc ? pbox->GetCpuId() : GetAlternateCore(pbox->GetCpuId());
const auto READ_THREAD_CORE = GetAlternateCore(WRITE_THREAD_CORE);
ThreadData t_data{pbox, size, rfunc, wfunc};
Thread t_read{};
R_TRY(threadCreate(&t_read, readFunc, std::addressof(t_data), nullptr, 1024*64, 0x20, READ_THREAD_CORE));
ON_SCOPE_EXIT(threadClose(&t_read));
Thread t_write{};
R_TRY(threadCreate(&t_write, writeFunc, std::addressof(t_data), nullptr, 1024*64, 0x20, 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) {
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);
}
}
// 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();
}
return t_data.GetResults();
}
} // namespace
Result Transfer(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, WriteCallback wfunc) {
return TransferInternal(pbox, size, rfunc, wfunc, nullptr);
}
Result TransferPull(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, StartCallback sfunc) {
return TransferInternal(pbox, size, rfunc, nullptr, [sfunc](StartThreadCallback start, PullCallback pull) -> Result {
R_TRY(start());
return sfunc(pull);
});
}
Result TransferPull(ui::ProgressBox* pbox, s64 size, ReadCallback rfunc, StartCallback2 sfunc) {
return TransferInternal(pbox, size, rfunc, nullptr, sfunc);
}
} // namespace::thread

View File

@@ -37,7 +37,7 @@ 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 Value: 0x%X", code, R_MODULE(code), R_DESCRIPTION(code), R_VALUE(code));
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), "An error occurred"_i18n.c_str());
}

View File

@@ -12,7 +12,8 @@ List::List(s64 row, s64 page, const Vec4& pos, const Vec4& v, const Vec2& pad)
, m_v{v}
, m_pad{pad} {
m_pos = pos;
SetScrollBarPos(SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200);
// SetScrollBarPos(SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200);
SetScrollBarPos(pos.x + pos.w, 100, SCREEN_HEIGHT-200);
}
auto List::ClampX(float x, s64 count) const -> float {
@@ -165,8 +166,8 @@ void List::OnUpdateHome(Controller* controller, TouchInfo* touch, s64 index, s64
}
void List::OnUpdateGrid(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback) {
const auto page_up_button = m_row == 1 ? Button::DPAD_LEFT : Button::L2;
const auto page_down_button = m_row == 1 ? Button::DPAD_RIGHT : Button::R2;
const auto page_up_button = GetPageJump() ? (m_row == 1 ? Button::DPAD_LEFT : Button::L2) : (Button::NONE);
const auto page_down_button = GetPageJump() ? (m_row == 1 ? Button::DPAD_RIGHT : Button::R2) : (Button::NONE);
if (controller->GotDown(Button::DOWN)) {
if (ScrollDown(index, m_row, count)) {
@@ -277,7 +278,11 @@ void List::DrawGrid(NVGcontext* vg, Theme* theme, s64 count, Callback callback)
const auto x = v.x;
for (; i < count; i++, v.x += v.w + m_pad.x) {
for (s64 row = 0; i < count; row++, i++, v.x += v.w + m_pad.x) {
if (row >= m_row) {
break;
}
// only draw if full x is in bounds
if (v.x + v.w > GetX() + GetW()) {
break;

View File

@@ -13,6 +13,7 @@
#include "yyjson_helper.hpp"
#include "swkbd.hpp"
#include "i18n.hpp"
#include "hasher.hpp"
#include "nro.hpp"
#include <minIni.h>
@@ -21,7 +22,7 @@
#include <yyjson.h>
#include <stb_image.h>
#include <minizip/unzip.h>
#include <mbedtls/md5.h>
#include <algorithm>
#include <ranges>
#include <utility>
@@ -35,6 +36,22 @@ constexpr auto URL_JSON = "https://switch.cdn.fortheusers.org/repo.json";
constexpr auto URL_POST_FEEDBACK = "http://switchbru.com/appstore/feedback";
constexpr auto URL_GET_FEEDACK = "http://switchbru.com/appstore/feedback";
constexpr const u8 UPDATE_IMAGE_DATA[]{
#embed <icons/UPDATE.png>
};
constexpr const u8 GET_IMAGE_DATA[]{
#embed <icons/GET.png>
};
constexpr const u8 LOCAL_IMAGE_DATA[]{
#embed <icons/LOCAL.png>
};
constexpr const u8 INSTALLED_IMAGE_DATA[]{
#embed <icons/INSTALLED.png>
};
constexpr const char* FILTER_STR[] = {
"All",
"Games",
@@ -172,7 +189,7 @@ auto LoadAndParseManifest(const Entry& e) -> ManifestEntries {
return ParseManifest(std::span{(const char*)data.data(), data.size()});
}
auto EntryLoadImageFile(fs::Fs& fs, const fs::FsPath& path, LazyImage& image) -> bool {
auto EntryLoadImageData(std::span<const u8> image_buf, LazyImage& image) -> bool {
// already have the image
if (image.image) {
// log_write("warning, tried to load image: %s when already loaded\n", path);
@@ -180,17 +197,29 @@ auto EntryLoadImageFile(fs::Fs& fs, const fs::FsPath& path, LazyImage& image) ->
}
auto vg = App::GetVg();
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));
std::memcpy(image.first_pixel, buf, sizeof(image.first_pixel));
image.image = nvgCreateImageRGBA(vg, image.w, image.h, 0, buf);
}
return image.image;
}
auto EntryLoadImageFile(fs::Fs& fs, const fs::FsPath& path, LazyImage& image) -> bool {
// already have the image
if (image.image) {
// log_write("warning, tried to load image: %s when already loaded\n", path);
return true;
}
std::vector<u8> image_buf;
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));
std::memcpy(image.first_pixel, buf, sizeof(image.first_pixel));
image.image = nvgCreateImageRGBA(vg, image.w, image.h, 0, buf);
}
EntryLoadImageData(image_buf, image);
}
if (!image.image) {
@@ -286,14 +315,12 @@ void ReadFromInfoJson(Entry& e) {
// this ignores ShouldExit() as leaving somthing in a half
// deleted state is a bad idea :)
auto UninstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
auto UninstallApp(ProgressBox* pbox, const Entry& entry) -> Result {
const auto manifest = LoadAndParseManifest(entry);
fs::FsNativeSd fs;
if (manifest.empty()) {
if (entry.binary.empty()) {
return false;
}
R_UNLESS(!entry.binary.empty(), 0x1);
fs.DeleteFile(entry.binary);
} else {
for (auto& e : manifest) {
@@ -313,13 +340,14 @@ auto UninstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
// remove directory, this will also delete manifest and info
const auto dir = BuildPackageCachePath(entry);
pbox->NewTransfer("Removing "_i18n + dir);
pbox->NewTransfer("Removing "_i18n + dir.toString());
if (R_FAILED(fs.DeleteDirectoryRecursively(dir))) {
log_write("failed to delete folder: %s\n", dir.s);
} else {
log_write("deleted: %s\n", dir.s);
}
return true;
R_SUCCEED();
}
// this is called by ProgressBox on a seperate thread
@@ -328,12 +356,12 @@ auto UninstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
// 2. md5 check the zip
// 3. parse manifest and unzip everything to placeholder
// 4. move everything from placeholder to normal location
auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
auto InstallApp(ProgressBox* pbox, const Entry& entry) -> Result {
static const fs::FsPath zip_out{"/switch/sphaira/cache/appstore/temp.zip"};
constexpr auto chunk_size = 1024 * 512; // 512KiB
std::vector<u8> buf(1024 * 512); // 512KiB
fs::FsNativeSd fs;
R_TRY_RESULT(fs.GetFsOpenResult(), false);
R_TRY(fs.GetFsOpenResult());
// 1. download the zip
if (!pbox->ShouldExit()) {
@@ -341,14 +369,13 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
log_write("starting download\n");
const auto url = BuildZipUrl(entry);
if (!curl::Api().ToFile(
const auto result = curl::Api().ToFile(
curl::Url{url},
curl::Path{zip_out},
curl::OnProgress{pbox->OnDownloadProgressCallback()}
).success) {
log_write("error with download\n");
return false;
}
);
R_UNLESS(result.success, 0x1);
}
ON_SCOPE_EXIT(fs.DeleteFile(zip_out));
@@ -358,77 +385,25 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
pbox->NewTransfer("Checking MD5"_i18n);
log_write("starting md5 check\n");
FsFile f;
if (R_FAILED(fs.OpenFile(zip_out, FsOpenMode_Read, &f))) {
return false;
}
ON_SCOPE_EXIT(fsFileClose(&f));
std::string hash_out;
R_TRY(hash::Hash(pbox, hash::Type::Md5, &fs, zip_out, hash_out));
s64 size;
if (R_FAILED(fsFileGetSize(&f, &size))) {
return false;
}
mbedtls_md5_context ctx;
mbedtls_md5_init(&ctx);
ON_SCOPE_EXIT(mbedtls_md5_free(&ctx));
if (mbedtls_md5_starts_ret(&ctx)) {
log_write("failed to start ret\n");
}
std::vector<u8> chunk(chunk_size);
s64 offset{};
while (offset < size) {
if (pbox->ShouldExit()) {
return false;
}
u64 bytes_read;
if (R_FAILED(fsFileRead(&f, offset, chunk.data(), chunk.size(), 0, &bytes_read))) {
log_write("failed to read file offset: %zd size: %zd\n", offset, size);
return false;
}
if (mbedtls_md5_update_ret(&ctx, chunk.data(), bytes_read)) {
log_write("failed to update ret\n");
return false;
}
offset += bytes_read;
pbox->UpdateTransfer(offset, size);
}
u8 md5_out[16];
if (mbedtls_md5_finish_ret(&ctx, (u8*)md5_out)) {
return false;
}
// convert md5 to hex string
char md5_str[sizeof(md5_out) * 2 + 1];
for (u32 i = 0; i < sizeof(md5_out); i++) {
std::sprintf(md5_str + i * 2, "%02x", md5_out[i]);
}
if (strncasecmp(md5_str, entry.md5.data(), entry.md5.length())) {
log_write("bad md5: %.*s vs %.*s\n", 32, md5_str, 32, entry.md5.c_str());
return false;
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);
}
}
// 3. extract the zip
if (!pbox->ShouldExit()) {
auto zfile = unzOpen64(zip_out);
if (!zfile) {
log_write("failed to open zip: %s\n", zip_out.s);
return false;
}
R_UNLESS(zfile, 0x1);
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");
return false;
R_THROW(0x1);
}
ManifestEntries new_manifest;
@@ -436,49 +411,30 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
{
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
log_write("failed to open current file\n");
return false;
R_THROW(0x1);
}
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");
return false;
R_THROW(0x1);
}
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");
return false;
R_THROW(0x1);
}
new_manifest = ParseManifest(manifest_data);
if (new_manifest.empty()) {
log_write("manifest is empty!\n");
return false;
R_THROW(0x1);
}
}
const auto unzip_to = [pbox, &fs, zfile](const fs::FsPath& inzip, fs::FsPath output) -> bool {
pbox->NewTransfer(inzip);
if (UNZ_END_OF_LIST_OF_FILE == unzLocateFile(zfile, inzip, 0)) {
log_write("failed to find %s\n", inzip.s);
return false;
}
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
log_write("failed to open current file\n");
return false;
}
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
unz_file_info64 info;
if (UNZ_OK != unzGetCurrentFileInfo64(zfile, &info, 0, 0, 0, 0, 0, 0)) {
log_write("failed to get current info\n");
return false;
}
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);
}
@@ -489,85 +445,132 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
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);
return false;
R_THROW(rc);
}
FsFile f;
if (R_FAILED(rc = fs.OpenFile(output, FsOpenMode_Write, &f))) {
log_write("failed to open file: %s 0x%04X\n", output.s, rc);
return false;
}
ON_SCOPE_EXIT(fsFileClose(&f));
fs::File f;
R_TRY(fs.OpenFile(output, FsOpenMode_Write, &f));
R_TRY(f.SetSize(info.uncompressed_size));
if (R_FAILED(rc = fsFileSetSize(&f, info.uncompressed_size))) {
log_write("failed to set file size: %s 0x%04X\n", output.s, rc);
return false;
}
std::vector<char> buf(chunk_size);
u64 offset{};
while (offset < info.uncompressed_size) {
if (pbox->ShouldExit()) {
return false;
}
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);
return false;
R_THROW(0x1);
}
if (R_FAILED(rc = fsFileWrite(&f, offset, buf.data(), bytes_read, FsWriteOption_None))) {
log_write("failed to write file: %s 0x%04X\n", output.s, rc);
return false;
}
R_TRY(f.Write(offset, buf.data(), bytes_read, FsWriteOption_None));
pbox->UpdateTransfer(offset, info.uncompressed_size);
offset += bytes_read;
}
return true;
R_SUCCEED();
};
// unzip manifest and info
if (!unzip_to("info.json", BuildInfoCachePath(entry))) {
return false;
}
if (!unzip_to("manifest.install", BuildManifestCachePath(entry))) {
return false;
}
const auto unzip_to = [&](const fs::FsPath& inzip, const fs::FsPath& output) -> Result {
pbox->NewTransfer(inzip);
for (auto& new_entry : new_manifest) {
if (pbox->ShouldExit()) {
return false;
if (UNZ_END_OF_LIST_OF_FILE == unzLocateFile(zfile, inzip, 0)) {
log_write("failed to find %s\n", inzip.s);
R_THROW(0x1);
}
switch (new_entry.command) {
case 'E': // both are the same?
case 'U':
break;
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
log_write("failed to open current file\n");
R_THROW(0x1);
}
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
case 'G': { // checks if file exists, if not, extract
if (fs.FileExists(fs::AppendPath("/", new_entry.path))) {
continue;
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);
}
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);
}
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);
}
} break;
}
default:
log_write("bad command: %c\n", new_entry.command);
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));
}
if (!unzip_to(new_entry.path, new_entry.path)) {
return false;
}
}
R_SUCCEED();
};
// unzip manifest, info and all entries.
TimeStamp ts;
R_TRY(unzip_to("info.json", BuildInfoCachePath(entry)));
R_TRY(unzip_to("manifest.install", BuildManifestCachePath(entry)));
R_TRY(unzip_all(new_manifest));
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
for (auto& old_entry : old_manifest) {
bool found = false;
for (auto& new_entry : new_manifest) {
if (!std::strcmp(old_entry.path, new_entry.path)) {
if (!strcasecmp(old_entry.path, new_entry.path)) {
found = true;
break;
}
@@ -586,7 +589,7 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
}
log_write("finished install :)\n");
return true;
R_SUCCEED();
}
// case-insensitive version of str.find()
@@ -600,7 +603,7 @@ auto FindCaseInsensitive(std::string_view base, std::string_view term) -> bool {
} // namespace
EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
: MenuBase{entry.title}
: MenuBase{entry.title, MenuFlag_None}
, m_entry{entry}
, m_default_icon{default_icon}
, m_menu{menu} {
@@ -775,27 +778,31 @@ void EntryMenu::UpdateOptions() {
const auto install = [this](){
App::Push(std::make_shared<ProgressBox>(m_entry.image.image, "Downloading "_i18n, m_entry.title, [this](auto pbox){
return InstallApp(pbox, m_entry);
}, [this](bool success){
if (success) {
}, [this](Result rc){
App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n);
if (R_SUCCEEDED(rc)) {
App::Notify("Downloaded "_i18n + m_entry.title);
m_entry.status = EntryStatus::Installed;
m_menu.SetDirty();
UpdateOptions();
}
}, 2));
}));
};
const auto uninstall = [this](){
App::Push(std::make_shared<ProgressBox>(m_entry.image.image, "Uninstalling "_i18n, m_entry.title, [this](auto pbox){
return UninstallApp(pbox, m_entry);
}, [this](bool success){
if (success) {
}, [this](Result rc){
App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n);
if (R_SUCCEEDED(rc)) {
App::Notify("Removed "_i18n + m_entry.title);
m_entry.status = EntryStatus::Get;
m_menu.SetDirty();
UpdateOptions();
}
}, 2));
}));
};
const Option install_option{"Install"_i18n, install};
@@ -845,7 +852,7 @@ void EntryMenu::SetIndex(s64 index) {
}
}
Menu::Menu() : grid::Menu{"AppStore"_i18n} {
Menu::Menu(u32 flags) : grid::Menu{"AppStore"_i18n, flags} {
fs::FsNativeSd fs;
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/icons");
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/banners");
@@ -1091,14 +1098,11 @@ void Menu::OnFocusGained() {
// log_write("saying we got focus base: size: %zu count: %zu\n", repo_json.size(), m_entries.size());
if (!m_default_image.image) {
if (R_SUCCEEDED(romfsInit())) {
ON_SCOPE_EXIT(romfsExit());
EntryLoadImageFile("romfs:/default.png", m_default_image);
EntryLoadImageFile("romfs:/UPDATE.png", m_update);
EntryLoadImageFile("romfs:/GET.png", m_get);
EntryLoadImageFile("romfs:/LOCAL.png", m_local);
EntryLoadImageFile("romfs:/INSTALLED.png", m_installed);
}
EntryLoadImageData(App::GetDefaultImageData(), m_default_image);
EntryLoadImageData(UPDATE_IMAGE_DATA, m_update);
EntryLoadImageData(GET_IMAGE_DATA, m_get);
EntryLoadImageData(LOCAL_IMAGE_DATA, m_local);
EntryLoadImageData(INSTALLED_IMAGE_DATA, m_installed);
}
if (m_entries.empty()) {
@@ -1143,6 +1147,9 @@ void Menu::SetIndex(s64 index) {
}
void Menu::ScanHomebrew() {
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
from_json(REPO_PATH, m_entries);
fs::FsNativeSd fs;

View File

@@ -6,28 +6,24 @@ namespace {
} // namespace
Menu::Menu(const fs::FsPath& path) : MenuBase{path}, m_path{path} {
Menu::Menu(const fs::FsPath& path) : MenuBase{path, MenuFlag_None}, m_path{path} {
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}});
std::string buf;
if (R_SUCCEEDED(m_fs.OpenFile(m_path, FsOpenMode_Read, &m_file))) {
fsFileGetSize(&m_file, &m_file_size);
m_file.GetSize(&m_file_size);
buf.resize(m_file_size + 1);
u64 read_bytes;
fsFileRead(&m_file, m_file_offset, buf.data(), buf.size(), 0, &read_bytes);
m_file.Read(m_file_offset, buf.data(), buf.size(), 0, &read_bytes);
buf[m_file_size] = '\0';
}
m_scroll_text = std::make_unique<ScrollableText>(buf, 0, 120, 500, 1150-110, 18);
}
Menu::~Menu() {
fsFileClose(&m_file);
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);

File diff suppressed because it is too large Load Diff

View File

@@ -142,7 +142,7 @@ void StreamFtp::Disable() {
m_active = false;
}
Menu::Menu() : MenuBase{"FTP Install (EXPERIMENTAL)"_i18n} {
Menu::Menu(u32 flags) : MenuBase{"FTP Install (EXPERIMENTAL)"_i18n, flags} {
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}});
@@ -151,6 +151,8 @@ Menu::Menu() : MenuBase{"FTP Install (EXPERIMENTAL)"_i18n} {
App::DisplayInstallOptions(false);
}});
App::SetAutoSleepDisabled(true);
mutexInit(&m_mutex);
m_was_ftp_enabled = App::GetFtpEnable();
if (!m_was_ftp_enabled) {
@@ -182,6 +184,7 @@ Menu::~Menu() {
App::SetFtpEnable(false);
}
App::SetAutoSleepDisabled(false);
log_write("closing data!!!!\n");
}
@@ -199,25 +202,25 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
log_write("set to progress\n");
m_state = State::Progress;
log_write("got connection\n");
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool {
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();
return false;
R_THROW(rc);
}
log_write("progress box is done\n");
return true;
}, [this](bool result){
R_SUCCEED();
}, [this](Result rc){
App::PushErrorBox(rc, "Ftp install failed!"_i18n);
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (result) {
if (R_SUCCEEDED(rc)) {
App::Notify("Ftp install success!"_i18n);
m_state = State::Done;
} else {
App::Notify("Ftp install failed!"_i18n);
m_state = State::Failed;
}
}));
@@ -236,9 +239,10 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (m_ip) {
if (m_type == NifmInternetConnectionType_WiFi) {
SetSubHeading("Connection Type: WiFi | Strength: "_i18n + std::to_string(m_strength));
const auto pdata = GetPolledData();
if (pdata.ip) {
if (pdata.type == NifmInternetConnectionType_WiFi) {
SetSubHeading("Connection Type: WiFi | Strength: "_i18n + std::to_string(pdata.strength));
} else {
SetSubHeading("Connection Type: Ethernet"_i18n);
}
@@ -261,15 +265,15 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
gfx::drawTextArgs(vg, bounds[2], start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED), __VA_ARGS__); \
start_y += spacing;
if (m_ip) {
draw("Host:"_i18n, " %u.%u.%u.%u", m_ip&0xFF, (m_ip>>8)&0xFF, (m_ip>>16)&0xFF, (m_ip>>24)&0xFF);
if (pdata.ip) {
draw("Host:"_i18n, " %u.%u.%u.%u", pdata.ip&0xFF, (pdata.ip>>8)&0xFF, (pdata.ip>>16)&0xFF, (pdata.ip>>24)&0xFF);
draw("Port:"_i18n, " %u", m_port);
if (!m_anon) {
draw("Username:"_i18n, " %s", m_user);
draw("Password:"_i18n, " %s", m_pass);
}
if (m_type == NifmInternetConnectionType_WiFi) {
if (pdata.type == NifmInternetConnectionType_WiFi) {
NifmNetworkProfileData profile{};
if (R_SUCCEEDED(nifmGetCurrentNetworkProfile(&profile))) {
const auto& settings = profile.wireless_setting_data;

View File

@@ -1,10 +1,9 @@
#include "app.hpp"
#include "log.hpp"
#include "fs.hpp"
#include "download.hpp"
#include "dumper.hpp"
#include "defines.hpp"
#include "i18n.hpp"
#include "location.hpp"
#include "ui/menus/game_menu.hpp"
#include "ui/sidebar.hpp"
@@ -20,8 +19,6 @@
#include "yati/container/base.hpp"
#include "yati/container/nsp.hpp"
#include "usb/usb_uploader.hpp"
#include <utility>
#include <cstring>
#include <algorithm>
@@ -46,23 +43,6 @@ enum ContentFlag {
ContentFlag_All = ContentFlag_Application | ContentFlag_Patch | ContentFlag_AddOnContent | ContentFlag_DataPatch,
};
enum DumpLocationType {
DumpLocationType_SdCard,
DumpLocationType_UsbS2S,
DumpLocationType_DevNull,
};
struct DumpLocation {
const DumpLocationType type;
const char* display_name;
};
constexpr DumpLocation DUMP_LOCATIONS[]{
{ DumpLocationType_SdCard, "microSD card (/dumps/NSP/)" },
{ DumpLocationType_UsbS2S, "USB transfer (Switch 2 Switch)" },
{ DumpLocationType_DevNull, "/dev/null (Speed Test)" },
};
struct NcmEntry {
const NcmStorageId storage_id;
NcmContentStorage cs{};
@@ -149,6 +129,8 @@ struct NspEntry {
s64 nsp_size{};
// copy of ncm cs, it is not closed.
NcmContentStorage cs{};
// copy of the icon, if invalid, it will use the default icon.
int icon{};
// todo: benchmark manual sdcard read and decryption vs ncm.
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) {
@@ -200,30 +182,21 @@ private:
}
};
struct BaseSource {
virtual ~BaseSource() = default;
virtual Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) = 0;
};
struct NspSource final : BaseSource {
NspSource(std::span<NspEntry> entries) : m_entries{entries} { }
struct NspSource final : dump::BaseSource {
NspSource(const std::vector<NspEntry>& entries) : m_entries{entries} { }
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
return path == e.path;
return path.find(e.path.s) != path.npos;
});
R_UNLESS(it != m_entries.end(), 0x1);
return it->Read(buf, off, size, bytes_read);
}
auto GetEntries() const -> std::span<const NspEntry> {
return m_entries;
}
auto GetName(const std::string& path) const -> std::string {
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
return path == e.path;
return path.find(e.path.s) != path.npos;
});
if (it != m_entries.end()) {
@@ -235,7 +208,7 @@ struct NspSource final : BaseSource {
auto GetSize(const std::string& path) const -> s64 {
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
return path == e.path;
return path.find(e.path.s) != path.npos;
});
if (it != m_entries.end()) {
@@ -245,209 +218,29 @@ struct NspSource final : BaseSource {
return 0;
}
private:
std::span<NspEntry> m_entries{};
};
auto GetIcon(const std::string& path) const -> int override {
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
return path.find(e.path.s) != path.npos;
});
struct UsbTest final : usb::upload::Usb {
UsbTest(ProgressBox* pbox, std::span<NspEntry> entries) : Usb{UINT64_MAX} {
m_source = std::make_unique<NspSource>(entries);
m_pbox = pbox;
}
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
if (m_path != path) {
m_path = path;
m_progress = 0;
m_size = m_source->GetSize(path);
m_pbox->SetTitle(m_source->GetName(path));
m_pbox->NewTransfer(m_path);
if (it != m_entries.end()) {
return it->icon;
}
R_TRY(m_source->Read(path, buf, off, size, bytes_read));
m_offset += *bytes_read;
m_progress += *bytes_read;
m_pbox->UpdateTransfer(m_progress, m_size);
R_SUCCEED();
return App::GetDefaultImage();
}
private:
std::unique_ptr<NspSource> m_source{};
ProgressBox* m_pbox{};
std::string m_path{};
s64 m_offset{};
s64 m_size{};
s64 m_progress{};
std::vector<NspEntry> m_entries{};
};
Result DumpNspToFile(ProgressBox* pbox, std::span<NspEntry> entries) {
static constexpr fs::FsPath DUMP_PATH{"/dumps/NSP"};
constexpr s64 BIG_FILE_SIZE = 1024ULL*1024ULL*1024ULL*4ULL;
fs::FsNativeSd fs{};
R_TRY(fs.GetFsOpenResult());
fs.CreateDirectoryRecursively(DUMP_PATH);
auto source = std::make_unique<NspSource>(entries);
for (const auto& e : entries) {
pbox->SetTitle(e.application_name);
pbox->NewTransfer(e.path);
const auto temp_path = fs::AppendPath(DUMP_PATH, e.path + ".temp");
fs.DeleteFile(temp_path);
const auto flags = e.nsp_size >= BIG_FILE_SIZE ? FsCreateOption_BigFile : 0;
R_TRY(fs.CreateFile(temp_path, e.nsp_size, flags));
ON_SCOPE_EXIT(fs.DeleteFile(temp_path));
{
FsFile file;
R_TRY(fs.OpenFile(temp_path, FsOpenMode_Write, &file));
ON_SCOPE_EXIT(fsFileClose(&file));
s64 offset{};
std::vector<u8> buf(1024*1024*4); // 4MiB
while (offset < e.nsp_size) {
if (pbox->ShouldExit()) {
R_THROW(0xFFFF);
}
u64 bytes_read;
R_TRY(source->Read(e.path, buf.data(), offset, buf.size(), &bytes_read));
pbox->Yield();
R_TRY(fsFileWrite(&file, offset, buf.data(), bytes_read, FsWriteOption_None));
pbox->Yield();
pbox->UpdateTransfer(offset, e.nsp_size);
offset += bytes_read;
}
}
const auto path = fs::AppendPath(DUMP_PATH, e.path);
fs.DeleteFile(path);
R_TRY(fs.RenameFile(temp_path, path));
}
R_SUCCEED();
}
Result DumpNspToUsbS2S(ProgressBox* pbox, std::span<NspEntry> entries) {
std::vector<std::string> file_list;
for (auto& e : entries) {
file_list.emplace_back(e.path);
}
auto usb = std::make_unique<UsbTest>(pbox, entries);
constexpr u64 timeout = 1e+9;
// todo: display progress bar during usb transfer.
while (!pbox->ShouldExit()) {
if (R_SUCCEEDED(usb->IsUsbConnected(timeout))) {
pbox->NewTransfer("USB connected, sending file list");
if (R_SUCCEEDED(usb->WaitForConnection(timeout, file_list))) {
pbox->NewTransfer("Sent file list, waiting for command...");
while (!pbox->ShouldExit()) {
const auto rc = usb->PollCommands();
if (rc == usb->Result_Exit) {
log_write("got exit command\n");
R_SUCCEED();
}
R_TRY(rc);
}
}
} else {
pbox->NewTransfer("waiting for usb connection...");
}
}
R_SUCCEED();
}
Result DumpNspToDevNull(ProgressBox* pbox, std::span<NspEntry> entries) {
auto source = std::make_unique<NspSource>(entries);
for (const auto& e : entries) {
pbox->SetTitle(e.application_name);
pbox->NewTransfer(e.path);
s64 offset{};
std::vector<u8> buf(1024*1024*4); // 4MiB
while (offset < e.nsp_size) {
if (pbox->ShouldExit()) {
R_THROW(0xFFFF);
}
u64 bytes_read;
R_TRY(source->Read(e.path, buf.data(), offset, buf.size(), &bytes_read));
pbox->Yield();
pbox->UpdateTransfer(offset, e.nsp_size);
offset += bytes_read;
}
}
R_SUCCEED();
}
Result DumpNspToNetwork(ProgressBox* pbox, const location::Entry& loc, std::span<NspEntry> entries) {
auto source = std::make_unique<NspSource>(entries);
for (const auto& e : entries) {
if (pbox->ShouldExit()) {
R_THROW(0xFFFF);
}
pbox->SetTitle(e.application_name);
pbox->NewTransfer(e.path);
s64 offset{};
const auto result = curl::Api().FromMemory(
CURL_LOCATION_TO_API(loc),
curl::OnProgress{pbox->OnDownloadProgressCallback()},
curl::UploadInfo{
e.path, e.nsp_size,
[&pbox, &e, &source, &offset](void *ptr, size_t size) -> size_t {
u64 bytes_read{};
if (R_FAILED(source->Read(e.path, ptr, offset, size, &bytes_read))) {
// curl will request past the size of the file, causing an error.
// only log the error if it failed in the middle of a transfer.
if (offset != e.nsp_size) {
log_write("failed to read in custom callback: %zd size: %zd\n", offset, e.nsp_size);
}
return 0;
}
offset += bytes_read;
return bytes_read;
}
},
curl::OnUploadSeek{
[&e, &offset](s64 new_offset){
offset = new_offset;
return true;
}
}
);
R_UNLESS(result.success, 0x1);
}
R_SUCCEED();
}
Result Notify(Result rc, const std::string& error_message) {
if (R_FAILED(rc)) {
App::Push(std::make_shared<ui::ErrorBox>(rc,
i18n::get(error_message)
));
} else {
App::Notify("Success");
App::Notify("Success"_i18n);
}
return rc;
@@ -664,7 +457,12 @@ auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status)
}
fs::FsPath path;
std::snprintf(path, sizeof(path), "%s/%s %s[%016lX][v%u][%s].nsp", name_buf.s, name_buf.s, version, status.application_id, status.version, ncm::GetMetaTypeShortStr(status.meta_type));
if (App::GetApp()->m_dump_app_folder.Get()) {
std::snprintf(path, sizeof(path), "%s/%s %s[%016lX][v%u][%s].nsp", name_buf.s, name_buf.s, version, status.application_id, status.version, ncm::GetMetaTypeShortStr(status.meta_type));
} else {
std::snprintf(path, sizeof(path), "%s %s[%016lX][v%u][%s].nsp", name_buf.s, version, status.application_id, status.version, ncm::GetMetaTypeShortStr(status.meta_type));
}
return path;
}
@@ -690,6 +488,7 @@ Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentIn
R_UNLESS(meta_total == 1, 0x1);
R_UNLESS(meta_entries_written == 1, 0x1);
std::vector<NcmContentInfo> cnmt_infos;
for (s32 i = 0; ; i++) {
s32 entries_written;
NcmContentInfo info_out;
@@ -714,9 +513,15 @@ Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentIn
}
}
out.content_infos.emplace_back(info_out);
if (info_out.content_type == NcmContentType_Meta) {
cnmt_infos.emplace_back(info_out);
} else {
out.content_infos.emplace_back(info_out);
}
}
// append cnmt at the end of the list, following StandardNSP spec.
out.content_infos.insert_range(out.content_infos.end(), cnmt_infos);
out.status = status;
R_SUCCEED();
}
@@ -784,7 +589,7 @@ Result BuildNspEntries(Entry& e, u32 flags, std::vector<NspEntry>& out) {
NspEntry nsp;
R_TRY(BuildNspEntry(e, info, nsp));
out.emplace_back(nsp);
out.emplace_back(nsp).icon = e.image;
}
R_UNLESS(!out.empty(), 0x1);
@@ -886,7 +691,7 @@ void ThreadData::Pop(std::vector<ThreadResultData>& out) {
m_result.clear();
}
Menu::Menu() : grid::Menu{"Games"_i18n} {
Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
this->SetActions(
std::make_pair(Button::L3, Action{[this](){
if (m_entries.empty()) {
@@ -998,7 +803,7 @@ Menu::Menu() : grid::Menu{"Games"_i18n} {
}
if (meta_entries.empty()) {
App::Notify("No meta entries found...\n");
App::Notify("No meta entries found...\n"_i18n);
return;
}
@@ -1010,7 +815,7 @@ Menu::Menu() : grid::Menu{"Games"_i18n} {
}
App::Push(std::make_shared<PopupList>(
"Entries", items, [this, meta_entries](auto op_index){
"Entries"_i18n, items, [this, meta_entries](auto op_index){
#if 0
if (op_index) {
const auto& e = meta_entries[*op_index];
@@ -1041,6 +846,10 @@ Menu::Menu() : grid::Menu{"Games"_i18n} {
}, true));
}, true));
options->Add(std::make_shared<SidebarEntryCallback>("Dump options"_i18n, [this](){
App::DisplayDumpOptions(false);
}));
// completely deletes the application record and all data.
options->Add(std::make_shared<SidebarEntryCallback>("Delete"_i18n, [this](){
const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].GetName() + "?";
@@ -1087,7 +896,7 @@ Menu::~Menu() {
void Menu::Update(Controller* controller, TouchInfo* touch) {
if (m_dirty) {
App::Notify("Updating application record list");
App::Notify("Updating application record list"_i18n);
SortAndFindLastFile(true);
}
@@ -1172,6 +981,9 @@ void Menu::ScanHomebrew() {
const auto hide_forwarders = m_hide_forwarders.Get();
TimeStamp ts;
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
FreeEntries();
m_entries.reserve(ENTRY_CHUNK_COUNT);
@@ -1285,7 +1097,7 @@ void Menu::OnLayoutChange() {
}
void Menu::DeleteGames() {
App::Push(std::make_shared<ProgressBox>(0, "Deleting"_i18n, "", [this](auto pbox) -> bool {
App::Push(std::make_shared<ProgressBox>(0, "Deleting"_i18n, "", [this](auto pbox) -> Result {
auto targets = GetSelectedEntries();
for (s64 i = 0; i < std::size(targets); i++) {
@@ -1294,76 +1106,39 @@ void Menu::DeleteGames() {
LoadControlEntry(e);
pbox->SetTitle(e.GetName());
pbox->UpdateTransfer(i + 1, std::size(targets));
nsDeleteApplicationCompletely(e.app_id);
R_TRY(nsDeleteApplicationCompletely(e.app_id));
}
return true;
}, [this](bool success){
R_SUCCEED();
}, [this](Result rc){
App::PushErrorBox(rc, "Delete failed!"_i18n);
ClearSelection();
m_dirty = true;
if (success) {
App::Notify("Delete successfull!");
} else {
App::Notify("Delete failed!");
if (R_SUCCEEDED(rc)) {
App::Notify("Delete successfull!"_i18n);
}
}));
}
void Menu::DumpGames(u32 flags) {
PopupList::Items items;
const auto network_locations = location::Load();
auto targets = GetSelectedEntries();
for (const auto&p : network_locations) {
items.emplace_back(p.name);
std::vector<NspEntry> nsp_entries;
for (auto& e : targets) {
BuildNspEntries(e, flags, nsp_entries);
}
for (const auto&p : DUMP_LOCATIONS) {
items.emplace_back(i18n::get(p.display_name));
std::vector<fs::FsPath> paths;
for (auto& e : nsp_entries) {
paths.emplace_back(fs::AppendPath("/dumps/NSP", e.path));
}
App::Push(std::make_shared<PopupList>(
"Select dump location"_i18n, items, [this, network_locations, flags](auto op_index){
if (!op_index) {
return;
}
const auto index = *op_index;
App::Push(std::make_shared<ProgressBox>(0, "Dumping"_i18n, "", [this, network_locations, index, flags](auto pbox) -> bool {
auto targets = GetSelectedEntries();
std::vector<NspEntry> nsp_entries;
for (auto& e : targets) {
BuildNspEntries(e, flags, nsp_entries);
}
const auto index2 = index - network_locations.size();
if (!network_locations.empty() && index < network_locations.size()) {
return R_SUCCEEDED(DumpNspToNetwork(pbox, network_locations[index], nsp_entries));
} else if (index2 == DumpLocationType_SdCard) {
return R_SUCCEEDED(DumpNspToFile(pbox, nsp_entries));
} else if (index2 == DumpLocationType_UsbS2S) {
return R_SUCCEEDED(DumpNspToUsbS2S(pbox, nsp_entries));
} else if (index2 == DumpLocationType_DevNull) {
return R_SUCCEEDED(DumpNspToDevNull(pbox, nsp_entries));
}
return false;
}, [this](bool success){
ClearSelection();
if (success) {
App::Notify("Dump successfull!");
log_write("dump successfull!!!\n");
} else {
App::Notify("Dump failed!");
log_write("dump failed!!!\n");
}
}));
}
));
auto source = std::make_shared<NspSource>(nsp_entries);
dump::Dump(source, paths, [this](Result rc){
ClearSelection();
});
}
} // namespace sphaira::ui::menu::game

View File

@@ -1,33 +1,178 @@
#include "ui/menus/gc_menu.hpp"
#include "ui/nvg_util.hpp"
#include "ui/sidebar.hpp"
#include "ui/popup_list.hpp"
#include "ui/option_box.hpp"
#include "yati/yati.hpp"
#include "yati/nx/nca.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
#include "download.hpp"
#include "dumper.hpp"
#include <cstring>
#include <algorithm>
namespace sphaira::ui::menu::gc {
namespace {
constexpr u32 XCI_MAGIC = std::byteswap(0x48454144);
constexpr u32 REMOUNT_ATTEMPT_MAX = 8; // same as nxdumptool.
enum DumpFileType {
DumpFileType_XCI,
DumpFileType_TrimmedXCI,
DumpFileType_Set,
DumpFileType_UID,
DumpFileType_Cert,
DumpFileType_Initial,
};
enum DumpFileFlag {
DumpFileFlag_XCI = 1 << 0,
DumpFileFlag_Set = 1 << 1,
DumpFileFlag_UID = 1 << 2,
DumpFileFlag_Cert = 1 << 3,
DumpFileFlag_Initial = 1 << 4,
DumpFileFlag_AllBin = DumpFileFlag_Set | DumpFileFlag_UID | DumpFileFlag_Cert | DumpFileFlag_Initial,
DumpFileFlag_All = DumpFileFlag_XCI | DumpFileFlag_AllBin,
};
const char *g_option_list[] = {
"Nand Install",
"SD Card Install",
"Install",
"Dump",
"Exit",
};
struct HashStr {
char str[0x21];
auto GetXciSizeFromRomSize(u8 rom_size) -> s64 {
switch (rom_size) {
case 0xFA: return 1024ULL * 1024ULL * 1024ULL * 1ULL;
case 0xF8: return 1024ULL * 1024ULL * 1024ULL * 2ULL;
case 0xF0: return 1024ULL * 1024ULL * 1024ULL * 4ULL;
case 0xE0: return 1024ULL * 1024ULL * 1024ULL * 8ULL;
case 0xE1: return 1024ULL * 1024ULL * 1024ULL * 16ULL;
case 0xE2: return 1024ULL * 1024ULL * 1024ULL * 32ULL;
}
return 0;
}
// taken from nxdumptool.
void utilsReplaceIllegalCharacters(char *str, bool ascii_only)
{
static const char g_illegalFileSystemChars[] = "\\/:*?\"<>|";
size_t str_size = 0, cur_pos = 0;
if (!str || !(str_size = strlen(str))) return;
u8 *ptr1 = (u8*)str, *ptr2 = ptr1;
ssize_t units = 0;
u32 code = 0;
bool repl = false;
while(cur_pos < str_size)
{
units = decode_utf8(&code, ptr1);
if (units < 0) break;
if (code < 0x20 || (!ascii_only && code == 0x7F) || (ascii_only && code >= 0x7F) || \
(units == 1 && memchr(g_illegalFileSystemChars, (int)code, std::size(g_illegalFileSystemChars))))
{
if (!repl)
{
*ptr2++ = '_';
repl = true;
}
} else {
if (ptr2 != ptr1) memmove(ptr2, ptr1, (size_t)units);
ptr2 += units;
repl = false;
}
ptr1 += units;
cur_pos += (size_t)units;
}
*ptr2 = '\0';
}
struct DebugEventInfo {
u32 event_type;
u32 flags;
u64 thread_id;
u64 title_id;
u64 process_id;
char process_name[12];
u32 mmu_flags;
u8 _0x30[0x10];
};
HashStr hexIdToStr(auto id) {
HashStr str{};
const auto id_lower = std::byteswap(*(u64*)id.c);
const auto id_upper = std::byteswap(*(u64*)(id.c + 0x8));
std::snprintf(str.str, 0x21, "%016lx%016lx", id_lower, id_upper);
return str;
auto GetDumpTypeStr(u8 type) -> const char* {
switch (type) {
case DumpFileType_TrimmedXCI:
if (App::GetApp()->m_dump_label_trim_xci.Get()) {
return " (trimmed).xci";
} [[fallthrough]];
case DumpFileType_XCI: return ".xci";
case DumpFileType_Set: return " (Card ID Set).bin";
case DumpFileType_UID: return " (Card UID).bin";
case DumpFileType_Cert: return " (Certificate).bin";
case DumpFileType_Initial: return " (Initial Data).bin";
}
return "";
}
auto BuildXciName(const ApplicationEntry& e) -> fs::FsPath {
fs::FsPath name_buf = e.lang_entry.name;
utilsReplaceIllegalCharacters(name_buf, true);
fs::FsPath path;
std::snprintf(path, sizeof(path), "%s [%016lX][v%u]", name_buf.s, e.app_id, e.version);
return path;
}
auto BuildXciBasePath(std::span<const ApplicationEntry> entries) -> fs::FsPath {
fs::FsPath path;
for (s64 i = 0; i < std::size(entries); i++) {
if (i) {
path += " + ";
}
path += BuildXciName(entries[i]);
}
return path;
}
#if 0
// builds path suiteable for usb transfer.
auto BuildFilePath(DumpFileType type, std::span<const ApplicationEntry> entries) -> fs::FsPath {
return BuildXciBasePath(entries) + GetDumpTypeStr(type);
}
#endif
// builds path suiteable for file dumps.
auto BuildFullDumpPath(DumpFileType type, std::span<const ApplicationEntry> entries) -> fs::FsPath {
const auto base_path = BuildXciBasePath(entries);
fs::FsPath out;
if (App::GetApp()->m_dump_app_folder.Get()) {
if (App::GetApp()->m_dump_append_folder_with_xci.Get()) {
out = base_path + ".xci/" + base_path + GetDumpTypeStr(type);
} else {
out = base_path + "/" + base_path + GetDumpTypeStr(type);
}
} else {
out = base_path + GetDumpTypeStr(type);
}
return fs::AppendPath("/dumps/Gamecard", out);
}
// @Gc is the mount point, S is for secure partion, the remaining is the
@@ -45,6 +190,101 @@ auto BuildGcPath(const char* name, const FsGameCardHandle* handle, FsGameCardPar
return path;
}
struct XciSource final : dump::BaseSource {
// application name.
std::string application_name{};
// extra
std::vector<u8> id_set{};
std::vector<u8> uid{};
std::vector<u8> cert{};
std::vector<u8> initial{};
// size of the entire xci.
s64 xci_size{};
Menu* menu{};
int icon{};
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
if (path.ends_with(GetDumpTypeStr(DumpFileType_XCI))) {
size = ClipSize(off, size, xci_size);
*bytes_read = size;
return menu->GcStorageRead(buf, off, size);
} else {
std::span<const u8> span;
if (path.ends_with(GetDumpTypeStr(DumpFileType_Set))) {
span = id_set;
} else if (path.ends_with(GetDumpTypeStr(DumpFileType_UID))) {
span = uid;
} else if (path.ends_with(GetDumpTypeStr(DumpFileType_Cert))) {
span = cert;
} else if (path.ends_with(GetDumpTypeStr(DumpFileType_Initial))) {
span = initial;
}
R_UNLESS(!span.empty(), 0x1);
size = ClipSize(off, size, span.size());
*bytes_read = size;
std::memcpy(buf, span.data() + off, size);
R_SUCCEED();
}
}
auto GetName(const std::string& path) const -> std::string override {
return application_name;
}
auto GetSize(const std::string& path) const -> s64 override {
if (path.ends_with(GetDumpTypeStr(DumpFileType_XCI))) {
return xci_size;
} else if (path.ends_with(GetDumpTypeStr(DumpFileType_Set))) {
return id_set.size();
} else if (path.ends_with(GetDumpTypeStr(DumpFileType_UID))) {
return uid.size();
} else if (path.ends_with(GetDumpTypeStr(DumpFileType_Cert))) {
return cert.size();
} else if (path.ends_with(GetDumpTypeStr(DumpFileType_Initial))) {
return initial.size();
}
return 0;
}
auto GetIcon(const std::string& path) const -> int override {
return icon;
}
private:
static auto InRange(s64 off, s64 offset, s64 size) -> bool {
return off < offset + size && off >= offset;
}
static auto ClipSize(s64 off, s64 size, s64 file_size) -> s64 {
return std::min(size, file_size - off);
}
};
struct HashStr {
char str[0x21];
};
HashStr hexIdToStr(auto id) {
HashStr str{};
const auto id_lower = std::byteswap(*(u64*)id.c);
const auto id_upper = std::byteswap(*(u64*)(id.c + 0x8));
std::snprintf(str.str, 0x21, "%016lx%016lx", id_lower, id_upper);
return str;
}
// from Gamecard-Installer-NX
Result fsOpenGameCardStorage(FsStorage* out, const FsGameCardHandle* handle, FsGameCardPartitionRaw partition) {
const struct {
FsGameCardHandle handle;
u32 partition;
} in = { *handle, (u32)partition };
return serviceDispatchIn(fsGetServiceSession(), 30, in, .out_num_objects = 1, .out_objects = &out->s);
}
Result fsOpenGameCardDetectionEventNotifier(FsEventNotifier* out) {
return serviceDispatch(fsGetServiceSession(), 501,
.out_num_objects = 1,
@@ -52,24 +292,24 @@ Result fsOpenGameCardDetectionEventNotifier(FsEventNotifier* out) {
);
}
auto InRange(u64 off, u64 offset, u64 size) -> bool {
return off < offset + size && off >= offset;
}
struct GcSource final : yati::source::Base {
GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs, bool sd_install);
~GcSource();
GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs);
Result Read(void* buf, s64 off, s64 size, u64* bytes_read);
yati::container::Collections m_collections{};
yati::ConfigOverride m_config{};
fs::FsNativeGameCard* m_fs{};
FsFile m_file{};
fs::File m_file{};
s64 m_offset{};
s64 m_size{};
private:
static auto InRange(s64 off, s64 offset, s64 size) -> bool {
return off < offset + size && off >= offset;
}
};
GcSource::GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs, bool sd_install)
GcSource::GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs)
: m_fs{fs} {
m_offset = -1;
@@ -112,21 +352,15 @@ GcSource::GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs, bool
}
// we don't need to verify the nca's, this speeds up installs.
m_config.sd_card_install = sd_install;
m_config.skip_nca_hash_verify = true;
m_config.skip_rsa_header_fixed_key_verify = true;
m_config.skip_rsa_npdm_fixed_key_verify = true;
}
GcSource::~GcSource() {
fsFileClose(&m_file);
}
Result GcSource::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
// check is we need to open a new file.
if (!InRange(off, m_offset, m_size)) {
fsFileClose(&m_file);
m_file = {};
m_file.Close();
// find new file based on the offset.
bool found = false;
@@ -144,7 +378,7 @@ Result GcSource::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
R_UNLESS(found, 0x1);
}
return fsFileRead(&m_file, off - m_offset, buf, size, 0, bytes_read);
return m_file.Read(off - m_offset, buf, size, 0, bytes_read);
}
} // namespace
@@ -168,13 +402,22 @@ auto ApplicationEntry::GetSize() const -> s64 {
return size;
}
Menu::Menu() : MenuBase{"GameCard"_i18n} {
Menu::Menu(u32 flags) : MenuBase{"GameCard"_i18n, flags} {
this->SetActions(
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}}),
std::make_pair(Button::X, Action{"Options"_i18n, [this](){
App::DisplayInstallOptions(false);
auto options = std::make_shared<Sidebar>("Game Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<SidebarEntryCallback>("Install options"_i18n, [this](){
App::DisplayInstallOptions(false);
}));
options->Add(std::make_shared<SidebarEntryCallback>("Dump options"_i18n, [this](){
App::DisplayDumpOptions(false);
}));
}})
);
@@ -193,13 +436,15 @@ Menu::~Menu() {
eventClose(std::addressof(m_event));
fsEventNotifierClose(std::addressof(m_event_notifier));
fsDeviceOperatorClose(std::addressof(m_dev_op));
nsExit();
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
// poll for the gamecard first before handling inputs as the gamecard
// may have been removed, thus pressing A would fail.
if (R_SUCCEEDED(eventWait(std::addressof(m_event), 0))) {
GcOnEvent();
if (m_dirty || R_SUCCEEDED(eventWait(std::addressof(m_event), 0))) {
GcOnEvent(m_dirty);
m_dirty = false;
}
MenuBase::Update(controller, touch);
@@ -222,12 +467,12 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
const auto size_sd_gb = (double)m_size_free_sd / 0x40000000;
const auto size_nand_gb = (double)m_size_free_nand / 0x40000000;
gfx::drawTextArgs(vg, 490, 135, 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "System memory %.1f GB", size_nand_gb);
gfx::drawTextArgs(vg, 490, 135, 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "System memory %.1f GB"_i18n.c_str(), size_nand_gb);
gfx::drawRect(vg, 480, 170, STORAGE_BAR_W, STORAGE_BAR_H, theme->GetColour(ThemeEntryID_TEXT));
gfx::drawRect(vg, 480 + 1, 170 + 1, STORAGE_BAR_W - 2, STORAGE_BAR_H - 2, theme->GetColour(ThemeEntryID_BACKGROUND));
gfx::drawRect(vg, 480 + 2, 170 + 2, STORAGE_BAR_W - (((double)m_size_free_nand / (double)m_size_total_nand) * STORAGE_BAR_W) - 4, STORAGE_BAR_H - 4, theme->GetColour(ThemeEntryID_TEXT));
gfx::drawTextArgs(vg, 870, 135, 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "microSD card %.1f GB", size_sd_gb);
gfx::drawTextArgs(vg, 870, 135, 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "microSD card %.1f GB"_i18n.c_str(), size_sd_gb);
gfx::drawRect(vg, 860, 170, STORAGE_BAR_W, STORAGE_BAR_H, theme->GetColour(ThemeEntryID_TEXT));
gfx::drawRect(vg, 860 + 1, 170 + 1, STORAGE_BAR_W - 2, STORAGE_BAR_H - 2, theme->GetColour(ThemeEntryID_BACKGROUND));
gfx::drawRect(vg, 860 + 2, 170 + 2, STORAGE_BAR_W - (((double)m_size_free_sd / (double)m_size_total_sd) * STORAGE_BAR_W) - 4, STORAGE_BAR_H - 4, theme->GetColour(ThemeEntryID_TEXT));
@@ -241,8 +486,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
nvgSave(vg);
nvgIntersectScissor(vg, 50, 90, 325, 555);
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", m_lang_entry.name);
gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", m_lang_entry.author);
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", e.lang_entry.name);
gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", e.lang_entry.author);
gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "App-ID: 0%lX", e.app_id);
gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key-Gen: %u (%s)", e.key_gen, nca::GetKeyGenStr(e.key_gen));
gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Size: %.2f GB", (double)size / 0x40000000);
@@ -265,7 +510,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
colour = ThemeEntryID_TEXT_INFO;
}
gfx::drawTextArgs(vg, x + 15, y + (h / 2.f), 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(colour), "%s", g_option_list[i]);
gfx::drawTextArgs(vg, x + 15, y + (h / 2.f), 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(colour), "%s", i18n::get(g_option_list[i]).c_str());
});
}
@@ -279,22 +524,23 @@ void Menu::OnFocusGained() {
Result Menu::GcMount() {
GcUnmount();
R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(m_handle)));
// after storage has been mounted, it will take X attempts to mount
// 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);
if (R_SUCCEEDED(m_fs->GetFsOpenResult())) {
break;
}
}
m_fs = std::make_unique<fs::FsNativeGameCard>(std::addressof(m_handle), FsGameCardPartition_Secure, false);
R_TRY(m_fs->GetFsOpenResult());
FsDir dir;
fs::Dir dir;
R_TRY(m_fs->OpenDirectory("/", FsDirOpenMode_ReadFiles, std::addressof(dir)));
ON_SCOPE_EXIT(fsDirClose(std::addressof(dir)));
s64 count;
R_TRY(m_fs->DirGetEntryCount(std::addressof(dir), std::addressof(count)));
std::vector<FsDirectoryEntry> buf(count);
s64 total_entries;
R_TRY(m_fs->DirRead(std::addressof(dir), std::addressof(total_entries), buf.size(), buf.data()));
R_UNLESS(buf.size() == total_entries, 0x1);
std::vector<FsDirectoryEntry> buf;
R_TRY(dir.ReadAll(buf));
yati::container::Collections ticket_collections;
for (const auto& e : buf) {
@@ -384,21 +630,63 @@ Result Menu::GcMount() {
e.tickets = ticket_collections;
}
// load all control data, icons are loaded when displayed.
for (auto& e : m_entries) {
R_TRY(LoadControlData(e));
NacpLanguageEntry* lang_entry{};
R_TRY(nacpGetLanguageEntry(&e.control->nacp, &lang_entry));
if (lang_entry) {
e.lang_entry = *lang_entry;
}
}
SetAction(Button::A, Action{"OK"_i18n, [this](){
if (m_option_index == 2) {
SetPop();
} else {
if (m_mounted) {
App::Push(std::make_shared<ui::ProgressBox>(m_icon, "Installing "_i18n, m_lang_entry.name, [this](auto pbox) mutable -> bool {
auto source = std::make_shared<GcSource>(m_entries[m_entry_index], m_fs.get(), m_option_index == 1);
return R_SUCCEEDED(yati::InstallFromCollections(pbox, source, source->m_collections, source->m_config));
}, [this](bool result){
if (result) {
App::Notify("Gc install success!"_i18n);
} else {
App::Notify("Gc install failed!"_i18n);
}
}));
if (!m_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 {
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);
}
}
}});
@@ -422,17 +710,148 @@ Result Menu::GcMount() {
}
void Menu::GcUnmount() {
GcUmountStorage();
m_fs.reset();
m_entries.clear();
m_entry_index = 0;
m_mounted = false;
m_lang_entry = {};
FreeImage();
RemoveAction(Button::L2);
RemoveAction(Button::R2);
}
Result Menu::GcMountStorage() {
GcUmountStorage();
R_TRY(GcMountPartition(FsGameCardPartitionRaw_Normal));
R_TRY(fsStorageGetSize(&m_storage, &m_storage_full_size));
u8 header[0x200];
R_TRY(fsStorageRead(&m_storage, 0, header, sizeof(header)));
u32 magic;
u32 trim_size;
u8 rom_size;
std::memcpy(&magic, header + 0x100, sizeof(magic));
std::memcpy(&rom_size, header + 0x10D, sizeof(rom_size));
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);
// 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_TRY(fsStorageGetSize(&m_storage, &m_parition_normal_size));
R_TRY(GcMountPartition(FsGameCardPartitionRaw_Secure));
R_TRY(fsStorageGetSize(&m_storage, &m_parition_secure_size));
m_storage_trimmed_size = sizeof(header) + trim_size * 512ULL;
m_storage_total_size = m_parition_normal_size + m_parition_secure_size;
m_storage_mounted = true;
log_write("[GC] m_storage_trimmed_size: %zd\n", m_storage_trimmed_size);
log_write("[GC] m_storage_total_size: %zd\n", m_storage_total_size);
R_SUCCEED();
}
void Menu::GcUmountStorage() {
if (m_storage_mounted) {
m_storage_mounted = false;
GcUnmountPartition();
}
}
Result Menu::GcMountPartition(FsGameCardPartitionRaw partition) {
if (m_partition == partition) {
R_SUCCEED();
}
GcUnmountPartition();
// first attempt always fails due to qlaunch having the secure area mounted.
// the 2nd attempt will succeeded, but qlaunch will fail to mount
// the gamecard as it will only attempt to mount once.
Result rc;
for (u32 i = 0; i < REMOUNT_ATTEMPT_MAX; i++) {
R_TRY(fsDeviceOperatorGetGameCardHandle(&m_dev_op, &m_handle));
if (R_SUCCEEDED(rc = fsOpenGameCardStorage(&m_storage, &m_handle, partition))){
break;
}
}
m_partition = partition;
return rc;
}
void Menu::GcUnmountPartition() {
if (m_partition != FsGameCardPartitionRaw_None) {
m_partition = FsGameCardPartitionRaw_None;
fsStorageClose(&m_storage);
}
}
Result Menu::GcStorageReadInternal(void* buf, s64 off, s64 size, u64* bytes_read) {
if (off < m_parition_normal_size) {
size = std::min<s64>(size, m_parition_normal_size - off);
R_TRY(GcMountPartition(FsGameCardPartitionRaw_Normal));
} else {
off = off - m_parition_normal_size;
R_TRY(GcMountPartition(FsGameCardPartitionRaw_Secure));
}
R_TRY(fsStorageRead(&m_storage, off, buf, size));
*bytes_read = size;
R_SUCCEED();
}
Result Menu::GcStorageRead(void* _buf, s64 off, s64 size) {
auto buf = static_cast<u8*>(_buf);
u64 bytes_read;
u8 data[0x200];
size = std::min(size, m_storage_total_size - off);
if (size <= 0) {
R_SUCCEED();
}
const auto unaligned_off = off % 0x200;
off -= unaligned_off;
if (size > 0 && unaligned_off) {
R_TRY(GcStorageReadInternal(data, off, sizeof(data), &bytes_read));
const auto csize = std::min<s64>(size, 0x200 - unaligned_off);
std::memcpy(buf, data + unaligned_off, csize);
off += bytes_read;
size -= csize;
buf += csize;
}
const auto unaligned_size = size % 0x200;
size -= unaligned_size;
while (size > 0) {
R_TRY(GcStorageReadInternal(buf, off, size, &bytes_read));
off += bytes_read;
size -= bytes_read;
buf += bytes_read;
}
if (unaligned_size) {
R_TRY(GcStorageReadInternal(data, off, sizeof(data), &bytes_read));
const auto csize = std::min<s64>(size, 0x200 - unaligned_size);
std::memcpy(buf, data + unaligned_size, csize);
}
R_SUCCEED();
}
Result Menu::GcPoll(bool* inserted) {
R_TRY(fsDeviceOperatorIsGameCardInserted(&m_dev_op, inserted));
@@ -448,11 +867,11 @@ Result Menu::GcPoll(bool* inserted) {
R_SUCCEED();
}
Result Menu::GcOnEvent() {
Result Menu::GcOnEvent(bool force) {
bool inserted{};
R_TRY(GcPoll(&inserted));
if (m_mounted != inserted) {
if (force || m_mounted != inserted) {
log_write("gc state changed\n");
m_mounted = inserted;
if (m_mounted) {
@@ -488,35 +907,15 @@ void Menu::FreeImage() {
}
}
void Menu::OnChangeIndex(s64 new_index) {
FreeImage();
m_entry_index = new_index;
const auto index = m_entries.empty() ? 0 : m_entry_index + 1;
this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size()));
const auto id = m_entries[m_entry_index].app_id;
Result Menu::LoadControlData(ApplicationEntry& e) {
const auto id = e.app_id;
e.control = std::make_unique<NsApplicationControlData>();
if (hosversionBefore(20,0,0)) {
TimeStamp ts;
auto control = std::make_unique<NsApplicationControlData>();
u64 control_size;
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, id, control.get(), sizeof(NsApplicationControlData), &control_size))) {
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, id, e.control.get(), sizeof(NsApplicationControlData), &e.control_size))) {
log_write("\t\t[ns control cache] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
NacpLanguageEntry* lang_entry{};
nacpGetLanguageEntry(&control->nacp, &lang_entry);
if (lang_entry) {
m_lang_entry = *lang_entry;
}
const auto jpeg_size = control_size - sizeof(NacpStruct);
m_icon = nvgCreateImageMem(App::GetVg(), 0, control->icon, jpeg_size);
if (m_icon > 0) {
return;
}
R_SUCCEED();
}
}
@@ -525,11 +924,9 @@ void Menu::OnChangeIndex(s64 new_index) {
// waiting 1-2s after mount, then calling seems to work.
// however, we can just manually parse the nca to get the data we need,
// which always works and *is* faster too ;)
for (auto& e : m_entries[m_entry_index].application) {
for (auto& collection : e) {
for (const auto& app : e.application) {
for (const auto& collection : app) {
if (collection.type == NcmContentType_Control) {
NacpStruct nacp;
std::vector<u8> icon;
const auto path = BuildGcPath(collection.name.c_str(), &m_handle);
u64 program_id = id | collection.id_offset;
@@ -538,27 +935,204 @@ void Menu::OnChangeIndex(s64 new_index) {
}
TimeStamp ts;
if (R_SUCCEEDED(nca::ParseControl(path, program_id, &nacp, sizeof(nacp), &icon))) {
std::vector<u8> icon;
if (R_SUCCEEDED(nca::ParseControl(path, program_id, &e.control->nacp, sizeof(e.control->nacp), &icon))) {
std::memcpy(e.control->icon, icon.data(), icon.size());
e.control_size = sizeof(e.control->nacp) + icon.size();
log_write("\t\tnca::ParseControl(): %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
log_write("managed to parse control nca %s\n", path.s);
NacpLanguageEntry* lang_entry{};
nacpGetLanguageEntry(&nacp, &lang_entry);
if (lang_entry) {
m_lang_entry = *lang_entry;
}
m_icon = nvgCreateImageMem(App::GetVg(), 0, icon.data(), icon.size());
if (m_icon > 0) {
return;
}
R_SUCCEED();
} else {
log_write("\tFAILED to parse control nca %s\n", path.s);
}
}
}
}
return 0x1;
}
void Menu::OnChangeIndex(s64 new_index) {
FreeImage();
m_entry_index = new_index;
if (m_entries.empty()) {
this->SetSubHeading("No GameCard inserted");
} else {
const auto index = m_entries.empty() ? 0 : m_entry_index + 1;
this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size()));
const auto& 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);
}
}
Result Menu::DumpGames(u32 flags) {
// first, try and mount the storage.
// this will fill out the xci header, verify and get sizes.
R_TRY(GcMountStorage());
const auto do_dump = [this](u32 flags) -> Result {
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
ON_SCOPE_EXIT(appletSetCpuBoostMode(ApmCpuBoostMode_Normal));
u32 location_flags = dump::DumpLocationFlag_All;
// if we need to dump any of the bins, read fs memory until we find
// what we are looking for.
// the below code, along with the structs is taken from nxdumptool.
GameCardSecurityInformation security_info;
if ((flags &~ DumpFileFlag_XCI)) {
location_flags &= ~dump::DumpLocationFlag_UsbS2S;
R_TRY(GcGetSecurityInfo(security_info));
}
auto source = std::make_shared<XciSource>();
source->menu = this;
source->application_name = m_entries[m_entry_index].lang_entry.name;
source->icon = m_icon;
std::vector<fs::FsPath> paths;
if (flags & DumpFileFlag_XCI) {
if (App::GetApp()->m_dump_trim_xci.Get()) {
source->xci_size = m_storage_trimmed_size;
paths.emplace_back(BuildFullDumpPath(DumpFileType_TrimmedXCI, m_entries));
} else {
source->xci_size = m_storage_total_size;
paths.emplace_back(BuildFullDumpPath(DumpFileType_XCI, m_entries));
}
}
if (flags & DumpFileFlag_Set) {
source->id_set.resize(sizeof(FsGameCardIdSet));
R_TRY(fsDeviceOperatorGetGameCardIdSet(&m_dev_op, source->id_set.data(), source->id_set.size(), source->id_set.size()));
paths.emplace_back(BuildFullDumpPath(DumpFileType_Set, m_entries));
}
if (flags & DumpFileFlag_UID) {
source->uid.resize(sizeof(security_info.specific_data.card_uid));
std::memcpy(source->uid.data(), &security_info.specific_data.card_uid, source->uid.size());
paths.emplace_back(BuildFullDumpPath(DumpFileType_UID, m_entries));
}
if (flags & DumpFileFlag_Cert) {
source->cert.resize(sizeof(security_info.certificate));
std::memcpy(source->cert.data(), &security_info.certificate, source->cert.size());
paths.emplace_back(BuildFullDumpPath(DumpFileType_Cert, m_entries));
}
if (flags & DumpFileFlag_Initial) {
source->initial.resize(sizeof(security_info.initial_data));
std::memcpy(source->initial.data(), &security_info.initial_data, source->initial.size());
paths.emplace_back(BuildFullDumpPath(DumpFileType_Initial, m_entries));
}
dump::Dump(source, paths, [](Result){}, location_flags);
R_SUCCEED();
};
// run some checks to see if the gamecard we can read past the trimmed size.
// if we can, then this is a full / valid gamecard.
// if it fails, it's likely a flashcart with a trimmed xci (will N check this?)
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);
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);
}
}
));
} else {
do_dump(flags);
}
R_SUCCEED();
}
Result Menu::GcGetSecurityInfo(GameCardSecurityInformation& out) {
R_TRY(GcMountPartition(FsGameCardPartitionRaw_Secure));
constexpr u64 title_id = 0x0100000000000000; // FS
Handle handle{};
DebugEventInfo event_info{};
u64 pids[0x50]{};
s32 process_count{};
R_TRY(svcGetProcessList(&process_count, pids, std::size(pids)));
for (s32 i = 0; i < (process_count - 1); i++) {
if (R_SUCCEEDED(svcDebugActiveProcess(&handle, pids[i]))) {
ON_SCOPE_EXIT(svcCloseHandle(handle));
if (R_FAILED(svcGetDebugEvent(&event_info, handle)) || title_id != event_info.title_id) {
continue;
}
const auto package_id = m_package_id;
static u64 addr{};
MemoryInfo mem_info{};
u32 page_info{};
std::vector<u8> data{};
for (;;) {
R_TRY(svcQueryDebugProcessMemory(&mem_info, &page_info, handle, addr));
// if addr=0 then we hit the reserved memory section
addr = mem_info.addr + mem_info.size;
if (!addr) {
break;
}
// skip memory that we don't want
if (mem_info.attr || !mem_info.size || (mem_info.perm & Perm_Rw) != Perm_Rw || (mem_info.type & MemState_Type) != MemType_CodeMutable) {
continue;
}
data.resize(mem_info.size);
R_TRY(svcReadDebugProcessMemory(data.data(), handle, mem_info.addr, data.size()));
for (s64 i = 0; i < data.size(); i += 8) {
if (i + sizeof(out.initial_data) >= data.size()) {
break;
}
if (!std::memcmp(&package_id, data.data() + i, sizeof(m_package_id))) [[unlikely]] {
log_write("[GC] found the package id\n");
u8 hash[SHA256_HASH_SIZE];
sha256CalculateHash(hash, data.data() + i, 0x200);
if (!std::memcmp(hash, m_initial_data_hash, sizeof(hash))) {
// successive calls will jump to the addr as the location will not change.
addr = mem_info.addr;
log_write("[GC] found the security info\n");
log_write("\tperm: 0x%X\n", mem_info.perm);
log_write("\ttype: 0x%X\n", mem_info.type & MemState_Type);
log_write("\taddr: 0x%016lX\n", mem_info.addr);
log_write("\toff: 0x%016lX\n", mem_info.addr + i);
std::memcpy(&out, data.data() + i - offsetof(GameCardSecurityInformation, initial_data), sizeof(out));
R_SUCCEED();
}
}
}
}
}
}
R_THROW(0x1);
}
} // namespace sphaira::ui::menu::gc

View File

@@ -81,32 +81,28 @@ void from_json(const fs::FsPath& path, GhApiEntry& e) {
);
}
auto DownloadApp(ProgressBox* pbox, const GhApiAsset& gh_asset, const AssetEntry* entry) -> bool {
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_RESULT(fs.GetFsOpenResult(), false);
R_TRY(fs.GetFsOpenResult());
ON_SCOPE_EXIT(fs.DeleteFile(temp_file));
if (gh_asset.browser_download_url.empty()) {
log_write("failed to find asset\n");
return false;
}
R_UNLESS(!gh_asset.browser_download_url.empty(), 0x1);
// 2. download the asset
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Downloading "_i18n + gh_asset.name);
log_write("starting download: %s\n", gh_asset.browser_download_url.c_str());
if (!curl::Api().ToFile(
const auto result = curl::Api().ToFile(
curl::Url{gh_asset.browser_download_url},
curl::Path{temp_file},
curl::OnProgress{pbox->OnDownloadProgressCallback()}
).success){
log_write("error with download\n");
return false;
}
);
R_UNLESS(result.success, 0x1);
}
fs::FsPath root_path{"/"};
@@ -118,28 +114,25 @@ auto DownloadApp(ProgressBox* pbox, const GhApiAsset& gh_asset, const AssetEntry
if (gh_asset.content_type.find("zip") != gh_asset.content_type.npos) {
log_write("found zip\n");
auto zfile = unzOpen64(temp_file);
if (!zfile) {
log_write("failed to open zip: %s\n", temp_file.s);
return false;
}
R_UNLESS(zfile, 0x1);
ON_SCOPE_EXIT(unzClose(zfile));
unz_global_info64 pglobal_info;
if (UNZ_OK != unzGetGlobalInfo64(zfile, &pglobal_info)) {
return false;
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");
return false;
R_THROW(0x1);
}
}
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
log_write("failed to open current file\n");
return false;
R_THROW(0x1);
}
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
@@ -147,7 +140,7 @@ auto DownloadApp(ProgressBox* pbox, const GhApiAsset& gh_asset, const AssetEntry
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");
return false;
R_THROW(0x1);
}
file_path = fs::AppendPath(root_path, file_path);
@@ -156,44 +149,35 @@ auto DownloadApp(ProgressBox* pbox, const GhApiAsset& gh_asset, const AssetEntry
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);
return false;
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);
return false;
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);
return false;
R_THROW(rc);
}
FsFile f;
if (R_FAILED(rc = fs.OpenFile(file_path, FsOpenMode_Write, &f))) {
log_write("failed to open file: %s 0x%04X\n", file_path.s, rc);
return false;
}
ON_SCOPE_EXIT(fsFileClose(&f));
if (R_FAILED(rc = fsFileSetSize(&f, info.uncompressed_size))) {
log_write("failed to set file size: %s 0x%04X\n", file_path.s, rc);
return false;
}
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);
return false;
R_THROW(0x1);
}
if (R_FAILED(rc = fsFileWrite(&f, offset, buf.data(), bytes_read, FsWriteOption_None))) {
log_write("failed to write file: %s 0x%04X\n", file_path.s, rc);
return false;
}
R_TRY(f.Write(offset, buf.data(), bytes_read, FsWriteOption_None));
pbox->UpdateTransfer(offset, info.uncompressed_size);
offset += bytes_read;
@@ -203,16 +187,14 @@ auto DownloadApp(ProgressBox* pbox, const GhApiAsset& gh_asset, const AssetEntry
} else {
fs.CreateDirectoryRecursivelyWithPath(root_path);
fs.DeleteFile(root_path);
if (R_FAILED(fs.RenameFile(temp_file, root_path))) {
log_write("failed to rename file: %s -> %s\n", temp_file.s, root_path.s);
}
R_TRY(fs.RenameFile(temp_file, root_path));
}
log_write("success\n");
return true;
R_SUCCEED();
}
auto DownloadAssetJson(ProgressBox* pbox, const std::string& url, GhApiEntry& out) -> bool {
auto DownloadAssetJson(ProgressBox* pbox, const std::string& url, GhApiEntry& out) -> Result {
// 1. download the json
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Downloading json"_i18n);
@@ -230,20 +212,17 @@ auto DownloadAssetJson(ProgressBox* pbox, const std::string& url, GhApiEntry& ou
}
);
if (!result.success) {
log_write("json empty\n");
return false;
}
R_UNLESS(result.success, 0x1);
from_json(result.path, out);
}
return !out.assets.empty();
R_UNLESS(!out.assets.empty(), 0x1);
R_SUCCEED();
}
} // namespace
Menu::Menu() : MenuBase{"GitHub"_i18n} {
Menu::Menu(u32 flags) : MenuBase{"GitHub"_i18n, flags} {
fs::FsNativeSd().CreateDirectoryRecursively(CACHE_PATH);
this->SetActions(
@@ -256,10 +235,12 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
static GhApiEntry gh_entry;
gh_entry = {};
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, GetEntry().repo, [this](auto pbox){
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, GetEntry().repo, [this](auto pbox) -> Result {
return DownloadAssetJson(pbox, GenerateApiUrl(GetEntry()), gh_entry);
}, [this](bool success){
if (success) {
}, [this](Result rc){
App::PushErrorBox(rc, "Failed to download json"_i18n);
if (R_SUCCEEDED(rc)) {
const auto& assets = GetEntry().assets;
PopupList::Items asset_items;
std::vector<const AssetEntry*> asset_ptr;
@@ -304,10 +285,12 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
}
const auto func = [this, &asset_entry, ptr](){
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, GetEntry().repo, [this, &asset_entry, ptr](auto pbox){
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, GetEntry().repo, [this, &asset_entry, ptr](auto pbox) -> Result {
return DownloadApp(pbox, asset_entry, ptr);
}, [this, ptr](bool success){
if (success) {
}, [this, ptr](Result rc){
App::PushErrorBox(rc, "Failed to download app!"_i18n);
if (R_SUCCEEDED(rc)) {
App::Notify("Downloaded "_i18n + GetEntry().repo);
auto post_install_message = GetEntry().post_install_message;
if (ptr && !ptr->post_install_message.empty()) {
@@ -318,7 +301,7 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
App::Push(std::make_shared<OptionBox>(post_install_message, "OK"_i18n));
}
}
}, 2));
}));
};
if (!pre_install_message.empty()) {
@@ -335,7 +318,7 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
}
}));
}
}, 2));
}));
}}),
std::make_pair(Button::B, Action{"Back"_i18n, [this](){

View File

@@ -18,6 +18,8 @@
namespace sphaira::ui::menu::homebrew {
namespace {
Menu* g_menu{};
auto GenerateStarPath(const fs::FsPath& nro_path) -> fs::FsPath {
fs::FsPath out{};
const auto dilem = std::strrchr(nro_path.s, '/');
@@ -32,7 +34,17 @@ void FreeEntry(NVGcontext* vg, NroEntry& e) {
} // namespace
Menu::Menu() : grid::Menu{"Homebrew"_i18n} {
auto GetNroEntries() -> std::span<const NroEntry> {
if (!g_menu) {
return {};
}
return g_menu->GetHomebrewList();
}
Menu::Menu() : grid::Menu{"Homebrew"_i18n, MenuFlag_Tab} {
g_menu = this;
this->SetActions(
std::make_pair(Button::A, Action{"Launch"_i18n, [this](){
nro_launch(m_entries[m_index].path);
@@ -129,6 +141,7 @@ Menu::Menu() : grid::Menu{"Homebrew"_i18n} {
}
Menu::~Menu() {
g_menu = {};
FreeEntries();
}
@@ -233,7 +246,7 @@ void Menu::SetIndex(s64 index) {
void Menu::InstallHomebrew() {
const auto& nro = m_entries[m_index];
InstallHomebrew(nro.path, nro.nacp, nro_get_icon(nro.path, nro.icon_size, nro.icon_offset));
InstallHomebrew(nro.path, nro_get_icon(nro.path, nro.icon_size, nro.icon_offset));
}
void Menu::ScanHomebrew() {
@@ -266,8 +279,6 @@ void Menu::ScanHomebrew() {
if (user->ini) {
if (!strcmp(Key, "timestamp")) {
user->ini->timestamp = atoi(Value);
} else if (!strcmp(Key, "launch_count")) {
user->ini->launch_count = atoi(Value);
}
}
@@ -412,19 +423,16 @@ void Menu::OnLayoutChange() {
grid::Menu::OnLayoutChange(m_list, m_layout.Get());
}
Result Menu::InstallHomebrew(const fs::FsPath& path, const NacpStruct& nacp, const std::vector<u8>& icon) {
Result Menu::InstallHomebrew(const fs::FsPath& path, const std::vector<u8>& icon) {
OwoConfig config{};
config.nro_path = path.toString();
config.nacp = nacp;
R_TRY(nro_get_nacp(path, config.nacp));
config.icon = icon;
return App::Install(config);
}
Result Menu::InstallHomebrewFromPath(const fs::FsPath& path) {
NacpStruct nacp;
R_TRY(nro_get_nacp(path, nacp))
const auto icon = nro_get_icon(path);
return InstallHomebrew(path, nacp, icon);
return InstallHomebrew(path, nro_get_icon(path));
}
} // namespace sphaira::ui::menu::homebrew

View File

@@ -75,7 +75,7 @@ void irsConvertConfigNormalToEx(const IrsImageTransferProcessorConfig* nor, IrsI
} // namespace
Menu::Menu() : MenuBase{"Irs"_i18n} {
Menu::Menu(u32 flags) : MenuBase{"Irs"_i18n, flags} {
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}});

View File

@@ -6,6 +6,8 @@
#include "ui/progress_box.hpp"
#include "ui/error_box.hpp"
#include "ui/menus/homebrew.hpp"
#include "ui/menus/filebrowser.hpp"
#include "ui/menus/irs_menu.hpp"
#include "ui/menus/themezer.hpp"
#include "ui/menus/ghdl.hpp"
@@ -32,41 +34,41 @@ constexpr const char* GITHUB_URL{"https://api.github.com/repos/ITotalJustice/sph
constexpr fs::FsPath CACHE_PATH{"/switch/sphaira/cache/sphaira_latest.json"};
template<typename T>
auto MiscMenuFuncGenerator() {
return std::make_shared<T>();
auto MiscMenuFuncGenerator(u32 flags) {
return std::make_shared<T>(flags);
}
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 = "Themezer", .title = "Themezer", .func = MiscMenuFuncGenerator<ui::menu::themezer::Menu>, .flag = MiscMenuFlag_Shortcut },
{ .name = "GitHub", .title = "GitHub", .func = MiscMenuFuncGenerator<ui::menu::gh::Menu>, .flag = MiscMenuFlag_Shortcut },
{ .name = "FTP", .title = "FTP Install", .func = MiscMenuFuncGenerator<ui::menu::ftp::Menu>, .flag = MiscMenuFlag_Install },
{ .name = "USB", .title = "USB Install", .func = MiscMenuFuncGenerator<ui::menu::usb::Menu>, .flag = MiscMenuFlag_Install },
{ .name = "GameCard", .title = "GameCard Install", .func = MiscMenuFuncGenerator<ui::menu::gc::Menu>, .flag = MiscMenuFlag_Shortcut|MiscMenuFlag_Install },
{ .name = "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 },
};
auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string version) -> bool {
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_RESULT(fs.GetFsOpenResult(), false);
R_TRY(fs.GetFsOpenResult());
// 1. download the zip
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Downloading "_i18n + version);
log_write("starting download: %s\n", url.c_str());
if (!curl::Api().ToFile(
const auto result = curl::Api().ToFile(
curl::Url{url},
curl::Path{zip_out},
curl::OnProgress{pbox->OnDownloadProgressCallback()}
).success) {
log_write("error with download\n");
return false;
}
);
R_UNLESS(result.success, 0x1);
}
ON_SCOPE_EXIT(fs.DeleteFile(zip_out));
@@ -74,28 +76,25 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
// 2. extract the zip
if (!pbox->ShouldExit()) {
auto zfile = unzOpen64(zip_out);
if (!zfile) {
log_write("failed to open zip: %s\n", zip_out.s);
return false;
}
R_UNLESS(zfile, 0x1);
ON_SCOPE_EXIT(unzClose(zfile));
unz_global_info64 pglobal_info;
if (UNZ_OK != unzGetGlobalInfo64(zfile, &pglobal_info)) {
return false;
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");
return false;
R_THROW(0x1);
}
}
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
log_write("failed to open current file\n");
return false;
R_THROW(0x1);
}
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
@@ -103,7 +102,7 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
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");
return false;
R_THROW(0x1);
}
if (file_path[0] != '/') {
@@ -118,26 +117,18 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
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);
return false;
R_THROW(0x1);
}
} 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);
return false;
R_THROW(rc);
}
FsFile f;
if (R_FAILED(rc = fs.OpenFile(file_path, FsOpenMode_Write, &f))) {
log_write("failed to open file: %s 0x%04X\n", file_path.s, rc);
return false;
}
ON_SCOPE_EXIT(fsFileClose(&f));
if (R_FAILED(rc = fsFileSetSize(&f, info.uncompressed_size))) {
log_write("failed to set file size: %s 0x%04X\n", file_path.s, rc);
return false;
}
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{};
@@ -145,13 +136,10 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
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());
return false;
R_THROW(0x1);
}
if (R_FAILED(rc = fsFileWrite(&f, offset, buf.data(), bytes_read, FsWriteOption_None))) {
log_write("failed to write file: %s 0x%04X\n", file_path.s, rc);
return false;
}
R_TRY(f.Write(offset, buf.data(), bytes_read, FsWriteOption_None));
pbox->UpdateTransfer(offset, info.uncompressed_size);
offset += bytes_read;
@@ -161,19 +149,44 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
}
log_write("finished update :)\n");
return true;
R_SUCCEED();
}
auto CreateRightSideMenu() -> std::shared_ptr<MenuBase> {
const auto name = App::GetApp()->m_right_side_menu.Get();
auto CreateLeftSideMenu(std::string& name_out) -> std::shared_ptr<MenuBase> {
const auto name = App::GetApp()->m_left_menu.Get();
for (auto& e : GetMiscMenuEntries()) {
if (e.name == name) {
return e.func();
name_out = name;
return e.func(MenuFlag_Tab);
}
}
return std::make_shared<ui::menu::appstore::Menu>();
name_out = "FileBrowser";
return std::make_shared<ui::menu::filebrowser::Menu>(MenuFlag_Tab);
}
auto CreateRightSideMenu(std::string_view left_name) -> std::shared_ptr<MenuBase> {
const auto name = App::GetApp()->m_right_menu.Get();
// handle if the user tries to mount the same menu twice.
if (name == left_name) {
// check if we can mount the default.
if (left_name != "AppStore") {
return std::make_shared<ui::menu::appstore::Menu>(MenuFlag_Tab);
} else {
// otherwise, fallback to left side default.
return std::make_shared<ui::menu::filebrowser::Menu>(MenuFlag_Tab);
}
}
for (auto& e : GetMiscMenuEntries()) {
if (e.name == name) {
return e.func(MenuFlag_Tab);
}
}
return std::make_shared<ui::menu::appstore::Menu>(MenuFlag_Tab);
}
} // namespace
@@ -212,7 +225,7 @@ MainMenu::MainMenu() {
const auto version = yyjson_get_str(tag_key);
R_UNLESS(version, false);
if (std::strcmp(APP_VERSION, version) >= 0) {
if (!App::IsVersionNewer(APP_VERSION, version)) {
m_update_state = UpdateState::None;
return true;
}
@@ -269,6 +282,7 @@ MainMenu::MainMenu() {
language_items.push_back("Russian"_i18n);
language_items.push_back("Swedish"_i18n);
language_items.push_back("Vietnamese"_i18n);
language_items.push_back("Ukrainian"_i18n);
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [](){
App::DisplayThemeOptions();
@@ -290,12 +304,22 @@ MainMenu::MainMenu() {
App::SetNxlinkEnable(enable);
}));
options->Add(std::make_shared<SidebarEntryBool>("Hdd"_i18n, App::GetHddEnable(), [](bool& enable){
App::SetHddEnable(enable);
}));
options->Add(std::make_shared<SidebarEntryBool>("Hdd write protect"_i18n, App::GetWriteProtect(), [](bool& enable){
App::SetWriteProtect(enable);
}));
if (m_update_state == UpdateState::Update) {
options->Add(std::make_shared<SidebarEntryCallback>("Download update: "_i18n + m_update_version, [this](){
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, "Sphaira v" + m_update_version, [this](auto pbox){
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, "Sphaira v" + m_update_version, [this](auto pbox) -> Result {
return InstallUpdate(pbox, m_update_url, m_update_version);
}, [this](bool success){
if (success) {
}, [this](Result rc){
App::PushErrorBox(rc, "Failed to download update"_i18n);
if (R_SUCCEEDED(rc)) {
m_update_state = UpdateState::None;
App::Notify("Updated to "_i18n + m_update_version);
App::Push(std::make_shared<OptionBox>(
@@ -303,10 +327,8 @@ MainMenu::MainMenu() {
App::ExitRestart();
}
));
} else {
App::Push(std::make_shared<ui::ErrorBox>(MAKERESULT(351, 1), "Failed to download update"_i18n));
}
}, 2));
}));
}));
}
}));
@@ -325,10 +347,13 @@ MainMenu::MainMenu() {
}})
);
m_homebrew_menu = std::make_shared<homebrew::Menu>();
m_filebrowser_menu = std::make_shared<filebrowser::Menu>(m_homebrew_menu->GetHomebrewList());
m_right_side_menu = CreateRightSideMenu();
m_current_menu = m_homebrew_menu;
m_centre_menu = std::make_shared<homebrew::Menu>();
m_current_menu = m_centre_menu;
std::string left_side_name;
m_left_menu = CreateLeftSideMenu(left_side_name);
m_right_menu = CreateRightSideMenu(left_side_name);
AddOnLRPress();
@@ -361,11 +386,11 @@ void MainMenu::OnFocusLost() {
void MainMenu::OnLRPress(std::shared_ptr<MenuBase> menu, Button b) {
m_current_menu->OnFocusLost();
if (m_current_menu == m_homebrew_menu) {
if (m_current_menu == m_centre_menu) {
m_current_menu = menu;
RemoveAction(b);
} else {
m_current_menu = m_homebrew_menu;
m_current_menu = m_centre_menu;
}
AddOnLRPress();
@@ -377,17 +402,17 @@ void MainMenu::OnLRPress(std::shared_ptr<MenuBase> menu, Button b) {
}
void MainMenu::AddOnLRPress() {
if (m_current_menu != m_filebrowser_menu) {
const auto label = m_current_menu == m_homebrew_menu ? m_filebrowser_menu->GetShortTitle() : m_homebrew_menu->GetShortTitle();
if (m_current_menu != m_left_menu) {
const auto label = m_current_menu == m_centre_menu ? m_left_menu->GetShortTitle() : m_centre_menu->GetShortTitle();
SetAction(Button::L, Action{i18n::get(label), [this]{
OnLRPress(m_filebrowser_menu, Button::L);
OnLRPress(m_left_menu, Button::L);
}});
}
if (m_current_menu != m_right_side_menu) {
const auto label = m_current_menu == m_homebrew_menu ? m_right_side_menu->GetShortTitle() : m_homebrew_menu->GetShortTitle();
if (m_current_menu != m_right_menu) {
const auto label = m_current_menu == m_centre_menu ? m_right_menu->GetShortTitle() : m_centre_menu->GetShortTitle();
SetAction(Button::R, Action{i18n::get(label), [this]{
OnLRPress(m_right_side_menu, Button::R);
OnLRPress(m_right_menu, Button::R);
}});
}
}

View File

@@ -6,11 +6,44 @@
namespace sphaira::ui::menu {
MenuBase::MenuBase(std::string title) : m_title{title} {
auto MenuBase::GetPolledData(bool force_refresh) -> PolledData {
static PolledData data{};
static TimeStamp timestamp{};
static bool has_init = false;
if (!has_init) {
has_init = true;
force_refresh = true;
}
// update every second, do this in Draw because Update() isn't called if it
// doesn't have focus.
if (force_refresh || timestamp.GetSeconds() >= 1) {
data.tm = {};
data.battery_percetange = {};
data.charger_type = {};
data.type = {};
data.status = {};
data.strength = {};
data.ip = {};
const auto t = std::time(NULL);
localtime_r(&t, &data.tm);
psmGetBatteryChargePercentage(&data.battery_percetange);
psmGetChargerType(&data.charger_type);
nifmGetInternetConnectionStatus(&data.type, &data.strength, &data.status);
nifmGetCurrentIpAddress(&data.ip);
timestamp.Update();
}
return data;
}
MenuBase::MenuBase(const std::string& title, u32 flags) : m_title{title}, m_flags{flags} {
// this->SetParent(this);
this->SetPos(30, 87, 1220 - 30, 646 - 87);
SetAction(Button::START, Action{App::Exit});
UpdateVars();
}
MenuBase::~MenuBase() {
@@ -24,11 +57,7 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
DrawElement(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, ThemeEntryID_BACKGROUND);
Widget::Draw(vg, theme);
// update every second, do this in Draw because Update() isn't called if it
// doesn't have focus.
if (m_poll_timestamp.GetSeconds() >= 1) {
UpdateVars();
}
const auto pdata = GetPolledData();
const float start_y = 70;
const float font_size = 22;
@@ -39,28 +68,30 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
nvgFontSize(vg, font_size);
#define draw(colour, ...) \
gfx::textBounds(vg, 0, 0, bounds, __VA_ARGS__); \
start_x -= bounds[2] - bounds[0]; \
gfx::drawTextArgs(vg, start_x, start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(colour), __VA_ARGS__); \
start_x -= spacing;
#define draw(colour, fixed, ...) \
gfx::drawTextArgs(vg, start_x, start_y, font_size, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(colour), __VA_ARGS__); \
if (fixed) { \
start_x -= fixed; \
} else { \
gfx::textBounds(vg, 0, 0, bounds, __VA_ARGS__); \
start_x -= spacing + (bounds[2] - bounds[0]); \
}
// draw("version %s", APP_VERSION);
draw(ThemeEntryID_TEXT, "%u\uFE6A", m_battery_percetange);
draw(ThemeEntryID_TEXT, 83, "%u\uFE6A", pdata.battery_percetange);
if (App::Get12HourTimeEnable()) {
draw(ThemeEntryID_TEXT, "%02u:%02u:%02u %s", (m_tm.tm_hour == 0 || m_tm.tm_hour == 12) ? 12 : m_tm.tm_hour % 12, m_tm.tm_min, m_tm.tm_sec, (m_tm.tm_hour < 12) ? "AM" : "PM");
draw(ThemeEntryID_TEXT, 175, "%02u:%02u:%02u %s", (pdata.tm.tm_hour == 0 || pdata.tm.tm_hour == 12) ? 12 : pdata.tm.tm_hour % 12, pdata.tm.tm_min, pdata.tm.tm_sec, (pdata.tm.tm_hour < 12) ? "AM" : "PM");
} else {
draw(ThemeEntryID_TEXT, "%02u:%02u:%02u", m_tm.tm_hour, m_tm.tm_min, m_tm.tm_sec);
draw(ThemeEntryID_TEXT, 133, "%02u:%02u:%02u", pdata.tm.tm_hour, pdata.tm.tm_min, pdata.tm.tm_sec);
}
if (m_ip) {
draw(ThemeEntryID_TEXT, "%u.%u.%u.%u", m_ip&0xFF, (m_ip>>8)&0xFF, (m_ip>>16)&0xFF, (m_ip>>24)&0xFF);
if (pdata.ip) {
draw(ThemeEntryID_TEXT, 0, "%u.%u.%u.%u", pdata.ip&0xFF, (pdata.ip>>8)&0xFF, (pdata.ip>>16)&0xFF, (pdata.ip>>24)&0xFF);
} else {
draw(ThemeEntryID_TEXT, ("No Internet"_i18n).c_str());
draw(ThemeEntryID_TEXT, 0, ("No Internet"_i18n).c_str());
}
if (!App::IsApplication()) {
draw(ThemeEntryID_ERROR, ("[Applet Mode]"_i18n).c_str());
draw(ThemeEntryID_ERROR, 0, ("[Applet Mode]"_i18n).c_str());
}
#undef draw
@@ -70,10 +101,13 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
nvgFontSize(vg, 28);
gfx::textBounds(vg, 0, 0, bounds, m_title.c_str());
gfx::drawTextArgs(vg, 80, start_y, 28.f, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT), m_title.c_str());
gfx::drawTextArgs(vg, 80 + (bounds[2] - bounds[0]) + 10, start_y, 16, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), m_title_sub_heading.c_str());
gfx::drawTextArgs(vg, 80, 685.f, 18, NVG_ALIGN_LEFT, theme->GetColour(ThemeEntryID_TEXT), "%s", m_sub_heading.c_str());
const auto text_w = SCREEN_WIDTH / 2 - 30;
const auto title_sub_x = 80 + (bounds[2] - bounds[0]) + 10;
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());
}
void MenuBase::SetTitle(std::string title) {
@@ -88,24 +122,4 @@ void MenuBase::SetSubHeading(std::string sub_heading) {
m_sub_heading = sub_heading;
}
void MenuBase::UpdateVars() {
m_tm = {};
m_poll_timestamp = {};
m_battery_percetange = {};
m_charger_type = {};
m_type = {};
m_status = {};
m_strength = {};
m_ip = {};
const auto t = time(NULL);
localtime_r(&t, &m_tm);
psmGetBatteryChargePercentage(&m_battery_percetange);
psmGetChargerType(&m_charger_type);
nifmGetInternetConnectionStatus(&m_type, &m_strength, &m_status);
nifmGetCurrentIpAddress(&m_ip);
m_poll_timestamp.Update();
}
} // namespace sphaira::ui::menu

View File

@@ -220,12 +220,12 @@ void from_json(const fs::FsPath& path, PackList& e) {
);
}
auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
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_RESULT(fs.GetFsOpenResult(), false);
R_TRY(fs.GetFsOpenResult());
DownloadPack download_pack;
@@ -243,7 +243,7 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
if (!result.success || result.data.empty()) {
log_write("error with download: %s\n", url.c_str());
return false;
R_THROW(0x1);
}
from_json(result.data, download_pack);
@@ -254,13 +254,13 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
pbox->NewTransfer("Downloading "_i18n + entry.details.name);
log_write("starting download: %s\n", download_pack.url.c_str());
if (!curl::Api().ToFile(
const auto result = curl::Api().ToFile(
curl::Url{download_pack.url},
curl::Path{zip_out},
curl::OnProgress{pbox->OnDownloadProgressCallback()}).success) {
log_write("error with download\n");
return false;
}
curl::OnProgress{pbox->OnDownloadProgressCallback()}
);
R_UNLESS(result.success, 0x1);
}
ON_SCOPE_EXIT(fs.DeleteFile(zip_out));
@@ -273,28 +273,25 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
// 3. extract the zip
if (!pbox->ShouldExit()) {
auto zfile = unzOpen64(zip_out);
if (!zfile) {
log_write("failed to open zip: %s\n", zip_out.s);
return false;
}
R_UNLESS(zfile, 0x1);
ON_SCOPE_EXIT(unzClose(zfile));
unz_global_info64 pglobal_info;
if (UNZ_OK != unzGetGlobalInfo64(zfile, &pglobal_info)) {
return false;
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");
return false;
R_THROW(0x1);
}
}
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
log_write("failed to open current file\n");
return false;
R_THROW(0x1);
}
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
@@ -302,7 +299,7 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
char name[512];
if (UNZ_OK != unzGetCurrentFileInfo64(zfile, &info, name, sizeof(name), 0, 0, 0, 0)) {
log_write("failed to get current info\n");
return false;
R_THROW(0x1);
}
const auto file_path = fs::AppendPath(dir_path, name);
@@ -311,38 +308,25 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
Result rc;
if (R_FAILED(rc = fs.CreateFile(file_path, info.uncompressed_size, 0)) && rc != FsError_PathAlreadyExists) {
log_write("failed to create file: %s 0x%04X\n", file_path.s, rc);
return false;
R_THROW(rc);
}
FsFile f;
if (R_FAILED(rc = fs.OpenFile(file_path, FsOpenMode_Write, &f))) {
log_write("failed to open file: %s 0x%04X\n", file_path.s, rc);
return false;
}
ON_SCOPE_EXIT(fsFileClose(&f));
if (R_FAILED(rc = fsFileSetSize(&f, info.uncompressed_size))) {
log_write("failed to set file size: %s 0x%04X\n", file_path.s, rc);
return false;
}
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) {
if (pbox->ShouldExit()) {
return false;
}
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());
return false;
R_THROW(0x1);
}
if (R_FAILED(rc = fsFileWrite(&f, offset, buf.data(), bytes_read, FsWriteOption_None))) {
log_write("failed to write file: %s 0x%04X\n", file_path.s, rc);
return false;
}
R_TRY(f.Write(offset, buf.data(), bytes_read, FsWriteOption_None));
pbox->UpdateTransfer(offset, info.uncompressed_size);
offset += bytes_read;
@@ -351,7 +335,7 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
}
log_write("finished install :)\n");
return true;
R_SUCCEED();
}
} // namespace
@@ -362,7 +346,7 @@ LazyImage::~LazyImage() {
}
}
Menu::Menu() : MenuBase{"Themezer"_i18n} {
Menu::Menu(u32 flags) : MenuBase{"Themezer"_i18n, flags} {
fs::FsNativeSd().CreateDirectoryRecursively(CACHE_PATH);
SetAction(Button::B, Action{"Back"_i18n, [this]{
@@ -386,13 +370,15 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
const auto& entry = page.m_packList[m_index];
const auto url = apiBuildUrlDownloadPack(entry);
App::Push(std::make_shared<ProgressBox>(entry.themes[0].preview.lazy_image.image, "Downloading "_i18n, entry.details.name, [this, &entry](auto pbox){
App::Push(std::make_shared<ProgressBox>(entry.themes[0].preview.lazy_image.image, "Downloading "_i18n, entry.details.name, [this, &entry](auto pbox) -> Result {
return InstallTheme(pbox, entry);
}, [this, &entry](bool success){
if (success) {
}, [this, &entry](Result rc){
App::PushErrorBox(rc, "Failed to download theme"_i18n);
if (R_SUCCEEDED(rc)) {
App::Notify("Downloaded "_i18n + entry.details.name);
}
}, 2));
}));
}
}
}
@@ -659,6 +645,9 @@ 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));
log_write("got themezer data\n");
if (!result.success) {
auto& page = m_pages[page_index-1];

View File

@@ -51,7 +51,7 @@ void thread_func(void* user) {
} // namespace
Menu::Menu() : MenuBase{"USB"_i18n} {
Menu::Menu(u32 flags) : MenuBase{"USB"_i18n, flags} {
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}});
@@ -109,31 +109,31 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
log_write("set to progress\n");
m_state = State::Progress;
log_write("got connection\n");
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool {
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) -> Result {
ON_SCOPE_EXIT(m_usb_source->Finished(FINISHED_TIMEOUT));
log_write("inside progress box\n");
for (const auto& file_name : m_names) {
m_usb_source->SetFileNameForTranfser(file_name);
const auto rc = yati::InstallFromSource(pbox, m_usb_source, file_name);
if (R_FAILED(rc)) {
m_usb_source->SignalCancel();
log_write("exiting usb install\n");
return false;
R_THROW(rc);
}
App::Notify("Installed via usb"_i18n);
}
return true;
}, [this](bool result){
if (result) {
R_SUCCEED();
}, [this](Result rc){
App::PushErrorBox(rc, "USB install failed"_i18n);
if (R_SUCCEEDED(rc)) {
App::Notify("Usb install success!"_i18n);
m_state = State::Done;
SetPop();
} else {
App::Notify("Usb install failed!"_i18n);
m_state = State::Failed;
}
}));

View File

@@ -19,6 +19,10 @@ void threadFunc(void* arg) {
} // namespace
ProgressBox::ProgressBox(int image, const std::string& action, const std::string& title, ProgressBoxCallback callback, ProgressBoxDoneCallback done, int cpuid, int prio, int stack_size) {
if (App::GetApp()->m_progress_boost_mode.Get()) {
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
}
SetAction(Button::B, Action{"Back"_i18n, [this](){
App::Push(std::make_shared<OptionBox>("Are you sure you wish to cancel?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){
if (op_index && *op_index) {
@@ -38,6 +42,7 @@ ProgressBox::ProgressBox(int image, const std::string& action, const std::string
m_action = action;
m_image = image;
m_cpuid = cpuid;
m_thread_data.pbox = this;
m_thread_data.callback = callback;
if (R_FAILED(threadCreate(&m_thread, threadFunc, &m_thread_data, nullptr, stack_size, prio, cpuid))) {
@@ -60,6 +65,8 @@ ProgressBox::~ProgressBox() {
FreeImage();
m_done(m_thread_data.result);
appletSetCpuBoostMode(ApmCpuBoostMode_Normal);
}
auto ProgressBox::Update(Controller* controller, TouchInfo* touch) -> void {
@@ -86,11 +93,19 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
const auto offset = m_offset;
const auto speed = m_speed;
const auto last_offset = m_last_offset;
auto image = m_image;
if (m_is_image_pending) {
FreeImage();
image = m_image = m_image_pending;
m_image_pending = 0;
m_is_image_pending = false;
}
mutexUnlock(&m_mutex);
if (!image_data.empty()) {
FreeImage();
m_image = nvgCreateImageMem(vg, 0, image_data.data(), image_data.size());
image = m_image = nvgCreateImageMem(vg, 0, image_data.data(), image_data.size());
m_own_image = true;
}
@@ -107,8 +122,8 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
nvgSave(vg);
nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH());
if (m_image) {
gfx::drawImage(vg, GetX() + 30, GetY() + 30, 128, 128, m_image, 5);
if (image) {
gfx::drawImage(vg, GetX() + 25, GetY() + 25, 120, 120, image, 5);
}
// shapes.
@@ -140,20 +155,32 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
char time_str[64];
if (hours) {
std::snprintf(time_str, sizeof(time_str), "%zu hours %zu minutes remaining", hours, minutes);
std::snprintf(time_str, sizeof(time_str), "%zu hours %zu minutes remaining"_i18n.c_str(), hours, minutes);
} else if (minutes) {
std::snprintf(time_str, sizeof(time_str), "%zu minutes %zu seconds remaining", minutes, seconds);
std::snprintf(time_str, sizeof(time_str), "%zu minutes %zu seconds remaining"_i18n.c_str(), minutes, seconds);
} else {
std::snprintf(time_str, sizeof(time_str), "%zu seconds remaining", seconds);
std::snprintf(time_str, sizeof(time_str), "%zu seconds remaining"_i18n.c_str(), seconds);
}
gfx::drawTextArgs(vg, center_x, prog_bar.y + prog_bar.h + 30, 18, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s (%s)", time_str, speed_str);
}
gfx::drawTextArgs(vg, center_x, m_pos.y + 40, 24, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), m_action.c_str());
gfx::drawTextArgs(vg, center_x, m_pos.y + 100, 22, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), title.c_str());
const auto draw_text = [&](ScrollingText& scroll, const std::string& txt, float y, float size, float pad, ThemeEntryID id){
float bounds[4];
nvgFontSize(vg, size);
gfx::textBounds(vg, 0, 0, bounds, txt.c_str());
const auto min_x = GetX() + pad;
const auto title_x = std::max(min_x, center_x - (bounds[2] - bounds[0]) / 2);
scroll.Draw(vg, true, title_x, y, GetW() - pad * 2, size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(id), txt.c_str());
};
draw_text(m_scroll_title, title, m_pos.y + 100, 22, 160, ThemeEntryID_TEXT);
if (!transfer.empty()) {
gfx::drawTextArgs(vg, center_x, m_pos.y + 150, 18, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", transfer.c_str());
draw_text(m_scroll_transfer, transfer, m_pos.y + 160, 18, 30, ThemeEntryID_TEXT_INFO);
}
nvgRestore(vg);
@@ -188,6 +215,14 @@ auto ProgressBox::UpdateTransfer(s64 offset, s64 size) -> ProgressBox& {
return *this;
}
auto ProgressBox::SetImage(int image) -> ProgressBox& {
mutexLock(&m_mutex);
m_image_pending = image;
m_is_image_pending = true;
mutexUnlock(&m_mutex);
return *this;
}
auto ProgressBox::SetImageData(std::vector<u8>& data) -> ProgressBox& {
mutexLock(&m_mutex);
std::swap(m_image_data, data);
@@ -211,41 +246,41 @@ auto ProgressBox::ShouldExit() -> bool {
return m_stop_source.stop_requested();
}
auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_path) -> Result {
fs::FsNativeSd fs;
R_TRY(fs.GetFsOpenResult());
auto ProgressBox::ShouldExitResult() -> Result {
if (ShouldExit()) {
R_THROW(0xFFFF);
}
R_SUCCEED();
}
FsFile src_file;
R_TRY(fs.OpenFile(src_path, FsOpenMode_Read, &src_file));
ON_SCOPE_EXIT(fsFileClose(&src_file));
auto ProgressBox::CopyFile(fs::Fs* fs_src, fs::Fs* fs_dst, const fs::FsPath& src_path, const fs::FsPath& dst_path) -> Result {
fs::File src_file;
R_TRY(fs_src->OpenFile(src_path, FsOpenMode_Read, &src_file));
s64 src_size;
R_TRY(fsFileGetSize(&src_file, &src_size));
R_TRY(src_file.GetSize(&src_size));
// this can fail if it already exists so we ignore the result.
// if the file actually failed to be created, the result is implicitly
// handled when we try and open it for writing.
fs.CreateFile(dst_path, src_size, 0);
fs_dst->CreateFile(dst_path, src_size, 0);
FsFile dst_file;
R_TRY(fs.OpenFile(dst_path, FsOpenMode_Write, &dst_file));
ON_SCOPE_EXIT(fsFileClose(&dst_file));
fs::File dst_file;
R_TRY(fs_dst->OpenFile(dst_path, FsOpenMode_Write, &dst_file));
R_TRY(fsFileSetSize(&dst_file, src_size));
R_TRY(dst_file.SetSize(src_size));
s64 offset{};
std::vector<u8> buf(1024*1024*4); // 4MiB
while (offset < src_size) {
if (ShouldExit()) {
R_THROW(0xFFFF);
}
R_TRY(ShouldExitResult());
u64 bytes_read;
R_TRY(fsFileRead(&src_file, offset, buf.data(), buf.size(), 0, &bytes_read));
R_TRY(src_file.Read(offset, buf.data(), buf.size(), 0, &bytes_read));
Yield();
R_TRY(fsFileWrite(&dst_file, offset, buf.data(), bytes_read, FsWriteOption_None));
R_TRY(dst_file.Write(offset, buf.data(), bytes_read, FsWriteOption_None));
Yield();
UpdateTransfer(offset, src_size);
@@ -255,6 +290,16 @@ auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_pat
R_SUCCEED();
}
auto ProgressBox::CopyFile(fs::Fs* fs, const fs::FsPath& src_path, const fs::FsPath& dst_path) -> Result {
return CopyFile(fs, fs, src_path, dst_path);
}
auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_path) -> Result {
fs::FsNativeSd fs;
R_TRY(fs.GetFsOpenResult());
return CopyFile(&fs, src_path, dst_path);
}
void ProgressBox::Yield() {
svcSleepThread(YieldType_WithoutCoreMigration);
}

View File

@@ -19,19 +19,26 @@
#include "usb/base.hpp"
#include "log.hpp"
#include "defines.hpp"
#include "app.hpp"
#include <ranges>
#include <cstring>
namespace sphaira::usb {
Base::Base(u64 transfer_timeout) {
App::SetAutoSleepDisabled(true);
m_transfer_timeout = transfer_timeout;
ueventCreate(GetCancelEvent(), true);
// this avoids allocations during transfers.
m_aligned.reserve(1024 * 1024 * 16);
}
Result Base::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) {
Base::~Base() {
App::SetAutoSleepDisabled(false);
}
Result Base::TransferPacketImpl(bool read, void *page, u32 remaining, u32 size, u32 *out_size_transferred, u64 timeout) {
u32 xfer_id;
/* If we're not configured yet, wait to become configured first. */
@@ -39,7 +46,7 @@ Result Base::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_t
/* Select the appropriate endpoint and begin a transfer. */
const auto ep = read ? UsbSessionEndpoint_Out : UsbSessionEndpoint_In;
R_TRY(TransferAsync(ep, page, size, std::addressof(xfer_id)));
R_TRY(TransferAsync(ep, page, remaining, size, std::addressof(xfer_id)));
/* Try to wait for the event. */
R_TRY(WaitTransferCompletion(ep, timeout));
@@ -77,7 +84,7 @@ Result Base::TransferAll(bool read, void *data, u32 size, u64 timeout) {
}
u32 out_size_transferred;
R_TRY(TransferPacketImpl(read, transfer_buf, size, &out_size_transferred, timeout));
R_TRY(TransferPacketImpl(read, transfer_buf, size, size, &out_size_transferred, timeout));
if (!alias && read) {
std::memcpy(buf, transfer_buf, out_size_transferred);

View File

@@ -45,7 +45,7 @@ Usb::Usb(u64 transfer_timeout) {
Usb::~Usb() {
}
Result Usb::WaitForConnection(u64 timeout, std::span<const std::string> names) {
Result Usb::WaitForConnection(u64 timeout, u8 flags, std::span<const std::string> names) {
R_TRY(m_usb->IsUsbConnected(timeout));
std::string names_list;
@@ -56,6 +56,7 @@ Result Usb::WaitForConnection(u64 timeout, std::span<const std::string> names) {
tinfoil::TUSHeader header{};
header.magic = tinfoil::Magic_List0;
header.nspListSize = names_list.length();
header.flags = flags;
R_TRY(m_usb->TransferAll(false, &header, sizeof(header), timeout));
R_TRY(m_usb->TransferAll(false, names_list.data(), names_list.length(), timeout));

View File

@@ -7,9 +7,20 @@
namespace sphaira::usb {
namespace {
// untested, should work tho.
// TODO: pr missing speed fields to libnx.
Result usbDsGetSpeed(u32 *out) {
enum { UsbDeviceSpeed_None = 0x0 };
enum { UsbDeviceSpeed_Low = 0x1 };
constexpr u16 DEVICE_SPEED[] = {
[UsbDeviceSpeed_None] = 0x0,
[UsbDeviceSpeed_Low] = 0x0,
[UsbDeviceSpeed_Full] = 0x40,
[UsbDeviceSpeed_High] = 0x200,
[UsbDeviceSpeed_Super] = 0x400,
};
// TODO: pr this to libnx.
Result usbDsGetSpeed(UsbDeviceSpeed *out) {
if (hosversionBefore(8,0,0)) {
return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
}
@@ -140,22 +151,22 @@ Result UsbDs::Init() {
endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
// Full Speed Config
endpoint_descriptor_in.wMaxPacketSize = 0x40;
endpoint_descriptor_out.wMaxPacketSize = 0x40;
endpoint_descriptor_in.wMaxPacketSize = DEVICE_SPEED[UsbDeviceSpeed_Full];
endpoint_descriptor_out.wMaxPacketSize = DEVICE_SPEED[UsbDeviceSpeed_Full];
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &interface_descriptor, USB_DT_INTERFACE_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
// High Speed Config
endpoint_descriptor_in.wMaxPacketSize = 0x200;
endpoint_descriptor_out.wMaxPacketSize = 0x200;
endpoint_descriptor_in.wMaxPacketSize = DEVICE_SPEED[UsbDeviceSpeed_High];
endpoint_descriptor_out.wMaxPacketSize = DEVICE_SPEED[UsbDeviceSpeed_High];
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &interface_descriptor, USB_DT_INTERFACE_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
// Super Speed Config
endpoint_descriptor_in.wMaxPacketSize = 0x400;
endpoint_descriptor_out.wMaxPacketSize = 0x400;
endpoint_descriptor_in.wMaxPacketSize = DEVICE_SPEED[UsbDeviceSpeed_Super];
endpoint_descriptor_out.wMaxPacketSize = DEVICE_SPEED[UsbDeviceSpeed_Super];
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &interface_descriptor, USB_DT_INTERFACE_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
@@ -174,7 +185,7 @@ Result UsbDs::Init() {
}
// the below code is taken from libnx, with the addition of a uevent to cancel.
Result UsbDs::IsUsbConnected(u64 timeout) {
Result UsbDs::WaitUntilConfigured(u64 timeout) {
Result rc;
UsbState state = UsbState_Detached;
@@ -218,8 +229,33 @@ Result UsbDs::IsUsbConnected(u64 timeout) {
return rc;
}
Result UsbDs::GetSpeed(UsbDeviceSpeed* out) {
return usbDsGetSpeed((u32*)out);
Result UsbDs::IsUsbConnected(u64 timeout) {
const auto rc = WaitUntilConfigured(timeout);
if (R_FAILED(rc)) {
m_max_packet_size = 0;
return rc;
}
if (!m_max_packet_size) {
UsbDeviceSpeed speed;
R_TRY(GetSpeed(&speed, &m_max_packet_size));
log_write("[USBDS] speed: %u max_packet: 0x%X\n", speed, m_max_packet_size);
}
R_SUCCEED();
}
Result UsbDs::GetSpeed(UsbDeviceSpeed* out, u16* max_packet_size) {
if (hosversionAtLeast(8,0,0)) {
R_TRY(usbDsGetSpeed(out));
} else {
// assume USB 2.0 speed (likely the case anyway).
*out = UsbDeviceSpeed_High;
}
*max_packet_size = DEVICE_SPEED[*out];
R_UNLESS(*max_packet_size > 0, 0x1);
R_SUCCEED();
}
Event *UsbDs::GetCompletionEvent(UsbSessionEndpoint ep) {
@@ -242,6 +278,7 @@ Result UsbDs::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) {
rc = Result_Cancelled;
} else if (R_SUCCEEDED(rc) && idx == waiters.size() - 2) {
log_write("got usbDsGetStateChangeEvent() event\n");
m_max_packet_size = 0;
rc = KERNELRESULT(TimedOut);
}
@@ -255,7 +292,14 @@ Result UsbDs::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) {
return rc;
}
Result UsbDs::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) {
Result UsbDs::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 remaining, u32 size, u32 *out_urb_id) {
if (remaining == size && size == m_max_packet_size) {
log_write("[USBDS] SetZlt(true)\n");
R_TRY(usbDsEndpoint_SetZlt(m_endpoints[ep], true));
} else {
R_TRY(usbDsEndpoint_SetZlt(m_endpoints[ep], false));
}
return usbDsEndpoint_PostBufferAsync(m_endpoints[ep], buffer, size, out_urb_id);
}

View File

@@ -184,7 +184,7 @@ Result UsbHs::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) {
return rc;
}
Result UsbHs::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_xfer_id) {
Result UsbHs::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 remaining, u32 size, u32 *out_xfer_id) {
return usbHsEpPostBufferAsync(&m_endpoints[ep], buffer, size, 0, out_xfer_id);
}

View File

@@ -112,9 +112,17 @@ auto Nsp::Build(std::span<CollectionEntry> entries, s64& size) -> std::vector<u8
string_offset += entries[i].name.length() + 1;
}
// align table
string_table.resize((string_table.size() + 0x1F) & ~0x1F);
// Add padding to the string table so that the header as a whole is well-aligned
const auto nameless_header_size = sizeof(Pfs0Header) + (file_table.size() * sizeof(Pfs0FileTableEntry));
auto padded_string_table_size = ((nameless_header_size + string_table.size() + 0x1F) & ~0x1F) - nameless_header_size;
// Add manual padding if the full Partition FS header would already be properly aligned.
if (padded_string_table_size == string_table.size()) {
padded_string_table_size += 0x20;
}
string_table.resize(padded_string_table_size);
header.magic = PFS0_MAGIC;
header.total_files = entries.size();
header.string_table_size = string_table.size();

View File

@@ -2,19 +2,13 @@
namespace sphaira::yati::source {
File::File(FsFileSystem* fs, const fs::FsPath& path) {
m_open_result = fsFsOpenFile(fs, path, FsOpenMode_Read, std::addressof(m_file));
}
File::~File() {
if (R_SUCCEEDED(GetOpenResult())) {
fsFileClose(std::addressof(m_file));
}
File::File(fs::Fs* fs, const fs::FsPath& path) : m_fs{fs} {
m_open_result = m_fs->OpenFile(path, FsOpenMode_Read, std::addressof(m_file));
}
Result File::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
R_TRY(GetOpenResult());
return fsFileRead(std::addressof(m_file), off, buf, size, 0, bytes_read);
return m_file.Read(off, buf, size, 0, bytes_read);
}
} // namespace sphaira::yati::source

View File

@@ -1,28 +0,0 @@
#include "yati/source/stdio.hpp"
namespace sphaira::yati::source {
Stdio::Stdio(const fs::FsPath& path) {
m_file = std::fopen(path, "rb");
if (!m_file) {
m_open_result = fsdevGetLastResult();
}
}
Stdio::~Stdio() {
if (R_SUCCEEDED(GetOpenResult())) {
std::fclose(m_file);
}
}
Result Stdio::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
R_TRY(GetOpenResult());
std::fseek(m_file, off, SEEK_SET);
R_TRY(fsdevGetLastResult());
*bytes_read = std::fread(buf, 1, size, m_file);
return fsdevGetLastResult();
}
} // namespace sphaira::yati::source

View File

@@ -3,19 +3,13 @@
namespace sphaira::yati::source {
StreamFile::StreamFile(FsFileSystem* fs, const fs::FsPath& path) {
m_open_result = fsFsOpenFile(fs, path, FsOpenMode_Read, std::addressof(m_file));
}
StreamFile::~StreamFile() {
if (R_SUCCEEDED(GetOpenResult())) {
fsFileClose(std::addressof(m_file));
}
StreamFile::StreamFile(fs::Fs* fs, const fs::FsPath& path) : m_fs{fs} {
m_open_result = m_fs->OpenFile(path, FsOpenMode_Read, std::addressof(m_file));
}
Result StreamFile::ReadChunk(void* buf, s64 size, u64* bytes_read) {
R_TRY(GetOpenResult());
const auto rc = fsFileRead(std::addressof(m_file), m_offset, buf, size, 0, bytes_read);
const auto rc = m_file.Read(m_offset, buf, size, 0, bytes_read);
m_offset += *bytes_read;
return rc;
}

View File

@@ -1,20 +1,3 @@
/*
* Copyright (c) Atmosphère-NX
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// The USB transfer code was taken from Haze (part of Atmosphere).
// The USB protocol was taken from Tinfoil, by Adubbz.
#include "yati/source/usb.hpp"
@@ -42,7 +25,8 @@ Result Usb::WaitForConnection(u64 timeout, std::vector<std::string>& out_names)
R_TRY(m_usb->TransferAll(true, &header, sizeof(header), timeout));
R_UNLESS(header.magic == tinfoil::Magic_List0, Result_BadMagic);
R_UNLESS(header.nspListSize > 0, Result_BadCount);
log_write("USB got header\n");
m_flags = header.flags;
log_write("[USB] got header, flags: 0x%X\n", m_flags);
std::vector<char> names(header.nspListSize);
R_TRY(m_usb->TransferAll(true, names.data(), names.size(), timeout));
@@ -96,9 +80,14 @@ Result Usb::SendFileRangeCmd(u64 off, u64 size, u64 timeout) {
}
Result Usb::Finished(u64 timeout) {
log_write("[USB] sending finished command\n");
return SendCmdHeader(tinfoil::USBCmdId::EXIT, 0, timeout);
}
bool Usb::IsStream() const {
return (m_flags & tinfoil::USBFlag_STREAM);
}
Result Usb::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
R_TRY(GetOpenResult());
R_TRY(SendFileRangeCmd(off, size, m_usb->GetTransferTimeout()));

View File

@@ -1,7 +1,6 @@
#include "yati/yati.hpp"
#include "yati/source/file.hpp"
#include "yati/source/stream_file.hpp"
#include "yati/source/stdio.hpp"
#include "yati/container/nsp.hpp"
#include "yati/container/xci.hpp"
@@ -754,7 +753,7 @@ struct BufHelper {
};
Yati::Yati(ui::ProgressBox* _pbox, std::shared_ptr<source::Base> _source) : pbox{_pbox}, source{_source} {
appletSetMediaPlaybackState(true);
App::SetAutoSleepDisabled(true);
}
Yati::~Yati() {
@@ -768,16 +767,11 @@ Yati::~Yati() {
ncmContentStorageClose(std::addressof(ncm_cs[i]));
}
appletSetMediaPlaybackState(false);
if (config.boost_mode) {
appletSetCpuBoostMode(ApmCpuBoostMode_Normal);
}
App::SetAutoSleepDisabled(false);
}
Result Yati::Setup(const ConfigOverride& override) {
config.sd_card_install = override.sd_card_install.value_or(App::GetApp()->m_install_sd.Get());
config.boost_mode = App::GetApp()->m_boost_mode.Get();
config.allow_downgrade = App::GetApp()->m_allow_downgrade.Get();
config.skip_if_already_installed = App::GetApp()->m_skip_if_already_installed.Get();
config.ticket_only = App::GetApp()->m_ticket_only.Get();
@@ -795,10 +789,6 @@ Result Yati::Setup(const ConfigOverride& override) {
config.lower_system_version = override.lower_system_version.value_or(App::GetApp()->m_lower_system_version.Get());
storage_id = config.sd_card_install ? NcmStorageId_SdCard : NcmStorageId_BuiltInUser;
if (config.boost_mode) {
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
}
R_TRY(source->GetOpenResult());
R_TRY(splCryptoInitialize());
R_TRY(nsInitialize());
@@ -1388,15 +1378,11 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
} // namespace
Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path, const ConfigOverride& override) {
Result InstallFromFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& path, const ConfigOverride& override) {
return InstallFromSource(pbox, std::make_shared<source::File>(fs, path), path, override);
// return InstallFromSource(pbox, std::make_shared<source::StreamFile>(fs, path), path, override);
}
Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path, const ConfigOverride& override) {
return InstallFromSource(pbox, std::make_shared<source::Stdio>(path), path, override);
}
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const fs::FsPath& path, const ConfigOverride& override) {
const auto ext = std::strrchr(path.s, '.');
R_UNLESS(ext, Result_ContainerNotFound);

View File

@@ -101,8 +101,9 @@ def send_nsp_list(nsp_dir, out_ep):
# Add all files with the extension .nsp in the provided dir
for nsp_path in [f for f in nsp_dir.iterdir() if f.is_file() and (f.suffix in EXTS)]:
nsp_path_list.append(nsp_path.__str__() + '\n')
nsp_path_list_len += len(nsp_path.__str__()) + 1
nsp_path = bytes(nsp_path.__str__(), 'utf8') + b'\n'
nsp_path_list.append(nsp_path)
nsp_path_list_len += len(nsp_path)
print('Sending header...')