78 Commits

Author SHA1 Message Date
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
ITotalJustice
74fddecebc bump version for new release 0.9.1 -> 0.10.0 2025-05-19 16:09:33 +01:00
ITotalJustice
a64d4dce7a remove ns event from games menu (see below).
turns out that the event is auto cleared when waited.
this meant that if sphaira handled the event before qlaunch got chance to handle it,
then qlaunch won't update when app records changed.

this can result in a gamecard not being mounted, deleted games still apearing, installed games
not being displayed etc...
2025-05-19 16:07:24 +01:00
ITotalJustice
5daca4354c add support for webdav uploads by creating missing folders, game now dumps to title/title[id].nsp 2025-05-19 16:00:03 +01:00
ITotalJustice
da9235f58e fix upload url path not being encoded, add seek api for uploads. 2025-05-19 12:06:43 +01:00
ITotalJustice
bd6566524c enable ftp in the ftp menu if it isn't already enabled. 2025-05-19 08:32:05 +01:00
ITotalJustice
eadc46b0e4 add support for file uploads in the file browser, optimise curl single thread download.
- curl now keeps the handle alive for single threaded downloads, rather than creating it each time.
2025-05-18 23:00:51 +01:00
ITotalJustice
71df5317be add game dump uploading, fix download progress using u32 instead of s64, add progress and title for usb game dump.
- added support for custom upload locations, set in /config/sphaira/locations.ini
- add support for various auth options for download/upload (port, pub/priv key, user/pass, bearer).
2025-05-18 20:30:04 +01:00
ITotalJustice
bd7eadc6a0 add game dumping, add game transfer (switch2switch) via usb, add multi game selecting, fix bugs (see below).
- added more es commands.
- fixed usb install potential hang if the exit command is sent, but the client stops responding (timeout is now 3s).
- added multi select to the games menu.
- added game dumping.
- added switch2switch support by having a switch act as a usb client to transfer games.
- replace std::find with std::ranges (in a few places).
- fix rounding of icon in progress box being too round.
- fix file copy helper in progress box not updating the progress bar.
2025-05-18 13:46:10 +01:00
ITotalJustice
544272925d fix IsEmunand() failing due to the paths not being page aligned. 2025-05-15 16:13:35 +01:00
ITotalJustice
70a31be134 don't fail if the control nca cannot be parsed during install, as it may depend on ticket not yet installed. 2025-05-15 15:25:03 +01:00
ITotalJustice
55ae2a63d9 fix installing failing during setup if prod.keys isn't found. 2025-05-15 15:14:02 +01:00
ITotalJustice
5a53947a3e fix crash in list layout caused by rendering all previous hidden entries. 2025-05-14 18:00:56 +01:00
BIGBIGSUI
3bbb5ccb3c Update zh.json (#136)
A latest zh.json. Hope it helps.
2025-05-14 00:29:04 +01:00
ITotalJustice
83472f1020 fix game menu forcefully disabled manual loading of control data.
this was done during testing / benchmarking, but i forgot to undo this.
2025-05-14 00:06:36 +01:00
ITotalJustice
0167bf034c gc menu now tries to load control data from ns cache before manually loading.
on fw 19 and below, loading from cache takes ~5ms, whereas manually loading takes ~20ms.
manually loading is still faster than relying on ns to load control from storage (~50ms).
2025-05-14 00:04:47 +01:00
ITotalJustice
35abe363a6 optimise theme meta loading. 2025-05-13 23:52:34 +01:00
ITotalJustice
97d3fd396e optimise game menu for fw 20
- loading the control data is ran on its own thread, it does not block the main thread. allows for smooth scrolling like nintendos home menu.
- on fw20+, sphaira manually parses the control data, rather than using ns. manually parsing takes 20-40ms, which is faster than ms which can take 50-500ms.
- on fw19 and below, if the control data is not in ns cache, sphaira will manually parse the data as its twice as fast as ns. You can see how fast this is by loading the gamecard menu as that manually parses everything, and it loads the gamecard faster than the home menu
2025-05-13 23:51:06 +01:00
ITotalJustice
b98ccb927e fix appstore status icons no longer being rounded (bug added in e279a70) 2025-05-11 20:19:43 +01:00
ITotalJustice
db23f072a2 add sysmmc / emummc install enable options.
allows the user to enable installs for one config and disable it for the other.
by default, it will load the install option found in the config, if found.
otherwise, it will load from the new config option.
2025-05-11 20:14:34 +01:00
ITotalJustice
4d3d7e81d4 update libhaze to silence gcc 15 warning and run on core2 instead of core0. 2025-05-11 03:45:48 +01:00
ITotalJustice
441807bc53 fix building for gcc 15 2025-05-11 03:00:04 +01:00
ITotalJustice
20e2d85843 remove bubbles, no one likes easter eggs apparently.
fixes #138
2025-05-11 02:41:56 +01:00
ITotalJustice
e279a70606 add layout options to grid based menues. 2025-05-11 02:39:03 +01:00
ITotalJustice
5d9e24af31 use ns application event to detect when to re-scan for record changes. 2025-05-04 20:42:57 +01:00
Ny'hrarr
078627e07b Update pt.json (#133)
* Update pt.json

* Translate new keys

* Translate FTP, USB and GameCard menus
2025-05-03 22:27:31 +01:00
ITotalJustice
365ae2d0cb fix freeze if the usb menu is closed whilst a usb cable is not connected, 0.9.0 -> 0.9.1 2025-05-03 21:15:21 +01:00
ITotalJustice
5b6e09b926 bump version for new release 0.8.2 -> 0.9.0 2025-05-03 18:01:49 +01:00
ITotalJustice
7072647611 reduce usb install exit latency by waiting on a cancel uevent, rather than relying on a timeout. 2025-05-03 17:17:53 +01:00
ITotalJustice
30cf4826f8 add code for calculating games size, stubbed for now as the ns calls are too slow to be usable. 2025-05-03 15:30:40 +01:00
ITotalJustice
ca47fc1f89 add (limited) sort options to game menu.
getting the list of title_ids is very fast (less than 1ms), however parsing the control info, such as title names
is very slow.
depending on how many games the user has, blocking until we read all control info can take several seconds...
we would only need to block if the user wants to sort by name.
normally, we lazy load the control data, so we don't suffer from slow load times at all.
i decided that its not worth slowing the whole system down just to give the option to sort by name.
2025-05-03 15:08:25 +01:00
ITotalJustice
16a2c84edd simplify right-side shortcuts impl, add gamecard and themezer to shortcut list, fix l2/r2 using wrong icons, sort l2/r2 so l2 displays first.
some other changes:
- shorten the next page and prev page to just next/prev in themezer.
- remove misc shortcut name. the function itself still exists.
2025-05-03 14:39:20 +01:00
ITotalJustice
df5e27dd06 fix filebrowser crash caused when trying to select all files (L2) whilst a hidden file/folder exists.
this crash was found by @WE1ZARD.
to trigger it, press L2 in the filebrowser whilst a hidden file exists and the hide hidden is enabled.
the was due to GetEntry(i) internally using m_entries_current, and the select all was using the index from m_entries_current.
this would result in an index that goes oob, and as its a write, it crashes.
2025-05-03 13:25:09 +01:00
ITotalJustice
d95226f8c0 i18n::get should accept a string_view rather than char*, simplifies calling. 2025-05-03 13:25:09 +01:00
ITotalJustice
164fec5b73 fix right-side shortcut not displaying the correct name (again) when using translations. 2025-05-03 13:25:09 +01:00
glitched_nx
8dad96f39f Update de.json (#130)
Commit changes to language file (de.json), with many improvements and corrections.

The changes include:
- Modification and correction of existing translations for greater clarity and accuracy.
- Updated translations to match the terminology for homebrew and Nintendo Switch UI elements.
- Added new 'de' translation for the “ 12h time format” setting function.
2025-05-02 22:48:48 +01:00
ITotalJustice
2244e73c53 change "yes, no" for boolean options to "on, off", to match N's sidebar. 2025-05-02 00:57:11 +01:00
ITotalJustice
456cb02d2a [breaking change] update forwarder id generation, add "hide forwarders" to game menu. 2025-05-02 00:52:08 +01:00
ITotalJustice
f310704472 bump nca key generation values for fw 20.0.0 2025-05-02 00:02:09 +01:00
ITotalJustice
96e5a7081b clip rect and text drawing that go offscreen.
this is already handled by the gpu, but cpu side still has to do some work.
this wasn't a performance issue (we only use 4%) but its a free win, so we might as well.
2025-05-01 23:49:01 +01:00
ITotalJustice
1c93e18822 replace all "enabled" and "disabled" options with the default "On" and "Off". 2025-05-01 22:10:47 +01:00
ITotalJustice
ac152454f0 fix menu shortcut setting translated names for the config. 2025-05-01 22:00:40 +01:00
ITotalJustice
7851f7f400 add option to extract zip to root, better name extract / compress options. 2025-05-01 18:28:35 +01:00
ITotalJustice
2b561dd438 add option to change the right-side menu. 2025-05-01 18:06:10 +01:00
ITotalJustice
3545f557fc add scrolling text to popup list, handling clipping inside scrolling text, game menu changes
- added delete entity in game menu
- added list meta records to game menu
2025-05-01 15:14:50 +01:00
ITotalJustice
8dfb9b9ba6 reduce time for scrolling text from 1.5s to 1s. 2025-05-01 00:04:32 +01:00
ITotalJustice
7cf36cd25f reduces rounding of icons in grid based menus (15 -> 5). 2025-04-30 23:59:05 +01:00
ITotalJustice
c53692022b add scrolling text to all grid based menus.
fixes #122
2025-04-30 23:56:40 +01:00
ITotalJustice
0f3b7da0b2 fix memleak when deleting homebrew, add game menu. 2025-04-30 22:45:52 +01:00
ITotalJustice
e22daefb08 slightly round edges for progress and option box, add image support to option boxes 2025-04-30 18:19:13 +01:00
ITotalJustice
6fb5319da3 bump version for new release 0.8.1 -> 0.8.2 2025-04-30 17:21:04 +01:00
ITotalJustice
6970fec554 irs connect to first available handle, irs display connected pad in the title. 2025-04-30 17:16:59 +01:00
ITotalJustice
36be56647f Revert "remove IRS menu"
This reverts commit 1dafa2748c.
2025-04-30 17:05:44 +01:00
ITotalJustice
cca6326314 filebrowser add select al option by pressing L2 2025-04-30 16:55:33 +01:00
ITotalJustice
9176c6780a filebrowser move install forwarder option out of the advanced menu. 2025-04-30 16:49:19 +01:00
ITotalJustice
b1a6b12cf3 add zip extraction, add zip creation, themezer now displays the file name its extracting. 2025-04-30 16:42:05 +01:00
ITotalJustice
c7cc11cc98 only add etag is dst file already exists, enable curl --compressed option.
curl/libcurl does not send Accept-Encoding by default.
many servers support sending compressed versions of files, to speed up transfers.
this is ideal for the switch as its io is shit, but the cpu is mostly idle (4% cpu usage for sphaira).

github and appstore support sending gzip json files, themezer doesn't seem to.
2025-04-30 00:40:04 +01:00
ITotalJustice
ec4b96b95d remove stale etag if the server stops sending etags back
workaround for appstore images which stopped sending etags back.
2025-04-29 22:41:33 +01:00
ITotalJustice
a2e343daa7 improve popup_list to highlight the currently selected item. 2025-04-29 22:40:32 +01:00
BIGBIGSUI
b811c9e3cd Update zh.json (#129)
A latest zh.json. Hope it will be helpful
2025-04-29 20:35:05 +01:00
ITotalJustice
8ffaa56bc3 bump version for new release 0.8.0 -> 0.8.1 2025-04-29 20:03:23 +01:00
ITotalJustice
eca3358e57 add option to download default music. 2025-04-29 20:01:51 +01:00
ITotalJustice
757e380e08 play sound effect when gamecard is inserted. 2025-04-29 19:23:37 +01:00
ITotalJustice
6c1b5de932 label the shortcut for misc 2025-04-29 19:06:33 +01:00
ITotalJustice
d79ac126f7 remove all strings and error codes in error_box.cpp, reduce binary by a further 60k. 2025-04-29 18:56:55 +01:00
ITotalJustice
2d7763444e remove Web menu
the web browser on the switch is really bad, it shouldnt be used.
i am removing this menu because its another option that gets in the way of other options, and code bloat.
2025-04-29 18:42:32 +01:00
ITotalJustice
1dafa2748c remove IRS menu
i added the irs menu when i wanted to mess around with the sensor on the joycon.
since then, i have used it a total of 0 times, and i don't think any users use it either.
2025-04-29 18:37:43 +01:00
ITotalJustice
9f7bf9581c add boost mode option for installing 2025-04-29 18:33:02 +01:00
ITotalJustice
8f39acbaa2 replace usb protocol with tinfoils protocol, in order to support applications supporting said protocol.
- replace the python script with the one included with tinfoil, minor changes such as changing the supported extension,
  removing unused imports.
- tested with the included script, fluffy and ns-usbloader on linux.
  a user was unable to get it working on mac however...
- added build instructions to the readme, i think they're correct.
- added install instructions to the readme.
2025-04-29 18:11:07 +01:00
ITotalJustice
81469d0ac9 remove PageAlignedVector from yati as it's no longer needed due to previous commit.
the previous commit changed usb transfers to always transfer to/from page aligned buffers.
i wanted to keep the commits seperate so that its easier revert or git bisect later on, if needed.
2025-04-29 14:19:37 +01:00
ITotalJustice
1eae35f072 simplify the usb transfer process by using an aligned buffer to transfer to/from. 2025-04-29 14:17:12 +01:00
ITotalJustice
5b82e07b1c fix building due to previous commit 2025-04-29 13:08:32 +01:00
ITotalJustice
73886c28ae add gc event waiting, fix control nca mounting, better skip nca support.
- gamecards now wait for an event to change, rather than polling each frame.
  this reduces cpu load on core 3 slightly (3-4% less).
- my understanding of fsOpenFileSystemWithId() was wrong, i thought it used the app_id for the id param.
  turns out it needs the program id (found in the nca header), this is why mounting some control ncas
  would fail.
  fs (and ncm) have a call in 17+ to get the program id, it does so by parsing the nca header.
  in yati, we already have the header so we can avoid the call.
  for the gamecard menu, we don't. so we can parse the nca header, or use the id offset (which we already have)
  to form the program id.
- std::find_if in yati now takes args by ref rather than by value, avoid quite large copies.
- stream installs can now parse the control nca.
- if an nca is already installed, it is now skipped. this is regardless to whether it is not in ncm db.
- nca skipping is technically supported for stream installs, however it is disabled for now as there needs
  to be a way to allow for the stream to continue reading and discarding data until the stream has finished.
  currently, if a ftp (stream) install is skipped, it will close the progress box and cause spahira to hang.
  this is because sphaira expects the stream to only be closed upon all data being read, so there's nothing more
  to process.
- renamed the title_id field in nca header to program_id.
2025-04-29 12:47:38 +01:00
ITotalJustice
eea09f6e57 [appstore] make author search case insensitive. 2025-04-28 22:06:44 +01:00
93 changed files with 6631 additions and 2941 deletions

1
.gitignore vendored
View File

@@ -11,6 +11,7 @@ old_code
created_ncas
assets/romfs/shaders
.vscode/settings.json
.idea
info/
romfs/shaders
assets/unused

View File

@@ -49,6 +49,40 @@ The `path` field is optional. If left out, it will use the name of the ini to fi
See `assets/romfs/assoc/` for more examples of file assoc entries.
## Installing (applications)
Sphaira can install applications (nsp, xci, nsz, xcz) from various sources (sd card, gamecard, ftp, usb).
For informantion about the install options, [see the wiki](https://github.com/ITotalJustice/sphaira/wiki/Install).
### Usb (install)
The USB protocol is the same as tinfoil, so tools such as [ns-usbloader](https://github.com/developersu/ns-usbloader) and [fluffy](https://github.com/fourminute/Fluffy) should work with sphaira. You may also use the provided python script found [here](tools/usb_install_pc.py).
### Ftp (install)
Once you have connected your ftp client to your switch, you can upload files to install into the `install` folder.
## Building from source
You will first need to install [devkitPro](https://devkitpro.org/wiki/Getting_Started).
Next you will need to install the dependencies:
```sh
sudo pacman -S switch-dev deko3d switch-cmake switch-curl switch-glm switch-zlib
```
Once devkitPro and all dependencies are installed, you can now build sphaira.
```sh
git clone https://github.com/ITotalJustice/sphaira.git
cd sphaira
cmake --preset MinSizeRel
cmake --build --preset MinSizeRel
```
The output will be found in `build/MinSizeRel/sphaira.nro`
## Credits
- borealis

View File

@@ -1,21 +1,21 @@
{
"[Applet Mode]": "[Applet-Modus]",
"No Internet": "Keine Internetverbindung",
"[Applet Mode]": " | Applet Modus |",
"No Internet": "Kein Internet",
"Files": "Dateien",
"Apps": "Apps",
"Store": "Store",
"Apps": "hb-Apps",
"Store": "hb-Store",
"Menu": "Menü",
"Options": "Optionen",
"OK": "OK",
"Back": "Zurück",
"Select": "Auswählen",
"Open": "Öffnen",
"Launch": "Starten",
"Open": "Öffne",
"Launch": "Starte",
"Info": "Info",
"Install": "Installieren",
"Delete": "Löschen",
"Restart": "Neustart",
"Changelog": "Changelog",
"Changelog": "Neuerungen",
"Details": "Details",
"Update": "Update",
"Remove": "Entfernen",
@@ -23,62 +23,62 @@
"Download": "Download",
"Next Page": "Nächste Seite",
"Prev Page": "Vorherige Seite",
"Unstar": "Favorit entfernen",
"Unstar": "Kein Favorit",
"Star": "Favorit",
"System memory": "System-Speicher",
"microSD card": "microSD-Karte",
"Sd": "SD",
"Image System memory": "System-Speicher Bild",
"Image microSD card": "microSD-Karten Bild",
"Slow": "Langsam",
"Normal": "Normal",
"Fast": "Schnell",
"System memory": "NAND Systemspeicher",
"microSD card": "SD-Karte",
"Sd": "SD-Karte | Root-Verzeichnis",
"Image System memory": "Album | NAND Systemspeicher",
"Image microSD card": "Album | SD-Karte",
"Slow": "Niedrig",
"Normal": "Mittel",
"Fast": "Hoch",
"Yes": "Ja",
"No": "Nein",
"Enabled": "Aktiviert",
"Disabled": "Deaktiviert",
"Enabled": "An",
"Disabled": "Aus",
"Sort By": "Sortieren nach",
"Sort Options": "Sortieroptionen",
"Filter": "Filter",
"Sort": "Sortieren",
"Order": "Reihenfolge",
"Sort By": "Sortierung",
"Sort Options": " Sortierung | Optionen",
"Filter": "Rubrik",
"Sort": "Sortiert nach",
"Order": "Anordnung",
"Search": "Suchen",
"Updated": "Aktualisiert",
"Updated (Star)": "Aktualisiert (Favoriten)",
"Updated": "zuletzt aktualisiert",
"Updated (Star)": "Favorit | zuletzt aktualisiert",
"Downloads": "Downloads",
"Size": "Größe",
"Size (Star)": "Größe (Favoriten)",
"Alphabetical": "Alphabetisch",
"Alphabetical (Star)": "Alphabetisch (Favoriten)",
"Likes": "Likes",
"ID": "ID",
"Descending": "Absteigend",
"Descending (down)": "Absteigend",
"Desc": "Abst.",
"Ascending": "Aufsteigend",
"Ascending (Up)": "Aufsteigend",
"Asc": "Aufst.",
"Size (Star)": "Favorit | Größe",
"Alphabetical": "Name",
"Alphabetical (Star)": "Favorit | Name",
"Likes": "Beliebtheit",
"ID": "Theme | Paket ID",
"Descending": "Absteigend",
"Descending (down)": "Absteigend",
"Desc": "",
"Ascending": "Aufsteigend",
"Ascending (Up)": "Aufsteigend",
"Asc": "",
"Menu Options": "Menü-Optionen",
"Theme": "Theme",
"Theme Options": "Theme-Optionen",
"Select Theme": "Theme auswählen",
"Menu Options": " Menü | Optionen",
"Theme": "Themes",
"Theme Options": " Themes | Optionen",
"Select Theme": "Theme wählen",
"Shuffle": "Zufällig",
"Music": "Musik",
"12 Hour Time": "",
"Network": "Netzwerk",
"Network Options": "Netzwerk-Optionen",
"12 Hour Time": "12-Std Zeitformat",
"Network": "Konnektivität",
"Network Options": "Konnektivität | Optionen",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink verbunden",
"Nxlink Upload": "Nxlink Upload",
"Nxlink Finished": "Nxlink abgeschlossen",
"Switch-Handheld!": "Switch-Handheld!",
"Switch-Docked!": "Switch-Dock-Modus!",
"Nxlink": "NXLink",
"Nxlink Connected": "NXLink | Verbunden",
"Nxlink Upload": "NXLink | wird hochgeladen...",
"Nxlink Finished": "NXLink | Hochladen beendet",
"Switch-Handheld!": "Handheld!",
"Switch-Docked!": "Angedockt!",
"Language": "Sprache",
"Auto": "Auto",
"Auto": "Systemsprache",
"English": "English",
"Japanese": "日本語",
"French": "Français",
@@ -87,55 +87,55 @@
"Spanish": "Español",
"Chinese": "中文",
"Korean": "한국어",
"Dutch": "Dutch",
"Dutch": "Nederlands",
"Portuguese": "Português",
"Russian": "Русский",
"Swedish": "Svenska",
"Vietnamese": "Vietnamese",
"Logging": "Logging",
"Replace hbmenu on exit": "hbmenu beim Beenden ersetzen",
"Misc": "Sonstiges",
"Misc Options": "Weitere Optionen",
"Web": "Web",
"Vietnamese": "tiếng Việt",
"Logging": "Protokollieren",
"Replace hbmenu on exit": "hbmenu durch sphaira ersetzen",
"Misc": "Extras",
"Misc Options": " Extras | Optionen",
"Web": "WEB Browser",
"Install forwarders": "Forwarder installieren",
"Install location": "Installationsort",
"Show install warning": "Installationswarnung anzeigen",
"Text scroll speed": "Textlaufgeschwindigkeit",
"Install location": "Einhängepunkt",
"Show install warning": "Warnungen anzeigen",
"Text scroll speed": "Laufschrift Tempo",
"FileBrowser": "Datei-Browser",
"FileBrowser": "Datei-Manager",
"%zd files": "%zd Dateien",
"%zd dirs": "%zd Ordner",
"File Options": "Datei-Optionen",
"Show Hidden": "Versteckte anzeigen",
"File Options": "Datei - Ordner | Optionen",
"Show Hidden": "Versteckte zeigen",
"Folders First": "Ordner zuerst",
"Hidden Last": "Versteckte zuletzt",
"Cut": "Ausschneiden",
"Copy": "Kopieren",
"Paste": "Einfügen",
"Paste ": "Einfügen ",
" file(s)?": " Datei(en)?",
"Paste ": "Einfügen von: ",
" file(s)?": " Datei/en?",
"Rename": "Umbenennen",
"Set New File Name": "Neuen Dateinamen eingeben",
"Advanced": "Erweitert",
"Advanced Options": "Erweiterte Optionen",
"Create File": "Datei erstellen",
"Set File Name": "Dateinamen eingeben",
"Create Folder": "Ordner erstellen",
"Set Folder Name": "Ordnernamen eingeben",
"View as text (unfinished)": "Als Text anzeigen (Beta)",
"Ignore read only": "Schreibschutz ignorieren",
"Mount": "Einbinden",
"Empty...": "Leer...",
"Open with DayBreak?": "Mit DayBreak öffnen?",
"Launch ": "Starten ",
"Launch option for: ": "Startoption für: ",
"Select launcher for: ": "Launcher auswählen für: ",
"Set New File Name": "Neuen Dateinamen festlegen",
"Advanced": "Erweitert...",
"Advanced Options": " Erweitert | Optionen",
"Create File": "Neue Datei",
"Set File Name": "Dateiname festlegen",
"Create Folder": "Neuer Ordner",
"Set Folder Name": "Ordner umbenennen",
"View as text (unfinished)": "Als Text anzeigen",
"Ignore read only": "Schreibschutz umgehen?",
"Mount": "Einhängen",
"Empty...": "Keine Daten...",
"Open with DayBreak?": "Mit Daybreak öffnen?",
"Launch ": "Starte ",
"Launch option for: ": "Start Option für: ",
"Select launcher for: ": "Wähle Launcher für: ",
"Homebrew": "Homebrew",
"Homebrew Options": "Homebrew-Optionen",
"Hide Sphaira": "Sphaira ausblenden",
"Homebrew": "hbmenu",
"Homebrew Options": " hbmenu | Optionen",
"Hide Sphaira": "Verstecke sphaira",
"Install Forwarder": "Forwarder installieren",
"WARNING: Installing forwarders will lead to a ban!": "WARNUNG: Installation von Forwardern führt zum Ban!",
"WARNING: Installing forwarders will lead to a ban!": "Installiere Forwarder-NSP´s mit VORSICHT.\nEs erhöht das Risiko eines Konsolen-Banns!",
"Installing Forwarder": "Installiere Forwarder",
"Creating Program": "Erstelle Programm",
"Creating Control": "Erstelle Control",
@@ -144,26 +144,26 @@
"Updating ncm databse": "Aktualisiere NCM-Datenbank",
"Pushing application record": "Übertrage Anwendungsdaten",
"Installed!": "Installiert!",
"Failed to install forwarder": "Forwarder-Installation fehlgeschlagen",
"Failed to install forwarder": "Fehler beim installieren des Forwarders",
"Unstarred ": "Favorit entfernt ",
"Starred ": "Favorit hinzugefügt ",
"Starred ": "Favorit ",
"AppStore": "AppStore",
"Filter: %s | Sort: %s | Order: %s": "Filter: %s | Sortierung: %s | Reihenfolge: %s",
"AppStore Options": "AppStore-Optionen",
"All": "Alle",
"AppStore": "hb-AppStore",
"Filter: %s | Sort: %s | Order: %s": "Rubrik: %s | Sort.nach.: %s | Ordnung: %s",
"AppStore Options": " hb-AppStore | Optionen",
"All": "Alles anzeigen",
"Games": "Spiele",
"Emulators": "Emulatoren",
"Tools": "Tools",
"Themes": "Themes",
"Legacy": "Legacy",
"Legacy": "Älteres",
"version: %s": "Version: %s",
"updated: %s": "Aktualisiert: %s",
"category: %s": "Kategorie: %s",
"extracted: %.2f MiB": "Entpackt: %.2f MiB",
"app_dls: %s": "Downloads: %s",
"More by Author": "Mehr vom Entwickler",
"Leave Feedback": "Feedback geben",
"updated: %s": "Letztes Update am: %s",
"category: %s": "Rubrik: %s",
"extracted: %.2f MiB": "Größe: %.2f MiB",
"app_dls: %s": "Anzahl Downloads: %s",
"More by Author": "Weitere Apps des Entwicklers",
"Leave Feedback": "Feedback hinterlassen",
"Irs": "IR-Sensor",
"Ambient Noise Level: ": "Umgebungsrauschen: ",
@@ -203,55 +203,55 @@
"External Light Filter": "Externes Lichtfilter",
"Load Default": "Standard laden",
"Themezer": "Themezer",
"Themezer Options": "Themezer-Optionen",
"Themezer": "Themezer | NX Themes",
"Themezer Options": " Themezer | Optionen",
"Nsfw": "NSFW",
"Page": "Seite",
"Page %zu / %zu": "Seite %zu / %zu",
"Enter Page Number": "Seitenzahl eingeben",
"Bad Page": "Ungültige Seite",
"Page": "Seiten Nr. wählen ",
"Page %zu / %zu": " %zu / %zu",
"Enter Page Number": "Zu Seite Nr.: ___",
"Bad Page": "Seite nicht gefunden",
"Download theme?": "Theme herunterladen?",
"GitHub": "GitHub",
"Downloading json": "Lade JSON herunter",
"Select asset to download for ": "Wähle Asset zum Download für ",
"Downloading json": "Lade JSON-File",
"Select asset to download for ": "Wähle Asset für den Download von ",
"Installing ": "Installiere ",
"Uninstalling ": "Deinstalliere ",
"Deleting ": "Lösche ",
"Deleting": "Lösche",
"Pasting ": "Füge ein ",
"Pasting": "Füge ein",
"Removing ": "Entferne ",
"Scanning ": "Scanne ",
"Creating ": "Erstelle ",
"Copying ": "Kopiere ",
"Trying to load ": "Lade ",
"Downloading ": "Lade herunter ",
"Downloaded ": "Heruntergeladen ",
"Removed ": "Entfernt ",
"Checking MD5": "Prüfe MD5",
"Loading...": "Lade...",
"Loading": "Lade",
"Empty!": "Leer!",
"Installing ": "Installiert wird: ",
"Uninstalling ": "Deinstalliert wird: ",
"Deleting ": "Gelöscht wird: ",
"Deleting": "Gelöscht wurde:",
"Pasting ": "Eingefügt wird: ",
"Pasting": "Eingefügt wurde:",
"Removing ": "Entfernt wird: ",
"Scanning ": "Gescannt wird: ",
"Creating ": "Erstellt wird: ",
"Copying ": "Kopiert wird: ",
"Trying to load ": "Versucht zu laden wird: ",
"Downloading ": "Heruntergeladen wird: ",
"Downloaded ": "Heruntergeladen wurde: ",
"Removed ": "Entfernt wurde: ",
"Checking MD5": "Checke MD5 Prüfsumme",
"Loading...": "Wird geladen...",
"Loading": "Wird geladen",
"Empty!": "Keine Daten!",
"Not Ready...": "Nicht bereit...",
"Error loading page!": "Fehler beim Laden!",
"Error loading page!": "Ladefehler!",
"Update avaliable: ": "Update verfügbar: ",
"Download update: ": "Update herunterladen: ",
"Updated to ": "Aktualisiert auf ",
"Press OK to restart Sphaira": "OK drücken um Sphaira neuzustarten",
"Restart Sphaira?": "Sphaira neustarten?",
"Failed to download update": "Update-Download fehlgeschlagen",
"Download update: ": " Herunterladen des Updates: ",
"Updated to ": "Aktualisiert auf: ",
"Press OK to restart Sphaira": "Drücke OK um sphaira erneut zustarten",
"Restart Sphaira?": "sphaira erneut starten?",
"Failed to download update": "Herunterladen des Updates fehlgeschlagen!",
"Restore hbmenu?": "hbmenu wiederherstellen?",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Konnte /switch/hbmenu.nro nicht finden\nBitte hbmenu über den AppStore neu installieren",
"Failed to restore hbmenu, please re-download hbmenu": "Wiederherstellung fehlgeschlagen, bitte hbmenu neu herunterladen",
"Failed to restore hbmenu, using sphaira instead": "Wiederherstellung fehlgeschlagen, verwende stattdessen Sphaira",
"Restored hbmenu, closing sphaira": "hbmenu wiederhergestellt, Sphaira wird beendet",
"Restored hbmenu": "hbmenu wiederhergestellt",
"Delete Selected files?": "Ausgewählte Dateien löschen?",
"Completely remove ": "Vollständig entfernen ",
"Are you sure you want to delete ": "Wirklich löschen ",
"Are you sure you wish to cancel?": "Wirklich abbrechen?",
"Audio disabled due to suspended game": "",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Die Datei /switch/hbmenu.nro fehlt.\nInstalliere hbmenu über den hb-AppStore.",
"Failed to restore hbmenu, please re-download hbmenu": "Fehler, hbmenu nicht wiederhergrstellt!\nInstalliere hbmenu über den hb-AppStore.",
"Failed to restore hbmenu, using sphaira instead": "Fehler, hbmenu nicht wiederhergrstellt!\nVerwende weiter sphaira",
"Restored hbmenu, closing sphaira": "hbmenu wurde wiederhergestellt, schließe sphaira",
"Restored hbmenu": "hbmenu wurde wiederhergestellt",
"Delete Selected files?": "Ausgewähle Dateien löschen?",
"Completely remove ": "Komplett gelöscht wird: ",
"Are you sure you want to delete ": "Bist du sicher zu löschen? Bestätige Löschung von: ",
"Are you sure you wish to cancel?": "Bist du sicher dass du abbrechen willst?",
"Audio disabled due to suspended game": "Audio deaktivert wegen Spielabbruch",
"If this message appears repeatedly, please open an issue.": "Bei wiederholtem Auftreten bitte Issue erstellen."
}
}

View File

@@ -21,6 +21,8 @@
"Remove": "Remover",
"Restore": "Restaurar",
"Download": "Baixar",
"Next": "Prómixo",
"Prev": "Anterior",
"Next Page": "Próxima página",
"Prev Page": "Página anterior",
"Unstar": "Desfavoritar",
@@ -35,6 +37,9 @@
"Fast": "Rápida",
"Yes": "Sim",
"No": "Não",
"On": "Sim",
"Off": "Não",
"Enable": "Habilitar",
"Enabled": "Sim",
"Disabled": "Não",
@@ -66,7 +71,8 @@
"Select Theme": "Tema atual",
"Shuffle": "Embaralhar temas",
"Music": "Música",
"12 Hour Time": "",
"12 Hour Time": "Relógio de 12 horas",
"Download Default Music": "Baixar música padrão",
"Network": "Rede",
"Network Options": "Opções de rede",
"Ftp": "Servidor FTP",
@@ -101,6 +107,7 @@
"Install location": "Local de instalação",
"Show install warning": "Mostrar aviso de instalação",
"Text scroll speed": "Rolagem do texto",
"Set right-side menu": "Menu direito (R)",
"FileBrowser": "Arquivos",
"%zd files": "%zd arquivo(s)",
@@ -115,6 +122,7 @@
"Paste ": "Colar ",
" file(s)?": " arquivo(s)?",
"Rename": "Renomear",
"Compress to zip": "Comprimir em zip",
"Set New File Name": "Defina o nome do novo arquivo",
"Advanced": "Avançado",
"Advanced Options": "Opções avançadas",
@@ -132,7 +140,7 @@
"Select launcher for: ": "Selecionar launcher para: ",
"Homebrew": "Aplicativos",
"Homebrew Options": "Opções do aplicativo",
"Homebrew Options": "Opções de aplicativo",
"Hide Sphaira": "Esconder sphaira",
"Install Forwarder": "Instalar atalho (forwarder)",
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Instalar atalhos pode\nresultar em um banimento!",
@@ -164,8 +172,16 @@
"app_dls: %s": "downloads: %s",
"More by Author": "Mais deste autor",
"Leave Feedback": "Deixar um feedback",
"Game Options": "Opções de jogo",
"Launch random game": "Iniciar jogo aleatório",
"List meta records": "Listar registro de metadados",
"Entries": "Entradas",
"Delete entity": "Excluir entidade",
"Hide forwarders": "Ocultar atalhos (forwarders)",
"Irs": "Sensor infravermelho",
"Irs": "Câmera de movimento IR",
"IRS (Infrared Joycon Camera)": "Câmera de movimento IR",
"Ambient Noise Level: ": "Nível de ruído ambiente: ",
"Controller": "Controle",
"Pad ": "Pad ",
@@ -216,6 +232,58 @@
"Downloading json": "Baixando JSON",
"Select asset to download for ": "Selecione o recurso para baixar de ",
"Install Options": "Opções de instalação",
"Install options": "Opções de instalação",
"Boost CPU clock": "Aumentar clock da CPU",
"Allow downgrade": "Permitir downgrade",
"Skip if already installed": "Pular se já instalado",
"Ticket only": "Instalar apenas ticket",
"Patch ticket": "Fazer patch de ticket",
"Skip base": "Pular base",
"Skip patch": "Pular patch",
"Skip dlc": "Pular DLC",
"Skip data patch": "Pular patch de dados",
"Skip ticket": "Pular ticket",
"skip NCA hash verify": "Pular verificação de hash NCA",
"Skip RSA header verify": "Pular verificação de header RSA",
"Skip RSA NPDM verify": "Pular verificação de NPDM RSA",
"Ignore distribution bit": "Ignorar bit de distribuição",
"Convert to standard crypto": "Convertr para crypto padrão",
"Lower master key": "Reduzir a master key",
"Lower system version": "Reduzir versão do sistema",
"Install Selected files?": "Instalar os arquivos selecionados?",
"Installed ": "Instalado ",
"FTP Install": "Instalação via FTP",
"USB Install": "Instalação via USB",
"GameCard Install": "Instalação de cartão de jogo",
"FTP Install (EXPERIMENTAL)": "Instalação via FTP (EXPERIMENTAL)",
"USB": "USB",
"GameCard": "Cartão de jogo",
"Disable MTP for usb install": "Escuta MTP desabilitada temporáriamente",
"Re-enabled MTP": "Escuta MTP reabilitada",
"Waiting for connection...": "Aguardando conexão...",
"Transferring data...": "Transferindo dados...",
"Ftp install success!": "Instalação via FTP concluída com sucesso.",
"Ftp install failed!": "Instalação via FTP falhou.",
"Usb install success!": "Instalação via USB concluída com sucesso.",
"Usb install failed!": "Instalação via USB falhou.",
"Gc install success!": "Instalação de cartão de jogo concluída com sucesso.",
"Gc install failed!": "Instalação de cartão de jogo falhou.",
"Installed via usb": "Instalado via USB",
"Failed to install via FTP, press B to exit...": "Falha ao instalar via FTP,\naperte B para sair.",
"Failed to init usb, press B to exit...": "Falha ao instalar via USB,\naperte B para sair.",
"Press B to exit...": "Aperte B para sair.",
"Connection Type: WiFi | Strength: ": "Conexão por rede Wi-Fi | Intensidade do sinal: ",
"Connection Type: Ethernet": "Conexão por cabo (ethernet)",
"Connection Type: None": "Sem conexão",
"Host:": "Host:",
"Port:": "Porta:",
"Username:": "Nome de usuário:",
"Password:": "Senha:",
"SSID:": "SSID:",
"Passphrase:": "Senha:",
"Installing ": "Instalando ",
"Uninstalling ": "Desinstalando ",
"Deleting ": "Excluindo ",
@@ -235,7 +303,7 @@
"Loading": "Carregando",
"Empty!": "Vazio",
"Not Ready...": "Não está pronto...",
"Error loading page!": "Erro ao carregar página!",
"Error loading page!": "Erro ao carregar página",
"Update avaliable: ": "Atualização disponível: ",
"Download update: ": "Baixar autalização: ",
"Updated to ": "Atualizado para ",
@@ -254,4 +322,5 @@
"Are you sure you wish to cancel?": "Você tem certeza que quer cancelar?",
"Audio disabled due to suspended game": "Áudio desativado devido ao software suspenso",
"If this message appears repeatedly, please open an issue.": "Se esta mensagem aparecer repetidamente, abra um issue."
}

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

@@ -0,0 +1,326 @@
{
"[Applet Mode]": "[Режим Аплету]",
"No Internet": "Без інтернету",
"Files": "Файли",
"Apps": "Програми",
"Store": "Магазин",
"Menu": "Меню",
"Options": "Налаштування",
"OK": "ОК",
"Back": "Назад",
"Select": "Вибрати",
"Open": "Відкрити",
"Launch": "Запустити",
"Info": "Інфо",
"Install": "Встановити",
"Delete": "Видалити",
"Restart": "Перезапустити",
"Changelog": "Журнал змін",
"Details": "Деталі",
"Update": "Оновити",
"Remove": "Видалити",
"Restore": "Відновити",
"Download": "Завантажити",
"Next": "Наступний",
"Prev": "Попередній",
"Next Page": "Наступна сторінка",
"Prev Page": "Попередня сторінка",
"Unstar": "Прибрати з обраного",
"Star": "Позначити зіркою",
"System memory": "Пам'ять консолі",
"microSD card": "SD-карта",
"Sd": "SD-карта",
"Image System memory": "Фото | Пам'ять консолі",
"Image microSD card": "Фото | SD-карта",
"Slow": "Повільно",
"Normal": "Нормально",
"Fast": "Швидко",
"Yes": "Так",
"No": "Ні",
"On": "Увімк.",
"Off": "Вимк.",
"Enable": "Увімк.",
"Enabled": "Увімк.",
"Disabled": "Вимк.",
"Sort By": "Сортувати за",
"Sort Options": "Опції сортування",
"Filter": "Фільтр",
"Sort": "Сортування",
"Order": "Порядок",
"Search": "Пошук",
"Updated": "Оновлено",
"Updated (Star)": "Оновлено (Зірка)",
"Downloads": "Завантаження",
"Size": "Розмір",
"Size (Star)": "Розмір (Зірка)",
"Alphabetical": "За алфавітом",
"Alphabetical (Star)": "За алфавітом (Зірка)",
"Likes": "Вподобання",
"ID": "ID",
"Descending": "За спаданням",
"Descending (down)": "За спаданням (вниз)",
"Desc": "Спад.",
"Ascending": "За зростанням",
"Ascending (Up)": "За зростанням (вгору)",
"Asc": "Зрост.",
"Menu Options": "Опції меню",
"Theme": "Тема",
"Theme Options": "Опції теми",
"Select Theme": "Вибрати тему",
"Shuffle": "Перемішати",
"Music": "Музика",
"12 Hour Time": "12-годинний формат часу",
"Download Default Music": "Завантажити музику за замовчуванням",
"Network": "Мережа",
"Network Options": "Опції мережі",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink підключено",
"Nxlink Upload": "Nxlink | Завантаження",
"Nxlink Finished": "Nxlink | Завершено",
"Switch-Handheld!": "Switch - Портатив!",
"Switch-Docked!": "Switch - Докований!",
"Language": "Мова",
"Auto": "Автоматично",
"English": "English",
"Japanese": "日本語",
"French": "Français",
"German": "Deutsch",
"Italian": "Italiano",
"Spanish": "Español",
"Chinese": "中文",
"Korean": "한국어",
"Dutch": "Nederlands",
"Portuguese": "Português",
"Russian": "Русский",
"Swedish": "Svenska",
"Vietnamese": "Tiếng Việt",
"Logging": "Логування",
"Replace hbmenu on exit": "Заміна hbmenu при виході",
"Misc": "Різне",
"Misc Options": "Опції різного",
"Web": "Веб",
"Install forwarders": "Встановити форвардери",
"Install location": "Місце встановлення",
"Show install warning": "Попередж. при встанов.",
"Text scroll speed": "Швидк. прокрутки",
"Set right-side menu": "Праве меню",
"FileBrowser": "Файловий менеджер",
"%zd files": "%zd файл(и)",
"%zd dirs": "%zd тек(и)",
"File Options": "Опції файлів",
"Show Hidden": "Показати приховані",
"Folders First": "Теки спочатку",
"Hidden Last": "Приховані в кінці",
"Cut": "Вирізати",
"Copy": "Копіювати",
"Paste": "Вставити",
"Paste ": "Вставити: ",
" file(s)?": " файл(и)?",
"Rename": "Перейменувати",
"Compress to zip": "Стиснути в zip",
"Set New File Name": "Введіть нове ім'я файлу",
"Advanced": "Додатково",
"Advanced Options": "Додаткові опції",
"Create File": "Створити файл",
"Set File Name": "Введіть ім'я файлу",
"Create Folder": "Створити теку",
"Set Folder Name": "Введіть ім'я теки",
"View as text (unfinished)": "Переглянути як текст (незавершено)",
"Ignore read only": "Ігнорувати лише читання",
"Mount": "Монтувати",
"Empty...": "Пусто...",
"Open with DayBreak?": "Відкрити за допомогою DayBreak?",
"Launch ": "Запустити ",
"Launch option for: ": "Опція запуску для: ",
"Select launcher for: ": "Виберіть лаунчер для: ",
"Homebrew": "Домашні програми",
"Homebrew Options": "Опції домашніх програм",
"Hide Sphaira": "Приховати Sphaira",
"Install Forwarder": "Встановити форвардер",
"WARNING: Installing forwarders will lead to a ban!": "УВАГА: Встановлення форвардерів може призвести до бану!",
"Installing Forwarder": "Встановлення форвардера",
"Creating Program": "Створення програми",
"Creating Control": "Створення контролера",
"Creating Meta": "Створення метаданих",
"Writing Nca": "Запис NCA",
"Updating ncm databse": "Оновлення бази даних NCM",
"Pushing application record": "Запис даних програми",
"Installed!": "Встановлено!",
"Failed to install forwarder": "Не вдалося встановити форвардер",
"Unstarred ": "Знято зірку з ",
"Starred ": "Позначено зіркою ",
"AppStore": "Магазин програм",
"Filter: %s | Sort: %s | Order: %s": "Фільтр: %s | Сорт.: %s | Порядок: %s",
"AppStore Options": "Опції магазину програм",
"All": "Всі",
"Games": "Ігри",
"Emulators": "Емулятори",
"Tools": "Інструменти",
"Themes": "Теми",
"Legacy": "Доступні оновлення",
"version: %s": "версія: %s",
"updated: %s": "оновлено: %s",
"category: %s": "категорія: %s",
"extracted: %.2f MiB": "розмір: %.2f MiB",
"app_dls: %s": "завантажень: %s",
"More by Author": "Більше від автора",
"Leave Feedback": "Залишити відгук",
"Game Options": "Опції ігор",
"Launch random game": "Запустити випадкову гру",
"List meta records": "Список метаданих записів",
"Entries": "Записи",
"Delete entity": "Видалити сутність",
"Hide forwarders": "Приховати форвардери",
"Irs": "ІЧ-сенсор",
"IRS (Infrared Joycon Camera)": "ІЧ (Інфрачервона камера Joycon)",
"Ambient Noise Level: ": "Рівень навколишнього шуму: ",
"Controller": "Контролер",
"Pad ": "Геймпад ",
" (Available)": " (Доступно)",
" (Unsupported)": " (Не підтримується)",
" (Unconnected)": " (Не підключено)",
"HandHeld": "Портативний режим",
"Rotation": "Обертання",
"0 (Sideways)": "0° (Збоку)",
"90 (Flat)": "90° (Плоско)",
"180 (-Sideways)": "180° (-Збоку)",
"270 (Upside down)": "270° (Догори дном)",
"Colour": "Колір",
"Grey": "Сірий",
"Ironbow": "Ironbow",
"Green": "Зелений",
"Red": "Червоний",
"Blue": "Синій",
"Light Target": "Ціль освітлення",
"All leds": "Всі світлодіоди",
"Bright group": "Яскрава група",
"Dim group": "Тьмяна група",
"None": "Немає",
"Gain": "Підсилення",
"Negative Image": "Негативне зображення",
"Normal image": "Нормальне зображення",
"Trimming Format": "Формат обрізки",
"External Light Filter": "Фільтр зовнішнього освітлення",
"Load Default": "Завантажити типові",
"Format": "Формат",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Themezer": "Themezer",
"Themezer Options": "Опції Themezer",
"Nsfw": "NSFW",
"Page": "Сторінка",
"Page %zu / %zu": "Сторінка %zu / %zu",
"Enter Page Number": "Введіть номер сторінки",
"Bad Page": "Неправильна сторінка",
"Download theme?": "Завантажити тему?",
"GitHub": "GitHub",
"Downloading json": "Завантаження JSON",
"Select asset to download for ": "Виберіть ресурс для завантаження для ",
"Install Options": "Опції встановлення",
"Install options": "Опції встановлення",
"Boost CPU clock": "Розігнати CPU",
"Allow downgrade": "Дозволити відкат",
"Skip if already installed": "Пропуск, якщо встановл.",
"Ticket only": "Тільки тікет",
"Patch ticket": "Змінити тікет",
"Skip base": "Пропустити базу",
"Skip patch": "Пропустити патч",
"Skip dlc": "Пропустити DLC",
"Skip data patch": "Пропустити патч даних",
"Skip ticket": "Пропустити тікет",
"skip NCA hash verify": "Пропуск перевірку хешу NCA",
"Skip RSA header verify": "Пропуск перевірку заголовка RSA",
"Skip RSA NPDM verify": "Пропуск перевірку NPDM RSA",
"Ignore distribution bit": "Ігнорувати біт розподілу",
"Convert to standard crypto": "Конвертувати у стандартне шифрування",
"Lower master key": "Знизити майстер-ключ",
"Lower system version": "Знизити версію системи",
"Install Selected files?": "Встановити вибрані файли?",
"Installed": "Встановлено",
"Installed ": "Встановлено ",
"FTP Install": "Встановлення через FTP",
"USB Install": "Встановлення через USB",
"GameCard Install": "Встановлення з картриджа",
"FTP Install (EXPERIMENTAL)": "Встановлення через FTP (ЕКСПЕРИМЕНТАЛЬНО)",
"USB": "USB",
"GameCard": "Картридж",
"Disable MTP for usb install": "Вимкнути MTP для встановлення через USB",
"Re-enabled MTP": "MTP знову увімкнено",
"Waiting for connection...": "Очікування підключення...",
"Transferring data...": "Передача даних...",
"Ftp install success!": "Встановлення через FTP успішно завершено.",
"Ftp install failed!": "Встановлення через FTP не вдалося.",
"Usb install success!": "Встановлення через USB успішно завершено.",
"Usb install failed!": "Встановлення через USB не вдалося.",
"Gc install success!": "Встановлення з картриджа успішно завершено.",
"Gc install failed!": "Встановлення з картриджа не вдалося.",
"Installed via usb": "Встановлено через USB",
"Failed to install via FTP, press B to exit...": "Не вдалося встановити через FTP, натисніть B для виходу...",
"Failed to init usb, press B to exit...": "Не вдалося ініціалізувати USB, натисніть B для виходу...",
"Press B to exit...": "Натисніть B для виходу...",
"Connection Type: WiFi | Strength:": "Тип підключення: WiFi | Сила сигналу:",
"Connection Type: WiFi | Strength: ": "Тип підключення: WiFi | Сила сигналу: ",
"Connection Type: Ethernet": "Тип підключення: Ethernet",
"Connection Type: None": "Тип підключення: Немає",
"Host:": "Хост:",
"Port:": "Порт:",
"Username:": "Ім'я користувача:",
"Password:": "Пароль:",
"SSID:": "SSID:",
"Passphrase:": "Кодова фраза:",
"Installing ": "Встановлення ",
"Uninstalling ": "Видалення ",
"Deleting ": "Видалення ",
"Deleting": "Видалення",
"Pasting ": "Вставлення ",
"Pasting": "Вставлення",
"Removing ": "Видалення ",
"Scanning ": "Сканування ",
"Creating ": "Створення ",
"Copying ": "Копіювання ",
"Trying to load ": "Спроба завантажити ",
"Downloading ": "Завантаження ",
"Downloaded ": "Завантажено ",
"Removed ": "Видалено ",
"Checking MD5": "Перевірка MD5",
"Loading...": "Завантаження...",
"Loading": "Завантаження",
"Empty!": "Пусто!",
"Not Ready...": "Не готово...",
"Error loading page!": "Помилка завантаження сторінки!",
"Update avaliable: ": "Доступне оновлення: ",
"Download update: ": "Завантажити оновлення: ",
"Updated to ": "Оновлено до ",
"Press OK to restart Sphaira": "Натисніть OK для перезапуску Sphaira",
"Restart Sphaira?": "Перезапустити Sphaira?",
"Failed to download update": "Не вдалося завантажити оновлення",
"Restore hbmenu?": "Відновити hbmenu?",
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Не вдалося знайти /switch/hbmenu.nro\nВикористовуйте Магазин програм для перевстановлення hbmenu",
"Failed to restore hbmenu, please re-download hbmenu": "Не вдалося відновити hbmenu, будь ласка, завантажте hbmenu знову",
"Failed to restore hbmenu, using sphaira instead": "Не вдалося відновити hbmenu, замість цього використовується Sphaira",
"Restored hbmenu, closing sphaira": "hbmenu відновлено, закриття Sphaira",
"Restored hbmenu": "hbmenu відновлено",
"Delete Selected files?": "Видалити вибрані файли?",
"Completely remove ": "Повністю видалити ",
"Are you sure you want to delete ": "Ви впевнені, що хочете видалити ",
"Are you sure you wish to cancel?": "Ви впевнені, що хочете скасувати?",
"Audio disabled due to suspended game": "Аудіо вимкнено через призупинену програму",
"If this message appears repeatedly, please open an issue.": "Якщо це повідомлення з'являється повторно, будь ласка, повідомте про проблему."
}

View File

@@ -21,6 +21,8 @@
"Remove": "删除",
"Restore": "恢复",
"Download": "下载",
"Next": "下一项",
"Prev": "上一项",
"Next Page": "下一页",
"Prev Page": "上一页",
"Unstar": "取消星标",
@@ -66,7 +68,8 @@
"Select Theme": "选择主题",
"Shuffle": "随机播放",
"Music": "音乐",
"12 Hour Time": "",
"12 Hour Time": "12小时制时间",
"Download Default Music": "下载默认音乐",
"Network": "网络",
"Network Options": "网络选项",
"Ftp": "FTP",
@@ -115,6 +118,7 @@
"Paste ": "粘贴 ",
" file(s)?": "个文件(夹)",
"Rename": "重命名",
"Compress to zip": "压缩到zip",
"Set New File Name": "输入新命名",
"Advanced": "高级",
"Advanced Options": "高级选项",
@@ -165,12 +169,19 @@
"More by Author": "作者更多作品",
"Leave Feedback": "留言反馈",
"Game Options": "游戏选项",
"Launch random game": "开启随机游戏",
"List meta records": "列出元数据记录",
"Entries": "条目",
"Delete entity": "删除整体",
"Hide forwarders": "隐藏前端启动",
"Irs": "红外成像",
"Ambient Noise Level: ": "环境噪声等级:",
"Controller": "控制器",
"Pad ": "手柄 ",
" (Available)": " (可用的)",
" (Unsupported)": "",
" (Unsupported)": " (不支持的)",
" (Unconnected)": " (未连接)",
"HandHeld": "掌机模式",
"Rotation": "旋转",
@@ -216,6 +227,58 @@
"Downloading json": "正在下载 json",
"Select asset to download for ": "选择要下载的资源用于 ",
"Install Options": "安装选项",
"Install options": "安装选项",
"Boost CPU clock": "提升 CPU 频率",
"Allow downgrade": "允许降级",
"Skip if already installed": "若已安装则跳过",
"Ticket only": "仅安装票据",
"Patch ticket": "修补票据",
"Skip base": "跳过基础部分",
"Skip patch": "跳过补丁",
"Skip dlc": "跳过 DLC可下载内容",
"Skip data patch": "跳过数据补丁",
"Skip ticket": "跳过票据",
"skip NCA hash verify": "跳过 NCA 哈希验证",
"Skip RSA header verify": "跳过 RSA 头部验证",
"Skip RSA NPDM verify": "跳过 RSA NPDM 验证",
"Ignore distribution bit": "忽略分布位",
"Convert to standard crypto": "转换为标准加密方式",
"Lower master key": "降低主密钥",
"Lower system version": "降低系统版本",
"Install Selected files?": "安装所选文件?",
"Installed": "已安装",
"FTP Install": "通过 FTP 安装",
"USB Install": "通过 USB 安装",
"GameCard Install": "卡带安装",
"FTP Install (EXPERIMENTAL)": "通过 FTP 安装(实验性)",
"USB": "USB",
"GameCard": "卡带",
"Disable MTP for usb install": "暂时禁用 USB 安装的 MTP 功能",
"Re-enabled MTP": "重新启用 MTP",
"Waiting for connection...": "等待连接中...",
"Transferring data...": "正在传输数据...",
"Ftp install success!": "通过 FTP 安装成功。",
"Ftp install failed!": "通过 FTP 安装失败。",
"Usb install success!": "通过 USB 安装成功。",
"Usb install failed!": "通过 USB 安装失败。",
"Gc install success!": "游戏安装成功。",
"Gc install failed!": "游戏安装失败。",
"Installed via usb": "通过 USB 安装",
"Failed to install via FTP, press B to exit...": "通过 FTP 安装失败,按 B 键退出...",
"Failed to init usb, press B to exit...": "USB 初始化失败,按 B 键退出...",
"Press B to exit...": "按 B 键退出...",
"Connection Type: WiFi | Strength:": "连接类型WiFi | 信号强度:",
"Connection Type: Ethernet": "连接类型:以太网",
"Connection Type: None": "连接类型:无",
"Host:": "主机:",
"Port:": "端口:",
"Username:": "用户名:",
"Password:": "密码:",
"SSID:": "网络名称:",
"Passphrase:": "密码:",
"Installing ": "正在安装 ",
"Uninstalling ": "正在卸载 ",
"Deleting ": "正在删除 ",
@@ -239,7 +302,7 @@
"Update avaliable: ": "有可用更新!",
"Download update: ": "下载更新:",
"Updated to ": "更新至 ",
"Press OK to restart Sphaira": "",
"Press OK to restart Sphaira": "按OK键以重启shphaira菜单",
"Restart Sphaira?": "重启 Sphaira",
"Failed to download update": "更新下载失败",
"Restore hbmenu?": "恢复 hbmenu",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.13)
set(sphaira_VERSION 0.8.0)
set(sphaira_VERSION 0.10.2)
project(sphaira
VERSION ${sphaira_VERSION}
@@ -49,6 +49,8 @@ add_executable(sphaira
source/ui/menus/usb_menu.cpp
source/ui/menus/ftp_menu.cpp
source/ui/menus/gc_menu.cpp
source/ui/menus/game_menu.cpp
source/ui/menus/grid_menu_base.cpp
source/ui/error_box.cpp
source/ui/notification.cpp
@@ -60,7 +62,7 @@ add_executable(sphaira
source/ui/sidebar.cpp
source/ui/widget.cpp
source/ui/list.cpp
source/ui/bubbles.cpp
source/ui/scrolling_text.cpp
source/app.cpp
source/download.cpp
@@ -68,16 +70,21 @@ add_executable(sphaira
source/evman.cpp
source/fs.cpp
source/image.cpp
source/location.cpp
source/log.cpp
source/main.cpp
source/nro.cpp
source/nxlink.cpp
source/owo.cpp
source/swkbd.cpp
source/web.cpp
source/i18n.cpp
source/ftpsrv_helper.cpp
source/usb/base.cpp
source/usb/usbds.cpp
source/usb/usbhs.cpp
source/usb/usb_uploader.cpp
source/yati/yati.cpp
source/yati/container/nsp.cpp
source/yati/container/xci.cpp
@@ -98,6 +105,7 @@ add_executable(sphaira
target_compile_definitions(sphaira PRIVATE
-DAPP_VERSION="${sphaira_VERSION}"
-DAPP_VERSION_HASH="${sphaira_VERSION_HASH}"
-DCURL_NO_OLDIES=1
)
target_compile_options(sphaira PRIVATE
@@ -153,7 +161,7 @@ FetchContent_Declare(ftpsrv
FetchContent_Declare(libhaze
GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git
GIT_TAG 3244b9e
GIT_TAG 04f1526
)
FetchContent_Declare(libpulsar
@@ -277,31 +285,6 @@ if (USE_VFS_GC)
)
endif()
# todo: upstream cmake
add_library(libhaze
${libhaze_SOURCE_DIR}/source/async_usb_server.cpp
${libhaze_SOURCE_DIR}/source/device_properties.cpp
${libhaze_SOURCE_DIR}/source/event_reactor.cpp
${libhaze_SOURCE_DIR}/source/haze.cpp
${libhaze_SOURCE_DIR}/source/ptp_object_database.cpp
${libhaze_SOURCE_DIR}/source/ptp_object_heap.cpp
${libhaze_SOURCE_DIR}/source/ptp_responder_android_operations.cpp
${libhaze_SOURCE_DIR}/source/ptp_responder_mtp_operations.cpp
${libhaze_SOURCE_DIR}/source/ptp_responder_ptp_operations.cpp
${libhaze_SOURCE_DIR}/source/ptp_responder.cpp
${libhaze_SOURCE_DIR}/source/usb_session.cpp
)
target_include_directories(libhaze PUBLIC ${libhaze_SOURCE_DIR}/include)
set_target_properties(libhaze PROPERTIES
C_STANDARD 11
C_EXTENSIONS ON
CXX_STANDARD 20
CXX_EXTENSIONS ON
# force optimisations in debug mode as otherwise vapor errors
# due to force_inline attribute failing...
COMPILE_OPTIONS "$<$<CONFIG:Debug>:-Os>"
)
add_library(stb INTERFACE)
target_include_directories(stb INTERFACE ${stb_SOURCE_DIR})

View File

@@ -8,6 +8,7 @@
#include "owo.hpp"
#include "option.hpp"
#include "fs.hpp"
#include "log.hpp"
#include <switch.h>
#include <vector>
@@ -77,6 +78,8 @@ public:
static auto GetLogEnable() -> bool;
static auto GetReplaceHbmenuEnable() -> bool;
static auto GetInstallEnable() -> bool;
static auto GetInstallSysmmcEnable() -> bool;
static auto GetInstallEmummcEnable() -> bool;
static auto GetInstallSdEnable() -> bool;
static auto GetInstallPrompt() -> bool;
static auto GetThemeMusicEnable() -> bool;
@@ -89,7 +92,8 @@ public:
static void SetNxlinkEnable(bool enable);
static void SetLogEnable(bool enable);
static void SetReplaceHbmenuEnable(bool enable);
static void SetInstallEnable(bool enable);
static void SetInstallSysmmcEnable(bool enable);
static void SetInstallEmummcEnable(bool enable);
static void SetInstallSdEnable(bool enable);
static void SetInstallPrompt(bool enable);
static void SetThemeMusicEnable(bool enable);
@@ -123,6 +127,10 @@ public:
void ScanThemes(const std::string& path);
void ScanThemeEntries();
// helper that converts 1.2.3 to a u32 used for comparisons.
static auto GetVersionFromString(const char* str) -> u32;
static auto IsVersionNewer(const char* current, const char* new_version) -> u32;
static auto IsApplication() -> bool {
const auto type = appletGetAppletType();
return type == AppletType_Application || type == AppletType_SystemApplication;
@@ -142,6 +150,21 @@ public:
return R_SUCCEEDED(pmdmntGetApplicationProcessId(&pid));
}
static auto IsEmunand() -> bool {
alignas(0x1000) struct EmummcPaths {
char unk[0x80];
char nintendo[0x80];
} paths{};
SecmonArgs args{};
args.X[0] = 0xF0000404; /* smcAmsGetEmunandConfig */
args.X[1] = 0; /* EXO_EMUMMC_MMC_NAND*/
args.X[2] = (u64)&paths; /* out path */
svcCallSecureMonitor(&args);
return (paths.unk[0] != '\0') || (paths.nintendo[0] != '\0');
}
// private:
static constexpr inline auto CONFIG_PATH = "/config/sphaira/config.ini";
@@ -184,11 +207,14 @@ public:
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
option::OptionBool m_12hour_time{INI_SECTION, "12hour_time", false};
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto
option::OptionString m_right_side_menu{INI_SECTION, "right_side_menu", "Appstore"};
// install options
option::OptionBool m_install{INI_SECTION, "install", false};
option::OptionBool m_install_sysmmc{INI_SECTION, "install_sysmmc", false};
option::OptionBool m_install_emummc{INI_SECTION, "install_emummc", false};
option::OptionBool m_install_sd{INI_SECTION, "install_sd", true};
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
option::OptionLong m_boost_mode{INI_SECTION, "boost_mode", false};
option::OptionBool m_allow_downgrade{INI_SECTION, "allow_downgrade", false};
option::OptionBool m_skip_if_already_installed{INI_SECTION, "skip_if_already_installed", true};
option::OptionBool m_ticket_only{INI_SECTION, "ticket_only", false};

View File

@@ -13,10 +13,14 @@ namespace sphaira::curl {
enum {
Flag_None = 0,
// requests to download send etag in the header.
// the received etag is then saved on success.
// this api is only available on downloading to file.
Flag_Cache = 1 << 0,
// sets CURLOPT_NOBODY.
Flag_NoBody = 1 << 1,
};
enum class Priority {
@@ -29,7 +33,9 @@ struct ApiResult;
using Path = fs::FsPath;
using OnComplete = std::function<void(ApiResult& result)>;
using OnProgress = std::function<bool(u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow)>;
using OnProgress = std::function<bool(s64 dltotal, s64 dlnow, s64 ultotal, s64 ulnow)>;
using OnUploadCallback = std::function<size_t(void *ptr, size_t size)>;
using OnUploadSeek = std::function<bool(s64 offset)>;
using StopToken = std::stop_token;
struct Url {
@@ -62,6 +68,55 @@ struct Flags {
u32 m_flags{Flag_None};
};
struct Port {
Port() = default;
Port(u16 port) : m_port{port} {}
u16 m_port{};
};
struct CustomRequest {
CustomRequest() = default;
CustomRequest(const std::string& str) : m_str{str} {}
std::string m_str;
};
struct UserPass {
UserPass() = default;
UserPass(const std::string& user) : m_user{user} {}
UserPass(const std::string& user, const std::string& pass) : m_user{user}, m_pass{pass} {}
std::string m_user;
std::string m_pass;
};
struct UploadInfo {
UploadInfo() = default;
UploadInfo(const std::string& name) : m_name{name} {}
UploadInfo(const std::string& name, s64 size, OnUploadCallback cb) : m_name{name}, m_size{size}, m_callback{cb} {}
UploadInfo(const std::string& name, const std::vector<u8>& data) : m_name{name}, m_data{data} {}
std::string m_name{};
std::vector<u8> m_data{};
s64 m_size{};
OnUploadCallback m_callback{};
};
struct Bearer {
Bearer() = default;
Bearer(const std::string& str) : m_str{str} {}
std::string m_str;
};
struct PubKey {
PubKey() = default;
PubKey(const std::string& str) : m_str{str} {}
std::string m_str;
};
struct PrivKey {
PrivKey() = default;
PrivKey(const std::string& str) : m_str{str} {}
std::string m_str;
};
struct ApiResult {
bool success;
long code;
@@ -76,16 +131,29 @@ struct DownloadEventData {
StopToken stoken;
};
// helper that generates the api using an location.
#define CURL_LOCATION_TO_API(loc) \
curl::Url{loc.url}, \
curl::UserPass{loc.user, loc.pass}, \
curl::Bearer{loc.bearer}, \
curl::PubKey{loc.pub_key}, \
curl::PrivKey{loc.priv_key}, \
curl::Port(loc.port)
auto Init() -> bool;
void Exit();
// sync functions
auto ToMemory(const Api& e) -> ApiResult;
auto ToFile(const Api& e) -> ApiResult;
auto FromMemory(const Api& e) -> ApiResult;
auto FromFile(const Api& e) -> ApiResult;
// async functions
auto ToMemoryAsync(const Api& e) -> bool;
auto ToFileAsync(const Api& e) -> bool;
auto FromMemoryAsync(const Api& e) -> bool;
auto FromFileAsync(const Api& e) -> bool;
// uses curl to convert string to their %XX
auto EscapeString(const std::string& str) -> std::string;
@@ -107,6 +175,15 @@ struct Api {
}
}
template <typename... Ts>
auto From(Ts&&... ts) {
if constexpr(std::disjunction_v<std::is_same<Path, Ts>...>) {
return FromFile(std::forward<Ts>(ts)...);
} else {
return FromMemory(std::forward<Ts>(ts)...);
}
}
template <typename... Ts>
auto ToAsync(Ts&&... ts) {
if constexpr(std::disjunction_v<std::is_same<Path, Ts>...>) {
@@ -116,6 +193,15 @@ struct Api {
}
}
template <typename... Ts>
auto FromAsync(Ts&&... ts) {
if constexpr(std::disjunction_v<std::is_same<Path, Ts>...>) {
return FromFileAsync(std::forward<Ts>(ts)...);
} else {
return FromMemoryAsync(std::forward<Ts>(ts)...);
}
}
template <typename... Ts>
auto ToMemory(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
@@ -125,6 +211,16 @@ struct Api {
return curl::ToMemory(*this);
}
template <typename... Ts>
auto FromMemory(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
static_assert(std::disjunction_v<std::is_same<UploadInfo, Ts>...>, "UploadInfo must be specified");
static_assert(!std::disjunction_v<std::is_same<Path, Ts>...>, "Path must not valid for memory");
static_assert(!std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must not be specified");
Api::set_option(std::forward<Ts>(ts)...);
return curl::FromMemory(*this);
}
template <typename... Ts>
auto ToFile(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
@@ -134,6 +230,16 @@ struct Api {
return curl::ToFile(*this);
}
template <typename... Ts>
auto FromFile(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
static_assert(std::disjunction_v<std::is_same<Path, Ts>...>, "Path must be specified");
static_assert(std::disjunction_v<std::is_same<UploadInfo, Ts>...>, "UploadInfo must be specified");
static_assert(!std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must not be specified");
Api::set_option(std::forward<Ts>(ts)...);
return curl::FromFile(*this);
}
template <typename... Ts>
auto ToMemoryAsync(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
@@ -144,6 +250,17 @@ struct Api {
return curl::ToMemoryAsync(*this);
}
template <typename... Ts>
auto FromMemoryAsync(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
static_assert(std::disjunction_v<std::is_same<UploadInfo, Ts>...>, "UploadInfo must be specified");
static_assert(std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must be specified");
static_assert(!std::disjunction_v<std::is_same<Path, Ts>...>, "Path must not valid for memory");
static_assert(std::disjunction_v<std::is_same<StopToken, Ts>...>, "StopToken must be specified");
Api::set_option(std::forward<Ts>(ts)...);
return curl::FromMemoryAsync(*this);
}
template <typename... Ts>
auto ToFileAsync(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
@@ -154,62 +271,55 @@ struct Api {
return curl::ToFileAsync(*this);
}
auto& GetUrl() const {
return m_url.m_str;
}
auto& GetFields() const {
return m_fields.m_str;
}
auto& GetHeader() const {
return m_header;
}
auto& GetFlags() const {
return m_flags.m_flags;
}
auto& GetPath() const {
return m_path;
}
auto& GetOnComplete() const {
return m_on_complete;
}
auto& GetOnProgress() const {
return m_on_progress;
}
auto& GetPriority() const {
return m_prio;
}
auto& GetToken() const {
return m_stoken;
template <typename... Ts>
auto FromFileAsync(Ts&&... ts) {
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
static_assert(std::disjunction_v<std::is_same<Path, Ts>...>, "Path must be specified");
static_assert(std::disjunction_v<std::is_same<UploadInfo, Ts>...>, "UploadInfo must be specified");
static_assert(std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must be specified");
static_assert(std::disjunction_v<std::is_same<StopToken, Ts>...>, "StopToken must be specified");
Api::set_option(std::forward<Ts>(ts)...);
return curl::FromFileAsync(*this);
}
private:
void SetOption(Url&& v) {
m_url = v;
}
void SetOption(Fields&& v) {
m_fields = v;
}
void SetOption(Header&& v) {
m_header = v;
}
void SetOption(Flags&& v) {
m_flags = v;
}
void SetOption(Path&& v) {
m_path = v;
}
void SetOption(OnComplete&& v) {
m_on_complete = v;
}
void SetOption(OnProgress&& v) {
m_on_progress = v;
}
void SetOption(Priority&& v) {
m_prio = v;
}
void SetOption(StopToken&& v) {
m_stoken = v;
}
void SetUpload(bool enable) { m_is_upload = enable; }
auto IsUpload() const { return m_is_upload; }
auto& GetUrl() const { return m_url.m_str; }
auto& GetFields() const { return m_fields.m_str; }
auto& GetHeader() const { return m_header; }
auto& GetFlags() const { return m_flags.m_flags; }
auto& GetPath() const { return m_path; }
auto& GetPort() const { return m_port.m_port; }
auto& GetCustomRequest() const { return m_custom_request.m_str; }
auto& GetUserPass() const { return m_userpass; }
auto& GetBearer() const { return m_bearer.m_str; }
auto& GetPubKey() const { return m_pub_key.m_str; }
auto& GetPrivKey() const { return m_priv_key.m_str; }
auto& GetUploadInfo() const { return m_info; }
auto& GetOnComplete() const { return m_on_complete; }
auto& GetOnProgress() const { return m_on_progress; }
auto& GetOnUploadSeek() const { return m_on_upload_seek; }
auto& GetPriority() const { return m_prio; }
auto& GetToken() const { return m_stoken; }
void SetOption(Url&& v) { m_url = v; }
void SetOption(Fields&& v) { m_fields = v; }
void SetOption(Header&& v) { m_header = v; }
void SetOption(Flags&& v) { m_flags = v; }
void SetOption(Path&& v) { m_path = v; }
void SetOption(Port&& v) { m_port = v; }
void SetOption(CustomRequest&& v) { m_custom_request = v; }
void SetOption(UserPass&& v) { m_userpass = v; }
void SetOption(Bearer&& v) { m_bearer = v; }
void SetOption(PubKey&& v) { m_pub_key = v; }
void SetOption(PrivKey&& v) { m_priv_key = v; }
void SetOption(UploadInfo&& v) { m_info = v; }
void SetOption(OnComplete&& v) { m_on_complete = v; }
void SetOption(OnProgress&& v) { m_on_progress = v; }
void SetOption(OnUploadSeek&& v) { m_on_upload_seek = v; }
void SetOption(Priority&& v) { m_prio = v; }
void SetOption(StopToken&& v) { m_stoken = v; }
template <typename T>
void set_option(T&& t) {
@@ -223,16 +333,25 @@ private:
}
private:
Url m_url;
Url m_url{};
Fields m_fields{};
Header m_header{};
Flags m_flags{};
Path m_path{};
OnComplete m_on_complete{nullptr};
OnProgress m_on_progress{nullptr};
Port m_port{};
CustomRequest m_custom_request{};
UserPass m_userpass{};
Bearer m_bearer{};
PubKey m_pub_key{};
PrivKey m_priv_key{};
UploadInfo m_info{};
OnComplete m_on_complete{};
OnProgress m_on_progress{};
OnUploadSeek m_on_upload_seek{};
Priority m_prio{Priority::High};
std::stop_source m_stop_source{};
StopToken m_stoken{m_stop_source.get_token()};
bool m_is_upload{};
};
} // namespace sphaira::curl

View File

@@ -1,18 +1,19 @@
#pragma once
#include <string>
#include <string_view>
namespace sphaira::i18n {
bool init(long index);
void exit();
std::string get(const char* str);
std::string get(std::string_view str);
} // namespace sphaira::i18n
inline namespace literals {
std::string operator"" _i18n(const char* str, size_t len);
std::string operator""_i18n(const char* str, size_t len);
} // namespace literals

View File

@@ -0,0 +1,24 @@
#pragma once
#include <string>
#include <vector>
#include <switch.h>
namespace sphaira::location {
struct Entry {
std::string name{};
std::string url{};
std::string user{};
std::string pass{};
std::string bearer{};
std::string pub_key{};
std::string priv_key{};
u16 port{};
};
using Entries = std::vector<Entry>;
auto Load() -> Entries;
void Add(const Entry& e);
} // namespace sphaira::location

View File

@@ -14,8 +14,12 @@ struct OptionBase {
{}
auto Get() -> T;
auto GetOr(const char* name) -> T;
void Set(T value);
private:
auto GetInternal(const char* name) -> T;
private:
const std::string m_section;
const std::string m_name;

View File

@@ -1,10 +0,0 @@
#include "ui/types.hpp"
#include "ui/object.hpp"
namespace sphaira::ui::bubble {
void Init();
void Draw(NVGcontext* vg, Theme* theme);
void Exit();
} // namespace sphaira::ui::bubble

View File

@@ -8,15 +8,14 @@ namespace sphaira::ui {
class ErrorBox final : public Widget {
public:
ErrorBox(Result code, const std::string& message);
ErrorBox(const std::string& message);
auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
private:
Result m_code{};
std::optional<Result> m_code{};
std::string m_message{};
std::string m_module_str{};
std::string m_description_str{};
};
} // namespace sphaira::ui

View File

@@ -5,6 +5,11 @@
namespace sphaira::ui {
struct List final : Object {
enum class Layout {
HOME,
GRID,
};
using Callback = std::function<void(NVGcontext* vg, Theme* theme, Vec4 v, s64 index)>;
using TouchCallback = std::function<void(bool touch, s64 index)>;
@@ -35,10 +40,36 @@ struct List final : Object {
return m_v.h + m_pad.y;
}
auto GetMaxX() const {
return m_v.w + m_pad.x;
}
auto GetLayout() const {
return m_layout;
}
void SetLayout(Layout layout) {
m_layout = layout;
}
auto GetRow() const {
return m_row;
}
auto GetPage() const {
return m_page;
}
private:
auto Draw(NVGcontext* vg, Theme* theme) -> void override {}
auto ClampX(float x, s64 count) const -> float;
auto ClampY(float y, s64 count) const -> float;
void OnUpdateHome(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback);
void OnUpdateGrid(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback);
void DrawHome(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const;
void DrawGrid(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const;
private:
const s64 m_row;
const s64 m_page;
@@ -52,6 +83,8 @@ private:
float m_yoff{};
// in progress y offset, used when scrolling.
float m_y_prog{};
Layout m_layout{Layout::GRID};
};
} // namespace sphaira::ui

View File

@@ -1,10 +1,11 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "ui/menus/grid_menu_base.hpp"
#include "ui/scrollable_text.hpp"
#include "ui/scrolling_text.hpp"
#include "ui/list.hpp"
#include "nro.hpp"
#include "fs.hpp"
#include "option.hpp"
#include <span>
namespace sphaira::ui::menu::appstore {
@@ -73,6 +74,7 @@ struct EntryMenu final : MenuBase {
EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu);
~EntryMenu();
auto GetShortTitle() const -> const char* override { return "Entry"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
// void OnFocusGained() override;
@@ -134,27 +136,25 @@ enum OrderType {
OrderType_Ascending,
};
struct Menu final : MenuBase {
Menu(const std::vector<NroEntry>& nro_entries);
using LayoutType = grid::LayoutType;
struct Menu final : grid::Menu {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "Store"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
void SetIndex(s64 index);
void ScanHomebrew();
void Sort();
void SetFilter(Filter filter);
void SetSort(SortType sort);
void SetOrder(OrderType order);
void SetSearch(const std::string& term);
void SetAuthor();
auto GetEntry(s64 i) -> Entry& {
return m_entries[m_entries_current[i]];
}
auto GetEntry() -> Entry& {
return m_entries[m_entries_current[m_index]];
return GetEntry(m_index);
}
auto SetDirty() {
@@ -162,16 +162,27 @@ struct Menu final : MenuBase {
}
private:
const std::vector<NroEntry>& m_nro_entries;
void SetIndex(s64 index);
void ScanHomebrew();
void Sort();
void SortAndFindLastFile();
void SetFilter();
void SetSearch(const std::string& term);
void OnLayoutChange();
private:
static constexpr inline const char* INI_SECTION = "appstore";
std::vector<Entry> m_entries{};
std::vector<EntryMini> m_entries_index[Filter_MAX]{};
std::vector<EntryMini> m_entries_index_author{};
std::vector<EntryMini> m_entries_index_search{};
std::span<EntryMini> m_entries_current{};
Filter m_filter{Filter::Filter_All};
SortType m_sort{SortType::SortType_Updated};
OrderType m_order{OrderType::OrderType_Descending};
option::OptionLong m_filter{INI_SECTION, "filter", Filter::Filter_All};
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Updated};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_GridDetail};
s64 m_index{}; // where i am in the array
LazyImage m_default_image{};

View File

@@ -10,6 +10,7 @@ struct Menu final : MenuBase {
Menu(const fs::FsPath& path);
~Menu();
auto GetShortTitle() const -> const char* override { return "File"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;

View File

@@ -124,6 +124,7 @@ struct Menu final : MenuBase {
Menu(const std::vector<NroEntry>& nro_entries);
~Menu();
auto GetShortTitle() const -> const char* override { return "Files"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
@@ -135,8 +136,12 @@ struct Menu final : MenuBase {
private:
void SetIndex(s64 index);
void InstallForwarder();
void InstallFile(const FileEntry& target);
void InstallFiles(const std::vector<FileEntry>& targets);
void InstallFiles();
void UnzipFiles(fs::FsPath folder);
void ZipFiles(fs::FsPath zip_path);
void UploadFiles();
auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result;
void LoadAssocEntriesPath(const fs::FsPath& path);
@@ -156,15 +161,15 @@ private:
}
auto GetSelectedEntries() const -> std::vector<FileEntry> {
if (!m_selected_count) {
return {};
}
std::vector<FileEntry> out;
for (auto&e : m_entries) {
if (e.IsSelected()) {
out.emplace_back(e);
if (!m_selected_count) {
out.emplace_back(GetEntry());
} else {
for (auto&e : m_entries) {
if (e.IsSelected()) {
out.emplace_back(e);
}
}
}
@@ -183,13 +188,6 @@ private:
m_selected_path = m_path;
}
void AddCurrentFileToSelection(SelectedType type) {
m_selected_files.emplace_back(GetEntry());
m_selected_count++;
m_selected_type = type;
m_selected_path = m_path;
}
void ResetSelection() {
m_selected_files.clear();
m_selected_count = 0;

View File

@@ -38,13 +38,14 @@ struct Menu final : MenuBase {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "FTP"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
// this should be private
// private:
std::shared_ptr<StreamFtp> m_source;
std::shared_ptr<StreamFtp> m_source{};
Thread m_thread{};
Mutex m_mutex{};
// the below are shared across threads, lock with the above mutex!
@@ -54,6 +55,7 @@ struct Menu final : MenuBase {
const char* m_pass{};
unsigned m_port{};
bool m_anon{};
bool m_was_ftp_enabled{};
};
} // namespace sphaira::ui::menu::ftp

View File

@@ -0,0 +1,153 @@
#pragma once
#include "ui/menus/grid_menu_base.hpp"
#include "ui/list.hpp"
#include "fs.hpp"
#include "option.hpp"
#include <memory>
#include <vector>
namespace sphaira::ui::menu::game {
enum class NacpLoadStatus {
// not yet attempted to be loaded.
None,
// started loading.
Progress,
// loaded, ready to parse.
Loaded,
// failed to load, do not attempt to load again!
Error,
};
struct Entry {
u64 app_id{};
char display_version[0x10]{};
NacpLanguageEntry lang{};
int image{};
bool selected{};
std::shared_ptr<NsApplicationControlData> control{};
u64 control_size{};
NacpLoadStatus status{NacpLoadStatus::None};
auto GetName() const -> const char* {
return lang.name;
}
auto GetAuthor() const -> const char* {
return lang.author;
}
auto GetDisplayVersion() const -> const char* {
return display_version;
}
};
struct ThreadResultData {
u64 id{};
std::shared_ptr<NsApplicationControlData> control{};
u64 control_size{};
char display_version[0x10]{};
NacpLanguageEntry lang{};
NacpLoadStatus status{NacpLoadStatus::None};
};
struct ThreadData {
ThreadData();
auto IsRunning() const -> bool;
void Run();
void Close();
void Push(u64 id);
void Push(std::span<const Entry> entries);
void Pop(std::vector<ThreadResultData>& out);
private:
UEvent m_uevent{};
Mutex m_mutex_id{};
Mutex m_mutex_result{};
// app_ids pushed to the queue, signal uevent when pushed.
std::vector<u64> m_ids{};
// control data pushed to the queue.
std::vector<ThreadResultData> m_result{};
std::atomic_bool m_running{};
};
enum SortType {
SortType_Updated,
};
enum OrderType {
OrderType_Descending,
OrderType_Ascending,
};
using LayoutType = grid::LayoutType;
struct Menu final : grid::Menu {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "Games"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
private:
void SetIndex(s64 index);
void ScanHomebrew();
void Sort();
void SortAndFindLastFile(bool scan);
void FreeEntries();
void OnLayoutChange();
auto GetSelectedEntries() const {
std::vector<Entry> out;
for (auto& e : m_entries) {
if (e.selected) {
out.emplace_back(e);
}
}
if (!m_entries.empty() && out.empty()) {
out.emplace_back(m_entries[m_index]);
}
return out;
}
void ClearSelection() {
for (auto& e : m_entries) {
e.selected = false;
}
m_selected_count = 0;
}
void DeleteGames();
void DumpGames(u32 flags);
private:
static constexpr inline const char* INI_SECTION = "games";
static constexpr inline const char* INI_SECTION_DUMP = "dump";
std::vector<Entry> m_entries{};
s64 m_index{}; // where i am in the array
s64 m_selected_count{};
std::unique_ptr<List> m_list{};
bool m_is_reversed{};
bool m_dirty{};
ThreadData m_thread_data{};
Thread m_thread{};
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Updated};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_GridDetail};
option::OptionBool m_hide_forwarders{INI_SECTION, "hide_forwarders", false};
};
} // namespace sphaira::ui::menu::game

View File

@@ -10,14 +10,16 @@
namespace sphaira::ui::menu::gc {
struct GcCollection : yati::container::CollectionEntry {
GcCollection(const char* _name, s64 _size, u8 _type) {
GcCollection(const char* _name, s64 _size, u8 _type, u8 _id_offset) {
name = _name;
size = _size;
type = _type;
id_offset = _id_offset;
}
// NcmContentType
u8 type{};
u8 id_offset{};
};
using GcCollections = std::vector<GcCollection>;
@@ -41,13 +43,16 @@ struct Menu final : MenuBase {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "GC"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
private:
Result GcMount();
void GcUnmount();
Result GcPoll(bool* inserted);
Result GcOnEvent();
Result UpdateStorageSize();
void FreeImage();
@@ -57,6 +62,8 @@ private:
FsDeviceOperator m_dev_op{};
FsGameCardHandle m_handle{};
std::unique_ptr<fs::FsNativeGameCard> m_fs{};
FsEventNotifier m_event_notifier{};
Event m_event{};
std::vector<ApplicationEntry> m_entries{};
std::unique_ptr<List> m_list{};

View File

@@ -45,6 +45,7 @@ struct Menu final : MenuBase {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "GitHub"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;

View File

@@ -0,0 +1,35 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "ui/scrolling_text.hpp"
#include "ui/list.hpp"
#include <string>
#include <memory>
namespace sphaira::ui::menu::grid {
enum LayoutType {
LayoutType_List,
LayoutType_Grid,
LayoutType_GridDetail,
};
struct Menu : MenuBase {
using MenuBase::MenuBase;
protected:
void OnLayoutChange(std::unique_ptr<List>& list, int layout);
void DrawEntry(NVGcontext* vg, Theme* theme, int layout, const Vec4& v, bool selected, int image, const char* name, const char* author, const char* version);
// same as above but doesn't draw image and returns image dimension.
Vec4 DrawEntryNoImage(NVGcontext* vg, Theme* theme, int layout, const Vec4& v, bool selected, const char* name, const char* author, const char* version);
private:
Vec4 DrawEntry(NVGcontext* vg, Theme* theme, bool draw_image, int layout, const Vec4& v, bool selected, int image, const char* name, const char* author, const char* version);
private:
ScrollingText m_scroll_name{};
ScrollingText m_scroll_author{};
ScrollingText m_scroll_version{};
};
} // namespace sphaira::ui::menu::grid

View File

@@ -1,6 +1,6 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "ui/menus/grid_menu_base.hpp"
#include "ui/list.hpp"
#include "nro.hpp"
#include "fs.hpp"
@@ -22,31 +22,37 @@ enum OrderType {
OrderType_Ascending,
};
struct Menu final : MenuBase {
using LayoutType = grid::LayoutType;
struct Menu final : grid::Menu {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "Apps"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
void SetIndex(s64 index);
void InstallHomebrew();
void ScanHomebrew();
void Sort();
void SortAndFindLastFile();
auto GetHomebrewList() const -> const std::vector<NroEntry>& {
return m_entries;
}
static Result InstallHomebrew(const fs::FsPath& path, const NacpStruct& nacp, const std::vector<u8>& icon);
static Result InstallHomebrewFromPath(const fs::FsPath& path);
private:
void SetIndex(s64 index);
void InstallHomebrew();
void ScanHomebrew();
void Sort();
void SortAndFindLastFile();
void FreeEntries();
void OnLayoutChange();
auto IsStarEnabled() -> bool {
return m_sort.Get() >= SortType_UpdatedStar;
}
static Result InstallHomebrew(const fs::FsPath& path, const NacpStruct& nacp, const std::vector<u8>& icon);
static Result InstallHomebrewFromPath(const fs::FsPath& path);
private:
static constexpr inline const char* INI_SECTION = "homebrew";
@@ -56,6 +62,7 @@ private:
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_AlphabeticalStar};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_GridDetail};
option::OptionBool m_hide_sphaira{INI_SECTION, "hide_sphaira", false};
};

View File

@@ -30,16 +30,19 @@ struct Menu final : MenuBase {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "IRS"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
private:
void PollCameraStatus(bool statup = false);
void LoadDefaultConfig();
void UpdateConfig(const IrsImageTransferProcessorExConfig* config);
void ResetImage();
void UpdateImage();
void updateColourArray();
auto GetEntryName(s64 i) -> std::string;
private:
Result m_init_rc{};

View File

@@ -3,7 +3,6 @@
#include "ui/widget.hpp"
#include "ui/menus/homebrew.hpp"
#include "ui/menus/filebrowser.hpp"
#include "ui/menus/appstore.hpp"
namespace sphaira::ui::menu::main {
@@ -18,6 +17,32 @@ enum class UpdateState {
Error,
};
using MiscMenuFunction = std::function<std::shared_ptr<ui::menu::MenuBase>(void)>;
enum MiscMenuFlag : u8 {
// can be set as the rightside menu.
MiscMenuFlag_Shortcut = 1 << 0,
// needs install option to be enabled.
MiscMenuFlag_Install = 1 << 1,
};
struct MiscMenuEntry {
const char* name;
const char* title;
MiscMenuFunction func;
u8 flag;
auto IsShortcut() const -> bool {
return flag & MiscMenuFlag_Shortcut;
}
auto IsInstall() const -> bool {
return flag & MiscMenuFlag_Install;
}
};
auto GetMiscMenuEntries() -> std::span<const MiscMenuEntry>;
// this holds 2 menus and allows for switching between them
struct MainMenu final : Widget {
MainMenu();
@@ -39,7 +64,7 @@ private:
private:
std::shared_ptr<homebrew::Menu> m_homebrew_menu{};
std::shared_ptr<filebrowser::Menu> m_filebrowser_menu{};
std::shared_ptr<appstore::Menu> m_app_store_menu{};
std::shared_ptr<MenuBase> m_right_side_menu{};
std::shared_ptr<MenuBase> m_current_menu{};
std::string m_update_url{};

View File

@@ -1,7 +1,6 @@
#pragma once
#include "ui/widget.hpp"
#include "nro.hpp"
#include <string>
namespace sphaira::ui::menu {
@@ -10,6 +9,7 @@ struct MenuBase : Widget {
MenuBase(std::string title);
virtual ~MenuBase();
virtual auto GetShortTitle() const -> const char* = 0;
virtual void Update(Controller* controller, TouchInfo* touch);
virtual void Draw(NVGcontext* vg, Theme* theme);
@@ -21,6 +21,10 @@ struct MenuBase : Widget {
void SetTitleSubHeading(std::string sub_heading);
void SetSubHeading(std::string sub_heading);
auto GetTitle() const {
return m_title;
}
private:
void UpdateVars();

View File

@@ -2,6 +2,7 @@
#include "ui/menus/menu_base.hpp"
#include "ui/scrollable_text.hpp"
#include "ui/scrolling_text.hpp"
#include "ui/list.hpp"
#include "option.hpp"
#include <span>
@@ -132,6 +133,7 @@ struct Menu final : MenuBase {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "Themezer"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
@@ -160,6 +162,9 @@ private:
s64 m_index{}; // where i am in the array
std::unique_ptr<List> m_list{};
ScrollingText m_scroll_name{};
ScrollingText m_scroll_author{};
// options
option::OptionLong m_sort{INI_SECTION, "sort", 0};
option::OptionLong m_order{INI_SECTION, "order", 0};

View File

@@ -8,8 +8,10 @@ namespace sphaira::ui::menu::usb {
enum class State {
// not connected.
None,
// just connected, waiting for file list.
Connected_WaitForFileList,
// just connected, starts the transfer.
Connected,
Connected_StartingTransfer,
// set whilst transfer is in progress.
Progress,
// set when the transfer is finished.
@@ -22,6 +24,7 @@ struct Menu final : MenuBase {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "USB"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
@@ -35,9 +38,8 @@ struct Menu final : MenuBase {
Mutex m_mutex{};
// the below are shared across threads, lock with the above mutex!
State m_state{State::None};
std::vector<std::string> m_names{};
bool m_usb_has_connection{};
u32 m_usb_speed{};
u32 m_usb_count{};
};
} // namespace sphaira::ui::menu::usb

View File

@@ -2,6 +2,7 @@
#include "nanovg.h"
#include "ui/types.hpp"
#include "ui/scrolling_text.hpp"
namespace sphaira::ui::gfx {
@@ -18,6 +19,9 @@ void drawRect(NVGcontext*, const Vec4& v, const NVGpaint& p, float rounding = 0.
void drawRectOutline(NVGcontext*, const Theme*, float size, float x, float y, float w, float h);
void drawRectOutline(NVGcontext*, const Theme*, float size, const Vec4& v);
void drawTriangle(NVGcontext*, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor& c);
void drawTriangle(NVGcontext*, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p);
void drawText(NVGcontext*, float x, float y, float size, const char* str, const char* end, int align, const NVGcolor& c);
void drawText(NVGcontext*, float x, float y, float size, const NVGcolor& c, const char* str, int align = NVG_ALIGN_LEFT | NVG_ALIGN_TOP, const char* end = nullptr);
void drawText(NVGcontext*, const Vec2& v, float size, const char* str, const char* end, int align, const NVGcolor& c);
@@ -35,6 +39,8 @@ void drawScrollbar(NVGcontext*, const Theme*, float x, float y, float h, u32 ind
void drawScrollbar2(NVGcontext*, const Theme*, float x, float y, float h, s64 index_off, s64 count, s64 row, s64 page);
void drawScrollbar2(NVGcontext*, const Theme*, s64 index_off, s64 count, s64 row, s64 page);
void drawAppLable(NVGcontext* vg, const Theme*, ScrollingText& st, float x, float y, float w, const char* name);
void updateHighlightAnimation();
void getHighlightAnimation(float* gradientX, float* gradientY, float* color);

View File

@@ -32,11 +32,9 @@ public:
using Options = std::vector<Option>;
public:
OptionBox(const std::string& message, const Option& a, Callback cb = [](auto){}); // confirm
OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb); // yesno
OptionBox(const std::string& message, const Option& a, const Option& b, s64 index, Callback cb); // yesno
OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, Callback cb); // tri
OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, s64 index, Callback cb); // tri
OptionBox(const std::string& message, const Option& a, Callback cb = [](auto){}, int image = 0); // confirm
OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb, int image = 0); // yesno
OptionBox(const std::string& message, const Option& a, const Option& b, s64 index, Callback cb, int image = 0); // yesno
auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
@@ -50,6 +48,7 @@ private:
private:
std::string m_message{};
Callback m_callback{};
int m_image{};
Vec4 m_spacer_line{};

View File

@@ -1,6 +1,7 @@
#pragma once
#include "ui/widget.hpp"
#include "ui/scrolling_text.hpp"
#include "ui/list.hpp"
#include <optional>
@@ -36,8 +37,10 @@ private:
Items m_items{};
Callback m_callback{};
s64 m_index{}; // index in list array
s64 m_starting_index{};
std::unique_ptr<List> m_list{};
ScrollingText m_scroll_text{};
float m_yoff{};
float m_line_top{};

View File

@@ -17,7 +17,7 @@ struct ProgressBox final : Widget {
const std::string& action,
const std::string& title,
ProgressBoxCallback callback, ProgressBoxDoneCallback done = [](bool success){},
int cpuid = 1, int prio = 0x2C, int stack_size = 1024*1024
int cpuid = 1, int prio = 0x2C, int stack_size = 1024*128
);
~ProgressBox();
@@ -38,11 +38,17 @@ struct ProgressBox final : Widget {
void Yield();
auto OnDownloadProgressCallback() {
return [this](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
return [this](s64 dltotal, s64 dlnow, s64 ultotal, s64 ulnow){
if (this->ShouldExit()) {
return false;
}
this->UpdateTransfer(dlnow, dltotal);
if (dltotal) {
this->UpdateTransfer(dlnow, dltotal);
} else {
this->UpdateTransfer(ulnow, ultotal);
}
return true;
};
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include "ui/widget.hpp"
#include <string>
namespace sphaira::ui {
struct ScrollingText final {
public:
void Draw(NVGcontext*, bool focus, float x, float y, float w, float size, int align, const NVGcolor& colour, const std::string& text_entry);
void DrawArgs(NVGcontext*, bool focus, float x, float y, float w, float size, int align, const NVGcolor& colour, const char* s, ...) __attribute__ ((format (printf, 10, 11)));
void Reset(const std::string& text_entry = "");
private:
std::string m_str;
s64 m_tick;
float m_text_xoff;
};
} // namespace sphaira::ui

View File

@@ -14,7 +14,7 @@ namespace sphaira {
#define SCREEN_WIDTH 1280.f
#define SCREEN_HEIGHT 720.f
struct [[nodiscard]] Vec2 {
struct Vec2 {
constexpr Vec2() = default;
constexpr Vec2(float _x, float _y) : x{_x}, y{_y} {}
@@ -53,7 +53,7 @@ struct [[nodiscard]] Vec2 {
float x{}, y{};
};
struct [[nodiscard]] Vec4 {
struct Vec4 {
constexpr Vec4() = default;
constexpr Vec4(float _x, float _y, float _w, float _h) : x{_x}, y{_y}, w{_w}, h{_h} {}
constexpr Vec4(Vec2 vec0, Vec2 vec1) : x{vec0.x}, y{vec0.y}, w{vec1.x}, h{vec1.y} {}
@@ -226,7 +226,6 @@ struct ThemeMeta {
struct Theme {
ThemeMeta meta;
PLSR_BFSTM music;
ElementEntry elements[ThemeEntryID_MAX];
auto GetColour(ThemeEntryID id) const {

View File

@@ -0,0 +1,101 @@
#pragma once
#include <vector>
#include <string>
#include <new>
#include <switch.h>
namespace sphaira::usb {
struct Base {
enum { USBModule = 523 };
enum : Result {
Result_Cancelled = MAKERESULT(USBModule, 100),
};
Base(u64 transfer_timeout);
// sets up usb.
virtual Result Init() = 0;
// returns 0 if usb is connected to a device.
virtual Result IsUsbConnected(u64 timeout) = 0;
// transfers a chunk of data, check out_size_transferred for how much was transferred.
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout);
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred) {
return TransferPacketImpl(read, page, size, out_size_transferred, m_transfer_timeout);
}
// transfers all data.
Result TransferAll(bool read, void *data, u32 size, u64 timeout);
Result TransferAll(bool read, void *data, u32 size) {
return TransferAll(read, data, size, m_transfer_timeout);
}
// returns the cancel event.
auto GetCancelEvent() {
return &m_uevent;
}
// cancels an in progress transfer.
void Cancel() {
ueventSignal(GetCancelEvent());
}
auto& GetTransferBuffer() {
return m_aligned;
}
auto GetTransferTimeout() const {
return m_transfer_timeout;
}
public:
// custom allocator for std::vector that respects alignment.
// https://en.cppreference.com/w/cpp/named_req/Allocator
template <typename T, std::size_t Align>
struct CustomVectorAllocator {
public:
// https://en.cppreference.com/w/cpp/memory/new/operator_new
auto allocate(std::size_t n) -> T* {
n = (n + (Align - 1)) &~ (Align - 1);
return new(align) T[n];
}
// https://en.cppreference.com/w/cpp/memory/new/operator_delete
auto deallocate(T* p, std::size_t n) noexcept -> void {
// ::operator delete[] (p, n, align);
::operator delete[] (p, align);
}
private:
static constexpr inline std::align_val_t align{Align};
};
template <typename T>
struct PageAllocator : CustomVectorAllocator<T, 0x1000> {
using value_type = T; // used by std::vector
};
using PageAlignedVector = std::vector<u8, PageAllocator<u8>>;
protected:
enum UsbSessionEndpoint {
UsbSessionEndpoint_In = 0,
UsbSessionEndpoint_Out = 1,
};
virtual Event *GetCompletionEvent(UsbSessionEndpoint ep) = 0;
virtual Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) = 0;
virtual Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_xfer_id) = 0;
virtual Result GetTransferResult(UsbSessionEndpoint ep, u32 xfer_id, u32 *out_requested_size, u32 *out_transferred_size) = 0;
private:
u64 m_transfer_timeout{};
UEvent m_uevent{};
PageAlignedVector m_aligned{};
};
} // namespace sphaira::usb

View File

@@ -0,0 +1,47 @@
#pragma once
#include <switch.h>
namespace sphaira::usb::tinfoil {
enum Magic : u32 {
Magic_List0 = 0x304C5554, // TUL0 (Tinfoil Usb List 0)
Magic_Command0 = 0x30435554, // TUC0 (Tinfoil USB Command 0)
};
enum USBCmdType : u8 {
REQUEST = 0,
RESPONSE = 1
};
enum USBCmdId : u32 {
EXIT = 0,
FILE_RANGE = 1
};
struct TUSHeader {
u32 magic; // TUL0 (Tinfoil Usb List 0)
u32 nspListSize;
u64 padding;
};
struct NX_PACKED USBCmdHeader {
u32 magic; // TUC0 (Tinfoil USB Command 0)
USBCmdType type;
u8 padding[0x3];
u32 cmdId;
u64 dataSize;
u8 reserved[0xC];
};
struct FileRangeCmdHeader {
u64 size;
u64 offset;
u64 nspNameLen;
u64 padding;
};
static_assert(sizeof(TUSHeader) == 0x10, "TUSHeader must be 0x10!");
static_assert(sizeof(USBCmdHeader) == 0x20, "USBCmdHeader must be 0x20!");
} // namespace sphaira::usb::tinfoil

View File

@@ -0,0 +1,47 @@
#pragma once
#include "usb/usbhs.hpp"
#include <string>
#include <memory>
#include <span>
#include <switch.h>
namespace sphaira::usb::upload {
struct Usb {
enum { USBModule = 523 };
enum : Result {
Result_BadMagic = MAKERESULT(USBModule, 0),
Result_Exit = MAKERESULT(USBModule, 1),
Result_BadCount = MAKERESULT(USBModule, 2),
Result_BadTransferSize = MAKERESULT(USBModule, 3),
Result_BadTotalSize = MAKERESULT(USBModule, 4),
Result_BadCommand = MAKERESULT(USBModule, 4),
};
Usb(u64 transfer_timeout);
virtual ~Usb();
virtual Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) = 0;
Result IsUsbConnected(u64 timeout) {
return m_usb->IsUsbConnected(timeout);
}
// waits for connection and then sends file list.
Result WaitForConnection(u64 timeout, std::span<const std::string> names);
// polls for command, executes transfer if possible.
// will return Result_Exit if exit command is recieved.
Result PollCommands();
private:
Result FileRangeCmd(u64 data_size);
private:
std::unique_ptr<usb::UsbHs> m_usb;
};
} // namespace sphaira::usb::upload

View File

@@ -0,0 +1,27 @@
#pragma once
#include "base.hpp"
namespace sphaira::usb {
// Device Host
struct UsbDs final : Base {
using Base::Base;
~UsbDs();
Result Init() override;
Result IsUsbConnected(u64 timeout) override;
Result GetSpeed(UsbDeviceSpeed* out);
private:
Event *GetCompletionEvent(UsbSessionEndpoint ep) override;
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) override;
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) override;
Result GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) override;
private:
UsbDsInterface* m_interface{};
UsbDsEndpoint* m_endpoints[2]{};
};
} // namespace sphaira::usb

View File

@@ -0,0 +1,33 @@
#pragma once
#include "base.hpp"
namespace sphaira::usb {
struct UsbHs final : Base {
UsbHs(u8 index, const UsbHsInterfaceFilter& filter, u64 transfer_timeout);
~UsbHs();
Result Init() override;
Result IsUsbConnected(u64 timeout) override;
private:
Event *GetCompletionEvent(UsbSessionEndpoint ep) override;
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) override;
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_xfer_id) override;
Result GetTransferResult(UsbSessionEndpoint ep, u32 xfer_id, u32 *out_requested_size, u32 *out_transferred_size) override;
Result Connect();
void Close();
private:
u8 m_index{};
UsbHsInterfaceFilter m_filter{};
UsbHsInterface m_interface{};
UsbHsClientIfSession m_s{};
UsbHsClientEpSession m_endpoints[2]{};
Event m_event{};
bool m_connected{};
};
} // namespace sphaira::usb

View File

@@ -1,13 +0,0 @@
#pragma once
#include <switch.h>
#include <string>
namespace sphaira {
// if show_error = true, it will display popup error box on
// faliure. set this to false if you want to handle errors
// from the caller.
auto WebShow(const std::string& url, bool show_error = true) -> Result;
} // namespace sphaira

View File

@@ -2,12 +2,16 @@
#include "base.hpp"
#include <switch.h>
#include <span>
namespace sphaira::yati::container {
struct Nsp final : Base {
using Base::Base;
Result GetCollections(Collections& out) override;
// builds nsp meta data and the size of the entier nsp.
static auto Build(std::span<CollectionEntry> collections, s64& size) -> std::vector<u8>;
};
} // namespace sphaira::yati::container

View File

@@ -69,7 +69,25 @@ struct EticketRsaDeviceKey {
static_assert(sizeof(EticketRsaDeviceKey) == 0x240);
// es functions.
Result ImportTicket(Service* srv, const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size);
Result Initialize();
void Exit();
Service* GetServiceSession();
// todo: find the ipc that gets personalised tickets.
// todo: if ipc doesn't exist, manually parse es personalised save.
// todo: add personalised -> common ticket conversion.
// todo: make the above an option for both dump and install.
Result ImportTicket(const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size);
Result CountCommonTicket(s32* count);
Result CountPersonalizedTicket(s32* count);
Result ListCommonTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count);
Result ListPersonalizedTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count);
Result ListMissingPersonalizedTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count); // untested
Result GetCommonTicketSize(u64 *size_out, const FsRightsId* rightsId);
Result GetCommonTicketData(u64 *size_out, void *tik_data, u64 tik_size, const FsRightsId* rightsId);
Result GetCommonTicketAndCertificateSize(u64 *tik_size_out, u64 *cert_size_out, const FsRightsId* rightsId); // [4.0.0+]
Result GetCommonTicketAndCertificateData(u64 *tik_size_out, u64 *cert_size_out, void* tik_buf, u64 tik_size, void* cert_buf, u64 cert_size, const FsRightsId* rightsId); // [4.0.0+]
// ticket functions.
Result GetTicketDataOffset(std::span<const u8> ticket, u64& out);

View File

@@ -1,7 +1,10 @@
#pragma once
#include <switch.h>
#include "fs.hpp"
#include "keys.hpp"
#include "ncm.hpp"
#include <switch.h>
#include <vector>
namespace sphaira::nca {
@@ -41,6 +44,7 @@ enum KeyGeneration {
KeyGeneration_1700 = 0x11,
KeyGeneration_1800 = 0x12,
KeyGeneration_1900 = 0x13,
KeyGeneration_2000 = 0x14,
KeyGeneration_Invalid = 0xFF,
};
@@ -175,7 +179,7 @@ struct Header {
u8 old_key_gen; // see KeyGenerationOld.
u8 kaek_index; // see KeyAreaEncryptionKeyIndex.
u64 size;
u64 title_id;
u64 program_id;
u32 context_id;
u32 sdk_version;
u8 key_gen; // see KeyGeneration.
@@ -215,6 +219,10 @@ Result DecryptKeak(const keys::Keys& keys, Header& header);
Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation);
Result VerifyFixedKey(const Header& header);
// helpers that parse an nca.
Result ParseCnmt(const fs::FsPath& path, u64 program_id, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos);
Result ParseControl(const fs::FsPath& path, u64 program_id, void* nacp_out = nullptr, s64 nacp_size = 0, std::vector<u8>* icon_out = nullptr);
auto GetKeyGenStr(u8 key_gen) -> const char*;
} // namespace sphaira::nca

View File

@@ -31,10 +31,16 @@ union ExtendedHeader {
NcmDataPatchMetaExtendedHeader data_patch;
};
auto GetMetaTypeStr(u8 meta_type) -> const char*;
auto GetStorageIdStr(u8 storage_id) -> const char*;
auto GetMetaTypeShortStr(u8 meta_type) -> const char*;
auto GetAppId(u8 meta_type, u64 id) -> u64;
auto GetAppId(const NcmContentMetaKey& key) -> u64;
auto GetAppId(const PackagedContentMeta& meta) -> u64;
auto GetContentIdFromStr(const char* str) -> NcmContentId;
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id);
Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id);

View File

@@ -0,0 +1,56 @@
#pragma once
#include <switch/types.h>
#include <switch/result.h>
#include <switch/kernel/mutex.h>
#include <switch/sf/service.h>
#include <switch/services/sm.h>
typedef struct ServiceGuard {
Mutex mutex;
u32 refCount;
} ServiceGuard;
NX_INLINE bool serviceGuardBeginInit(ServiceGuard* g)
{
mutexLock(&g->mutex);
return (g->refCount++) == 0;
}
NX_INLINE Result serviceGuardEndInit(ServiceGuard* g, Result rc, void (*cleanupFunc)(void))
{
if (R_FAILED(rc)) {
cleanupFunc();
--g->refCount;
}
mutexUnlock(&g->mutex);
return rc;
}
NX_INLINE void serviceGuardExit(ServiceGuard* g, void (*cleanupFunc)(void))
{
mutexLock(&g->mutex);
if (g->refCount && (--g->refCount) == 0)
cleanupFunc();
mutexUnlock(&g->mutex);
}
#define NX_GENERATE_SERVICE_GUARD_PARAMS(name, _paramdecl, _parampass) \
\
static ServiceGuard g_##name##Guard; \
NX_INLINE Result _##name##Initialize _paramdecl; \
static void _##name##Cleanup(void); \
\
Result name##Initialize _paramdecl \
{ \
Result rc = 0; \
if (serviceGuardBeginInit(&g_##name##Guard)) \
rc = _##name##Initialize _parampass; \
return serviceGuardEndInit(&g_##name##Guard, rc, _##name##Cleanup); \
} \
\
void name##Exit(void) \
{ \
serviceGuardExit(&g_##name##Guard, _##name##Cleanup); \
}
#define NX_GENERATE_SERVICE_GUARD(name) NX_GENERATE_SERVICE_GUARD_PARAMS(name, (void), ())

View File

@@ -14,6 +14,10 @@ struct Base {
return false;
}
virtual void SignalCancel() {
}
Result GetOpenResult() const {
return m_open_result;
}

View File

@@ -2,6 +2,10 @@
#include "base.hpp"
#include "fs.hpp"
#include "usb/usbds.hpp"
#include <string>
#include <memory>
#include <switch.h>
namespace sphaira::yati::source {
@@ -21,32 +25,26 @@ struct Usb final : Base {
~Usb();
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
Result Finished() const;
Result Finished(u64 timeout);
Result Init();
Result WaitForConnection(u64 timeout, u32& speed, u32& count);
Result GetFileInfo(std::string& name_out, u64& size_out);
Result IsUsbConnected(u64 timeout) {
return m_usb->IsUsbConnected(timeout);
}
Result WaitForConnection(u64 timeout, std::vector<std::string>& out_names);
void SetFileNameForTranfser(const std::string& name);
void SignalCancel() override {
m_usb->Cancel();
}
private:
enum UsbSessionEndpoint {
UsbSessionEndpoint_In = 0,
UsbSessionEndpoint_Out = 1,
};
Result SendCommand(s64 off, s64 size) const;
Result InternalRead(void* buf, s64 off, s64 size) const;
bool GetConfigured() const;
Event *GetCompletionEvent(UsbSessionEndpoint ep) const;
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) const;
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) const;
Result GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) const;
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) const;
Result SendCmdHeader(u32 cmdId, size_t dataSize, u64 timeout);
Result SendFileRangeCmd(u64 offset, u64 size, u64 timeout);
private:
UsbDsInterface* m_interface{};
UsbDsEndpoint* m_endpoints[2]{};
u64 m_transfer_timeout{};
std::unique_ptr<usb::UsbDs> m_usb;
std::string m_transfer_file_name{};
};
} // namespace sphaira::yati::source

View File

@@ -10,7 +10,6 @@
#include "fs.hpp"
#include "source/base.hpp"
#include "container/base.hpp"
#include "nx/ncm.hpp"
#include "ui/progress_box.hpp"
#include <memory>
#include <optional>
@@ -71,6 +70,10 @@ enum : Result {
struct Config {
bool sd_card_install{};
// sets the performance mode to FastLoad which boosts the CPU clock
// and lowers the GPU clock.
bool boost_mode{};
// enables downgrading patch / data patch (dlc) version.
bool allow_downgrade{};
@@ -132,7 +135,4 @@ Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> so
Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr<container::Base> container, const ConfigOverride& override = {});
Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const container::Collections& collections, const ConfigOverride& override = {});
Result ParseCnmtNca(const fs::FsPath& path, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos);
Result ParseControlNca(const fs::FsPath& path, u64 id, void* nacp_out = nullptr, s64 nacp_size = 0, std::vector<u8>* icon_out = nullptr);
} // namespace sphaira::yati

View File

@@ -1,5 +1,4 @@
#include "ui/option_box.hpp"
#include "ui/bubbles.hpp"
#include "ui/sidebar.hpp"
#include "ui/popup_list.hpp"
#include "ui/option_box.hpp"
@@ -7,12 +6,6 @@
#include "ui/error_box.hpp"
#include "ui/menus/main_menu.hpp"
#include "ui/menus/irs_menu.hpp"
#include "ui/menus/themezer.hpp"
#include "ui/menus/ghdl.hpp"
#include "ui/menus/usb_menu.hpp"
#include "ui/menus/ftp_menu.hpp"
#include "ui/menus/gc_menu.hpp"
#include "app.hpp"
#include "log.hpp"
@@ -26,7 +19,6 @@
#include "defines.hpp"
#include "i18n.hpp"
#include "ftpsrv_helper.hpp"
#include "web.hpp"
#include <nanovg_dk.h>
#include <minIni.h>
@@ -46,8 +38,33 @@ extern "C" {
namespace sphaira {
namespace {
constexpr fs::FsPath DEFAULT_MUSIC_PATH = "/config/sphaira/themes/default_music.bfstm";
constexpr const char* DEFAULT_MUSIC_URL = "https://files.catbox.moe/1ovji1.bfstm";
// constexpr const char* DEFAULT_MUSIC_URL = "https://raw.githubusercontent.com/ITotalJustice/sphaira/refs/heads/master/assets/default_music.bfstm";
void download_default_music() {
App::Push(std::make_shared<ui::ProgressBox>(0, "Downloading "_i18n, "default_music.bfstm", [](auto pbox){
const auto result = curl::Api().ToFile(
curl::Url{DEFAULT_MUSIC_URL},
curl::Path{DEFAULT_MUSIC_PATH},
curl::OnProgress{pbox->OnDownloadProgressCallback()}
);
return result.success;
}, [](bool success){
if (success) {
App::Notify("Downloaded "_i18n + "default_music.bfstm");
App::SetTheme(App::GetThemeIndex());
} else {
App::Push(std::make_shared<ui::ErrorBox>(
"Failed to download default_music.bfstm, please try again"_i18n
));
}
}));
}
struct ThemeData {
fs::FsPath music_path{"/config/sphaira/themes/default_music.bfstm"};
fs::FsPath music_path{DEFAULT_MUSIC_PATH};
std::string elements[ThemeEntryID_MAX]{};
};
@@ -283,30 +300,33 @@ auto GetNroIcon(const std::vector<u8>& nro_icon) -> std::vector<u8> {
auto LoadThemeMeta(const fs::FsPath& path, ThemeMeta& meta) -> bool {
meta = {};
char buf[FS_MAX_PATH]{};
int len{};
len = ini_gets("meta", "name", "", buf, sizeof(buf) - 1, path);
if (len <= 1) {
auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
auto meta = static_cast<ThemeMeta*>(UserData);
if (!std::strcmp(Section, "meta")) {
if (!std::strcmp(Key, "name")) {
meta->name = Value;
} else if (!std::strcmp(Key, "author")) {
meta->author = Value;
} else if (!std::strcmp(Key, "version")) {
meta->version = Value;
} else if (!std::strcmp(Key, "inherit")) {
meta->inherit = Value;
}
return 1;
}
return 0;
};
if (!ini_browse(cb, &meta, path)) {
return false;
}
meta.name = buf;
len = ini_gets("meta", "author", "", buf, sizeof(buf) - 1, path);
if (len <= 1) {
if (meta.name.empty() || meta.author.empty() || meta.version.empty()) {
return false;
}
meta.author = buf;
len = ini_gets("meta", "version", "", buf, sizeof(buf) - 1, path);
if (len <= 1) {
return false;
}
meta.version = buf;
len = ini_gets("meta", "inherit", "", buf, sizeof(buf) - 1, path);
if (len > 1) {
meta.inherit = buf;
}
log_write("loaded meta from: %s\n", path.s);
meta.ini_path = path;
@@ -339,7 +359,7 @@ void LoadThemeInternal(ThemeMeta meta, ThemeData& theme_data, int inherit_level
}
}
static constexpr auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
auto theme_data = static_cast<ThemeData*>(UserData);
if (!std::strcmp(Section, "theme")) {
@@ -581,7 +601,19 @@ auto App::GetReplaceHbmenuEnable() -> bool {
}
auto App::GetInstallEnable() -> bool {
return g_app->m_install.Get();
if (IsEmunand()) {
return GetInstallEmummcEnable();
} else {
return GetInstallSysmmcEnable();
}
}
auto App::GetInstallSysmmcEnable() -> bool {
return g_app->m_install_sysmmc.GetOr("install");
}
auto App::GetInstallEmummcEnable() -> bool {
return g_app->m_install_emummc.GetOr("install");
}
auto App::GetInstallSdEnable() -> bool {
@@ -687,7 +719,7 @@ void App::SetReplaceHbmenuEnable(bool enable) {
}
if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
if (std::strcmp(sphaira_nacp.display_version, hbmenu_nacp.display_version) < 0) {
if (IsVersionNewer(sphaira_nacp.display_version, hbmenu_nacp.display_version)) {
if (R_FAILED(rc = fs.copy_entire_file(sphaira_path, "/hbmenu.nro"))) {
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path.s, rc, R_MODULE(rc), R_DESCRIPTION(rc));
} else {
@@ -737,8 +769,12 @@ void App::SetReplaceHbmenuEnable(bool enable) {
}
}
void App::SetInstallEnable(bool enable) {
g_app->m_install.Set(enable);
void App::SetInstallSysmmcEnable(bool enable) {
g_app->m_install_sysmmc.Set(enable);
}
void App::SetInstallEmummcEnable(bool enable) {
g_app->m_install_emummc.Set(enable);
}
void App::SetInstallSdEnable(bool enable) {
@@ -762,7 +798,7 @@ void App::SetMtpEnable(bool enable) {
if (App::GetMtpEnable() != enable) {
g_app->m_mtp_enabled.Set(enable);
if (enable) {
hazeInitialize(haze_callback);
hazeInitialize(haze_callback, 0x2C, 2);
} else {
hazeExit();
}
@@ -1013,7 +1049,6 @@ void App::Draw() {
}
m_notif_manager.Draw(vg, &m_theme);
ui::bubble::Draw(vg, &m_theme);
nvgResetTransform(vg);
nvgEndFrame(this->vg);
@@ -1116,7 +1151,6 @@ void App::CloseTheme() {
if (m_sound_ids[SoundEffect_Music]) {
plsrPlayerFree(m_sound_ids[SoundEffect_Music]);
m_sound_ids[SoundEffect_Music] = nullptr;
plsrBFSTMClose(&m_theme.music);
}
for (auto& e : m_theme.elements) {
@@ -1146,10 +1180,12 @@ void App::LoadTheme(const ThemeMeta& meta) {
// load music
if (!theme_data.music_path.empty()) {
if (R_SUCCEEDED(plsrBFSTMOpen(theme_data.music_path, &m_theme.music))) {
if (R_SUCCEEDED(plsrPlayerLoadStream(&m_theme.music, &m_sound_ids[SoundEffect_Music]))) {
PLSR_BFSTM music_stream;
if (R_SUCCEEDED(plsrBFSTMOpen(theme_data.music_path, &music_stream))) {
if (R_SUCCEEDED(plsrPlayerLoadStream(&music_stream, &m_sound_ids[SoundEffect_Music]))) {
PlaySoundEffect(SoundEffect_Music);
}
plsrBFSTMClose(&music_stream);
}
}
}
@@ -1193,11 +1229,14 @@ void App::ScanThemeEntries() {
ScanThemes("romfs:/themes/");
romfsExit();
}
// then load custom entries
ScanThemes("/config/sphaira/themes/");
}
App::App(const char* argv0) {
TimeStamp ts;
g_app = this;
m_start_timestamp = armGetSystemTick();
if (!std::strncmp(argv0, "sdmc:/", 6)) {
@@ -1221,10 +1260,11 @@ App::App(const char* argv0) {
if (App::GetLogEnable()) {
log_file_init();
log_write("hello world\n");
App::Notify("Warning! Logs are enabled, Sphaira will run slowly!"_i18n);
}
if (App::GetMtpEnable()) {
hazeInitialize(haze_callback);
hazeInitialize(haze_callback, 0x2C, 2);
}
if (App::GetFtpEnable()) {
@@ -1312,16 +1352,21 @@ App::App(const char* argv0) {
if (R_SUCCEEDED(plsrBFSAROpen("qlaunch:/sound/qlaunch.bfsar", &qlaunch_bfsar))) {
ON_SCOPE_EXIT(plsrBFSARClose(&qlaunch_bfsar));
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconFocus", &m_sound_ids[SoundEffect_Focus]);
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconScroll", &m_sound_ids[SoundEffect_Scroll]);
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconLimit", &m_sound_ids[SoundEffect_Limit]);
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeStartupMenu_game", &m_sound_ids[SoundEffect_Startup]);
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconAdd", &m_sound_ids[SoundEffect_Install]);
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeInsertError", &m_sound_ids[SoundEffect_Error]);
const auto load_sound = [&](const char* name, u32 id) {
if (R_FAILED(plsrPlayerLoadSoundByName(&qlaunch_bfsar, name, &m_sound_ids[id]))) {
log_write("[PLSR] failed to load sound effect: %s\n", name);
}
};
load_sound("SeGameIconFocus", SoundEffect_Focus);
load_sound("SeGameIconScroll", SoundEffect_Scroll);
load_sound("SeGameIconLimit", SoundEffect_Limit);
load_sound("StartupMenu_Game", SoundEffect_Startup);
load_sound("SeGameIconAdd", SoundEffect_Install);
load_sound("SeInsertError", SoundEffect_Error);
plsrPlayerSetVolume(m_sound_ids[SoundEffect_Limit], 2.0f);
plsrPlayerSetVolume(m_sound_ids[SoundEffect_Focus], 0.5f);
PlaySoundEffect(SoundEffect_Startup);
}
} else {
log_write("failed to mount romfs 0x0100000000001000\n");
@@ -1362,6 +1407,10 @@ App::App(const char* argv0) {
// padInitializeDefault(&m_pad);
padInitializeAny(&m_pad);
// usbHsFsSetFileSystemMountFlags(UsbHsFsMountFlags_ReadOnly);
// usbHsFsSetPopulateCallback();
// usbHsFsInitialize(0);
m_prev_timestamp = ini_getl("paths", "timestamp", 0, App::CONFIG_PATH);
const auto last_launch_path_size = ini_gets("paths", "last_launch_path", "", m_prev_last_launch, sizeof(m_prev_last_launch), App::CONFIG_PATH);
fs::FsPath last_launch_path;
@@ -1395,36 +1444,8 @@ App::App(const char* argv0) {
}
}
struct EventDay {
u8 day;
u8 month;
};
static constexpr EventDay event_days[] = {
{ .day = 1, .month = 1 }, // New years
{ .day = 3, .month = 3 }, // March 3 (switch 1)
{ .day = 10, .month = 5 }, // June 10 (switch 2)
{ .day = 15, .month = 5 }, // June 15
{ .day = 25, .month = 12 }, // Christmas
{ .day = 26, .month = 12 },
{ .day = 27, .month = 12 },
{ .day = 28, .month = 12 },
};
const auto time = std::time(nullptr);
const auto tm = std::localtime(&time);
for (auto e : event_days) {
if (e.day == tm->tm_mday && e.month == (tm->tm_mon + 1)) {
ui::bubble::Init();
break;
}
}
App::Push(std::make_shared<ui::menu::main::MainMenu>());
log_write("finished app constructor\n");
log_write("finished app constructor, time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
}
void App::PlaySoundEffect(SoundEffect effect) {
@@ -1450,17 +1471,34 @@ void App::DisplayThemeOptions(bool left_side) {
auto options = std::make_shared<ui::Sidebar>("Theme Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<ui::SidebarEntryArray>("Select Theme"_i18n, theme_items, [theme_items](s64& index_out){
options->Add(std::make_shared<ui::SidebarEntryArray>("Select Theme"_i18n, theme_items, [](s64& index_out){
App::SetTheme(index_out);
}, App::GetThemeIndex()));
options->Add(std::make_shared<ui::SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [](bool& enable){
App::SetThemeMusicEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [](bool& enable){
App::Set12HourTimeEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<ui::SidebarEntryCallback>("Download Default Music"_i18n, [](){
// check if we already have music
if (fs::FileExists(DEFAULT_MUSIC_PATH)) {
App::Push(std::make_shared<ui::OptionBox>(
"Overwrite current default music?"_i18n,
"No"_i18n, "Yes"_i18n, 0, [](auto op_index){
if (op_index && *op_index) {
download_default_music();
}
}
));
} else {
download_default_music();
}
}));
}
void App::DisplayNetworkOptions(bool left_side) {
@@ -1471,37 +1509,17 @@ void App::DisplayMiscOptions(bool left_side) {
auto options = std::make_shared<ui::Sidebar>("Misc Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<ui::SidebarEntryCallback>("Themezer"_i18n, [](){
App::Push(std::make_shared<ui::menu::themezer::Menu>());
}));
options->Add(std::make_shared<ui::SidebarEntryCallback>("GitHub"_i18n, [](){
App::Push(std::make_shared<ui::menu::gh::Menu>());
}));
options->Add(std::make_shared<ui::SidebarEntryCallback>("Irs"_i18n, [](){
App::Push(std::make_shared<ui::menu::irs::Menu>());
}));
if (App::IsApplication()) {
options->Add(std::make_shared<ui::SidebarEntryCallback>("Web"_i18n, [](){
WebShow("https://lite.duckduckgo.com/lite");
}));
}
if (App::GetApp()->m_install.Get()) {
if (App::GetFtpEnable()) {
options->Add(std::make_shared<ui::SidebarEntryCallback>("Ftp Install"_i18n, [](){
App::Push(std::make_shared<ui::menu::ftp::Menu>());
}));
for (auto& e : ui::menu::main::GetMiscMenuEntries()) {
if (e.name == g_app->m_right_side_menu.Get()) {
continue;
}
options->Add(std::make_shared<ui::SidebarEntryCallback>("Usb Install"_i18n, [](){
App::Push(std::make_shared<ui::menu::usb::Menu>());
}));
if (e.IsInstall() && !App::GetInstallEnable()) {
continue;
}
options->Add(std::make_shared<ui::SidebarEntryCallback>("GameCard Install"_i18n, [](){
App::Push(std::make_shared<ui::menu::gc::Menu>());
options->Add(std::make_shared<ui::SidebarEntryCallback>(i18n::get(e.title), [e](){
App::Push(e.func());
}));
}
}
@@ -1515,17 +1533,52 @@ void App::DisplayAdvancedOptions(bool left_side) {
text_scroll_speed_items.push_back("Normal"_i18n);
text_scroll_speed_items.push_back("Fast"_i18n);
std::vector<const char*> menu_names;
for (auto& e : ui::menu::main::GetMiscMenuEntries()) {
if (!e.IsShortcut()) {
continue;
}
if (e.IsInstall() && !App::GetInstallEnable()) {
continue;
}
menu_names.emplace_back(e.name);
}
ui::SidebarEntryArray::Items right_side_menu_items;
for (auto& str : menu_names) {
right_side_menu_items.push_back(i18n::get(str));
}
const auto it = std::find(menu_names.cbegin(), menu_names.cend(), g_app->m_right_side_menu.Get());
if (it == menu_names.cend()) {
g_app->m_right_side_menu.Set(menu_names[0]);
}
options->Add(std::make_shared<ui::SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [](bool& enable){
App::SetLogEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [](bool& enable){
App::SetReplaceHbmenuEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<ui::SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [](s64& index_out){
App::SetTextScrollSpeed(index_out);
}, (s64)App::GetTextScrollSpeed()));
}, App::GetTextScrollSpeed()));
options->Add(std::make_shared<ui::SidebarEntryArray>("Set right-side menu"_i18n, right_side_menu_items, [menu_names](s64& index_out){
const auto e = menu_names[index_out];
if (g_app->m_right_side_menu.Get() != e) {
g_app->m_right_side_menu.Set(e);
App::Push(std::make_shared<ui::OptionBox>(
"Press OK to restart Sphaira"_i18n, "OK"_i18n, [](auto){
App::ExitRestart();
}
));
}
}, i18n::get(g_app->m_right_side_menu.Get())));
options->Add(std::make_shared<ui::SidebarEntryCallback>("Install options"_i18n, [left_side](){
App::DisplayInstallOptions(left_side);
@@ -1540,77 +1593,85 @@ void App::DisplayInstallOptions(bool left_side) {
install_items.push_back("System memory"_i18n);
install_items.push_back("microSD card"_i18n);
options->Add(std::make_shared<ui::SidebarEntryBool>("Enable"_i18n, App::GetInstallEnable(), [](bool& enable){
App::SetInstallEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("Enable sysmmc"_i18n, App::GetInstallSysmmcEnable(), [](bool& enable){
App::SetInstallSysmmcEnable(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [](bool& enable){
App::SetInstallPrompt(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<ui::SidebarEntryBool>("Enable emummc"_i18n, App::GetInstallEmummcEnable(), [](bool& enable){
App::SetInstallEmummcEnable(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Show install warning"_i18n, App::GetApp()->m_install_prompt.Get(), [](bool& enable){
App::GetApp()->m_install_prompt.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryArray>("Install location"_i18n, install_items, [](s64& index_out){
App::SetInstallSdEnable(index_out);
}, (s64)App::GetInstallSdEnable()));
options->Add(std::make_shared<ui::SidebarEntryBool>("Boost CPU clock"_i18n, App::GetApp()->m_boost_mode.Get(), [](bool& enable){
App::GetApp()->m_boost_mode.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Allow downgrade"_i18n, App::GetApp()->m_allow_downgrade.Get(), [](bool& enable){
App::GetApp()->m_allow_downgrade.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip if already installed"_i18n, App::GetApp()->m_skip_if_already_installed.Get(), [](bool& enable){
App::GetApp()->m_skip_if_already_installed.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Ticket only"_i18n, App::GetApp()->m_ticket_only.Get(), [](bool& enable){
App::GetApp()->m_ticket_only.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip base"_i18n, App::GetApp()->m_skip_base.Get(), [](bool& enable){
App::GetApp()->m_skip_base.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip Patch"_i18n, App::GetApp()->m_skip_patch.Get(), [](bool& enable){
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip patch"_i18n, App::GetApp()->m_skip_patch.Get(), [](bool& enable){
App::GetApp()->m_skip_patch.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip addon"_i18n, App::GetApp()->m_skip_addon.Get(), [](bool& enable){
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip dlc"_i18n, App::GetApp()->m_skip_addon.Get(), [](bool& enable){
App::GetApp()->m_skip_addon.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip data patch"_i18n, App::GetApp()->m_skip_data_patch.Get(), [](bool& enable){
App::GetApp()->m_skip_data_patch.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip ticket"_i18n, App::GetApp()->m_skip_ticket.Get(), [](bool& enable){
App::GetApp()->m_skip_ticket.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [](bool& enable){
App::GetApp()->m_skip_nca_hash_verify.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip RSA header verify"_i18n, App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(), [](bool& enable){
App::GetApp()->m_skip_rsa_header_fixed_key_verify.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip RSA NPDM verify"_i18n, App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(), [](bool& enable){
App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Ignore distribution bit"_i18n, App::GetApp()->m_ignore_distribution_bit.Get(), [](bool& enable){
App::GetApp()->m_ignore_distribution_bit.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [](bool& enable){
App::GetApp()->m_convert_to_standard_crypto.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Lower master key"_i18n, App::GetApp()->m_lower_master_key.Get(), [](bool& enable){
App::GetApp()->m_lower_master_key.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Lower system version"_i18n, App::GetApp()->m_lower_system_version.Get(), [](bool& enable){
App::GetApp()->m_lower_system_version.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
}
App::~App() {
@@ -1619,8 +1680,6 @@ App::~App() {
i18n::exit();
curl::Exit();
ui::bubble::Exit();
// this has to be called before any cleanup to ensure the lifetime of
// nvg is still active as some widgets may need to free images.
m_widgets.clear();
@@ -1685,7 +1744,7 @@ App::~App() {
// found sphaira, now lets get compare version
if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
if (std::strcmp(hbmenu_nacp.display_version, sphaira_nacp.display_version) < 0) {
if (IsVersionNewer(hbmenu_nacp.display_version, sphaira_nacp.display_version)) {
if (R_FAILED(rc = fs.copy_entire_file(GetExePath(), sphaira_path))) {
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path.s, rc, R_MODULE(rc), R_DESCRIPTION(rc));
} else {
@@ -1723,6 +1782,16 @@ App::~App() {
ini_putl("paths", "timestamp", timestamp, App::CONFIG_PATH);
}
auto App::GetVersionFromString(const char* str) -> u32 {
u32 major{}, minor{}, macro{};
std::sscanf(str, "%u.%u.%u", &major, &minor, &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() {
this->swapchain = nullptr;

View File

@@ -3,12 +3,15 @@
#include "defines.hpp"
#include "evman.hpp"
#include "fs.hpp"
#include <switch.h>
#include <cstring>
#include <cassert>
#include <vector>
#include <deque>
#include <mutex>
#include <algorithm>
#include <ranges>
#include <curl/curl.h>
#include <yyjson.h>
@@ -25,7 +28,7 @@ namespace {
log_write("curl_share_setopt(%s, %s) msg: %s\n", #opt, #v, curl_share_strerror(r)); \
} \
constexpr auto API_AGENT = "ITotalJustice";
constexpr auto API_AGENT = "TotalJustice";
constexpr u64 CHUNK_SIZE = 1024*1024;
constexpr auto MAX_THREADS = 4;
constexpr int THREAD_PRIO = 0x2C;
@@ -33,8 +36,18 @@ constexpr int THREAD_CORE = 1;
std::atomic_bool g_running{};
CURLSH* g_curl_share{};
// this is used for single threaded blocking installs.
// avoids the needed for re-creating the handle each time.
CURL* g_curl_single{};
Mutex g_mutex_share[CURL_LOCK_DATA_LAST]{};
struct UploadStruct {
std::span<const u8> data;
s64 offset{};
s64 size{};
FsFile f{};
};
struct DataStruct {
std::vector<u8> data;
s64 offset{};
@@ -42,6 +55,16 @@ struct DataStruct {
s64 file_offset{};
};
struct SeekCustomData {
OnUploadSeek cb{};
s64 size{};
};
// helper for creating webdav folders as libcurl does not have built-in
// support for it.
// only creates the folders if they don't exist.
auto WebdavCreateFolder(CURL* curl, const Api& e) -> bool;
auto generate_key_from_path(const fs::FsPath& path) -> std::string {
const auto key = crc32Calculate(path.s, path.size());
return std::to_string(key);
@@ -195,6 +218,8 @@ private:
} else {
const auto update_entry = [this, &hash_key](const char* tag, const std::string& value) {
if (value.empty()) {
// workaround for appstore accepting etags but not returning them.
yyjson_mut_obj_remove_str(hash_key, tag);
return true;
} else {
auto key = yyjson_mut_obj_get(hash_key, tag);
@@ -300,7 +325,7 @@ struct ThreadQueue {
threadClose(&m_thread);
}
auto Add(const Api& api) -> bool {
auto Add(const Api& api, bool is_upload = false) -> bool {
if (api.GetUrl().empty() || api.GetPath().empty() || !api.GetOnComplete()) {
return false;
}
@@ -310,10 +335,10 @@ struct ThreadQueue {
switch (api.GetPriority()) {
case Priority::Normal:
m_entries.emplace_back(api);
m_entries.emplace_back(api).api.SetUpload(is_upload);
break;
case Priority::High:
m_entries.emplace_front(api);
m_entries.emplace_front(api).api.SetUpload(is_upload);
break;
}
@@ -364,6 +389,94 @@ auto ProgressCallbackFunc2(void *clientp, curl_off_t dltotal, curl_off_t dlnow,
return 0;
}
auto SeekCallback(void *clientp, curl_off_t offset, int origin) -> int {
if (!g_running) {
return 0;
}
auto data_struct = static_cast<UploadStruct*>(clientp);
if (origin == SEEK_SET) {
offset = offset;
} else if (origin == SEEK_CUR) {
offset = data_struct->offset + offset;
} else if (origin == SEEK_END) {
offset = data_struct->size;
}
if (offset < 0 || offset > data_struct->size) {
return CURL_SEEKFUNC_CANTSEEK;
}
data_struct->offset = offset;
return CURL_SEEKFUNC_OK;
}
auto SeekCustomCallback(void *clientp, curl_off_t offset, int origin) -> int {
if (!g_running) {
return 0;
}
auto data_struct = static_cast<SeekCustomData*>(clientp);
if (origin != SEEK_SET || offset < 0 || offset > data_struct->size) {
return CURL_SEEKFUNC_CANTSEEK;
}
if (!data_struct->cb(offset)) {
return CURL_SEEKFUNC_CANTSEEK;
}
return CURL_SEEKFUNC_OK;
}
auto ReadFileCallback(char *ptr, size_t size, size_t nmemb, void *userp) -> size_t {
if (!g_running) {
return 0;
}
auto data_struct = static_cast<UploadStruct*>(userp);
const auto realsize = size * nmemb;
u64 bytes_read;
if (R_FAILED(fsFileRead(&data_struct->f, data_struct->offset, ptr, realsize, FsReadOption_None, &bytes_read))) {
log_write("reading file error\n");
return 0;
}
data_struct->offset += bytes_read;
svcSleepThread(YieldType_WithoutCoreMigration);
return bytes_read;
}
auto ReadMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp) -> size_t {
if (!g_running) {
return 0;
}
auto data_struct = static_cast<UploadStruct*>(userp);
auto realsize = size * nmemb;
realsize = std::min(realsize, data_struct->data.size() - data_struct->offset);
std::memcpy(ptr, data_struct->data.data(), realsize);
data_struct->offset += realsize;
svcSleepThread(YieldType_WithoutCoreMigration);
return realsize;
}
auto ReadCustomCallback(char *ptr, size_t size, size_t nmemb, void *userp) -> size_t {
if (!g_running) {
return 0;
}
auto data_struct = static_cast<UploadInfo*>(userp);
auto realsize = size * nmemb;
const auto result = data_struct->m_callback(ptr, realsize);
svcSleepThread(YieldType_WithoutCoreMigration);
return result;
}
auto WriteMemoryCallback(void *contents, size_t size, size_t num_files, void *userp) -> size_t {
if (!g_running) {
return 0;
@@ -379,11 +492,9 @@ auto WriteMemoryCallback(void *contents, size_t size, size_t num_files, void *us
data_struct->data.resize(data_struct->offset + realsize);
std::memcpy(data_struct->data.data() + data_struct->offset, contents, realsize);
data_struct->offset += realsize;
svcSleepThread(YieldType_WithoutCoreMigration);
return realsize;
}
@@ -442,6 +553,121 @@ auto header_callback(char* b, size_t size, size_t nitems, void* userdata) -> siz
return numbytes;
}
auto EscapeString(CURL* curl, const std::string& str) -> std::string {
char* s{};
if (!curl) {
s = curl_escape(str.data(), str.length());
} else {
s = curl_easy_escape(curl, str.data(), str.length());
}
if (!s) {
return str;
}
const std::string result = s;
curl_free(s);
return result;
}
auto EncodeUrl(std::string url) -> std::string {
log_write("[CURL] encoding url\n");
if (url.starts_with("webdav://")) {
log_write("[CURL] updating host\n");
url.replace(0, std::strlen("webdav"), "https");
log_write("[CURL] updated host: %s\n", url.c_str());
}
auto clu = curl_url();
R_UNLESS(clu, url);
ON_SCOPE_EXIT(curl_url_cleanup(clu));
log_write("[CURL] setting url\n");
CURLUcode clu_code;
clu_code = curl_url_set(clu, CURLUPART_URL, url.c_str(), CURLU_URLENCODE);
R_UNLESS(clu_code == CURLUE_OK, url);
log_write("[CURL] set url success\n");
char* encoded_url;
clu_code = curl_url_get(clu, CURLUPART_URL, &encoded_url, 0);
R_UNLESS(clu_code == CURLUE_OK, url);
log_write("[CURL] encoded url: %s [vs]: %s\n", encoded_url, url.c_str());
const std::string out = encoded_url;
curl_free(encoded_url);
return out;
}
void SetCommonCurlOptions(CURL* curl, const Api& e) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_USERAGENT, API_AGENT);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FOLLOWLOCATION, 1L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYPEER, 0L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYHOST, 0L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FAILONERROR, 1L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_NOPROGRESS, 0L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SHARE, g_curl_share);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_BUFFERSIZE, 1024*512);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_UPLOAD_BUFFERSIZE, 1024*512);
// enable all forms of compression supported by libcurl.
CURL_EASY_SETOPT_LOG(curl, CURLOPT_ACCEPT_ENCODING, "");
// for smb / ftp, try and use ssl if possible.
CURL_EASY_SETOPT_LOG(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_TRY);
// in most cases, this will use CURLAUTH_BASIC.
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HTTPAUTH, (long)CURLAUTH_ANY);
// enable TE is server supports it.
CURL_EASY_SETOPT_LOG(curl, CURLOPT_TRANSFER_ENCODING, 1L);
// set flags.
if (e.GetFlags() & Flag_NoBody) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_NOBODY, 1L);
}
// set custom request.
if (!e.GetCustomRequest().empty()) {
log_write("[CURL] setting custom request: %s\n", e.GetCustomRequest().c_str());
CURL_EASY_SETOPT_LOG(curl, CURLOPT_CUSTOMREQUEST, e.GetCustomRequest().c_str());
}
// set oath2 bearer.
if (!e.GetBearer().empty()) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XOAUTH2_BEARER, e.GetBearer().c_str());
}
// set ssh pub/priv key file.
if (!e.GetPubKey().empty()) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSH_PUBLIC_KEYFILE, e.GetPubKey().c_str());
}
if (!e.GetPrivKey().empty()) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSH_PRIVATE_KEYFILE, e.GetPrivKey().c_str());
}
// set auth.
if (!e.GetUserPass().m_user.empty()) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_USERPWD, e.GetUserPass().m_user.c_str());
}
if (!e.GetUserPass().m_pass.empty()) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_PASSWORD, e.GetUserPass().m_pass.c_str());
}
// set port, if valid.
if (e.GetPort()) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_PORT, (long)e.GetPort());
}
// progress calls.
if (e.GetOnProgress()) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &e);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc2);
} else {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc1);
}
}
auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
// check if stop has been requested before starting download
if (e.GetToken().stop_requested()) {
@@ -451,6 +677,7 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
fs::FsPath tmp_buf;
const bool has_file = !e.GetPath().empty() && e.GetPath() != "";
const bool has_post = !e.GetFields().empty() && e.GetFields() != "";
const auto encoded_url = EncodeUrl(e.GetUrl());
DataStruct chunk;
Header header_in = e.GetHeader();
@@ -471,7 +698,8 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
return {};
}
if (e.GetFlags() & Flag_Cache) {
// only add etag if the dst file still exists.
if ((e.GetFlags() & Flag_Cache) && fs::FileExists(&fs.m_fs, e.GetPath())) {
g_cache.get(e.GetPath(), header_in);
}
}
@@ -480,14 +708,9 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
chunk.data.reserve(CHUNK_SIZE);
curl_easy_reset(curl);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, e.GetUrl().c_str());
CURL_EASY_SETOPT_LOG(curl, CURLOPT_USERAGENT, "TotalJustice");
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FOLLOWLOCATION, 1L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYPEER, 0L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYHOST, 0L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FAILONERROR, 1L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SHARE, g_curl_share);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_BUFFERSIZE, 1024*512);
SetCommonCurlOptions(curl, e);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, encoded_url.c_str());
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERFUNCTION, header_callback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERDATA, &header_out);
@@ -521,15 +744,6 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HTTPHEADER, list);
}
// progress calls.
if (e.GetOnProgress()) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &e);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc2);
} else {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc1);
}
CURL_EASY_SETOPT_LOG(curl, CURLOPT_NOPROGRESS, 0L);
// write calls.
CURL_EASY_SETOPT_LOG(curl, CURLOPT_WRITEFUNCTION, has_file ? WriteFileCallback : WriteMemoryCallback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_WRITEDATA, &chunk);
@@ -558,6 +772,15 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
g_cache.set(e.GetPath(), header_out);
}
// enable to log received headers.
#if 0
log_write("\n\nLOGGING HEADER\n");
for (auto [a, b] : header_out.m_map) {
log_write("\t%s: %s\n", a.c_str(), b.c_str());
}
log_write("\n\n");
#endif
fs.DeleteFile(e.GetPath());
fs.CreateDirectoryRecursivelyWithPath(e.GetPath());
if (R_FAILED(fs.RenameFile(tmp_buf, e.GetPath()))) {
@@ -573,18 +796,210 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
}
}
log_write("Downloaded %s %s\n", e.GetUrl().c_str(), curl_easy_strerror(res));
log_write("Downloaded %s code: %ld %s\n", e.GetUrl().c_str(), http_code, curl_easy_strerror(res));
return {success, http_code, header_out, chunk.data, e.GetPath()};
}
auto DownloadInternal(const Api& e) -> ApiResult {
auto curl = curl_easy_init();
if (!curl) {
log_write("curl init failed\n");
auto UploadInternal(CURL* curl, const Api& e) -> ApiResult {
// check if stop has been requested before starting download
if (e.GetToken().stop_requested()) {
return {};
}
ON_SCOPE_EXIT(curl_easy_cleanup(curl));
return DownloadInternal(curl, e);
if (e.GetUrl().starts_with("webdav://")) {
if (!WebdavCreateFolder(curl, e)) {
log_write("[CURL] failed to create webdav folder, aborting\n");
return {};
}
}
const auto& info = e.GetUploadInfo();
const auto url = e.GetUrl() + "/" + info.m_name;
const auto encoded_url = EncodeUrl(url);
const bool has_file = !e.GetPath().empty() && e.GetPath() != "";
UploadStruct chunk{};
DataStruct chunk_out{};
SeekCustomData seek_data{};
Header header_in = e.GetHeader();
Header header_out;
fs::FsNativeSd fs{};
if (has_file) {
if (R_FAILED(fs.OpenFile(e.GetPath(), FsOpenMode_Read, &chunk.f))) {
log_write("failed to open file: %s\n", e.GetPath().s);
return {};
}
fsFileGetSize(&chunk.f, &chunk.size);
log_write("got chunk size: %zd\n", chunk.size);
} else {
if (info.m_callback) {
chunk.size = info.m_size;
log_write("setting upload size: %zu\n", chunk.size);
} else {
chunk.size = info.m_data.size();
chunk.data = info.m_data;
}
}
if (url.starts_with("file://")) {
const auto folder_path = fs::AppendPath("/", url.substr(std::strlen("file://")));
log_write("creating local folder: %s\n", folder_path.s);
// create the folder as libcurl doesn't seem to manually create it.
fs.CreateDirectoryRecursivelyWithPath(folder_path);
// remove the path so that libcurl can upload over it.
fs.DeleteFile(folder_path);
}
// reserve the first chunk
chunk_out.data.reserve(CHUNK_SIZE);
curl_easy_reset(curl);
SetCommonCurlOptions(curl, e);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, encoded_url.c_str());
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERFUNCTION, header_callback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERDATA, &header_out);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_UPLOAD, 1L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)chunk.size);
// instruct libcurl to create ftp folders if they don't yet exist.
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FTP_CREATE_MISSING_DIRS, CURLFTP_CREATE_DIR_RETRY);
struct curl_slist* list = NULL;
ON_SCOPE_EXIT(if (list) { curl_slist_free_all(list); } );
for (const auto& [key, value] : header_in.m_map) {
if (value.empty()) {
continue;
}
// create header key value pair.
const auto header_str = key + ": " + value;
// try to append header chunk.
auto temp = curl_slist_append(list, header_str.c_str());
if (temp) {
log_write("adding header: %s\n", header_str.c_str());
list = temp;
} else {
log_write("failed to append header\n");
}
}
if (list) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HTTPHEADER, list);
}
// set callback for reading more data.
if (info.m_callback) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_READFUNCTION, ReadCustomCallback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_READDATA, &info);
if (e.GetOnUploadSeek()) {
seek_data.cb = e.GetOnUploadSeek();
seek_data.size = chunk.size;
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SEEKFUNCTION, SeekCustomCallback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SEEKDATA, &seek_data);
}
} else {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_READFUNCTION, has_file ? ReadFileCallback : ReadMemoryCallback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_READDATA, &chunk);
// allow for seeking upon uploads, may be used for ftp and http.
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SEEKFUNCTION, SeekCallback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SEEKDATA, &chunk);
}
// write calls.
CURL_EASY_SETOPT_LOG(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_WRITEDATA, &chunk_out);
// perform upload and cleanup after and report the result.
const auto res = curl_easy_perform(curl);
bool success = res == CURLE_OK;
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
if (has_file) {
fsFileClose(&chunk.f);
}
log_write("Uploaded %s code: %ld %s\n", url.c_str(), http_code, curl_easy_strerror(res));
return {success, http_code, header_out, chunk_out.data};
}
auto WebdavCreateFolder(CURL* curl, const Api& e) -> bool {
// if using webdav, extract the file path and create the directories.
// https://github.com/WebDAVDevs/webdav-request-samples/blob/master/webdav_curl.md
if (e.GetUrl().starts_with("webdav://")) {
log_write("[CURL] found webdav url\n");
const auto info = e.GetUploadInfo();
if (info.m_name.empty()) {
return true;
}
const auto& file_path = info.m_name;
log_write("got file path: %s\n", file_path.c_str());
const auto file_loc = file_path.find_last_of('/');
if (file_loc == file_path.npos) {
log_write("failed to find last slash\n");
return true;
}
const auto path_view = file_path.substr(0, file_loc);
log_write("got folder path: %s\n", path_view.c_str());
auto e2 = e;
e2.SetOption(Path{});
e2.SetOption(Url{e.GetUrl() + "/" + path_view});
e2.SetOption(Flags{e.GetFlags() | Flag_NoBody});
e2.SetOption(CustomRequest{"PROPFIND"});
e2.SetOption(Header{
{ "Depth", "0" },
});
// test to see if the directory exists first.
const auto exist_result = DownloadInternal(curl, e2);
if (exist_result.success) {
log_write("[CURL] folder already exist: %s\n", path_view.c_str());
return true;
} else {
log_write("[CURL] folder does NOT exist, manually creating: %s\n", path_view.c_str());
}
// make the request to create the folder.
std::string folder;
for (const auto dir : std::views::split(path_view, '/')) {
if (dir.empty()) {
continue;
}
folder += "/" + std::string{dir.data(), dir.size()};
e2.SetOption(Url{e.GetUrl() + folder});
e2.SetOption(Header{});
e2.SetOption(CustomRequest{"MKCOL"});
const auto result = DownloadInternal(curl, e2);
if (result.code == 201) {
log_write("[CURL] created webdav directory\n");
} else if (result.code == 405) {
log_write("[CURL] webdav directory already exists: %ld\n", result.code);
} else {
log_write("[CURL] failed to create webdav directory: %ld\n", result.code);
return false;
}
}
} else {
log_write("[CURL] not a webdav url: %s\n", e.GetUrl().c_str());
}
return true;
}
void my_lock(CURL *handle, curl_lock_data data, curl_lock_access laccess, void *useptr) {
@@ -608,10 +1023,12 @@ void ThreadEntry::ThreadFunc(void* p) {
continue;
}
const auto result = DownloadInternal(data->m_curl, data->m_api);
const auto result = data->m_api.IsUpload() ? UploadInternal(data->m_curl, data->m_api) : DownloadInternal(data->m_curl, data->m_api);
if (g_running && data->m_api.GetOnComplete() && !data->m_api.GetToken().stop_requested()) {
const DownloadEventData event_data{data->m_api.GetOnComplete(), result, data->m_api.GetToken()};
evman::push(std::move(event_data), false);
evman::push(
DownloadEventData{data->m_api.GetOnComplete(), result, data->m_api.GetToken()},
false
);
}
data->m_in_progress = false;
@@ -708,6 +1125,11 @@ auto Init() -> bool {
}
}
g_curl_single = curl_easy_init();
if (!g_curl_single) {
log_write("failed to create g_curl_single\n");
}
log_write("finished creating threads\n");
if (!g_cache.init()) {
@@ -722,6 +1144,11 @@ void Exit() {
g_thread_queue.Close();
if (g_curl_single) {
curl_easy_cleanup(g_curl_single);
g_curl_single = nullptr;
}
for (auto& entry : g_threads) {
entry.Close();
}
@@ -739,14 +1166,28 @@ auto ToMemory(const Api& e) -> ApiResult {
if (!e.GetPath().empty()) {
return {};
}
return DownloadInternal(e);
return DownloadInternal(g_curl_single, e);
}
auto ToFile(const Api& e) -> ApiResult {
if (e.GetPath().empty()) {
return {};
}
return DownloadInternal(e);
return DownloadInternal(g_curl_single, e);
}
auto FromMemory(const Api& e) -> ApiResult {
if (!e.GetPath().empty()) {
return {};
}
return UploadInternal(g_curl_single, e);
}
auto FromFile(const Api& e) -> ApiResult {
if (e.GetPath().empty()) {
return {};
}
return UploadInternal(g_curl_single, e);
}
auto ToMemoryAsync(const Api& api) -> bool {
@@ -757,14 +1198,16 @@ auto ToFileAsync(const Api& e) -> bool {
return g_thread_queue.Add(e);
}
auto FromMemoryAsync(const Api& api) -> bool {
return g_thread_queue.Add(api, true);
}
auto FromFileAsync(const Api& e) -> bool {
return g_thread_queue.Add(e, true);
}
auto EscapeString(const std::string& str) -> std::string {
std::string result;
const auto s = curl_escape(str.data(), str.length());
if (s) {
result = s;
curl_free(s);
}
return result;
return EscapeString(nullptr, str);
}
} // namespace sphaira::curl

View File

@@ -13,8 +13,8 @@ yyjson_doc* json;
yyjson_val* root;
std::unordered_map<std::string, std::string> g_tr_cache;
std::string get_internal(const char* str, size_t len) {
const std::string kkey = {str, len};
std::string get_internal(std::string_view str) {
const std::string kkey = {str.data(), str.length()};
if (auto it = g_tr_cache.find(kkey); it != g_tr_cache.end()) {
return it->second;
@@ -28,7 +28,7 @@ std::string get_internal(const char* str, size_t len) {
return kkey;
}
auto key = yyjson_obj_getn(root, str, len);
auto key = yyjson_obj_getn(root, str.data(), str.length());
if (!key) {
log_write("\tfailed to find key: [%s]\n", kkey.c_str());
return kkey;
@@ -78,6 +78,7 @@ bool init(long index) {
case 11: setLanguage = SetLanguage_RU; break; // "Russian"
case 12: lang_name = "se"; break; // "Swedish"
case 13: lang_name = "vi"; break; // "Vietnamese"
case 14: lang_name = "uk"; break; // "Ukrainian"
}
switch (setLanguage) {
@@ -86,7 +87,7 @@ bool init(long index) {
case SetLanguage_DE: lang_name = "de"; break;
case SetLanguage_IT: lang_name = "it"; break;
case SetLanguage_ES: lang_name = "es"; break;
case SetLanguage_ZHCN: lang_name = "zh"; break;
case SetLanguage_ZHCN: lang_name = "zh"; break;
case SetLanguage_KO: lang_name = "ko"; break;
case SetLanguage_NL: lang_name = "nl"; break;
case SetLanguage_PT: lang_name = "pt"; break;
@@ -134,16 +135,16 @@ void exit() {
g_i18n_data.clear();
}
std::string get(const char* str) {
return get_internal(str, std::strlen(str));
std::string get(std::string_view str) {
return get_internal(str);
}
} // namespace sphaira::i18n
namespace literals {
std::string operator"" _i18n(const char* str, size_t len) {
return sphaira::i18n::get_internal(str, len);
std::string operator""_i18n(const char* str, size_t len) {
return sphaira::i18n::get_internal({str, len});
}
} // namespace literals

View File

@@ -0,0 +1,75 @@
#include "location.hpp"
#include "fs.hpp"
#include <cstring>
#include <minIni.h>
namespace sphaira::location {
namespace {
constexpr fs::FsPath location_path{"/config/sphaira/locations.ini"};
} // namespace
void Add(const Entry& e) {
if (e.name.empty() || e.url.empty()) {
return;
}
ini_puts(e.name.c_str(), "url", e.url.c_str(), location_path);
if (!e.user.empty()) {
ini_puts(e.name.c_str(), "user", e.user.c_str(), location_path);
}
if (!e.pass.empty()) {
ini_puts(e.name.c_str(), "pass", e.pass.c_str(), location_path);
}
if (!e.bearer.empty()) {
ini_puts(e.name.c_str(), "bearer", e.bearer.c_str(), location_path);
}
if (!e.pub_key.empty()) {
ini_puts(e.name.c_str(), "pub_key", e.pub_key.c_str(), location_path);
}
if (!e.priv_key.empty()) {
ini_puts(e.name.c_str(), "priv_key", e.priv_key.c_str(), location_path);
}
if (e.port) {
ini_putl(e.name.c_str(), "port", e.port, location_path);
}
}
auto Load() -> Entries {
Entries out{};
auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
auto e = static_cast<Entries*>(UserData);
// add new entry if use section changed.
if (e->empty() || std::strcmp(Section, e->back().name.c_str())) {
e->emplace_back(Section);
}
if (!std::strcmp(Key, "url")) {
e->back().url = Value;
} else if (!std::strcmp(Key, "user")) {
e->back().user = Value;
} else if (!std::strcmp(Key, "pass")) {
e->back().pass = Value;
} else if (!std::strcmp(Key, "bearer")) {
e->back().bearer = Value;
} else if (!std::strcmp(Key, "pub_key")) {
e->back().pub_key = Value;
} else if (!std::strcmp(Key, "priv_key")) {
e->back().priv_key = Value;
} else if (!std::strcmp(Key, "port")) {
e->back().port = std::atoi(Value);
}
return 1;
};
ini_browse(cb, &out, location_path);
return out;
}
} // namespace sphaira::location

View File

@@ -6,21 +6,35 @@
namespace sphaira::option {
template<typename T>
auto OptionBase<T>::Get() -> T {
auto OptionBase<T>::GetInternal(const char* name) -> T {
if (!m_value.has_value()) {
if constexpr(std::is_same_v<T, bool>) {
m_value = ini_getbool(m_section.c_str(), m_name.c_str(), m_default_value, App::CONFIG_PATH);
m_value = ini_getbool(m_section.c_str(), name, m_default_value, App::CONFIG_PATH);
} else if constexpr(std::is_same_v<T, long>) {
m_value = ini_getl(m_section.c_str(), m_name.c_str(), m_default_value, App::CONFIG_PATH);
m_value = ini_getl(m_section.c_str(), name, m_default_value, App::CONFIG_PATH);
} else if constexpr(std::is_same_v<T, std::string>) {
char buf[FS_MAX_PATH];
ini_gets(m_section.c_str(), m_name.c_str(), m_default_value.c_str(), buf, sizeof(buf), App::CONFIG_PATH);
ini_gets(m_section.c_str(), name, m_default_value.c_str(), buf, sizeof(buf), App::CONFIG_PATH);
m_value = buf;
}
}
return m_value.value();
}
template<typename T>
auto OptionBase<T>::Get() -> T {
return GetInternal(m_name.c_str());
}
template<typename T>
auto OptionBase<T>::GetOr(const char* name) -> T {
if (ini_haskey(m_section.c_str(), m_name.c_str(), App::CONFIG_PATH)) {
return Get();
} else {
return GetInternal(name);
}
}
template<typename T>
void OptionBase<T>::Set(T value) {
m_value = value;

View File

@@ -715,7 +715,7 @@ void write_nca_header_encypted(nca::Header& nca_header, u64 tid, const keys::Key
nca_header.magic = NCA3_MAGIC;
nca_header.distribution_type = nca::DistributionType_System;
nca_header.content_type = type;
nca_header.title_id = tid;
nca_header.program_id = tid;
nca_header.sdk_version = 0x000C1100;
nca_header.size = buf.tell();
@@ -857,7 +857,8 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
u64 hash_data[SHA256_HASH_SIZE / sizeof(u64)];
const auto hash_path = config.nro_path + config.args;
sha256CalculateHash(hash_data, hash_path.data(), hash_path.length());
const u64 tid = 0x0100000000000000 | (hash_data[0] & 0x00FFFFFFFFFFF000);
const u64 old_tid = 0x0100000000000000 | (hash_data[0] & 0x00FFFFFFFFFFF000);
const u64 tid = 0x0500000000000000 | (hash_data[0] & 0x00FFFFFFFFFFF000);
std::vector<NcaEntry> nca_entries;
@@ -980,6 +981,12 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
R_TRY(nsIsAnyApplicationEntityInstalled(tid, &already_installed));
}
// remove old id for forwarders.
const auto rc = nsDeleteApplicationCompletely(old_tid);
if (R_FAILED(rc) && rc != 0x410) { // not found
App::Notify("Failed to remove old forwarder, please manually remove it!");
}
// remove previous application record
if (already_installed || hosversionBefore(2,0,0)) {
const auto rc = ns::DeleteApplicationRecord(srv_ptr, tid);

View File

@@ -1,115 +0,0 @@
#include "ui/types.hpp"
#include "ui/object.hpp"
#include "ui/nvg_util.hpp"
#include "app.hpp"
namespace sphaira::ui::bubble {
namespace {
constexpr auto MAX_BUBBLES = 20;
struct Bubble {
int start_x;
int texture;
int x,y,w,h;
int y_inc;
float sway_inc;
float sway;
bool sway_right_flag;
};
Bubble bubbles[MAX_BUBBLES]{};
int g_textures[3];
bool g_is_init = false;
void setup_bubble(Bubble *bubble) {
// setup normal vars.
bubble->texture = (randomGet64() % std::size(g_textures));
bubble->start_x = randomGet64() % (int)SCREEN_WIDTH;
bubble->x = bubble->start_x;
bubble->y = (int)SCREEN_HEIGHT - ( randomGet64() % 60 );
const int size = (randomGet64() % 50) + 40;
bubble->w = size;
bubble->h = size;
bubble->y_inc = (randomGet64() % 5) + 1;
bubble->sway_inc = ((randomGet64() % 6) + 3) / 10;
bubble->sway = 0;
}
void setup_bubbles(void) {
for (auto& bubble : bubbles) {
setup_bubble(&bubble);
}
}
void update_bubbles(void) {
for (auto& bubble : bubbles) {
if (bubble.y + bubble.h < 0) {
setup_bubble(&bubble);
} else {
bubble.y -= bubble.y_inc;
if (bubble.sway_right_flag) {
bubble.x = bubble.start_x + (bubble.sway -= bubble.sway_inc);
if (bubble.sway <= 0) {
bubble.sway_right_flag = false;
}
} else {
bubble.x = bubble.start_x + (bubble.sway += bubble.sway_inc);
if (bubble.sway > 30) {
bubble.sway_right_flag = true;
}
}
}
}
}
} // namespace
void Init() {
if (g_is_init) {
return;
}
if (R_SUCCEEDED(romfsInit())) {
ON_SCOPE_EXIT(romfsExit());
auto vg = App::GetVg();
g_textures[0] = nvgCreateImage(vg, "romfs:/theme/bubble1.png", 0);
g_textures[1] = nvgCreateImage(vg, "romfs:/theme/bubble2.png", 0);
g_textures[2] = nvgCreateImage(vg, "romfs:/theme/bubble3.png", 0);
setup_bubbles();
g_is_init = true;
}
}
void Draw(NVGcontext* vg, Theme* theme) {
if (!g_is_init) {
return;
}
update_bubbles();
for (auto& bubble : bubbles) {
gfx::drawImage(vg, bubble.x, bubble.y, bubble.w, bubble.h, g_textures[bubble.texture]);
}
}
void Exit() {
if (!g_is_init) {
return;
}
auto vg = App::GetVg();
for (auto& texture : g_textures) {
if (texture) {
nvgDeleteImage(vg, texture);
texture = 0;
}
}
g_is_init = false;
}
} // namespace sphaira::ui::bubble

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
#include "ui/nvg_util.hpp"
#include "app.hpp"
#include "log.hpp"
#include <algorithm>
namespace sphaira::ui {
@@ -14,6 +15,11 @@ List::List(s64 row, s64 page, const Vec4& pos, const Vec4& v, const Vec2& pad)
SetScrollBarPos(SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200);
}
auto List::ClampX(float x, s64 count) const -> float {
const float x_max = count * GetMaxX();
return std::clamp(x, 0.F, x_max);
}
auto List::ClampY(float y, s64 count) const -> float {
float y_max = 0;
@@ -25,16 +31,140 @@ auto List::ClampY(float y, s64 count) const -> float {
y_max = (count - m_page) / m_row * GetMaxY();
}
if (y < 0) {
y = 0;
} else if (y > y_max) {
y = y_max;
}
return y;
return std::clamp(y, 0.F, y_max);
}
void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback) {
switch (m_layout) {
case Layout::HOME:
OnUpdateHome(controller, touch, index, count, callback);
break;
case Layout::GRID:
OnUpdateGrid(controller, touch, index, count, callback);
break;
}
}
void List::Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const {
switch (m_layout) {
case Layout::HOME:
DrawHome(vg, theme, count, callback);
break;
case Layout::GRID:
DrawGrid(vg, theme, count, callback);
break;
}
}
auto List::ScrollDown(s64& index, s64 step, s64 count) -> bool {
const auto old_index = index;
const auto max = m_layout == Layout::GRID ? GetMaxY() : GetMaxX();
if (!count) {
return false;
}
if (index + step < count) {
index += step;
} else {
index = count - 1;
}
if (index != old_index) {
App::PlaySoundEffect(SoundEffect_Scroll);
s64 delta = index - old_index;
s64 start = m_yoff / max * m_row;
while (index < start) {
start -= m_row;
m_yoff -= max;
}
if (index - start >= m_page) {
do {
start += m_row;
delta -= m_row;
m_yoff += max;
} while (delta > 0 && start + m_page < count);
}
return true;
}
return false;
}
auto List::ScrollUp(s64& index, s64 step, s64 count) -> bool {
const auto old_index = index;
const auto max = m_layout == Layout::GRID ? GetMaxY() : GetMaxX();
if (!count) {
return false;
}
if (index >= step) {
index -= step;
} else {
index = 0;
}
if (index != old_index) {
App::PlaySoundEffect(SoundEffect_Scroll);
s64 start = m_yoff / max * m_row;
while (index < start) {
start -= m_row;
m_yoff -= max;
}
while (index - start >= m_page && start + m_page < count) {
start += m_row;
m_yoff += max;
}
return true;
}
return false;
}
void List::OnUpdateHome(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback) {
if (controller->GotDown(Button::RIGHT)) {
if (ScrollDown(index, m_row, count)) {
callback(false, index);
}
} else if (controller->GotDown(Button::LEFT)) {
if (ScrollUp(index, m_row, count)) {
callback(false, index);
}
} else if (touch->is_clicked && touch->in_range(GetPos())) {
auto v = m_v;
v.x -= ClampX(m_yoff + m_y_prog, count);
for (s64 i = 0; i < count; i++, v.x += v.w + m_pad.x) {
if (v.x > GetX() + GetW()) {
break;
}
Vec4 vv = v;
// if not drawing, only return clipped v as its used for touch
vv.w = std::min(v.x + v.w, m_pos.x + m_pos.w) - v.x;
vv.h = std::min(v.y + v.h, m_pos.y + m_pos.h) - v.y;
if (touch->in_range(vv)) {
callback(true, i);
return;
}
}
} else if (touch->is_scroll && touch->in_range(GetPos())) {
m_y_prog = (float)touch->initial.x - (float)touch->cur.x;
} else if (touch->is_end) {
m_yoff = ClampX(m_yoff + m_y_prog, count);
m_y_prog = 0;
}
}
void List::OnUpdateGrid(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback) {
const auto page_up_button = m_row == 1 ? Button::DPAD_LEFT : Button::L2;
const auto page_down_button = m_row == 1 ? Button::DPAD_RIGHT : Button::R2;
@@ -105,7 +235,31 @@ void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 index, s64 cou
}
}
void List::Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const {
void List::DrawHome(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const {
const auto yoff = ClampX(m_yoff + m_y_prog, count);
auto v = m_v;
v.x -= yoff;
nvgSave(vg);
nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH());
for (s64 i = 0; i < count; i++, v.x += v.w + m_pad.x) {
// skip anything not visible
if (v.x + v.w < GetX()) {
continue;
}
if (v.x > GetX() + GetW()) {
break;
}
callback(vg, theme, v, i);
}
nvgRestore(vg);
}
void List::DrawGrid(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const {
const auto yoff = ClampY(m_yoff + m_y_prog, count);
const s64 start = yoff / GetMaxY() * m_row;
gfx::drawScrollbar2(vg, theme, m_scrollbar.x, m_scrollbar.y, m_scrollbar.h, start, count, m_row, m_page);
@@ -114,7 +268,7 @@ void List::Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) cons
v.y -= yoff;
nvgSave(vg);
nvgScissor(vg, GetX(), GetY(), GetW(), GetH());
nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH());
for (s64 i = 0; i < count; v.y += v.h + m_pad.y) {
if (v.y > GetY() + GetH()) {
@@ -143,74 +297,4 @@ void List::Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) cons
nvgRestore(vg);
}
auto List::ScrollDown(s64& index, s64 step, s64 count) -> bool {
const auto old_index = index;
if (!count) {
return false;
}
if (index + step < count) {
index += step;
} else {
index = count - 1;
}
if (index != old_index) {
App::PlaySoundEffect(SoundEffect_Scroll);
s64 delta = index - old_index;
s64 start = m_yoff / GetMaxY() * m_row;
while (index < start) {
start -= m_row;
m_yoff -= GetMaxY();
}
if (index - start >= m_page) {
do {
start += m_row;
delta -= m_row;
m_yoff += GetMaxY();
} while (delta > 0 && start + m_page < count);
}
return true;
}
return false;
}
auto List::ScrollUp(s64& index, s64 step, s64 count) -> bool {
const auto old_index = index;
if (!count) {
return false;
}
if (index >= step) {
index -= step;
} else {
index = 0;
}
if (index != old_index) {
App::PlaySoundEffect(SoundEffect_Scroll);
s64 start = m_yoff / GetMaxY() * m_row;
while (index < start) {
start -= m_row;
m_yoff -= GetMaxY();
}
while (index - start >= m_page && start + m_page < count) {
start += m_row;
m_yoff += GetMaxY();
}
return true;
}
return false;
}
} // namespace sphaira::ui

View File

@@ -13,6 +13,7 @@
#include "yyjson_helper.hpp"
#include "swkbd.hpp"
#include "i18n.hpp"
#include "nro.hpp"
#include <minIni.h>
#include <string>
@@ -34,8 +35,6 @@ constexpr auto URL_JSON = "https://switch.cdn.fortheusers.org/repo.json";
constexpr auto URL_POST_FEEDBACK = "http://switchbru.com/appstore/feedback";
constexpr auto URL_GET_FEEDACK = "http://switchbru.com/appstore/feedback";
constexpr const char* INI_SECTION = "appstore";
constexpr const char* FILTER_STR[] = {
"All",
"Games",
@@ -236,7 +235,7 @@ void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, float x, f
bool crop = false;
if (iw < w || ih < h) {
rounded_image = false;
gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded ? 15 : 0);
gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded ? 5 : 0);
}
if (iw > w || ih > h) {
crop = true;
@@ -244,7 +243,7 @@ void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, float x, f
nvgIntersectScissor(vg, x, y, w, h);
}
gfx::drawImage(vg, ix, iy, iw, ih, i.image, rounded_image ? 15 : 0);
gfx::drawImage(vg, ix, iy, iw, ih, i.image, rounded_image ? 5 : 0);
if (crop) {
nvgRestore(vg);
}
@@ -590,6 +589,14 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
return true;
}
// case-insensitive version of str.find()
auto FindCaseInsensitive(std::string_view base, std::string_view term) -> bool {
const auto it = std::search(base.cbegin(), base.cend(), term.cbegin(), term.cend(), [](char a, char b){
return std::toupper(a) == std::toupper(b);
});
return it != base.cend();
}
} // namespace
EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
@@ -838,19 +845,42 @@ void EntryMenu::SetIndex(s64 index) {
}
}
auto toLower(const std::string& str) -> std::string {
std::string lower;
std::transform(str.cbegin(), str.cend(), std::back_inserter(lower), tolower);
return lower;
}
Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}, m_nro_entries{nro_entries} {
Menu::Menu() : grid::Menu{"AppStore"_i18n} {
fs::FsNativeSd fs;
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/icons");
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/banners");
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/screens");
this->SetActions(
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
if (m_is_author) {
m_is_author = false;
if (m_is_search) {
SetSearch(m_search_term);
} else {
SetFilter();
}
SetIndex(m_entry_author_jump_back);
if (m_entry_author_jump_back >= 9) {
m_list->SetYoff((((m_entry_author_jump_back - 9) + 3) / 3) * m_list->GetMaxY());
} else {
m_list->SetYoff(0);
}
} else if (m_is_search) {
m_is_search = false;
SetFilter();
SetIndex(m_entry_search_jump_back);
if (m_entry_search_jump_back >= 9) {
m_list->SetYoff(0);
m_list->SetYoff((((m_entry_search_jump_back - 9) + 3) / 3) * m_list->GetMaxY());
} else {
m_list->SetYoff(0);
}
} else {
SetPop();
}
}}),
std::make_pair(Button::A, Action{"Info"_i18n, [this](){
if (m_entries_current.empty()) {
// log_write("pushing A when empty: size: %zu count: %zu\n", repo_json.size(), m_entries_current.size());
@@ -882,17 +912,30 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
order_items.push_back("Descending"_i18n);
order_items.push_back("Ascending"_i18n);
options->Add(std::make_shared<SidebarEntryArray>("Filter"_i18n, filter_items, [this, filter_items](s64& index_out){
SetFilter((Filter)index_out);
}, (s64)m_filter));
SidebarEntryArray::Items layout_items;
layout_items.push_back("List"_i18n);
layout_items.push_back("Icon"_i18n);
layout_items.push_back("Grid"_i18n);
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
SetSort((SortType)index_out);
}, (s64)m_sort));
options->Add(std::make_shared<SidebarEntryArray>("Filter"_i18n, filter_items, [this](s64& index_out){
m_filter.Set(index_out);
SetFilter();
}, m_filter.Get()));
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](s64& index_out){
SetOrder((OrderType)index_out);
}, (s64)m_order));
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this](s64& index_out){
m_sort.Set(index_out);
SortAndFindLastFile();
}, m_sort.Get()));
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this](s64& index_out){
m_order.Set(index_out);
SortAndFindLastFile();
}, m_order.Get()));
options->Add(std::make_shared<SidebarEntryArray>("Layout"_i18n, layout_items, [this](s64& index_out){
m_layout.Set(index_out);
OnLayoutChange();
}, m_layout.Get()));
options->Add(std::make_shared<SidebarEntryCallback>("Search"_i18n, [this](){
std::string out;
@@ -922,14 +965,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
}
});
m_filter = (Filter)ini_getl(INI_SECTION, "filter", m_filter, App::CONFIG_PATH);
m_sort = (SortType)ini_getl(INI_SECTION, "sort", m_sort, App::CONFIG_PATH);
m_order = (OrderType)ini_getl(INI_SECTION, "order", m_order, App::CONFIG_PATH);
const Vec4 v{75, 110, 370, 155};
const Vec2 pad{10, 10};
m_list = std::make_unique<List>(3, 9, m_pos, v, pad);
Sort();
OnLayoutChange();
}
Menu::~Menu() {
@@ -1024,43 +1060,27 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
}
}
auto text_id = ThemeEntryID_TEXT;
if (pos == m_index) {
text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, theme, 4.f, v);
} else {
DrawElement(x, y, w, h, ThemeEntryID_GRID);
}
const auto selected = pos == m_index;
const auto image_vec = DrawEntryNoImage(vg, theme, m_layout.Get(), v, selected, e.title.c_str(), e.author.c_str(), e.version.c_str());
constexpr double image_scale = 256.0 / 115.0;
// const float image_size = 256 / image_scale;
// const float image_size_h = 150 / image_scale;
DrawIcon(vg, e.image, m_default_image, x + 20, y + 20, 115, 115, true, image_scale);
const auto image_scale = 256.0 / image_vec.w;
DrawIcon(vg, e.image, m_default_image, image_vec.x, image_vec.y, image_vec.w, image_vec.h, true, image_scale);
// gfx::drawImage(vg, x + 20, y + 20, image_size, image_size_h, image.image ? image.image : m_default_image);
nvgSave(vg);
nvgIntersectScissor(vg, v.x, v.y, w - 30.f, h); // clip
{
const float font_size = 18;
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.title.c_str());
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.author.c_str());
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.version.c_str());
}
nvgRestore(vg);
// todo: fix position on non-grid layout.
float i_size = 22;
switch (e.status) {
case EntryStatus::Get:
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image, 15);
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image, 20);
break;
case EntryStatus::Installed:
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image, 15);
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image, 20);
break;
case EntryStatus::Local:
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image, 15);
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image, 20);
break;
case EntryStatus::Update:
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image, 15);
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image, 20);
break;
}
});
@@ -1096,12 +1116,16 @@ void Menu::OnFocusGained() {
for (u32 i = 0; i < m_entries_current.size(); i++) {
if (current_entry.name == m_entries[m_entries_current[i]].name) {
SetIndex(i);
if (i >= 9) {
m_list->SetYoff((((i - 9) + 3) / 3) * m_list->GetMaxY());
const auto index = i;
const auto row = m_list->GetRow();
const auto page = m_list->GetPage();
// guesstimate where the position is
if (index >= page) {
m_list->SetYoff((((index - page) + row) / row) * m_list->GetMaxY());
} else {
m_list->SetYoff(0);
}
SetIndex(i);
break;
}
}
@@ -1185,15 +1209,20 @@ void Menu::ScanHomebrew() {
index.shrink_to_fit();
}
SetFilter(Filter_All);
SetFilter();
SetIndex(0);
Sort();
}
void Menu::Sort() {
// log_write("doing sort: size: %zu count: %zu\n", repo_json.size(), m_entries.size());
const auto sort = m_sort.Get();
const auto order = m_order.Get();
const auto filter = m_filter.Get();
// returns true if lhs should be before rhs
const auto sorter = [this](EntryMini _lhs, EntryMini _rhs) -> bool {
const auto sorter = [this, sort, order](EntryMini _lhs, EntryMini _rhs) -> bool {
const auto& lhs = m_entries[_lhs];
const auto& rhs = m_entries[_rhs];
@@ -1211,11 +1240,11 @@ void Menu::Sort() {
} else if (!(lhs.status == EntryStatus::Local) && rhs.status == EntryStatus::Local) {
return false;
} else {
switch (m_sort) {
switch (sort) {
case SortType_Updated: {
if (lhs.updated_num == rhs.updated_num) {
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
} else if (m_order == OrderType_Descending) {
} else if (order == OrderType_Descending) {
return lhs.updated_num > rhs.updated_num;
} else {
return lhs.updated_num < rhs.updated_num;
@@ -1224,7 +1253,7 @@ void Menu::Sort() {
case SortType_Downloads: {
if (lhs.app_dls == rhs.app_dls) {
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
} else if (m_order == OrderType_Descending) {
} else if (order == OrderType_Descending) {
return lhs.app_dls > rhs.app_dls;
} else {
return lhs.app_dls < rhs.app_dls;
@@ -1233,14 +1262,14 @@ void Menu::Sort() {
case SortType_Size: {
if (lhs.extracted == rhs.extracted) {
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
} else if (m_order == OrderType_Descending) {
} else if (order == OrderType_Descending) {
return lhs.extracted > rhs.extracted;
} else {
return lhs.extracted < rhs.extracted;
}
} break;
case SortType_Alphabetical: {
if (m_order == OrderType_Descending) {
if (order == OrderType_Descending) {
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
} else {
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) > 0;
@@ -1254,34 +1283,43 @@ void Menu::Sort() {
char subheader[128]{};
std::snprintf(subheader, sizeof(subheader), "Filter: %s | Sort: %s | Order: %s"_i18n.c_str(), i18n::get(FILTER_STR[m_filter]).c_str(), i18n::get(SORT_STR[m_sort]).c_str(), i18n::get(ORDER_STR[m_order]).c_str());
std::snprintf(subheader, sizeof(subheader), "Filter: %s | Sort: %s | Order: %s"_i18n.c_str(), i18n::get(FILTER_STR[filter]).c_str(), i18n::get(SORT_STR[sort]).c_str(), i18n::get(ORDER_STR[order]).c_str());
SetTitleSubHeading(subheader);
std::sort(m_entries_current.begin(), m_entries_current.end(), sorter);
}
void Menu::SetFilter(Filter filter) {
void Menu::SortAndFindLastFile() {
const auto name = GetEntry().name;
Sort();
SetIndex(0);
s64 index = -1;
for (u64 i = 0; i < m_entries_current.size(); i++) {
if (name == GetEntry(i).name) {
index = i;
break;
}
}
if (index >= 0) {
const auto row = m_list->GetRow();
const auto page = m_list->GetPage();
// guesstimate where the position is
if (index >= page) {
m_list->SetYoff((((index - page) + row) / row) * m_list->GetMaxY());
} else {
m_list->SetYoff(0);
}
SetIndex(index);
}
}
void Menu::SetFilter() {
m_is_search = false;
m_is_author = false;
RemoveAction(Button::B);
m_filter = filter;
m_entries_current = m_entries_index[m_filter];
ini_putl(INI_SECTION, "filter", m_filter, App::CONFIG_PATH);
SetIndex(0);
Sort();
}
void Menu::SetSort(SortType sort) {
m_sort = sort;
ini_putl(INI_SECTION, "sort", m_sort, App::CONFIG_PATH);
SetIndex(0);
Sort();
}
void Menu::SetOrder(OrderType order) {
m_order = order;
ini_putl(INI_SECTION, "order", m_order, App::CONFIG_PATH);
m_entries_current = m_entries_index[m_filter.Get()];
SetIndex(0);
Sort();
}
@@ -1293,27 +1331,15 @@ void Menu::SetSearch(const std::string& term) {
m_search_term = term;
m_entries_index_search.clear();
const auto query = toLower(m_search_term);
const auto npos = std::string::npos;
const auto query = m_search_term;
for (u64 i = 0; i < m_entries.size(); i++) {
const auto& e = m_entries[i];
if (toLower(e.title).find(query) != npos || toLower(e.author).find(query) != npos || toLower(e.details).find(query) != npos || toLower(e.description).find(query) != npos) {
if (FindCaseInsensitive(e.title, query) || FindCaseInsensitive(e.author, query) || FindCaseInsensitive(e.description, query)) {
m_entries_index_search.emplace_back(i);
}
}
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetFilter(m_filter);
SetIndex(m_entry_search_jump_back);
if (m_entry_search_jump_back >= 9) {
m_list->SetYoff(0);
m_list->SetYoff((((m_entry_search_jump_back - 9) + 3) / 3) * m_list->GetMaxY());
} else {
m_list->SetYoff(0);
}
}});
m_is_search = true;
m_entries_current = m_entries_index_search;
SetIndex(0);
@@ -1327,35 +1353,26 @@ void Menu::SetAuthor() {
m_author_term = m_entries[m_entries_current[m_index]].author;
m_entries_index_author.clear();
const auto query = m_author_term;
for (u64 i = 0; i < m_entries.size(); i++) {
const auto& e = m_entries[i];
if (e.author.find(m_author_term) != std::string::npos) {
if (FindCaseInsensitive(e.author, query)) {
m_entries_index_author.emplace_back(i);
}
}
SetAction(Button::B, Action{"Back"_i18n, [this](){
if (m_is_search) {
SetSearch(m_search_term);
} else {
SetFilter(m_filter);
}
SetIndex(m_entry_author_jump_back);
if (m_entry_author_jump_back >= 9) {
m_list->SetYoff((((m_entry_author_jump_back - 9) + 3) / 3) * m_list->GetMaxY());
} else {
m_list->SetYoff(0);
}
}});
m_is_author = true;
m_entries_current = m_entries_index_author;
SetIndex(0);
Sort();
}
void Menu::OnLayoutChange() {
m_index = 0;
grid::Menu::OnLayoutChange(m_list, m_layout.Get());
}
LazyImage::~LazyImage() {
if (image) {
nvgDeleteImage(App::GetVg(), image);

View File

@@ -18,10 +18,13 @@
#include "owo.hpp"
#include "swkbd.hpp"
#include "i18n.hpp"
#include "location.hpp"
#include "yati/yati.hpp"
#include "yati/source/file.hpp"
#include <minIni.h>
#include <minizip/zip.h>
#include <minizip/unzip.h>
#include <dirent.h>
#include <cstring>
@@ -57,6 +60,14 @@ constexpr std::string_view IMAGE_EXTENSIONS[] = {
constexpr std::string_view INSTALL_EXTENSIONS[] = {
"nsp", "xci", "nsz", "xcz",
};
// these are files that are already compressed or encrypted and should
// be stored raw in a zip file.
constexpr std::string_view COMPRESSED_EXTENSIONS[] = {
"zip", "xz", "7z", "rar", "tar", "nca", "nsp", "xci", "nsz", "xcz"
};
constexpr std::string_view ZIP_EXTENSIONS[] = {
"zip",
};
struct RomDatabaseEntry {
@@ -267,6 +278,25 @@ auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, const
Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i18n}, m_nro_entries{nro_entries} {
this->SetActions(
std::make_pair(Button::L2, Action{[this](){
if (!m_selected_files.empty()) {
ResetSelection();
}
const auto set = m_selected_count != m_entries_current.size();
for (u32 i = 0; i < m_entries_current.size(); i++) {
auto& e = GetEntry(i);
if (e.selected != set) {
e.selected = set;
if (set) {
m_selected_count++;
} else {
m_selected_count--;
}
}
}
}}),
std::make_pair(Button::R2, Action{[this](){
if (!m_selected_files.empty()) {
ResetSelection();
@@ -309,7 +339,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
}
}));
} else if (App::GetInstallEnable() && IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) {
InstallFile(GetEntry());
InstallFiles();
} else {
const auto assoc_list = FindFileAssocFor();
if (!assoc_list.empty()) {
@@ -384,42 +414,31 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
options->Add(std::make_shared<SidebarEntryBool>("Show Hidden"_i18n, m_show_hidden.Get(), [this](bool& v_out){
m_show_hidden.Set(v_out);
SortAndFindLastFile();
}, "Yes"_i18n, "No"_i18n));
}));
options->Add(std::make_shared<SidebarEntryBool>("Folders First"_i18n, m_folders_first.Get(), [this](bool& v_out){
m_folders_first.Set(v_out);
SortAndFindLastFile();
}, "Yes"_i18n, "No"_i18n));
}));
options->Add(std::make_shared<SidebarEntryBool>("Hidden Last"_i18n, m_hidden_last.Get(), [this](bool& v_out){
m_hidden_last.Set(v_out);
SortAndFindLastFile();
}, "Yes"_i18n, "No"_i18n));
}));
}));
if (m_entries_current.size()) {
options->Add(std::make_shared<SidebarEntryCallback>("Cut"_i18n, [this](){
if (!m_selected_count) {
AddCurrentFileToSelection(SelectedType::Cut);
} else {
AddSelectedEntries(SelectedType::Cut);
}
AddSelectedEntries(SelectedType::Cut);
}, true));
options->Add(std::make_shared<SidebarEntryCallback>("Copy"_i18n, [this](){
if (!m_selected_count) {
AddCurrentFileToSelection(SelectedType::Copy);
} else {
AddSelectedEntries(SelectedType::Copy);
}
AddSelectedEntries(SelectedType::Copy);
}, true));
options->Add(std::make_shared<SidebarEntryCallback>("Delete"_i18n, [this](){
if (!m_selected_count) {
AddCurrentFileToSelection(SelectedType::Delete);
} else {
AddSelectedEntries(SelectedType::Delete);
}
AddSelectedEntries(SelectedType::Delete);
log_write("clicked on delete\n");
App::Push(std::make_shared<OptionBox>(
"Delete Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 0, [this](auto op_index){
@@ -475,32 +494,96 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
}));
}
// if install is enabled, check if all currently selected files are installable.
if (m_entries_current.size() && App::GetInstallEnable()) {
bool should_install = true;
// returns true if all entries match the ext array.
const auto check_all_ext = [this](auto& exts){
if (!m_selected_count) {
should_install = IsExtension(GetEntry().GetExtension(), INSTALL_EXTENSIONS);
return IsExtension(GetEntry().GetExtension(), exts);
} else {
const auto entries = GetSelectedEntries();
for (auto&e : entries) {
if (!IsExtension(e.GetExtension(), INSTALL_EXTENSIONS)) {
should_install = false;
break;
if (!IsExtension(e.GetExtension(), exts)) {
return false;
}
}
}
return true;
};
if (should_install) {
// if install is enabled, check if all currently selected files are installable.
if (m_entries_current.size() && App::GetInstallEnable()) {
if (check_all_ext(INSTALL_EXTENSIONS)) {
options->Add(std::make_shared<SidebarEntryCallback>("Install"_i18n, [this](){
if (!m_selected_count) {
InstallFile(GetEntry());
InstallFiles();
}));
}
}
if (m_fs_type == FsType::Sd && m_entries_current.size()) {
if (App::GetInstallEnable() && HasTypeInSelectedEntries(FsDirEntryType_File) && !m_selected_count && (GetEntry().GetExtension() == "nro" || !FindFileAssocFor().empty())) {
options->Add(std::make_shared<SidebarEntryCallback>("Install Forwarder"_i18n, [this](){;
if (App::GetInstallPrompt()) {
App::Push(std::make_shared<OptionBox>(
"WARNING: Installing forwarders will lead to a ban!"_i18n,
"Back"_i18n, "Install"_i18n, 0, [this](auto op_index){
if (op_index && *op_index) {
InstallForwarder();
}
}
));
} else {
InstallFiles(GetSelectedEntries());
InstallForwarder();
}
}));
}
}
if (m_entries_current.size()) {
if (check_all_ext(ZIP_EXTENSIONS)) {
options->Add(std::make_shared<SidebarEntryCallback>("Extract zip"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Extract Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<SidebarEntryCallback>("Extract here"_i18n, [this](){
UnzipFiles("");
}));
options->Add(std::make_shared<SidebarEntryCallback>("Extract to root"_i18n, [this](){
App::Push(std::make_shared<OptionBox>("Are you sure you want to extract to root?"_i18n,
"No"_i18n, "Yes"_i18n, 0, [this](auto op_index){
if (op_index && *op_index) {
UnzipFiles("/");
}
}));
}));
options->Add(std::make_shared<SidebarEntryCallback>("Extract to..."_i18n, [this](){
std::string out;
if (R_SUCCEEDED(swkbd::ShowText(out, "Enter the path to the folder to extract into", fs::AppendPath(m_path, ""))) && !out.empty()) {
UnzipFiles(out);
}
}));
}));
}
if (!check_all_ext(ZIP_EXTENSIONS) || m_selected_count) {
options->Add(std::make_shared<SidebarEntryCallback>("Compress to zip"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Compress Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<SidebarEntryCallback>("Compress"_i18n, [this](){
ZipFiles("");
}));
options->Add(std::make_shared<SidebarEntryCallback>("Compress to..."_i18n, [this](){
std::string out;
if (R_SUCCEEDED(swkbd::ShowText(out, "Enter the path to the folder to extract into", m_path)) && !out.empty()) {
ZipFiles(out);
}
}));
}));
}
}
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
@@ -529,7 +612,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
options->Add(std::make_shared<SidebarEntryCallback>("Create Folder"_i18n, [this](){
std::string out;
if (R_SUCCEEDED(swkbd::ShowText(out, "Set Folder Name"_i18n.c_str())) && !out.empty()) {
if (R_SUCCEEDED(swkbd::ShowText(out, "Set Folder Name"_i18n.c_str(), fs::AppendPath(m_path, ""))) && !out.empty()) {
App::PopToMenu();
fs::FsPath full_path;
@@ -555,28 +638,15 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
}
if (m_fs_type == FsType::Sd && m_entries_current.size()) {
if (App::GetInstallEnable() && HasTypeInSelectedEntries(FsDirEntryType_File) && !m_selected_count && (GetEntry().GetExtension() == "nro" || !FindFileAssocFor().empty())) {
options->Add(std::make_shared<SidebarEntryCallback>("Install Forwarder"_i18n, [this](){;
if (App::GetInstallPrompt()) {
App::Push(std::make_shared<OptionBox>(
"WARNING: Installing forwarders will lead to a ban!"_i18n,
"Back"_i18n, "Install"_i18n, 0, [this](auto op_index){
if (op_index && *op_index) {
InstallForwarder();
}
}
));
} else {
InstallForwarder();
}
}));
}
options->Add(std::make_shared<SidebarEntryCallback>("Upload"_i18n, [this](){
UploadFiles();
}));
}
options->Add(std::make_shared<SidebarEntryBool>("Ignore read only"_i18n, m_ignore_read_only.Get(), [this](bool& v_out){
m_ignore_read_only.Set(v_out);
m_fs->SetIgnoreReadOnly(v_out);
}, "Yes"_i18n, "No"_i18n));
}));
SidebarEntryArray::Items mount_items;
mount_items.push_back("Sd"_i18n);
@@ -650,7 +720,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
}
auto text_id = ThemeEntryID_TEXT;
if (m_index == i) {
const auto selected = m_index == i;
if (selected) {
text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, theme, 4.f, v);
} else {
@@ -673,7 +744,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
} else if (IsExtension(ext, INSTALL_EXTENSIONS)) {
// todo: maybe replace this icon with something else?
icon = ThemeEntryID_ICON_NRO;
} else if (IsExtension(ext, "zip")) {
} else if (IsExtension(ext, ZIP_EXTENSIONS)) {
icon = ThemeEntryID_ICON_ZIP;
} else if (IsExtension(ext, "nro")) {
icon = ThemeEntryID_ICON_NRO;
@@ -820,12 +891,9 @@ void Menu::InstallForwarder() {
));
}
void Menu::InstallFile(const FileEntry& target) {
std::vector<FileEntry> targets{target};
InstallFiles(targets);
}
void Menu::InstallFiles() {
const auto targets = GetSelectedEntries();
void Menu::InstallFiles(const std::vector<FileEntry>& targets) {
App::Push(std::make_shared<OptionBox>("Install Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 0, [this, targets](auto op_index){
if (op_index && *op_index) {
App::PopToMenu();
@@ -848,6 +916,343 @@ void Menu::InstallFiles(const std::vector<FileEntry>& targets) {
}));
}
void Menu::UnzipFiles(fs::FsPath dir_path) {
const auto targets = GetSelectedEntries();
// set to current path.
if (dir_path.empty()) {
dir_path = m_path;
}
App::Push(std::make_shared<ui::ProgressBox>(0, "Extracting "_i18n, "", [this, dir_path, targets](auto pbox) mutable -> bool {
constexpr auto chunk_size = 1024 * 512; // 512KiB
auto& fs = *m_fs.get();
for (auto& e : targets) {
pbox->SetTitle(e.GetName());
const auto zip_out = GetNewPath(e);
auto zfile = unzOpen64(zip_out);
if (!zfile) {
log_write("failed to open zip: %s\n", zip_out.s);
return false;
}
ON_SCOPE_EXIT(unzClose(zfile));
unz_global_info64 pglobal_info;
if (UNZ_OK != unzGetGlobalInfo64(zfile, &pglobal_info)) {
return false;
}
for (int i = 0; i < pglobal_info.number_entry; i++) {
if (i > 0) {
if (UNZ_OK != unzGoToNextFile(zfile)) {
log_write("failed to unzGoToNextFile\n");
return false;
}
}
if (UNZ_OK != unzOpenCurrentFile(zfile)) {
log_write("failed to open current file\n");
return false;
}
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
unz_file_info64 info;
char name[512];
if (UNZ_OK != unzGetCurrentFileInfo64(zfile, &info, name, sizeof(name), 0, 0, 0, 0)) {
log_write("failed to get current info\n");
return false;
}
const auto file_path = fs::AppendPath(dir_path, name);
pbox->NewTransfer(name);
// create directories
fs.CreateDirectoryRecursivelyWithPath(file_path);
Result rc;
if (R_FAILED(rc = fs.CreateFile(file_path, info.uncompressed_size, 0)) && rc != FsError_PathAlreadyExists) {
log_write("failed to create file: %s 0x%04X\n", file_path.s, rc);
return false;
}
FsFile f;
if (R_FAILED(rc = fs.OpenFile(file_path, FsOpenMode_Write, &f))) {
log_write("failed to open file: %s 0x%04X\n", file_path.s, rc);
return false;
}
ON_SCOPE_EXIT(fsFileClose(&f));
if (R_FAILED(rc = fsFileSetSize(&f, info.uncompressed_size))) {
log_write("failed to set file size: %s 0x%04X\n", file_path.s, rc);
return false;
}
std::vector<char> buf(chunk_size);
s64 offset{};
while (offset < info.uncompressed_size) {
if (pbox->ShouldExit()) {
return false;
}
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
if (bytes_read <= 0) {
log_write("failed to read zip file: %s\n", name);
return false;
}
if (R_FAILED(rc = fsFileWrite(&f, offset, buf.data(), bytes_read, FsWriteOption_None))) {
log_write("failed to write file: %s 0x%04X\n", file_path.s, rc);
return false;
}
pbox->UpdateTransfer(offset, info.uncompressed_size);
offset += bytes_read;
}
}
}
return true;
}, [this](bool success){
if (success) {
App::Notify("Extract success!");
} else {
App::Notify("Extract failed!");
}
Scan(m_path);
log_write("did extract\n");
}));
}
void Menu::ZipFiles(fs::FsPath zip_out) {
const auto targets = GetSelectedEntries();
// set to current path.
if (zip_out.empty()) {
if (std::size(targets) == 1) {
const auto name = targets[0].name;
const auto ext = std::strrchr(targets[0].name, '.');
fs::FsPath file_path;
if (!ext) {
std::snprintf(file_path, sizeof(file_path), "%s.zip", name);
} else {
std::snprintf(file_path, sizeof(file_path), "%.*s.zip", (int)(ext - name), name);
}
zip_out = fs::AppendPath(m_path, file_path);
log_write("zip out: %s name: %s file_path: %s\n", zip_out.s, name, file_path.s);
} else {
// loop until we find an unused file name.
for (u64 i = 0; ; i++) {
fs::FsPath file_path = "Archive.zip";
if (i) {
std::snprintf(file_path, sizeof(file_path), "Archive (%zu).zip", i);
}
zip_out = fs::AppendPath(m_path, file_path);
if (!fs::FileExists(&m_fs->m_fs, zip_out)) {
break;
}
}
}
} else {
if (!std::string_view(zip_out).ends_with(".zip")) {
zip_out += ".zip";
}
}
App::Push(std::make_shared<ui::ProgressBox>(0, "Compressing "_i18n, "", [this, zip_out, targets](auto pbox) mutable -> bool {
constexpr auto chunk_size = 1024 * 512; // 512KiB
auto& fs = *m_fs.get();
const auto t = std::time(NULL);
const auto tm = std::localtime(&t);
// pre-calculate the time rather than calculate it in the loop.
zip_fileinfo zip_info{};
zip_info.tmz_date.tm_sec = tm->tm_sec;
zip_info.tmz_date.tm_min = tm->tm_min;
zip_info.tmz_date.tm_hour = tm->tm_hour;
zip_info.tmz_date.tm_mday = tm->tm_mday;
zip_info.tmz_date.tm_mon = tm->tm_mon;
zip_info.tmz_date.tm_year = tm->tm_year;
auto zfile = zipOpen(zip_out, APPEND_STATUS_CREATE);
if (!zfile) {
log_write("failed to open zip: %s\n", zip_out.s);
return false;
}
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_VERSION_HASH));
const auto zip_add = [&](const fs::FsPath& file_path){
// the file name needs to be relative to the current directory.
const char* file_name_in_zip = file_path.s + std::strlen(m_path);
// root paths are banned in zips, they will warn when extracting otherwise.
if (file_name_in_zip[0] == '/') {
file_name_in_zip++;
}
pbox->NewTransfer(file_name_in_zip);
const auto ext = std::strrchr(file_name_in_zip, '.');
const auto raw = ext && IsExtension(ext + 1, COMPRESSED_EXTENSIONS);
if (ZIP_OK != zipOpenNewFileInZip2(zfile, file_name_in_zip, &zip_info, NULL, 0, NULL, 0, NULL, Z_DEFLATED, Z_DEFAULT_COMPRESSION, raw)) {
return false;
}
ON_SCOPE_EXIT(zipCloseFileInZip(zfile));
FsFile f;
Result rc;
if (R_FAILED(rc = fs.OpenFile(file_path, FsOpenMode_Read, &f))) {
log_write("failed to open file: %s 0x%04X\n", file_path.s, rc);
return false;
}
ON_SCOPE_EXIT(fsFileClose(&f));
s64 file_size;
if (R_FAILED(rc = fsFileGetSize(&f, &file_size))) {
log_write("failed to get file size: %s 0x%04X\n", file_path.s, rc);
return false;
}
std::vector<char> buf(chunk_size);
s64 offset{};
while (offset < file_size) {
if (pbox->ShouldExit()) {
return false;
}
u64 bytes_read;
if (R_FAILED(rc = fsFileRead(&f, offset, buf.data(), buf.size(), FsReadOption_None, &bytes_read))) {
log_write("failed to write file: %s 0x%04X\n", file_path.s, rc);
return false;
}
if (ZIP_OK != zipWriteInFileInZip(zfile, buf.data(), bytes_read)) {
log_write("failed to write zip file: %s\n", file_path.s);
return false;
}
pbox->UpdateTransfer(offset, file_size);
offset += bytes_read;
}
return true;
};
for (auto& e : targets) {
pbox->SetTitle(e.GetName());
if (e.IsFile()) {
const auto file_path = GetNewPath(e);
if (!zip_add(file_path)) {
return false;
}
} else {
FsDirCollections collections;
get_collections(GetNewPath(e), e.name, collections);
for (const auto& collection : collections) {
for (const auto& file : collection.files) {
const auto file_path = fs::AppendPath(collection.path, file.name);
if (!zip_add(file_path)) {
return false;
}
}
}
}
}
return true;
}, [this](bool success){
if (success) {
App::Notify("Compress success!");
} else {
App::Notify("Compress failed!");
}
Scan(m_path);
log_write("did compress\n");
}));
}
void Menu::UploadFiles() {
const auto targets = GetSelectedEntries();
const auto network_locations = location::Load();
if (network_locations.empty()) {
App::Notify("No upload locations set!");
return;
}
PopupList::Items items;
for (const auto&p : network_locations) {
items.emplace_back(p.name);
}
App::Push(std::make_shared<PopupList>(
"Select upload location"_i18n, items, [this, network_locations](auto op_index){
if (!op_index) {
return;
}
const auto loc = network_locations[*op_index];
App::Push(std::make_shared<ProgressBox>(0, "Uploading"_i18n, "", [this, loc](auto pbox) -> bool {
auto targets = GetSelectedEntries();
const auto file_add = [&](const fs::FsPath& file_path, const char* name){
// the file name needs to be relative to the current directory.
const auto relative_file_name = file_path.s + std::strlen(m_path);
pbox->SetTitle(name);
pbox->NewTransfer(relative_file_name);
const auto result = curl::Api().FromFile(
CURL_LOCATION_TO_API(loc),
curl::Path{file_path},
curl::OnProgress{pbox->OnDownloadProgressCallback()},
curl::UploadInfo{relative_file_name}
);
return result.success;
};
for (auto& e : targets) {
if (e.IsFile()) {
const auto file_path = GetNewPath(e);
if (!file_add(file_path, e.GetName().c_str())) {
return false;
}
} else {
FsDirCollections collections;
get_collections(GetNewPath(e), e.name, collections);
for (const auto& collection : collections) {
for (const auto& file : collection.files) {
const auto file_path = fs::AppendPath(collection.path, file.name);
if (!file_add(file_path, file.name)) {
return false;
}
}
}
}
}
return true;
}, [this](bool success){
ResetSelection();
if (success) {
App::Notify("Upload successfull!");
log_write("Upload successfull!!!\n");
} else {
App::Notify("Upload failed!");
log_write("Upload failed!!!\n");
}
}));
}
));
}
auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
log_write("new scan path: %s\n", new_path.s);
if (!is_walk_up && !m_path.empty() && !m_entries_current.empty()) {

View File

@@ -7,6 +7,7 @@
#include "i18n.hpp"
#include "ftpsrv_helper.hpp"
#include <cstring>
#include <algorithm>
namespace sphaira::ui::menu::ftp {
namespace {
@@ -151,6 +152,12 @@ Menu::Menu() : MenuBase{"FTP Install (EXPERIMENTAL)"_i18n} {
}});
mutexInit(&m_mutex);
m_was_ftp_enabled = App::GetFtpEnable();
if (!m_was_ftp_enabled) {
log_write("[FTP] wasn't enabled, forcefully enabling\n");
App::SetFtpEnable(true);
}
ftpsrv::InitInstallMode(this, OnInstallStart, OnInstallWrite, OnInstallClose);
m_port = ftpsrv::GetPort();
@@ -170,6 +177,11 @@ Menu::~Menu() {
m_source->Disable();
}
if (!m_was_ftp_enabled) {
log_write("[FTP] disabling on exit\n");
App::SetFtpEnable(false);
}
log_write("closing data!!!!\n");
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
#include <cstring>
#include <algorithm>
namespace sphaira::ui::menu::gc {
namespace {
@@ -44,6 +45,13 @@ auto BuildGcPath(const char* name, const FsGameCardHandle* handle, FsGameCardPar
return path;
}
Result fsOpenGameCardDetectionEventNotifier(FsEventNotifier* out) {
return serviceDispatch(fsGetServiceSession(), 501,
.out_num_objects = 1,
.out_objects = &out->s
);
}
auto InRange(u64 off, u64 offset, u64 size) -> bool {
return off < offset + size && off >= offset;
}
@@ -174,30 +182,24 @@ Menu::Menu() : MenuBase{"GameCard"_i18n} {
const Vec2 pad{0, 125 - v.h};
m_list = std::make_unique<List>(1, 3, m_pos, v, pad);
nsInitialize();
fsOpenDeviceOperator(std::addressof(m_dev_op));
UpdateStorageSize();
fsOpenGameCardDetectionEventNotifier(std::addressof(m_event_notifier));
fsEventNotifierGetEventHandle(std::addressof(m_event_notifier), std::addressof(m_event), true);
}
Menu::~Menu() {
GcUnmount();
eventClose(std::addressof(m_event));
fsEventNotifierClose(std::addressof(m_event_notifier));
fsDeviceOperatorClose(std::addressof(m_dev_op));
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
// poll for the gamecard first before handling inputs as the gamecard
// may have been removed, thus pressing A would fail.
bool inserted{};
GcPoll(&inserted);
if (m_mounted != inserted) {
log_write("gc state changed\n");
m_mounted = inserted;
if (m_mounted) {
log_write("trying to mount\n");
m_mounted = R_SUCCEEDED(GcMount());
} else {
log_write("trying to unmount\n");
GcUnmount();
}
if (R_SUCCEEDED(eventWait(std::addressof(m_event), 0))) {
GcOnEvent();
}
MenuBase::Update(controller, touch);
@@ -267,6 +269,13 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
});
}
void Menu::OnFocusGained() {
MenuBase::OnFocusGained();
GcOnEvent();
UpdateStorageSize();
}
Result Menu::GcMount() {
GcUnmount();
@@ -312,7 +321,7 @@ Result Menu::GcMount() {
std::vector<u8> extended_header;
std::vector<NcmPackagedContentInfo> infos;
const auto path = BuildGcPath(e.name, &m_handle);
R_TRY(yati::ParseCnmtNca(path, header, extended_header, infos));
R_TRY(nca::ParseCnmt(path, 0, header, extended_header, infos));
u8 key_gen;
FsRightsId rights_id;
@@ -321,23 +330,24 @@ Result Menu::GcMount() {
// always add tickets, yati will ignore them if not needed.
GcCollections collections;
// add cnmt file.
collections.emplace_back(e.name, e.file_size, NcmContentType_Meta);
collections.emplace_back(e.name, e.file_size, NcmContentType_Meta, 0);
for (const auto& info : infos) {
for (const auto& packed_info : infos) {
const auto& info = packed_info.info;
// these don't exist for gamecards, however i may copy/paste this code
// somewhere so i'm future proofing against myself.
if (info.info.content_type == NcmContentType_DeltaFragment) {
if (info.content_type == NcmContentType_DeltaFragment) {
continue;
}
// find the nca file, this will never fail for gamecards, see above comment.
const auto str = hexIdToStr(info.info.content_id);
const auto str = hexIdToStr(info.content_id);
const auto it = std::find_if(buf.cbegin(), buf.cend(), [str](auto& e){
return !std::strncmp(str.str, e.name, std::strlen(str.str));
});
R_UNLESS(it != buf.cend(), yati::Result_NcaNotFound);
collections.emplace_back(it->name, it->file_size, info.info.content_type);
collections.emplace_back(it->name, it->file_size, info.content_type, info.id_offset);
}
const auto app_id = ncm::GetAppId(header);
@@ -388,20 +398,18 @@ Result Menu::GcMount() {
} else {
App::Notify("Gc install failed!"_i18n);
}
UpdateStorageSize();
}));
}
}
}});
if (m_entries.size() > 1) {
SetAction(Button::L, Action{"Prev"_i18n, [this](){
SetAction(Button::L2, Action{"Prev"_i18n, [this](){
if (m_entry_index != 0) {
OnChangeIndex(m_entry_index - 1);
}
}});
SetAction(Button::R, Action{"Next"_i18n, [this](){
SetAction(Button::R2, Action{"Next"_i18n, [this](){
if (m_entry_index < m_entries.size()) {
OnChangeIndex(m_entry_index + 1);
}
@@ -409,6 +417,7 @@ Result Menu::GcMount() {
}
OnChangeIndex(0);
m_mounted = true;
R_SUCCEED();
}
@@ -420,8 +429,8 @@ void Menu::GcUnmount() {
m_lang_entry = {};
FreeImage();
RemoveAction(Button::L);
RemoveAction(Button::R);
RemoveAction(Button::L2);
RemoveAction(Button::R2);
}
Result Menu::GcPoll(bool* inserted) {
@@ -439,6 +448,28 @@ Result Menu::GcPoll(bool* inserted) {
R_SUCCEED();
}
Result Menu::GcOnEvent() {
bool inserted{};
R_TRY(GcPoll(&inserted));
if (m_mounted != inserted) {
log_write("gc state changed\n");
m_mounted = inserted;
if (m_mounted) {
log_write("trying to mount\n");
m_mounted = R_SUCCEEDED(GcMount());
if (m_mounted) {
App::PlaySoundEffect(SoundEffect::SoundEffect_Startup);
}
} else {
log_write("trying to unmount\n");
GcUnmount();
}
}
R_SUCCEED();
}
Result Menu::UpdateStorageSize() {
fs::FsNativeContentStorage fs_nand{FsContentStorageId_User};
fs::FsNativeContentStorage fs_sd{FsContentStorageId_SdCard};
@@ -464,6 +495,31 @@ void Menu::OnChangeIndex(s64 new_index) {
const auto index = m_entries.empty() ? 0 : m_entry_index + 1;
this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size()));
const auto id = m_entries[m_entry_index].app_id;
if (hosversionBefore(20,0,0)) {
TimeStamp ts;
auto control = std::make_unique<NsApplicationControlData>();
u64 control_size;
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, id, control.get(), sizeof(NsApplicationControlData), &control_size))) {
log_write("\t\t[ns control cache] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
NacpLanguageEntry* lang_entry{};
nacpGetLanguageEntry(&control->nacp, &lang_entry);
if (lang_entry) {
m_lang_entry = *lang_entry;
}
const auto jpeg_size = control_size - sizeof(NacpStruct);
m_icon = nvgCreateImageMem(App::GetVg(), 0, control->icon, jpeg_size);
if (m_icon > 0) {
return;
}
}
}
// nsGetApplicationControlData() will fail if it's the first time
// mounting a gamecard if the image is not already cached.
// waiting 1-2s after mount, then calling seems to work.
@@ -475,7 +531,16 @@ void Menu::OnChangeIndex(s64 new_index) {
NacpStruct nacp;
std::vector<u8> icon;
const auto path = BuildGcPath(collection.name.c_str(), &m_handle);
if (R_SUCCEEDED(yati::ParseControlNca(path, m_entries[m_entry_index].app_id, &nacp, sizeof(nacp), &icon))) {
u64 program_id = id | collection.id_offset;
if (hosversionAtLeast(17, 0, 0)) {
fsGetProgramId(&program_id, path, FsContentAttributes_All);
}
TimeStamp ts;
if (R_SUCCEEDED(nca::ParseControl(path, program_id, &nacp, sizeof(nacp), &icon))) {
log_write("\t\tnca::ParseControl(): %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
log_write("managed to parse control nca %s\n", path.s);
NacpLanguageEntry* lang_entry{};
nacpGetLanguageEntry(&nacp, &lang_entry);

View File

@@ -0,0 +1,81 @@
#include "app.hpp"
#include "ui/menus/grid_menu_base.hpp"
#include "ui/nvg_util.hpp"
namespace sphaira::ui::menu::grid {
void Menu::DrawEntry(NVGcontext* vg, Theme* theme, int layout, const Vec4& v, bool selected, int image, const char* name, const char* author, const char* version) {
DrawEntry(vg, theme, true, layout, v, selected, image, name, author, version);
}
Vec4 Menu::DrawEntryNoImage(NVGcontext* vg, Theme* theme, int layout, const Vec4& v, bool selected, const char* name, const char* author, const char* version) {
return DrawEntry(vg, theme, false, layout, v, selected, 0, name, author, version);
}
Vec4 Menu::DrawEntry(NVGcontext* vg, Theme* theme, bool draw_image, int layout, const Vec4& v, bool selected, int image, const char* name, const char* author, const char* version) {
const auto& [x, y, w, h] = v;
auto text_id = ThemeEntryID_TEXT;
if (selected) {
text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, theme, 4.f, v);
} else {
DrawElement(v, ThemeEntryID_GRID);
}
Vec4 image_v = v;
if (layout == LayoutType_GridDetail) {
image_v.x += 20;
image_v.y += 20;
image_v.w = 115;
image_v.h = 115;
const auto text_off = 148;
const auto text_x = x + text_off;
const auto text_clip_w = w - 30.f - text_off;
const float font_size = 18;
m_scroll_name.Draw(vg, selected, text_x, y + 45, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), name);
m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), author);
m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), version);
} else {
if (selected) {
gfx::drawAppLable(vg, theme, m_scroll_name, x, y, w, name);
}
}
if (draw_image) {
gfx::drawImage(vg, image_v, image ?: App::GetDefaultImage(), 5);
}
return image_v;
}
void Menu::OnLayoutChange(std::unique_ptr<List>& list, int layout) {
m_scroll_name.Reset();
m_scroll_author.Reset();
m_scroll_version.Reset();
switch (layout) {
case LayoutType_List: {
const Vec2 pad{14, 14};
const Vec4 v{106, 194, 256, 256};
list = std::make_unique<List>(1, 4, m_pos, v, pad);
list->SetLayout(List::Layout::HOME);
} break;
case LayoutType_Grid: {
const Vec2 pad{10, 10};
const Vec4 v{93, 186, 174, 174};
list = std::make_unique<List>(6, 6*2, m_pos, v, pad);
} break;
case LayoutType_GridDetail: {
const Vec2 pad{10, 10};
const Vec4 v{75, 110, 370, 155};
list = std::make_unique<List>(3, 3*3, m_pos, v, pad);
} break;
}
}
} // namespace sphaira::ui::menu::grid

View File

@@ -13,6 +13,7 @@
#include <minIni.h>
#include <utility>
#include <algorithm>
namespace sphaira::ui::menu::homebrew {
namespace {
@@ -24,9 +25,14 @@ auto GenerateStarPath(const fs::FsPath& nro_path) -> fs::FsPath {
return out;
}
void FreeEntry(NVGcontext* vg, NroEntry& e) {
nvgDeleteImage(vg, e.image);
e.image = 0;
}
} // namespace
Menu::Menu() : MenuBase{"Homebrew"_i18n} {
Menu::Menu() : grid::Menu{"Homebrew"_i18n} {
this->SetActions(
std::make_pair(Button::A, Action{"Launch"_i18n, [this](){
nro_launch(m_entries[m_index].path);
@@ -52,6 +58,11 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
order_items.push_back("Descending"_i18n);
order_items.push_back("Ascending"_i18n);
SidebarEntryArray::Items layout_items;
layout_items.push_back("List"_i18n);
layout_items.push_back("Icon"_i18n);
layout_items.push_back("Grid"_i18n);
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
m_sort.Set(index_out);
SortAndFindLastFile();
@@ -62,9 +73,14 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
SortAndFindLastFile();
}, m_order.Get()));
options->Add(std::make_shared<SidebarEntryArray>("Layout"_i18n, layout_items, [this](s64& index_out){
m_layout.Set(index_out);
OnLayoutChange();
}, m_layout.Get()));
options->Add(std::make_shared<SidebarEntryBool>("Hide Sphaira"_i18n, m_hide_sphaira.Get(), [this](bool& enable){
m_hide_sphaira.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
}));
#if 0
@@ -80,11 +96,12 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
"Back"_i18n, "Delete"_i18n, 1, [this](auto op_index){
if (op_index && *op_index) {
if (R_SUCCEEDED(fs::FsNativeSd().DeleteFile(m_entries[m_index].path))) {
FreeEntry(App::GetVg(), m_entries[m_index]);
m_entries.erase(m_entries.begin() + m_index);
SetIndex(m_index ? m_index - 1 : 0);
}
}
}
}, m_entries[m_index].image
));
}, true));
@@ -97,7 +114,7 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
if (op_index && *op_index) {
InstallHomebrew();
}
}
}, m_entries[m_index].image
));
} else {
InstallHomebrew();
@@ -108,17 +125,11 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
}})
);
const Vec4 v{75, 110, 370, 155};
const Vec2 pad{10, 10};
m_list = std::make_unique<List>(3, 9, m_pos, v, pad);
OnLayoutChange();
}
Menu::~Menu() {
auto vg = App::GetVg();
for (auto&p : m_entries) {
nvgDeleteImage(vg, p.image);
}
FreeEntries();
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
@@ -141,7 +152,6 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
int image_load_count = 0;
m_list->Draw(vg, theme, m_entries.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
const auto& [x, y, w, h] = v;
auto& e = m_entries[pos];
// lazy load image
@@ -159,34 +169,24 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
}
}
auto text_id = ThemeEntryID_TEXT;
if (pos == m_index) {
text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, theme, 4.f, v);
} else {
DrawElement(v, ThemeEntryID_GRID);
}
const float image_size = 115;
gfx::drawImage(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage(), 15);
nvgSave(vg);
nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip
{
bool has_star = false;
if (IsStarEnabled()) {
if (!e.has_star.has_value()) {
e.has_star = fs::FsNativeSd().FileExists(GenerateStarPath(e.path));
}
has_star = e.has_star.value();
bool has_star = false;
if (IsStarEnabled()) {
if (!e.has_star.has_value()) {
e.has_star = fs::FsNativeSd().FileExists(GenerateStarPath(e.path));
}
const float font_size = 18;
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), "%s%s", has_star ? "\u2605 " : "", e.GetName());
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetAuthor());
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetDisplayVersion());
has_star = e.has_star.value();
}
nvgRestore(vg);
std::string name;
if (has_star) {
name = std::string("\u2605 ") + e.GetName();
} else {
name = e.GetName();
}
const auto selected = pos == m_index;
DrawEntry(vg, theme, m_layout.Get(), v, selected, e.image, name.c_str(), e.GetAuthor(), e.GetDisplayVersion());
});
}
@@ -238,6 +238,7 @@ void Menu::InstallHomebrew() {
void Menu::ScanHomebrew() {
TimeStamp ts;
FreeEntries();
nro_scan("/switch", m_entries, m_hide_sphaira.Get());
log_write("nros found: %zu time_taken: %.2f\n", m_entries.size(), ts.GetSecondsD());
@@ -384,9 +385,11 @@ void Menu::SortAndFindLastFile() {
}
if (index >= 0) {
const auto row = m_list->GetRow();
const auto page = m_list->GetPage();
// guesstimate where the position is
if (index >= 9) {
m_list->SetYoff((((index - 9) + 3) / 3) * m_list->GetMaxY());
if (index >= page) {
m_list->SetYoff((((index - page) + row) / row) * m_list->GetMaxY());
} else {
m_list->SetYoff(0);
}
@@ -394,6 +397,21 @@ void Menu::SortAndFindLastFile() {
}
}
void Menu::FreeEntries() {
auto vg = App::GetVg();
for (auto&p : m_entries) {
FreeEntry(vg, p);
}
m_entries.clear();
}
void Menu::OnLayoutChange() {
m_index = 0;
grid::Menu::OnLayoutChange(m_list, m_layout.Get());
}
Result Menu::InstallHomebrew(const fs::FsPath& path, const NacpStruct& nacp, const std::vector<u8>& icon) {
OwoConfig config{};
config.nro_path = path.toString();

View File

@@ -86,20 +86,7 @@ Menu::Menu() : MenuBase{"Irs"_i18n} {
SidebarEntryArray::Items controller_str;
for (u32 i = 0; i < IRS_MAX_CAMERAS; i++) {
const auto& e = m_entries[i];
std::string text = "Pad "_i18n + (i == 8 ? "HandHeld"_i18n : std::to_string(i));
switch (e.status) {
case IrsIrCameraStatus_Available:
text += " (Available)"_i18n;
break;
case IrsIrCameraStatus_Unsupported:
text += " (Unsupported)"_i18n;
break;
case IrsIrCameraStatus_Unconnected:
text += " (Unconnected)"_i18n;
break;
}
controller_str.emplace_back(text);
controller_str.emplace_back(GetEntryName(i));
}
SidebarEntryArray::Items rotation_str;
@@ -189,7 +176,7 @@ Menu::Menu() : MenuBase{"Irs"_i18n} {
options->Add(std::make_shared<SidebarEntryBool>("External Light Filter"_i18n, m_config.is_external_light_filter_enabled, [this](bool& enable){
m_config.is_external_light_filter_enabled = enable;
UpdateConfig(&m_config);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
}
options->Add(std::make_shared<SidebarEntryCallback>("Load Default"_i18n, [this](){
@@ -233,6 +220,7 @@ Menu::~Menu() {
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
PollCameraStatus();
SetTitleSubHeading(GetEntryName(m_index));
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
@@ -308,6 +296,20 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
void Menu::OnFocusGained() {
MenuBase::OnFocusGained();
if (m_entries[m_index].status != IrsIrCameraStatus_Available) {
// poll to get first available handle.
PollCameraStatus(false);
// find the first available entry and connect to that.
for (s64 i = 0; i < std::size(m_entries); i++) {
if (m_entries[i].status == IrsIrCameraStatus_Available) {
m_index = i;
UpdateConfig(&m_config);
break;
}
}
}
}
void Menu::PollCameraStatus(bool statup) {
@@ -530,4 +532,21 @@ void Menu::updateColourArray() {
UpdateImage();
}
auto Menu::GetEntryName(s64 i) -> std::string {
const auto& e = m_entries[i];
std::string text = "Pad "_i18n + (i == 8 ? "HandHeld"_i18n : std::to_string(i));
switch (e.status) {
case IrsIrCameraStatus_Available:
text += " (Available)"_i18n;
break;
case IrsIrCameraStatus_Unsupported:
text += " (Unsupported)"_i18n;
break;
case IrsIrCameraStatus_Unconnected:
text += " (Unconnected)"_i18n;
break;
}
return text;
}
} // namespace sphaira::ui::menu::irs

View File

@@ -6,6 +6,15 @@
#include "ui/progress_box.hpp"
#include "ui/error_box.hpp"
#include "ui/menus/irs_menu.hpp"
#include "ui/menus/themezer.hpp"
#include "ui/menus/ghdl.hpp"
#include "ui/menus/usb_menu.hpp"
#include "ui/menus/ftp_menu.hpp"
#include "ui/menus/gc_menu.hpp"
#include "ui/menus/game_menu.hpp"
#include "ui/menus/appstore.hpp"
#include "app.hpp"
#include "log.hpp"
#include "download.hpp"
@@ -22,6 +31,22 @@ namespace {
constexpr const char* GITHUB_URL{"https://api.github.com/repos/ITotalJustice/sphaira/releases/latest"};
constexpr fs::FsPath CACHE_PATH{"/switch/sphaira/cache/sphaira_latest.json"};
template<typename T>
auto MiscMenuFuncGenerator() {
return std::make_shared<T>();
}
const MiscMenuEntry MISC_MENU_ENTRIES[] = {
{ .name = "Appstore", .title = "Appstore", .func = MiscMenuFuncGenerator<ui::menu::appstore::Menu>, .flag = MiscMenuFlag_Shortcut },
{ .name = "Games", .title = "Games", .func = MiscMenuFuncGenerator<ui::menu::game::Menu>, .flag = MiscMenuFlag_Shortcut },
{ .name = "Themezer", .title = "Themezer", .func = MiscMenuFuncGenerator<ui::menu::themezer::Menu>, .flag = MiscMenuFlag_Shortcut },
{ .name = "GitHub", .title = "GitHub", .func = MiscMenuFuncGenerator<ui::menu::gh::Menu>, .flag = MiscMenuFlag_Shortcut },
{ .name = "FTP", .title = "FTP Install", .func = MiscMenuFuncGenerator<ui::menu::ftp::Menu>, .flag = MiscMenuFlag_Install },
{ .name = "USB", .title = "USB Install", .func = MiscMenuFuncGenerator<ui::menu::usb::Menu>, .flag = MiscMenuFlag_Install },
{ .name = "GameCard", .title = "GameCard Install", .func = MiscMenuFuncGenerator<ui::menu::gc::Menu>, .flag = MiscMenuFlag_Shortcut|MiscMenuFlag_Install },
{ .name = "IRS", .title = "IRS (Infrared Joycon Camera)", .func = MiscMenuFuncGenerator<ui::menu::irs::Menu>, .flag = MiscMenuFlag_Shortcut },
};
auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string version) -> bool {
static fs::FsPath zip_out{"/switch/sphaira/cache/update.zip"};
constexpr auto chunk_size = 1024 * 512; // 512KiB
@@ -139,8 +164,24 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
return true;
}
auto CreateRightSideMenu() -> std::shared_ptr<MenuBase> {
const auto name = App::GetApp()->m_right_side_menu.Get();
for (auto& e : GetMiscMenuEntries()) {
if (e.name == name) {
return e.func();
}
}
return std::make_shared<ui::menu::appstore::Menu>();
}
} // namespace
auto GetMiscMenuEntries() -> std::span<const MiscMenuEntry> {
return MISC_MENU_ENTRIES;
}
MainMenu::MainMenu() {
curl::Api().ToFileAsync(
curl::Url{GITHUB_URL},
@@ -171,7 +212,7 @@ MainMenu::MainMenu() {
const auto version = yyjson_get_str(tag_key);
R_UNLESS(version, false);
if (std::strcmp(APP_VERSION, version) >= 0) {
if (!App::IsVersionNewer(APP_VERSION, version)) {
m_update_state = UpdateState::None;
return true;
}
@@ -208,6 +249,7 @@ MainMenu::MainMenu() {
this->SetActions(
std::make_pair(Button::START, Action{App::Exit}),
std::make_pair(Button::SELECT, Action{App::DisplayMiscOptions}),
std::make_pair(Button::Y, Action{"Menu"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Menu Options"_i18n, "v" APP_VERSION_HASH, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(options));
@@ -227,6 +269,7 @@ MainMenu::MainMenu() {
language_items.push_back("Russian"_i18n);
language_items.push_back("Swedish"_i18n);
language_items.push_back("Vietnamese"_i18n);
language_items.push_back("Ukrainian"_i18n);
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [](){
App::DisplayThemeOptions();
@@ -238,15 +281,15 @@ MainMenu::MainMenu() {
options->Add(std::make_shared<SidebarEntryBool>("Ftp"_i18n, App::GetFtpEnable(), [](bool& enable){
App::SetFtpEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<SidebarEntryBool>("Mtp"_i18n, App::GetMtpEnable(), [](bool& enable){
App::SetMtpEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<SidebarEntryBool>("Nxlink"_i18n, App::GetNxlinkEnable(), [](bool& enable){
App::SetNxlinkEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
if (m_update_state == UpdateState::Update) {
options->Add(std::make_shared<SidebarEntryCallback>("Download update: "_i18n + m_update_version, [this](){
@@ -285,7 +328,7 @@ MainMenu::MainMenu() {
m_homebrew_menu = std::make_shared<homebrew::Menu>();
m_filebrowser_menu = std::make_shared<filebrowser::Menu>(m_homebrew_menu->GetHomebrewList());
m_app_store_menu = std::make_shared<appstore::Menu>(m_homebrew_menu->GetHomebrewList());
m_right_side_menu = CreateRightSideMenu();
m_current_menu = m_homebrew_menu;
AddOnLRPress();
@@ -336,16 +379,16 @@ void MainMenu::OnLRPress(std::shared_ptr<MenuBase> menu, Button b) {
void MainMenu::AddOnLRPress() {
if (m_current_menu != m_filebrowser_menu) {
const auto label = m_current_menu == m_homebrew_menu ? "Files" : "Apps";
const auto label = m_current_menu == m_homebrew_menu ? m_filebrowser_menu->GetShortTitle() : m_homebrew_menu->GetShortTitle();
SetAction(Button::L, Action{i18n::get(label), [this]{
OnLRPress(m_filebrowser_menu, Button::L);
}});
}
if (m_current_menu != m_app_store_menu) {
const auto label = m_current_menu == m_homebrew_menu ? "Store" : "Apps";
if (m_current_menu != m_right_side_menu) {
const auto label = m_current_menu == m_homebrew_menu ? m_right_side_menu->GetShortTitle() : m_homebrew_menu->GetShortTitle();
SetAction(Button::R, Action{i18n::get(label), [this]{
OnLRPress(m_app_store_menu, Button::R);
OnLRPress(m_right_side_menu, Button::R);
}});
}
}

View File

@@ -306,6 +306,7 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
}
const auto file_path = fs::AppendPath(dir_path, name);
pbox->NewTransfer(name);
Result rc;
if (R_FAILED(rc = fs.CreateFile(file_path, info.uncompressed_size, 0)) && rc != FsError_PathAlreadyExists) {
@@ -414,7 +415,7 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
options->Add(std::make_shared<SidebarEntryBool>("Nsfw"_i18n, m_nsfw.Get(), [this](bool& v_out){
m_nsfw.Set(v_out);
InvalidateAllPages();
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
if (m_sort.Get() != index_out) {
@@ -452,7 +453,7 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
}
}));
}}),
std::make_pair(Button::R, Action{"Next Page"_i18n, [this](){
std::make_pair(Button::R2, Action{"Next"_i18n, [this](){
m_page_index++;
if (m_page_index >= m_page_index_max) {
m_page_index = m_page_index_max - 1;
@@ -460,7 +461,7 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
PackListDownload();
}
}}),
std::make_pair(Button::L, Action{"Prev Page"_i18n, [this](){
std::make_pair(Button::L2, Action{"Prev"_i18n, [this](){
if (m_page_index) {
m_page_index--;
PackListDownload();
@@ -536,7 +537,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
auto& e = page.m_packList[pos];
auto text_id = ThemeEntryID_TEXT;
if (pos == m_index) {
const auto selected = pos == m_index;
if (selected) {
text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, theme, 4.f, v);
} else {
@@ -603,16 +605,14 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
}
}
gfx::drawImage(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage(), 15);
gfx::drawImage(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage(), 5);
}
nvgSave(vg);
nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip
{
gfx::drawTextArgs(vg, x + xoff, y + 180 + 20, 18, NVG_ALIGN_LEFT, theme->GetColour(text_id), "%s", e.details.name.c_str());
gfx::drawTextArgs(vg, x + xoff, y + 180 + 55, 18, NVG_ALIGN_LEFT, theme->GetColour(text_id), "%s", e.creator.display_name.c_str());
}
nvgRestore(vg);
const auto text_x = x + xoff;
const auto text_clip_w = w - 30.f - xoff;
const float font_size = 18;
m_scroll_name.Draw(vg, selected, text_x, y + 180 + 20, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.details.name.c_str());
m_scroll_author.Draw(vg, selected, text_x, y + 180 + 55, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.creator.display_name.c_str());
});
}

View File

@@ -10,8 +10,9 @@
namespace sphaira::ui::menu::usb {
namespace {
constexpr u64 CONNECTION_TIMEOUT = 1e+9 * 3;
constexpr u64 TRANSFER_TIMEOUT = 1e+9 * 5;
constexpr u64 CONNECTION_TIMEOUT = UINT64_MAX;
constexpr u64 TRANSFER_TIMEOUT = UINT64_MAX;
constexpr u64 FINISHED_TIMEOUT = 1e+9 * 3; // 3 seconds.
void thread_func(void* user) {
auto app = static_cast<Menu*>(user);
@@ -21,17 +22,29 @@ void thread_func(void* user) {
break;
}
const auto rc = app->m_usb_source->WaitForConnection(CONNECTION_TIMEOUT, app->m_usb_speed, app->m_usb_count);
const auto rc = app->m_usb_source->IsUsbConnected(CONNECTION_TIMEOUT);
if (rc == ::sphaira::usb::UsbDs::Result_Cancelled) {
break;
}
// set connected status
mutexLock(&app->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&app->m_mutex));
if (R_SUCCEEDED(rc)) {
app->m_state = State::Connected_WaitForFileList;
} else {
app->m_state = State::None;
}
mutexUnlock(&app->m_mutex);
if (R_SUCCEEDED(rc)) {
app->m_state = State::Connected;
break;
} else if (R_FAILED(rc) && R_VALUE(rc) != 0xEA01) {
log_write("got: 0x%X value: 0x%X\n", rc, R_VALUE(rc));
app->m_state = State::Failed;
break;
std::vector<std::string> names;
if (R_SUCCEEDED(app->m_usb_source->WaitForConnection(CONNECTION_TIMEOUT, names))) {
mutexLock(&app->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&app->m_mutex));
app->m_state = State::Connected_StartingTransfer;
app->m_names = names;
break;
}
}
}
}
@@ -59,14 +72,10 @@ Menu::Menu() : MenuBase{"USB"_i18n} {
if (R_FAILED(m_usb_source->GetOpenResult())) {
log_write("usb init open\n");
m_state = State::Failed;
} else {
if (R_FAILED(m_usb_source->Init())) {
log_write("usb init failed\n");
m_state = State::Failed;
}
}
mutexInit(&m_mutex);
if (m_state != State::Failed) {
threadCreate(&m_thread, thread_func, this, nullptr, 1024*32, 0x2C, 1);
threadStart(&m_thread);
@@ -76,6 +85,7 @@ Menu::Menu() : MenuBase{"USB"_i18n} {
Menu::~Menu() {
// signal for thread to exit and wait.
m_stop_source.request_stop();
m_usb_source->SignalCancel();
threadWaitForExit(&m_thread);
threadClose(&m_thread);
@@ -95,54 +105,38 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
switch (m_state) {
case State::None:
break;
if (m_state == State::Connected_StartingTransfer) {
log_write("set to progress\n");
m_state = State::Progress;
log_write("got connection\n");
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool {
ON_SCOPE_EXIT(m_usb_source->Finished(FINISHED_TIMEOUT));
case State::Connected:
log_write("set to progress\n");
m_state = State::Progress;
log_write("got connection\n");
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool {
log_write("inside progress box\n");
for (u32 i = 0; i < m_usb_count; i++) {
std::string file_name;
u64 file_size;
if (R_FAILED(m_usb_source->GetFileInfo(file_name, file_size))) {
return false;
}
log_write("inside progress box\n");
for (const auto& file_name : m_names) {
m_usb_source->SetFileNameForTranfser(file_name);
log_write("got file name: %s size: %lX\n", file_name.c_str(), file_size);
const auto rc = yati::InstallFromSource(pbox, m_usb_source, file_name);
if (R_FAILED(rc)) {
return false;
}
App::Notify("Installed via usb"_i18n);
m_usb_source->Finished();
const auto rc = yati::InstallFromSource(pbox, m_usb_source, file_name);
if (R_FAILED(rc)) {
m_usb_source->SignalCancel();
log_write("exiting usb install\n");
return false;
}
return true;
}, [this](bool result){
if (result) {
App::Notify("Usb install success!"_i18n);
m_state = State::Done;
} else {
App::Notify("Usb install failed!"_i18n);
m_state = State::Failed;
}
}));
break;
App::Notify("Installed via usb"_i18n);
}
case State::Progress:
break;
case State::Done:
break;
case State::Failed:
break;
return true;
}, [this](bool result){
if (result) {
App::Notify("Usb install success!"_i18n);
m_state = State::Done;
SetPop();
} else {
App::Notify("Usb install failed!"_i18n);
m_state = State::Failed;
}
}));
}
}
@@ -157,7 +151,12 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str());
break;
case State::Connected:
case State::Connected_WaitForFileList:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Connected, waiting for file list..."_i18n.c_str());
break;
case State::Connected_StartingTransfer:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Connected, starting transfer..."_i18n.c_str());
break;
case State::Progress:

View File

@@ -1,4 +1,5 @@
#include "ui/nvg_util.hpp"
#include "log.hpp"
#include <cstddef>
#include <cstdio>
#include <cstdarg>
@@ -10,6 +11,9 @@
namespace sphaira::ui::gfx {
namespace {
constexpr auto ALIGN_HOR = NVG_ALIGN_LEFT|NVG_ALIGN_CENTER|NVG_ALIGN_RIGHT;
constexpr auto ALIGN_VER = NVG_ALIGN_TOP|NVG_ALIGN_MIDDLE|NVG_ALIGN_BOTTOM|NVG_ALIGN_BASELINE;
constexpr std::array buttons = {
std::pair{Button::A, "\uE0E0"},
std::pair{Button::B, "\uE0E1"},
@@ -17,10 +21,8 @@ constexpr std::array buttons = {
std::pair{Button::Y, "\uE0E3"},
std::pair{Button::L, "\uE0E4"},
std::pair{Button::R, "\uE0E5"},
std::pair{Button::L, "\uE0E6"},
std::pair{Button::R, "\uE0E7"},
std::pair{Button::L2, "\uE0E8"},
std::pair{Button::R2, "\uE0E9"},
std::pair{Button::L2, "\uE0E6"},
std::pair{Button::R2, "\uE0E7"},
std::pair{Button::UP, "\uE0EB"},
std::pair{Button::DOWN, "\uE0EC"},
std::pair{Button::LEFT, "\uE0ED"},
@@ -33,8 +35,29 @@ constexpr std::array buttons = {
std::pair{Button::R3, "\uE105"},
};
// software based clipping, saves a few cpu cycles.
bool ClipRect(float x, float y) {
return x >= SCREEN_WIDTH || y >= SCREEN_HEIGHT;
}
bool ClipText(float x, float y, int align) {
if ((!(align & ALIGN_HOR) || (align & NVG_ALIGN_LEFT)) && x >= SCREEN_WIDTH) {
return true;
}
if ((!(align & ALIGN_VER) || (align & NVG_ALIGN_TOP)) && y >= SCREEN_HEIGHT) {
return true;
}
return false;
}
// NEW ---------------------
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) {
if (ClipRect(v.x, v.y)) {
return;
}
nvgBeginPath(vg);
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
nvgFillColor(vg, c);
@@ -42,6 +65,10 @@ void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rou
}
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) {
if (ClipRect(v.x, v.y)) {
return;
}
nvgBeginPath(vg);
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
nvgFillPaint(vg, p);
@@ -120,6 +147,10 @@ void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, con
}
void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, const Vec4& v, const NVGcolor& c) {
if (ClipRect(v.x, v.y)) {
return;
}
const auto corner_radius = 0.5;
drawRectOutlineInternal(vg, theme, size, v);
nvgBeginPath(vg);
@@ -129,6 +160,10 @@ void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, con
}
void drawTextIntenal(NVGcontext* vg, const Vec2& v, float size, const char* str, const char* end, int align, const NVGcolor& c) {
if (ClipText(v.x, v.y, align)) {
return;
}
nvgBeginPath(vg);
nvgFontSize(vg, size);
nvgTextAlign(vg, align);
@@ -136,6 +171,24 @@ void drawTextIntenal(NVGcontext* vg, const Vec2& v, float size, const char* str,
nvgText(vg, v.x, v.y, str, end);
}
void drawTriangleInternal(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor& c) {
nvgBeginPath(vg);
nvgMoveTo(vg, aX, aY);
nvgLineTo(vg, bX, bY);
nvgLineTo(vg, cX, cY);
nvgFillColor(vg, c);
nvgFill(vg);
}
void drawTriangleInternal(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p) {
nvgBeginPath(vg);
nvgMoveTo(vg, aX, aY);
nvgLineTo(vg, bX, bY);
nvgLineTo(vg, cX, cY);
nvgFillPaint(vg, p);
nvgFill(vg);
}
} // namespace
const char* getButton(const Button want) {
@@ -166,6 +219,10 @@ void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture,
}
void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, const NVGcolor& c, const char* str, int align, const char* end) {
if (ClipText(x, y, align)) {
return;
}
nvgBeginPath(vg);
nvgFontSize(vg, size);
nvgTextAlign(vg, align);
@@ -270,6 +327,63 @@ void drawScrollbar2(NVGcontext* vg, const Theme* theme, s64 index_off, s64 count
drawScrollbar2(vg, theme, SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200, index_off, count, row, page);
}
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor& c) {
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, c);
}
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p) {
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, p);
}
void drawAppLable(NVGcontext* vg, const Theme* theme, ScrollingText& st, float x, float y, float w, const char* name) {
// todo: no more 5am code
const float max_box_w = 392.f;
const float box_h = 48.f;
// used for adjusting the position of the box.
const float clip_pad = 25.f;
const float clip_left = clip_pad;
const float clip_right = 1220.f - clip_pad;
const float text_pad = 25.f;
const float font_size = 22.f;
nvgTextAlign(vg, NVG_ALIGN_LEFT);
nvgFontSize(vg, font_size);
float bounds[4]{};
nvgTextBounds(vg, 0, 0, name, NULL, bounds);
const float trinaglex = x + (w / 2.f) - 9.f;
const float trinagley = y - 14.f;
const float center_x = x + (w / 2.f);
const float y_offset = y - 62.f; // top of box
const float text_width = bounds[2];
float box_w = text_width + text_pad * 2;
if (box_w > max_box_w) {
box_w = max_box_w;
}
float box_x = center_x - (box_w / 2.f);
if (box_x < clip_left) {
box_x = clip_left;
}
if ((box_x + box_w) > clip_right) {
// box_x -= ((box_x + box_w) - clip_right) / 2;
box_x = (clip_right - box_w);
}
const float text_x = box_x + text_pad;
const float text_y = y_offset + (box_h / 2.f);
drawRect(vg, {x-4, y-4, w+8, w+8}, theme->GetColour(ThemeEntryID_GRID));
nvgBeginPath(vg);
nvgRoundedRect(vg, box_x, y_offset, box_w, box_h, 3.f);
nvgFillColor(vg, theme->GetColour(ThemeEntryID_SELECTED_BACKGROUND));
nvgFill(vg);
drawTriangle(vg, trinaglex, trinagley, trinaglex + 18.f, trinagley, trinaglex + 9.f, trinagley + 12.f, theme->GetColour(ThemeEntryID_SELECTED_BACKGROUND));
st.Draw(vg, true, text_x, text_y, box_w - text_pad * 2, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED), name);
}
#define HIGHLIGHT_SPEED 350.0
static double highlightGradientX = 0;

View File

@@ -23,7 +23,7 @@ auto OptionBoxEntry::Selected(bool enable) -> void {
m_selected = enable;
}
OptionBox::OptionBox(const std::string& message, const Option& a, Callback cb)
OptionBox::OptionBox(const std::string& message, const Option& a, Callback cb, int image)
: m_message{message}
, m_callback{cb} {
@@ -40,14 +40,15 @@ OptionBox::OptionBox(const std::string& message, const Option& a, Callback cb)
Setup(0);
}
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb)
: OptionBox{message, a, b, 0, cb} {
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb, int image)
: OptionBox{message, a, b, 0, cb, image} {
}
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, s64 index, Callback cb)
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, s64 index, Callback cb, int image)
: m_message{message}
, m_callback{cb} {
, m_callback{cb}
, m_image{image} {
m_pos.w = 770.f;
m_pos.h = 295.f;
@@ -65,17 +66,6 @@ OptionBox::OptionBox(const std::string& message, const Option& a, const Option&
Setup(index);
}
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, Callback cb)
: OptionBox{message, a, b, c, 0, cb} {
}
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, s64 index, Callback cb)
: m_message{message}
, m_callback{cb} {
}
auto OptionBox::Update(Controller* controller, TouchInfo* touch) -> void {
Widget::Update(controller, touch);
@@ -92,13 +82,25 @@ auto OptionBox::Update(Controller* controller, TouchInfo* touch) -> void {
}
auto OptionBox::Draw(NVGcontext* vg, Theme* theme) -> void {
const float padding = 15;
gfx::dimBackground(vg);
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP));
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP), 5);
nvgSave(vg);
nvgTextLineHeight(vg, 1.5);
gfx::drawTextBox(vg, m_pos.x + padding, m_pos.y + 110.f, 26.f, m_pos.w - padding*2, theme->GetColour(ThemeEntryID_TEXT), m_message.c_str(), NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
if (m_image) {
Vec4 image{m_pos};
image.x += 40;
image.y += 40;
image.w = 150;
image.h = 150;
const float padding = 40;
gfx::drawImage(vg, image, m_image, 5);
gfx::drawTextBox(vg, image.x + image.w + padding, m_pos.y + 110.f, 22.f, m_pos.w - (image.x - m_pos.x) - image.w - padding*2, theme->GetColour(ThemeEntryID_TEXT), m_message.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_BASELINE);
} else {
const float padding = 30;
gfx::drawTextBox(vg, m_pos.x + padding, m_pos.y + 110.f, 24.f, m_pos.w - padding*2, theme->GetColour(ThemeEntryID_TEXT), m_message.c_str(), NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
}
nvgRestore(vg);
gfx::drawRect(vg, m_spacer_line, theme->GetColour(ThemeEntryID_LINE_SEPARATOR));

View File

@@ -2,6 +2,7 @@
#include "ui/nvg_util.hpp"
#include "app.hpp"
#include "i18n.hpp"
#include <algorithm>
namespace sphaira::ui {
@@ -73,6 +74,8 @@ PopupList::PopupList(std::string title, Items items, Callback cb, s64 index)
}})
);
m_starting_index = m_index;
m_pos.w = 1280.f;
const float a = std::min(370.f, (60.f * static_cast<float>(m_items.size())));
m_pos.h = 80.f + 140.f + a;
@@ -110,15 +113,24 @@ auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void {
m_list->Draw(vg, theme, m_items.size(), [this](auto* vg, auto* theme, auto v, auto i) {
const auto& [x, y, w, h] = v;
if (m_index == i) {
auto colour = ThemeEntryID_TEXT;
const auto selected = m_index == i;
if (selected) {
gfx::drawRectOutline(vg, theme, 4.f, v);
gfx::drawText(vg, x + m_text_xoffset, y + (h / 2.f), 20.f, m_items[i].c_str(), NULL, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED));
} else {
if (i != m_items.size() - 1) {
gfx::drawRect(vg, x, y + h, w, 1.f, theme->GetColour(ThemeEntryID_LINE_SEPARATOR));
}
gfx::drawText(vg, x + m_text_xoffset, y + (h / 2.f), 20.f, m_items[i].c_str(), NULL, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT));
}
if (m_starting_index == i) {
colour = ThemeEntryID_TEXT_SELECTED;
gfx::drawText(vg, x + w - m_text_xoffset, y + (h / 2.f), 20.f, "\uE14B", NULL, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(colour));
}
const auto text_x = x + m_text_xoffset;
const auto text_clip_w = w - 60.f - m_text_xoffset;
m_scroll_text.Draw(vg, selected, text_x, y + (h / 2.f), text_clip_w, 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(colour), m_items[i]);
});
Widget::Draw(vg, theme);

View File

@@ -95,7 +95,7 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
}
gfx::dimBackground(vg);
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP));
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP), 5);
// The pop up shape.
// const Vec4 box = { 255, 145, 770, 430 };
@@ -108,7 +108,7 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH());
if (m_image) {
gfx::drawImage(vg, GetX() + 30, GetY() + 30, 128, 128, m_image, 10);
gfx::drawImage(vg, GetX() + 30, GetY() + 30, 128, 128, m_image, 5);
}
// shapes.
@@ -234,7 +234,7 @@ auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_pat
R_TRY(fsFileSetSize(&dst_file, src_size));
s64 offset{};
std::vector<u8> buf(1024*1024*8); // 8MiB
std::vector<u8> buf(1024*1024*4); // 4MiB
while (offset < src_size) {
if (ShouldExit()) {
@@ -248,6 +248,7 @@ auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_pat
R_TRY(fsFileWrite(&dst_file, offset, buf.data(), bytes_read, FsWriteOption_None));
Yield();
UpdateTransfer(offset, src_size);
offset += bytes_read;
}

View File

@@ -0,0 +1,85 @@
#include "ui/scrolling_text.hpp"
#include "ui/nvg_util.hpp"
#include "app.hpp"
#include <cstdarg>
namespace sphaira::ui {
namespace {
auto GetTextScrollSpeed() -> float {
switch (App::GetTextScrollSpeed()) {
case 0: return 0.5;
default: case 1: return 1.0;
case 2: return 1.5;
}
}
void DrawClipped(NVGcontext* vg, const Vec4& clip, float x, float y, float size, int align, const NVGcolor& colour, const std::string& str) {
nvgSave(vg);
nvgIntersectScissor(vg, clip.x, clip.y, clip.w, clip.h); // clip
gfx::drawText(vg, x, y, size, colour, str.c_str(), align);
nvgRestore(vg);
}
} // namespace
void ScrollingText::Draw(NVGcontext* vg, bool focus, float x, float y, float w, float size, int align, const NVGcolor& colour, const std::string& text_entry) {
const Vec4 clip{x, 0, w, 720};
if (!focus) {
DrawClipped(vg, clip, x, y, size, align, colour, text_entry);
return;
}
if (m_str != text_entry) {
Reset(text_entry);
}
float bounds[4];
auto value_str = text_entry;
nvgFontSize(vg, size);
nvgTextAlign(vg, align);
nvgTextBounds(vg, 0, 0, value_str.c_str(), nullptr, bounds);
if (focus) {
const auto scroll_amount = GetTextScrollSpeed();
if (bounds[2] > w) {
value_str += " ";
nvgTextBounds(vg, 0, 0, value_str.c_str(), nullptr, bounds);
if (!m_text_xoff) {
m_tick++;
if (m_tick >= 60) {
m_tick = 0;
m_text_xoff += scroll_amount;
}
} else if (bounds[2] > m_text_xoff) {
m_text_xoff += std::min(scroll_amount, bounds[2] - m_text_xoff);
} else {
m_text_xoff = 0;
}
value_str += text_entry;
}
}
x -= m_text_xoff;
DrawClipped(vg, clip, x, y, size, align, colour, value_str);
}
void ScrollingText::DrawArgs(NVGcontext* vg, bool focus, float x, float y, float w, float size, int align, const NVGcolor& colour, const char* s, ...) {
std::va_list v{};
va_start(v, s);
char buffer[0x100];
std::vsnprintf(buffer, sizeof(buffer), s, v);
va_end(v);
Draw(vg, focus, x, y, w, size, align, colour, buffer);
}
void ScrollingText::Reset(const std::string& text_entry) {
m_str = text_entry;
m_tick = 0;
m_text_xoff = 0;
}
} // namespace sphaira::ui

View File

@@ -3,6 +3,7 @@
#include "ui/popup_list.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
#include <algorithm>
namespace sphaira::ui {
namespace {
@@ -43,6 +44,13 @@ SidebarEntryBool::SidebarEntryBool(std::string title, bool option, Callback cb,
, m_true_str{std::move(true_str)}
, m_false_str{std::move(false_str)} {
if (m_true_str == "On") {
m_true_str = i18n::get(m_true_str);
}
if (m_false_str == "Off") {
m_false_str = i18n::get(m_false_str);
}
SetAction(Button::A, Action{"OK"_i18n, [this](){
m_option ^= 1;
m_callback(m_option);

View File

@@ -86,6 +86,11 @@ auto Widget::GetUiButtons() const -> uiButtons {
uiButtons draw_actions;
draw_actions.reserve(m_actions.size());
const std::pair<Button, Button> swap_buttons[] = {
{Button::L, Button::R},
{Button::L2, Button::R2},
};
// build array
for (const auto& [button, action] : m_actions) {
if (action.IsHidden() || action.m_hint.empty()) {
@@ -94,13 +99,19 @@ auto Widget::GetUiButtons() const -> uiButtons {
uiButton ui_button{button, action};
// swap
if (button == Button::R && draw_actions.size() && draw_actions.back().m_button == Button::L) {
const auto s = draw_actions.back();
draw_actions.back().m_button = button;
draw_actions.back().m_action = action;
draw_actions.emplace_back(s);
} else {
bool should_swap = false;
for (auto [left, right] : swap_buttons) {
if (button == right && draw_actions.size() && draw_actions.back().m_button == left) {
const auto s = draw_actions.back();
draw_actions.back().m_button = button;
draw_actions.back().m_action = action;
draw_actions.emplace_back(s);
should_swap = true;
break;
}
}
if (!should_swap) {
draw_actions.emplace_back(ui_button);
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (c) Atmosphère-NX
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// The USB transfer code was taken from Haze (part of Atmosphere).
#include "usb/base.hpp"
#include "log.hpp"
#include "defines.hpp"
#include <ranges>
#include <cstring>
namespace sphaira::usb {
Base::Base(u64 transfer_timeout) {
m_transfer_timeout = transfer_timeout;
ueventCreate(GetCancelEvent(), true);
// this avoids allocations during transfers.
m_aligned.reserve(1024 * 1024 * 16);
}
Result Base::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) {
u32 xfer_id;
/* If we're not configured yet, wait to become configured first. */
R_TRY(IsUsbConnected(timeout));
/* Select the appropriate endpoint and begin a transfer. */
const auto ep = read ? UsbSessionEndpoint_Out : UsbSessionEndpoint_In;
R_TRY(TransferAsync(ep, page, size, std::addressof(xfer_id)));
/* Try to wait for the event. */
R_TRY(WaitTransferCompletion(ep, timeout));
/* Return what we transferred. */
return GetTransferResult(ep, xfer_id, nullptr, out_size_transferred);
}
// while it may seem like a bad idea to transfer data to a buffer and copy it
// in practice, this has no impact on performance.
// the switch is *massively* bottlenecked by slow io (nand and sd).
// so making usb transfers zero-copy provides no benefit other than increased
// code complexity and the increase of future bugs if/when sphaira is forked
// an changes are made.
// yati already goes to great lengths to be zero-copy during installing
// by swapping buffers and inflating in-place.
// NOTE: it is now possible to request the transfer buffer using GetTransferBuffer(),
// which will always be aligned and have the size aligned.
// this allows for zero-copy transferrs to take place.
// this is used in usb_upload.cpp.
// do note that this relies of the host sending / receiving buffers of an aligned size.
Result Base::TransferAll(bool read, void *data, u32 size, u64 timeout) {
auto buf = static_cast<u8*>(data);
auto transfer_buf = m_aligned.data();
const auto alias = buf == transfer_buf;
if (!alias) {
m_aligned.resize(size);
}
while (size) {
if (!alias && !read) {
std::memcpy(transfer_buf, buf, size);
}
u32 out_size_transferred;
R_TRY(TransferPacketImpl(read, transfer_buf, size, &out_size_transferred, timeout));
if (!alias && read) {
std::memcpy(buf, transfer_buf, out_size_transferred);
}
if (alias) {
transfer_buf += out_size_transferred;
} else {
buf += out_size_transferred;
}
size -= out_size_transferred;
}
R_SUCCEED();
}
} // namespace sphaira::usb

View File

@@ -0,0 +1,112 @@
// The USB protocol was taken from Tinfoil, by Adubbz.
#include "usb/usb_uploader.hpp"
#include "usb/tinfoil.hpp"
#include "log.hpp"
#include "defines.hpp"
namespace sphaira::usb::upload {
namespace {
namespace tinfoil = usb::tinfoil;
const UsbHsInterfaceFilter FILTER{
.Flags = UsbHsInterfaceFilterFlags_idVendor |
UsbHsInterfaceFilterFlags_idProduct |
UsbHsInterfaceFilterFlags_bcdDevice_Min |
UsbHsInterfaceFilterFlags_bcdDevice_Max |
UsbHsInterfaceFilterFlags_bDeviceClass |
UsbHsInterfaceFilterFlags_bDeviceSubClass |
UsbHsInterfaceFilterFlags_bDeviceProtocol |
UsbHsInterfaceFilterFlags_bInterfaceClass |
UsbHsInterfaceFilterFlags_bInterfaceSubClass |
UsbHsInterfaceFilterFlags_bInterfaceProtocol,
.idVendor = 0x057e,
.idProduct = 0x3000,
.bcdDevice_Min = 0x0100,
.bcdDevice_Max = 0x0100,
.bDeviceClass = 0x00,
.bDeviceSubClass = 0x00,
.bDeviceProtocol = 0x00,
.bInterfaceClass = USB_CLASS_VENDOR_SPEC,
.bInterfaceSubClass = USB_CLASS_VENDOR_SPEC,
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
};
constexpr u8 INDEX = 0;
} // namespace
Usb::Usb(u64 transfer_timeout) {
m_usb = std::make_unique<usb::UsbHs>(INDEX, FILTER, transfer_timeout);
m_usb->Init();
}
Usb::~Usb() {
}
Result Usb::WaitForConnection(u64 timeout, std::span<const std::string> names) {
R_TRY(m_usb->IsUsbConnected(timeout));
std::string names_list;
for (auto& name : names) {
names_list += name + '\n';
}
tinfoil::TUSHeader header{};
header.magic = tinfoil::Magic_List0;
header.nspListSize = names_list.length();
R_TRY(m_usb->TransferAll(false, &header, sizeof(header), timeout));
R_TRY(m_usb->TransferAll(false, names_list.data(), names_list.length(), timeout));
R_SUCCEED();
}
Result Usb::PollCommands() {
tinfoil::USBCmdHeader header;
R_TRY(m_usb->TransferAll(true, &header, sizeof(header)));
R_UNLESS(header.magic == tinfoil::Magic_Command0, Result_BadMagic);
if (header.cmdId == tinfoil::USBCmdId::EXIT) {
return Result_Exit;
} else if (header.cmdId == tinfoil::USBCmdId::FILE_RANGE) {
return FileRangeCmd(header.dataSize);
} else {
return Result_BadCommand;
}
}
Result Usb::FileRangeCmd(u64 data_size) {
tinfoil::FileRangeCmdHeader header;
R_TRY(m_usb->TransferAll(true, &header, sizeof(header)));
std::string path(header.nspNameLen, '\0');
R_TRY(m_usb->TransferAll(true, path.data(), header.nspNameLen));
// send response header.
R_TRY(m_usb->TransferAll(false, &header, sizeof(header)));
s64 curr_off = 0x0;
s64 end_off = header.size;
s64 read_size = header.size;
// use transfer buffer directly to avoid copy overhead.
auto& buf = m_usb->GetTransferBuffer();
buf.resize(header.size);
while (curr_off < end_off) {
if (curr_off + read_size >= end_off) {
read_size = end_off - curr_off;
}
u64 bytes_read;
R_TRY(Read(path, buf.data(), header.offset + curr_off, read_size, &bytes_read));
R_TRY(m_usb->TransferAll(false, buf.data(), bytes_read));
curr_off += bytes_read;
}
R_SUCCEED();
}
} // namespace sphaira::usb::upload

View File

@@ -0,0 +1,272 @@
#include "usb/usbds.hpp"
#include "log.hpp"
#include "defines.hpp"
#include <ranges>
#include <cstring>
namespace sphaira::usb {
namespace {
// untested, should work tho.
// TODO: pr missing speed fields to libnx.
Result usbDsGetSpeed(u32 *out) {
if (hosversionBefore(8,0,0)) {
return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
}
serviceAssumeDomain(usbDsGetServiceSession());
return serviceDispatchOut(usbDsGetServiceSession(), hosversionAtLeast(11,0,0) ? 11 : 12, *out);
}
} // namespace
UsbDs::~UsbDs() {
usbDsExit();
}
Result UsbDs::Init() {
log_write("doing USB init\n");
R_TRY(usbDsInitialize());
static SetSysSerialNumber serial_number{};
R_TRY(setsysInitialize());
ON_SCOPE_EXIT(setsysExit());
R_TRY(setsysGetSerialNumber(&serial_number));
u8 iManufacturer, iProduct, iSerialNumber;
static constexpr u16 supported_langs[1] = {0x0409};
// Send language descriptor
R_TRY(usbDsAddUsbLanguageStringDescriptor(nullptr, supported_langs, std::size(supported_langs)));
// Send manufacturer
R_TRY(usbDsAddUsbStringDescriptor(&iManufacturer, "Nintendo"));
// Send product
R_TRY(usbDsAddUsbStringDescriptor(&iProduct, "Nintendo Switch"));
// Send serial number
R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, serial_number.number));
// Send device descriptors
struct usb_device_descriptor device_descriptor = {
.bLength = USB_DT_DEVICE_SIZE,
.bDescriptorType = USB_DT_DEVICE,
.bcdUSB = 0x0110,
.bDeviceClass = 0x00,
.bDeviceSubClass = 0x00,
.bDeviceProtocol = 0x00,
.bMaxPacketSize0 = 0x40,
.idVendor = 0x057e,
.idProduct = 0x3000,
.bcdDevice = 0x0100,
.iManufacturer = iManufacturer,
.iProduct = iProduct,
.iSerialNumber = iSerialNumber,
.bNumConfigurations = 0x01
};
// Full Speed is USB 1.1
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Full, &device_descriptor));
// High Speed is USB 2.0
device_descriptor.bcdUSB = 0x0200;
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_High, &device_descriptor));
// Super Speed is USB 3.0
device_descriptor.bcdUSB = 0x0300;
// Upgrade packet size to 512
device_descriptor.bMaxPacketSize0 = 0x09;
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Super, &device_descriptor));
// Define Binary Object Store
const u8 bos[0x16] = {
0x05, // .bLength
USB_DT_BOS, // .bDescriptorType
0x16, 0x00, // .wTotalLength
0x02, // .bNumDeviceCaps
// USB 2.0
0x07, // .bLength
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
0x02, // .bDevCapabilityType
0x02, 0x00, 0x00, 0x00, // dev_capability_data
// USB 3.0
0x0A, // .bLength
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
0x03, /* .bDevCapabilityType */
0x00, /* .bmAttributes */
0x0E, 0x00, /* .wSpeedSupported */
0x03, /* .bFunctionalitySupport */
0x00, /* .bU1DevExitLat */
0x00, 0x00 /* .bU2DevExitLat */
};
R_TRY(usbDsSetBinaryObjectStore(bos, sizeof(bos)));
struct usb_interface_descriptor interface_descriptor = {
.bLength = USB_DT_INTERFACE_SIZE,
.bDescriptorType = USB_DT_INTERFACE,
.bInterfaceNumber = USBDS_DEFAULT_InterfaceNumber, // set below
.bNumEndpoints = static_cast<u8>(std::size(m_endpoints)),
.bInterfaceClass = USB_CLASS_VENDOR_SPEC,
.bInterfaceSubClass = USB_CLASS_VENDOR_SPEC,
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
};
struct usb_endpoint_descriptor endpoint_descriptor_in = {
.bLength = USB_DT_ENDPOINT_SIZE,
.bDescriptorType = USB_DT_ENDPOINT,
.bEndpointAddress = USB_ENDPOINT_IN,
.bmAttributes = USB_TRANSFER_TYPE_BULK,
};
struct usb_endpoint_descriptor endpoint_descriptor_out = {
.bLength = USB_DT_ENDPOINT_SIZE,
.bDescriptorType = USB_DT_ENDPOINT,
.bEndpointAddress = USB_ENDPOINT_OUT,
.bmAttributes = USB_TRANSFER_TYPE_BULK,
};
const struct usb_ss_endpoint_companion_descriptor endpoint_companion = {
.bLength = sizeof(struct usb_ss_endpoint_companion_descriptor),
.bDescriptorType = USB_DT_SS_ENDPOINT_COMPANION,
.bMaxBurst = 0x0F,
.bmAttributes = 0x00,
.wBytesPerInterval = 0x00,
};
R_TRY(usbDsRegisterInterface(&m_interface));
interface_descriptor.bInterfaceNumber = m_interface->interface_index;
endpoint_descriptor_in.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
// Full Speed Config
endpoint_descriptor_in.wMaxPacketSize = 0x40;
endpoint_descriptor_out.wMaxPacketSize = 0x40;
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &interface_descriptor, USB_DT_INTERFACE_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
// High Speed Config
endpoint_descriptor_in.wMaxPacketSize = 0x200;
endpoint_descriptor_out.wMaxPacketSize = 0x200;
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &interface_descriptor, USB_DT_INTERFACE_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
// Super Speed Config
endpoint_descriptor_in.wMaxPacketSize = 0x400;
endpoint_descriptor_out.wMaxPacketSize = 0x400;
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &interface_descriptor, USB_DT_INTERFACE_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
//Setup endpoints.
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_In], endpoint_descriptor_in.bEndpointAddress));
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_Out], endpoint_descriptor_out.bEndpointAddress));
R_TRY(usbDsInterface_EnableInterface(m_interface));
R_TRY(usbDsEnable());
log_write("success USB init\n");
R_SUCCEED();
}
// the below code is taken from libnx, with the addition of a uevent to cancel.
Result UsbDs::IsUsbConnected(u64 timeout) {
Result rc;
UsbState state = UsbState_Detached;
rc = usbDsGetState(&state);
if (R_FAILED(rc)) return rc;
if (state == UsbState_Configured) return 0;
bool has_timeout = timeout != UINT64_MAX;
u64 deadline = 0;
const std::array waiters{
waiterForEvent(usbDsGetStateChangeEvent()),
waiterForUEvent(GetCancelEvent()),
};
if (has_timeout)
deadline = armGetSystemTick() + armNsToTicks(timeout);
do {
if (has_timeout) {
s64 remaining = deadline - armGetSystemTick();
timeout = remaining > 0 ? armTicksToNs(remaining) : 0;
}
s32 idx;
rc = waitObjects(&idx, waiters.data(), waiters.size(), timeout);
eventClear(usbDsGetStateChangeEvent());
// check if we got one of the cancel events.
if (R_SUCCEEDED(rc) && idx == waiters.size() - 1) {
rc = Result_Cancelled;
break;
}
rc = usbDsGetState(&state);
} while (R_SUCCEEDED(rc) && state != UsbState_Configured && timeout > 0);
if (R_SUCCEEDED(rc) && state != UsbState_Configured && timeout == 0)
return KERNELRESULT(TimedOut);
return rc;
}
Result UsbDs::GetSpeed(UsbDeviceSpeed* out) {
return usbDsGetSpeed((u32*)out);
}
Event *UsbDs::GetCompletionEvent(UsbSessionEndpoint ep) {
return std::addressof(m_endpoints[ep]->CompletionEvent);
}
Result UsbDs::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) {
const std::array waiters{
waiterForEvent(GetCompletionEvent(ep)),
waiterForEvent(usbDsGetStateChangeEvent()),
waiterForUEvent(GetCancelEvent()),
};
s32 idx;
auto rc = waitObjects(&idx, waiters.data(), waiters.size(), timeout);
// check if we got one of the cancel events.
if (R_SUCCEEDED(rc) && idx == waiters.size() - 1) {
log_write("got usb cancel event\n");
rc = Result_Cancelled;
} else if (R_SUCCEEDED(rc) && idx == waiters.size() - 2) {
log_write("got usbDsGetStateChangeEvent() event\n");
rc = KERNELRESULT(TimedOut);
}
if (R_FAILED(rc)) {
R_TRY(usbDsEndpoint_Cancel(m_endpoints[ep]));
eventClear(GetCompletionEvent(ep));
eventClear(usbDsGetStateChangeEvent());
}
return rc;
}
Result UsbDs::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) {
return usbDsEndpoint_PostBufferAsync(m_endpoints[ep], buffer, size, out_urb_id);
}
Result UsbDs::GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) {
UsbDsReportData report_data;
R_TRY(eventClear(GetCompletionEvent(ep)));
R_TRY(usbDsEndpoint_GetReportData(m_endpoints[ep], std::addressof(report_data)));
R_TRY(usbDsParseReportData(std::addressof(report_data), urb_id, out_requested_size, out_transferred_size));
R_SUCCEED();
}
} // namespace sphaira::usb

View File

@@ -0,0 +1,202 @@
#include "usb/usbhs.hpp"
#include "log.hpp"
#include "defines.hpp"
#include <ranges>
#include <cstring>
namespace sphaira::usb {
namespace {
struct Bcd {
constexpr Bcd(u16 v) : value{v} {}
u8 major() const { return (value >> 8) & 0xFF; }
u8 minor() const { return (value >> 4) & 0xF; }
u8 macro() const { return (value >> 0) & 0xF; }
const u16 value;
};
Result usbHsParseReportData(UsbHsXferReport* reports, u32 count, u32 xferId, u32 *requestedSize, u32 *transferredSize) {
Result rc = 0;
u32 pos;
UsbHsXferReport *entry = NULL;
if (count>8) count = 8;
for(pos=0; pos<count; pos++) {
entry = &reports[pos];
if (entry->xferId == xferId) break;
}
if (pos == count) return MAKERESULT(Module_Libnx, LibnxError_NotFound);
rc = entry->res;
if (R_SUCCEEDED(rc)) {
if (requestedSize) *requestedSize = entry->requestedSize;
if (transferredSize) *transferredSize = entry->transferredSize;
}
return rc;
}
} // namespace
UsbHs::UsbHs(u8 index, const UsbHsInterfaceFilter& filter, u64 transfer_timeout)
: Base{transfer_timeout}
, m_index{index}
, m_filter{filter} {
}
UsbHs::~UsbHs() {
Close();
usbHsDestroyInterfaceAvailableEvent(std::addressof(m_event), m_index);
usbHsExit();
}
Result UsbHs::Init() {
log_write("doing USB init\n");
R_TRY(usbHsInitialize());
R_TRY(usbHsCreateInterfaceAvailableEvent(&m_event, true, m_index, &m_filter));
log_write("success USB init\n");
R_SUCCEED();
}
Result UsbHs::IsUsbConnected(u64 timeout) {
if (m_connected) {
R_SUCCEED();
}
const std::array waiters{
waiterForEvent(&m_event),
waiterForUEvent(GetCancelEvent()),
};
s32 idx;
R_TRY(waitObjects(&idx, waiters.data(), waiters.size(), timeout));
if (idx == waiters.size() - 1) {
return Result_Cancelled;
}
return Connect();
}
Result UsbHs::Connect() {
Close();
s32 total;
R_TRY(usbHsQueryAvailableInterfaces(&m_filter, &m_interface, sizeof(m_interface), &total));
R_TRY(usbHsAcquireUsbIf(&m_s, &m_interface));
const auto bcdUSB = Bcd{m_interface.device_desc.bcdUSB};
const auto bcdDevice = Bcd{m_interface.device_desc.bcdDevice};
// log lsusb style.
log_write("[USBHS] pathstr: %s\n", m_interface.pathstr);
log_write("Bus: %03u Device: %03u ID: %04x:%04x\n\n", m_interface.busID, m_interface.deviceID, m_interface.device_desc.idVendor, m_interface.device_desc.idProduct);
log_write("Device Descriptor:\n");
log_write("\tbLength: %u\n", m_interface.device_desc.bLength);
log_write("\tbDescriptorType: %u\n", m_interface.device_desc.bDescriptorType);
log_write("\tbcdUSB: %u:%u%u\n", bcdUSB.major(), bcdUSB.minor(), bcdUSB.macro());
log_write("\tbDeviceClass: %u\n", m_interface.device_desc.bDeviceClass);
log_write("\tbDeviceSubClass: %u\n", m_interface.device_desc.bDeviceSubClass);
log_write("\tbDeviceProtocol: %u\n", m_interface.device_desc.bDeviceProtocol);
log_write("\tbMaxPacketSize0: %u\n", m_interface.device_desc.bMaxPacketSize0);
log_write("\tidVendor: 0x%x\n", m_interface.device_desc.idVendor);
log_write("\tidProduct: 0x%x\n", m_interface.device_desc.idProduct);
log_write("\tbcdDevice: %u:%u%u\n", bcdDevice.major(), bcdDevice.minor(), bcdDevice.macro());
log_write("\tiManufacturer: %u\n", m_interface.device_desc.iManufacturer);
log_write("\tiProduct: %u\n", m_interface.device_desc.iProduct);
log_write("\tiSerialNumber: %u\n", m_interface.device_desc.iSerialNumber);
log_write("\tbNumConfigurations: %u\n", m_interface.device_desc.bNumConfigurations);
log_write("\tConfiguration Descriptor:\n");
log_write("\t\tbLength: %u\n", m_interface.config_desc.bLength);
log_write("\t\tbDescriptorType: %u\n", m_interface.config_desc.bDescriptorType);
log_write("\t\twTotalLength: %u\n", m_interface.config_desc.wTotalLength);
log_write("\t\tbNumInterfaces: %u\n", m_interface.config_desc.bNumInterfaces);
log_write("\t\tbConfigurationValue: %u\n", m_interface.config_desc.bConfigurationValue);
log_write("\t\tiConfiguration: %u\n", m_interface.config_desc.iConfiguration);
log_write("\t\tbmAttributes: 0x%x\n", m_interface.config_desc.bmAttributes);
log_write("\t\tMaxPower: %u (%u mA)\n", m_interface.config_desc.MaxPower, m_interface.config_desc.MaxPower * 2);
struct usb_endpoint_descriptor invalid_desc{};
for (u8 i = 0; i < std::size(m_s.inf.inf.input_endpoint_descs); i++) {
const auto& desc = m_s.inf.inf.input_endpoint_descs[i];
if (std::memcmp(&desc, &invalid_desc, sizeof(desc))) {
log_write("\t[USBHS] desc[%u] wMaxPacketSize: 0x%X\n", i, desc.wMaxPacketSize);
}
}
auto& input_descs = m_s.inf.inf.input_endpoint_descs[0];
R_TRY(usbHsIfOpenUsbEp(&m_s, &m_endpoints[UsbSessionEndpoint_Out], 1, input_descs.wMaxPacketSize, &input_descs));
auto& output_descs = m_s.inf.inf.output_endpoint_descs[0];
R_TRY(usbHsIfOpenUsbEp(&m_s, &m_endpoints[UsbSessionEndpoint_In], 1, output_descs.wMaxPacketSize, &output_descs));
m_connected = true;
R_SUCCEED();
}
void UsbHs::Close() {
usbHsEpClose(std::addressof(m_endpoints[UsbSessionEndpoint_In]));
usbHsEpClose(std::addressof(m_endpoints[UsbSessionEndpoint_Out]));
usbHsIfClose(std::addressof(m_s));
m_endpoints[UsbSessionEndpoint_In] = {};
m_endpoints[UsbSessionEndpoint_Out] = {};
m_s = {};
m_connected = false;
}
Event *UsbHs::GetCompletionEvent(UsbSessionEndpoint ep) {
return usbHsEpGetXferEvent(&m_endpoints[ep]);
}
Result UsbHs::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) {
const std::array waiters{
waiterForEvent(GetCompletionEvent(ep)),
waiterForEvent(usbHsGetInterfaceStateChangeEvent()),
waiterForUEvent(GetCancelEvent()),
};
s32 idx;
auto rc = waitObjects(&idx, waiters.data(), waiters.size(), timeout);
// check if we got one of the cancel events.
if (R_SUCCEEDED(rc) && idx == waiters.size() - 1) {
log_write("got usb cancel event\n");
rc = Result_Cancelled;
} else if (R_SUCCEEDED(rc) && idx == waiters.size() - 2) {
log_write("got usb timeout event\n");
rc = KERNELRESULT(TimedOut);
Close();
}
if (R_FAILED(rc)) {
log_write("failed to wait for event\n");
eventClear(GetCompletionEvent(ep));
eventClear(usbHsGetInterfaceStateChangeEvent());
}
return rc;
}
Result UsbHs::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_xfer_id) {
return usbHsEpPostBufferAsync(&m_endpoints[ep], buffer, size, 0, out_xfer_id);
}
Result UsbHs::GetTransferResult(UsbSessionEndpoint ep, u32 xfer_id, u32 *out_requested_size, u32 *out_transferred_size) {
u32 count;
UsbHsXferReport report_data[8];
R_TRY(eventClear(GetCompletionEvent(ep)));
R_TRY(usbHsEpGetXferReport(&m_endpoints[ep], report_data, std::size(report_data), std::addressof(count)));
R_TRY(usbHsParseReportData(report_data, count, xfer_id, out_requested_size, out_transferred_size));
R_SUCCEED();
}
} // namespace sphaira::usb

View File

@@ -1,54 +0,0 @@
#include "web.hpp"
#include "log.hpp"
#include "defines.hpp"
#include <cstring>
namespace sphaira {
auto WebShow(const std::string& url, bool show_error) -> Result {
// showError("Running in applet mode\nPlease launch hbmenu by holding R on an APP (e.g. a game) NOT an applet (e.g. Gallery)", "", 0);
// showError("Error: Nag active, check more details", "Browser won't launch if supernag is active\n\nUse gagorder or switch-sys-tweak (the latter is bundled with BrowseNX) to disable supernag.", 0);
// log_write("web show with url: %s\n", url.c_str());
// return 0;
WebCommonConfig config{};
WebCommonReply reply{};
WebExitReason reason{};
AccountUid account_uid{};
char last_url[FS_MAX_PATH]{};
size_t last_url_len{};
// WebBackgroundKind_Unknown1 = shows background
// WebBackgroundKind_Unknown2 = shows background faded
if (R_FAILED(accountTrySelectUserWithoutInteraction(&account_uid, false))) { log_write("failed: accountTrySelectUserWithoutInteraction\n"); }
if (R_FAILED(webPageCreate(&config, url.c_str()))) { log_write("failed: webPageCreate\n"); }
if (R_FAILED(webConfigSetWhitelist(&config, "^http"))) { log_write("failed: webConfigSetWhitelist\n"); }
if (R_FAILED(webConfigSetEcClientCert(&config, true))) { log_write("failed: webConfigSetEcClientCert\n"); }
if (R_FAILED(webConfigSetScreenShot(&config, true))) { log_write("failed: webConfigSetScreenShot\n"); }
if (R_FAILED(webConfigSetBootDisplayKind(&config, WebBootDisplayKind_Black))) { log_write("failed: webConfigSetBootDisplayKind\n"); }
if (R_FAILED(webConfigSetBackgroundKind(&config, WebBackgroundKind_Default))) { log_write("failed: webConfigSetBackgroundKind\n"); }
if (R_FAILED(webConfigSetPointer(&config, true))) { log_write("failed: webConfigSetPointer\n"); }
if (R_FAILED(webConfigSetLeftStickMode(&config, WebLeftStickMode_Pointer))) { log_write("failed: webConfigSetLeftStickMode\n"); }
// if (R_FAILED(webConfigSetBootAsMediaPlayer(&config, true))) { log_write("failed: webConfigSetBootAsMediaPlayer\n"); }
if (R_FAILED(webConfigSetJsExtension(&config, true))) { log_write("failed: webConfigSetJsExtension\n"); }
if (R_FAILED(webConfigSetMediaPlayerAutoClose(&config, true))) { log_write("failed: webConfigSetMediaPlayerAutoClose\n"); }
if (R_FAILED(webConfigSetPageCache(&config, true))) { log_write("failed: webConfigSetPageCache\n"); }
if (R_FAILED(webConfigSetFooterFixedKind(&config, WebFooterFixedKind_Hidden))) { log_write("failed: webConfigSetFooterFixedKind\n"); }
if (R_FAILED(webConfigSetPageFade(&config, true))) { log_write("failed: webConfigSetPageFade\n"); }
if (R_FAILED(webConfigSetPageScrollIndicator(&config, true))) { log_write("failed: webConfigSetPageScrollIndicator\n"); }
// if (R_FAILED(webConfigSetMediaPlayerSpeedControl(&config, true))) { log_write("failed: webConfigSetMediaPlayerSpeedControl\n"); }
if (R_FAILED(webConfigSetBootMode(&config, WebSessionBootMode_AllForeground))) { log_write("failed: webConfigSetBootMode\n"); }
if (R_FAILED(webConfigSetTransferMemory(&config, true))) { log_write("failed: webConfigSetTransferMemory\n"); }
if (R_FAILED(webConfigSetTouchEnabledOnContents(&config, true))) { log_write("failed: webConfigSetTouchEnabledOnContents\n"); }
// if (R_FAILED(webConfigSetMediaPlayerUi(&config, true))) { log_write("failed: webConfigSetMediaPlayerUi\n"); }
// if (R_FAILED(webConfigSetWebAudio(&config, true))) { log_write("failed: webConfigSetWebAudio\n"); }
if (R_FAILED(webConfigSetPageCache(&config, true))) { log_write("failed: webConfigSetPageCache\n"); }
if (R_FAILED(webConfigSetBootLoadingIcon(&config, true))) { log_write("failed: webConfigSetBootLoadingIcon\n"); }
if (R_FAILED(webConfigSetUid(&config, account_uid))) { log_write("failed: webConfigSetUid\n"); }
if (R_FAILED(webConfigShow(&config, &reply))) { log_write("failed: webConfigShow\n"); }
if (R_FAILED(webReplyGetExitReason(&reply, &reason))) { log_write("failed: webReplyGetExitReason\n"); }
if (R_FAILED(webReplyGetLastUrl(&reply, last_url, sizeof(last_url), &last_url_len))) { log_write("failed: webReplyGetLastUrl\n"); }
log_write("last url: %s\n", last_url);
R_SUCCEED();
}
} // namespace sphaira

View File

@@ -2,6 +2,7 @@
#include "defines.hpp"
#include "log.hpp"
#include <memory>
#include <cstring>
namespace sphaira::yati::container {
namespace {
@@ -22,6 +23,38 @@ struct Pfs0FileTableEntry {
u32 padding;
};
// stdio-like wrapper for std::vector
struct BufHelper {
BufHelper() = default;
BufHelper(std::span<const u8> data) {
write(data);
}
void write(const void* data, u64 size) {
if (offset + size >= buf.size()) {
buf.resize(offset + size);
}
std::memcpy(buf.data() + offset, data, size);
offset += size;
}
void write(std::span<const u8> data) {
write(data.data(), data.size());
}
void seek(u64 where_to) {
offset = where_to;
}
[[nodiscard]]
auto tell() const {
return offset;
}
std::vector<u8> buf;
u64 offset{};
};
} // namespace
Result Nsp::GetCollections(Collections& out) {
@@ -56,4 +89,48 @@ Result Nsp::GetCollections(Collections& out) {
R_SUCCEED();
}
auto Nsp::Build(std::span<CollectionEntry> entries, s64& size) -> std::vector<u8> {
BufHelper buf;
Pfs0Header header{};
std::vector<Pfs0FileTableEntry> file_table(entries.size());
std::vector<char> string_table;
u64 string_offset{};
u64 data_offset{};
for (u32 i = 0; i < entries.size(); i++) {
file_table[i].data_offset = data_offset;
file_table[i].data_size = entries[i].size;
file_table[i].name_offset = string_offset;
file_table[i].padding = 0;
string_table.resize(string_offset + entries[i].name.length() + 1);
std::memcpy(string_table.data() + string_offset, entries[i].name.c_str(), entries[i].name.length() + 1);
data_offset += file_table[i].data_size;
string_offset += entries[i].name.length() + 1;
}
// align table
string_table.resize((string_table.size() + 0x1F) & ~0x1F);
header.magic = PFS0_MAGIC;
header.total_files = entries.size();
header.string_table_size = string_table.size();
header.padding = 0;
buf.write(&header, sizeof(header));
buf.write(file_table.data(), sizeof(Pfs0FileTableEntry) * file_table.size());
buf.write(string_table.data(), string_table.size());
// calculate nsp size.
size = buf.tell();
for (const auto& e : file_table) {
size += e.data_size;
}
return buf.buf;
}
} // namespace sphaira::yati::container

View File

@@ -1,6 +1,7 @@
#include "yati/nx/es.hpp"
#include "yati/nx/crypto.hpp"
#include "yati/nx/nxdumptool_rsa.h"
#include "yati/nx/service_guard.h"
#include "defines.hpp"
#include "log.hpp"
#include <memory>
@@ -9,12 +10,124 @@
namespace sphaira::es {
namespace {
Service g_esSrv;
NX_GENERATE_SERVICE_GUARD(es);
Result _esInitialize() {
return smGetService(&g_esSrv, "es");
}
void _esCleanup() {
serviceClose(&g_esSrv);
}
Result ListTicket(u32 cmd_id, s32 *out_entries_written, FsRightsId* out_ids, s32 count) {
struct {
u32 num_rights_ids_written;
} out;
const Result rc = serviceDispatchInOut(&g_esSrv, cmd_id, *out_entries_written, out,
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
.buffers = { { out_ids, count * sizeof(*out_ids) } },
);
if (R_SUCCEEDED(rc) && out_entries_written) *out_entries_written = out.num_rights_ids_written;
return rc;
}
} // namespace
Result ImportTicket(Service* srv, const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size) {
return serviceDispatch(srv, 1,
Result Initialize() {
return esInitialize();
}
void Exit() {
esExit();
}
Service* GetServiceSession() {
return &g_esSrv;
}
Result ImportTicket(const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size) {
return serviceDispatch(&g_esSrv, 1,
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In, SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
.buffers = { { tik_buf, tik_size }, { cert_buf, cert_size } });
.buffers = { { tik_buf, tik_size }, { cert_buf, cert_size } }
);
}
Result CountCommonTicket(s32* count) {
return serviceDispatchOut(&g_esSrv, 9, *count);
}
Result CountPersonalizedTicket(s32* count) {
return serviceDispatchOut(&g_esSrv, 10, *count);
}
Result ListCommonTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count) {
return ListTicket(11, out_entries_written, out_ids, count);
}
Result ListPersonalizedTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count) {
return ListTicket(12, out_entries_written, out_ids, count);
}
Result ListMissingPersonalizedTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count) {
return ListTicket(13, out_entries_written, out_ids, count);
}
Result GetCommonTicketSize(u64 *size_out, const FsRightsId* rightsId) {
return serviceDispatchInOut(&g_esSrv, 14, *rightsId, *size_out);
}
Result GetCommonTicketData(u64 *size_out, void *tik_data, u64 tik_size, const FsRightsId* rightsId) {
return serviceDispatchInOut(&g_esSrv, 16, *rightsId, *size_out,
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
.buffers = { { tik_data, tik_size } },
);
}
Result GetCommonTicketAndCertificateSize(u64 *tik_size_out, u64 *cert_size_out, const FsRightsId* rightsId) {
if (hosversionBefore(4,0,0)) {
return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
}
struct {
u64 ticket_size;
u64 cert_size;
} out;
const Result rc = serviceDispatchInOut(&g_esSrv, 22, *rightsId, out);
if (R_SUCCEEDED(rc)) {
*tik_size_out = out.ticket_size;
*cert_size_out = out.cert_size;
}
return rc;
}
Result GetCommonTicketAndCertificateData(u64 *tik_size_out, u64 *cert_size_out, void* tik_buf, u64 tik_size, void* cert_buf, u64 cert_size, const FsRightsId* rightsId) {
if (hosversionBefore(4,0,0)) {
return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
}
struct {
u64 ticket_size;
u64 cert_size;
} out;
const Result rc = serviceDispatchInOut(&g_esSrv, 23, *rightsId, out,
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out, SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
.buffers = { { tik_buf, tik_size }, { cert_buf, cert_size } }
);
if (R_SUCCEEDED(rc)) {
*tik_size_out = out.ticket_size;
*cert_size_out = out.cert_size;
}
return rc;
}
typedef enum {

View File

@@ -101,25 +101,26 @@ Result parse_keys(Keys& out, bool read_from_file) {
ON_SCOPE_EXIT(setcalExit());
R_TRY(setcalGetEticketDeviceKey(std::addressof(out.eticket_device_key)));
R_UNLESS(ini_browse(cb, std::addressof(out), "/switch/prod.keys"), 0x1);
// it doesn't matter if this fails, its just that title decryption will also fail.
if (ini_browse(cb, std::addressof(out), "/switch/prod.keys")) {
// decrypt eticket device key.
if (out.eticket_rsa_kek.IsValid()) {
auto rsa_key = (es::EticketRsaDeviceKey*)out.eticket_device_key.key;
// decrypt eticket device key.
if (out.eticket_rsa_kek.IsValid()) {
auto rsa_key = (es::EticketRsaDeviceKey*)out.eticket_device_key.key;
Aes128CtrContext eticket_aes_ctx{};
aes128CtrContextCreate(&eticket_aes_ctx, &out.eticket_rsa_kek, rsa_key->ctr);
aes128CtrCrypt(&eticket_aes_ctx, &(rsa_key->private_exponent), &(rsa_key->private_exponent), sizeof(es::EticketRsaDeviceKey) - sizeof(rsa_key->ctr));
Aes128CtrContext eticket_aes_ctx{};
aes128CtrContextCreate(&eticket_aes_ctx, &out.eticket_rsa_kek, rsa_key->ctr);
aes128CtrCrypt(&eticket_aes_ctx, &(rsa_key->private_exponent), &(rsa_key->private_exponent), sizeof(es::EticketRsaDeviceKey) - sizeof(rsa_key->ctr));
const auto public_exponent = std::byteswap(rsa_key->public_exponent);
if (public_exponent != 0x10001) {
log_write("etick decryption fail: 0x%X\n", public_exponent);
if (public_exponent == 0) {
log_write("eticket device id is NULL\n");
const auto public_exponent = std::byteswap(rsa_key->public_exponent);
if (public_exponent != 0x10001) {
log_write("etick decryption fail: 0x%X\n", public_exponent);
if (public_exponent == 0) {
log_write("eticket device id is NULL\n");
}
R_THROW(0x1);
} else {
log_write("eticket match\n");
}
R_THROW(0x1);
} else {
log_write("eticket match\n");
}
}
}

View File

@@ -151,6 +151,74 @@ Result VerifyFixedKey(const Header& header) {
R_SUCCEED();
}
Result ParseCnmt(const fs::FsPath& path, u64 program_id, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos) {
FsFileSystem fs;
R_TRY(fsOpenFileSystemWithId(std::addressof(fs), program_id, FsFileSystemType_ContentMeta, path, FsContentAttributes_All));
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
FsDir dir;
R_TRY(fsFsOpenDirectory(std::addressof(fs), fs::FsPath{"/"}, FsDirOpenMode_ReadFiles, std::addressof(dir)));
ON_SCOPE_EXIT(fsDirClose(std::addressof(dir)));
s64 total_entries;
FsDirectoryEntry buf;
R_TRY(fsDirRead(std::addressof(dir), std::addressof(total_entries), 1, std::addressof(buf)));
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::AppendPath("/", buf.name), FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
s64 offset{};
u64 bytes_read;
R_TRY(fsFileRead(std::addressof(file), offset, std::addressof(header), sizeof(header), 0, std::addressof(bytes_read)));
offset += bytes_read;
// read extended header
extended_header.resize(header.meta_header.extended_header_size);
R_TRY(fsFileRead(std::addressof(file), offset, extended_header.data(), extended_header.size(), 0, std::addressof(bytes_read)));
offset += bytes_read;
// read infos.
infos.resize(header.meta_header.content_count);
R_TRY(fsFileRead(std::addressof(file), offset, infos.data(), infos.size() * sizeof(NcmPackagedContentInfo), 0, std::addressof(bytes_read)));
offset += bytes_read;
R_SUCCEED();
}
Result ParseControl(const fs::FsPath& path, u64 program_id, void* nacp_out, s64 nacp_size, std::vector<u8>* icon_out) {
FsFileSystem fs;
R_TRY(fsOpenFileSystemWithId(std::addressof(fs), program_id, FsFileSystemType_ContentControl, path, FsContentAttributes_All));
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
// read nacp.
if (nacp_out) {
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/control.nacp"}, FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
u64 bytes_read;
R_TRY(fsFileRead(&file, 0, nacp_out, nacp_size, 0, &bytes_read));
}
// read icon.
if (icon_out) {
// todo: use matching icon based on the language version.
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/icon_AmericanEnglish.dat"}, FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
s64 size;
R_TRY(fsFileGetSize(std::addressof(file), std::addressof(size)));
icon_out->resize(size);
u64 bytes_read;
R_TRY(fsFileRead(&file, 0, icon_out->data(), icon_out->size(), 0, &bytes_read));
}
R_SUCCEED();
}
auto GetKeyGenStr(u8 key_gen) -> const char* {
switch (key_gen) {
case KeyGenerationOld_100: return "1.0.0";
@@ -172,6 +240,7 @@ auto GetKeyGenStr(u8 key_gen) -> const char* {
case KeyGeneration_1700: return "17.0.0";
case KeyGeneration_1800: return "18.0.0";
case KeyGeneration_1900: return "19.0.0";
case KeyGeneration_2000: return "20.0.0";
}
return "Unknown";

View File

@@ -1,12 +1,66 @@
#include "yati/nx/ncm.hpp"
#include "defines.hpp"
#include <memory>
#include <bit>
#include <cstring>
#include <cstdlib>
namespace sphaira::ncm {
namespace {
} // namespace
auto GetMetaTypeStr(u8 meta_type) -> const char* {
switch (meta_type) {
case NcmContentMetaType_Unknown: return "Unknown";
case NcmContentMetaType_SystemProgram: return "SystemProgram";
case NcmContentMetaType_SystemData: return "SystemData";
case NcmContentMetaType_SystemUpdate: return "SystemUpdate";
case NcmContentMetaType_BootImagePackage: return "BootImagePackage";
case NcmContentMetaType_BootImagePackageSafe: return "BootImagePackageSafe";
case NcmContentMetaType_Application: return "Application";
case NcmContentMetaType_Patch: return "Patch";
case NcmContentMetaType_AddOnContent: return "AddOnContent";
case NcmContentMetaType_Delta: return "Delta";
case NcmContentMetaType_DataPatch: return "DataPatch";
}
return "Unknown";
}
// taken from nxdumptool
auto GetMetaTypeShortStr(u8 meta_type) -> const char* {
switch (meta_type) {
case NcmContentMetaType_Unknown: return "UNK";
case NcmContentMetaType_SystemProgram: return "SYSPRG";
case NcmContentMetaType_SystemData: return "SYSDAT";
case NcmContentMetaType_SystemUpdate: return "SYSUPD";
case NcmContentMetaType_BootImagePackage: return "BIP";
case NcmContentMetaType_BootImagePackageSafe: return "BIPS";
case NcmContentMetaType_Application: return "BASE";
case NcmContentMetaType_Patch: return "UPD";
case NcmContentMetaType_AddOnContent: return "DLC";
case NcmContentMetaType_Delta: return "DELTA";
case NcmContentMetaType_DataPatch: return "DLCUPD";
}
return "UNK";
}
auto GetStorageIdStr(u8 storage_id) -> const char* {
switch (storage_id) {
case NcmStorageId_None: return "None";
case NcmStorageId_Host: return "Host";
case NcmStorageId_GameCard: return "GameCard";
case NcmStorageId_BuiltInSystem: return "BuiltInSystem";
case NcmStorageId_BuiltInUser: return "BuiltInUser";
case NcmStorageId_SdCard: return "SdCard";
case NcmStorageId_Any: return "Any";
}
return "Unknown";
}
auto GetAppId(u8 meta_type, u64 id) -> u64 {
if (meta_type == NcmContentMetaType_Patch) {
return id ^ 0x800;
@@ -25,6 +79,18 @@ auto GetAppId(const PackagedContentMeta& meta) -> u64 {
return GetAppId(meta.meta_type, meta.title_id);
}
auto GetContentIdFromStr(const char* str) -> NcmContentId {
char lowerU64[0x11]{};
char upperU64[0x11]{};
std::memcpy(lowerU64, str, 0x10);
std::memcpy(upperU64, str + 0x10, 0x10);
NcmContentId nca_id{};
*(u64*)nca_id.c = std::byteswap(std::strtoul(lowerU64, nullptr, 0x10));
*(u64*)(nca_id.c + 8) = std::byteswap(std::strtoul(upperU64, nullptr, 0x10));
return nca_id;
}
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id) {
bool has;
R_TRY(ncmContentStorageHas(cs, std::addressof(has), content_id));

View File

@@ -14,368 +14,95 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Most of the usb transfer code was taken from Haze.
// The USB transfer code was taken from Haze (part of Atmosphere).
// The USB protocol was taken from Tinfoil, by Adubbz.
#include "yati/source/usb.hpp"
#include "usb/tinfoil.hpp"
#include "log.hpp"
#include <ranges>
namespace sphaira::yati::source {
namespace {
constexpr u32 MAGIC = 0x53504841;
constexpr u32 VERSION = 2;
struct SendHeader {
u32 magic;
u32 version;
};
struct RecvHeader {
u32 magic;
u32 version;
u32 bcdUSB;
u32 count;
};
namespace tinfoil = usb::tinfoil;
} // namespace
Usb::Usb(u64 transfer_timeout) {
m_open_result = usbDsInitialize();
m_transfer_timeout = transfer_timeout;
m_usb = std::make_unique<usb::UsbDs>(transfer_timeout);
m_open_result = m_usb->Init();
}
Usb::~Usb() {
if (R_SUCCEEDED(GetOpenResult())) {
usbDsExit();
}
}
Result Usb::Init() {
log_write("doing USB init\n");
R_TRY(m_open_result);
Result Usb::WaitForConnection(u64 timeout, std::vector<std::string>& out_names) {
tinfoil::TUSHeader header;
R_TRY(m_usb->TransferAll(true, &header, sizeof(header), timeout));
R_UNLESS(header.magic == tinfoil::Magic_List0, Result_BadMagic);
R_UNLESS(header.nspListSize > 0, Result_BadCount);
log_write("USB got header\n");
u8 iManufacturer, iProduct, iSerialNumber;
static const u16 supported_langs[1] = {0x0409};
// Send language descriptor
R_TRY(usbDsAddUsbLanguageStringDescriptor(NULL, supported_langs, sizeof(supported_langs)/sizeof(u16)));
// Send manufacturer
R_TRY(usbDsAddUsbStringDescriptor(&iManufacturer, "Nintendo"));
// Send product
R_TRY(usbDsAddUsbStringDescriptor(&iProduct, "Nintendo Switch"));
// Send serial number
R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, "SerialNumber"));
std::vector<char> names(header.nspListSize);
R_TRY(m_usb->TransferAll(true, names.data(), names.size(), timeout));
// Send device descriptors
struct usb_device_descriptor device_descriptor = {
.bLength = USB_DT_DEVICE_SIZE,
.bDescriptorType = USB_DT_DEVICE,
.bcdUSB = 0x0110,
.bDeviceClass = 0x00,
.bDeviceSubClass = 0x00,
.bDeviceProtocol = 0x00,
.bMaxPacketSize0 = 0x40,
.idVendor = 0x057e,
.idProduct = 0x3000,
.bcdDevice = 0x0100,
.iManufacturer = iManufacturer,
.iProduct = iProduct,
.iSerialNumber = iSerialNumber,
.bNumConfigurations = 0x01
};
// Full Speed is USB 1.1
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Full, &device_descriptor));
// High Speed is USB 2.0
device_descriptor.bcdUSB = 0x0200;
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_High, &device_descriptor));
// Super Speed is USB 3.0
device_descriptor.bcdUSB = 0x0300;
// Upgrade packet size to 512
device_descriptor.bMaxPacketSize0 = 0x09;
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Super, &device_descriptor));
// Define Binary Object Store
const u8 bos[0x16] = {
0x05, // .bLength
USB_DT_BOS, // .bDescriptorType
0x16, 0x00, // .wTotalLength
0x02, // .bNumDeviceCaps
// USB 2.0
0x07, // .bLength
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
0x02, // .bDevCapabilityType
0x02, 0x00, 0x00, 0x00, // dev_capability_data
// USB 3.0
0x0A, // .bLength
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
0x03, /* .bDevCapabilityType */
0x00, /* .bmAttributes */
0x0E, 0x00, /* .wSpeedSupported */
0x03, /* .bFunctionalitySupport */
0x00, /* .bU1DevExitLat */
0x00, 0x00 /* .bU2DevExitLat */
};
R_TRY(usbDsSetBinaryObjectStore(bos, sizeof(bos)));
struct usb_interface_descriptor interface_descriptor = {
.bLength = USB_DT_INTERFACE_SIZE,
.bDescriptorType = USB_DT_INTERFACE,
.bInterfaceNumber = USBDS_DEFAULT_InterfaceNumber, // set below
.bNumEndpoints = static_cast<u8>(std::size(m_endpoints)),
.bInterfaceClass = USB_CLASS_VENDOR_SPEC,
.bInterfaceSubClass = USB_CLASS_VENDOR_SPEC,
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
};
struct usb_endpoint_descriptor endpoint_descriptor_in = {
.bLength = USB_DT_ENDPOINT_SIZE,
.bDescriptorType = USB_DT_ENDPOINT,
.bEndpointAddress = USB_ENDPOINT_IN,
.bmAttributes = USB_TRANSFER_TYPE_BULK,
.wMaxPacketSize = 0x40,
};
struct usb_endpoint_descriptor endpoint_descriptor_out = {
.bLength = USB_DT_ENDPOINT_SIZE,
.bDescriptorType = USB_DT_ENDPOINT,
.bEndpointAddress = USB_ENDPOINT_OUT,
.bmAttributes = USB_TRANSFER_TYPE_BULK,
.wMaxPacketSize = 0x40,
};
const struct usb_ss_endpoint_companion_descriptor endpoint_companion = {
.bLength = sizeof(struct usb_ss_endpoint_companion_descriptor),
.bDescriptorType = USB_DT_SS_ENDPOINT_COMPANION,
.bMaxBurst = 0x0F,
.bmAttributes = 0x00,
.wBytesPerInterval = 0x00,
};
R_TRY(usbDsRegisterInterface(&m_interface));
interface_descriptor.bInterfaceNumber = m_interface->interface_index;
endpoint_descriptor_in.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
// Full Speed Config
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &interface_descriptor, USB_DT_INTERFACE_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
// High Speed Config
endpoint_descriptor_in.wMaxPacketSize = 0x200;
endpoint_descriptor_out.wMaxPacketSize = 0x200;
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &interface_descriptor, USB_DT_INTERFACE_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
// Super Speed Config
endpoint_descriptor_in.wMaxPacketSize = 0x400;
endpoint_descriptor_out.wMaxPacketSize = 0x400;
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &interface_descriptor, USB_DT_INTERFACE_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
//Setup endpoints.
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_In], endpoint_descriptor_in.bEndpointAddress));
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_Out], endpoint_descriptor_out.bEndpointAddress));
R_TRY(usbDsInterface_EnableInterface(m_interface));
R_TRY(usbDsEnable());
log_write("success USB init\n");
R_SUCCEED();
}
Result Usb::WaitForConnection(u64 timeout, u32& speed, u32& count) {
const SendHeader send_header{
.magic = MAGIC,
.version = VERSION,
};
alignas(0x1000) u8 aligned[0x1000]{};
std::memcpy(aligned, std::addressof(send_header), sizeof(send_header));
// send header.
u32 transferredSize;
R_TRY(TransferPacketImpl(false, aligned, sizeof(send_header), &transferredSize, timeout));
// receive header.
struct RecvHeader recv_header{};
R_TRY(TransferPacketImpl(true, aligned, sizeof(recv_header), &transferredSize, timeout));
// copy data into header struct.
std::memcpy(&recv_header, aligned, sizeof(recv_header));
// validate received header.
R_UNLESS(recv_header.magic == MAGIC, Result_BadMagic);
R_UNLESS(recv_header.version == VERSION, Result_BadVersion);
R_UNLESS(recv_header.count > 0, Result_BadCount);
count = recv_header.count;
speed = recv_header.bcdUSB;
R_SUCCEED();
}
Result Usb::GetFileInfo(std::string& name_out, u64& size_out) {
struct {
u64 size;
u64 name_length;
} file_info_meta;
alignas(0x1000) u8 aligned[0x1000]{};
// receive meta.
u32 transferredSize;
R_TRY(TransferPacketImpl(true, aligned, sizeof(file_info_meta), &transferredSize, m_transfer_timeout));
std::memcpy(&file_info_meta, aligned, sizeof(file_info_meta));
R_UNLESS(file_info_meta.name_length < sizeof(aligned), 0x1);
R_TRY(TransferPacketImpl(true, aligned, file_info_meta.name_length, &transferredSize, m_transfer_timeout));
name_out.resize(file_info_meta.name_length);
std::memcpy(name_out.data(), aligned, name_out.size());
size_out = file_info_meta.size;
R_SUCCEED();
}
bool Usb::GetConfigured() const {
UsbState usb_state;
usbDsGetState(std::addressof(usb_state));
return usb_state == UsbState_Configured;
}
Event *Usb::GetCompletionEvent(UsbSessionEndpoint ep) const {
return std::addressof(m_endpoints[ep]->CompletionEvent);
}
Result Usb::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) const {
auto event = GetCompletionEvent(ep);
const auto rc = eventWait(event, timeout);
if (R_FAILED(rc)) {
R_TRY(usbDsEndpoint_Cancel(m_endpoints[ep]));
eventClear(event);
}
return rc;
}
Result Usb::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) const {
return usbDsEndpoint_PostBufferAsync(m_endpoints[ep], buffer, size, out_urb_id);
}
Result Usb::GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) const {
UsbDsReportData report_data;
R_TRY(eventClear(std::addressof(m_endpoints[ep]->CompletionEvent)));
R_TRY(usbDsEndpoint_GetReportData(m_endpoints[ep], std::addressof(report_data)));
R_TRY(usbDsParseReportData(std::addressof(report_data), urb_id, out_requested_size, out_transferred_size));
R_SUCCEED();
}
Result Usb::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) const {
u32 urb_id;
/* If we're not configured yet, wait to become configured first. */
// R_TRY(usbDsWaitReady(timeout));
if (!GetConfigured()) {
R_TRY(eventWait(usbDsGetStateChangeEvent(), timeout));
R_TRY(eventClear(usbDsGetStateChangeEvent()));
R_THROW(0xEA01);
}
/* Select the appropriate endpoint and begin a transfer. */
const auto ep = read ? UsbSessionEndpoint_Out : UsbSessionEndpoint_In;
R_TRY(TransferAsync(ep, page, size, std::addressof(urb_id)));
/* Try to wait for the event. */
R_TRY(WaitTransferCompletion(ep, timeout));
/* Return what we transferred. */
return GetTransferResult(ep, urb_id, nullptr, out_size_transferred);
}
Result Usb::SendCommand(s64 off, s64 size) const {
struct {
u32 hash;
u32 magic;
s64 off;
s64 size;
} meta{0, 0, off, size};
alignas(0x1000) static u8 aligned[0x1000]{};
std::memcpy(aligned, std::addressof(meta), sizeof(meta));
u32 transferredSize;
return TransferPacketImpl(false, aligned, sizeof(meta), &transferredSize, m_transfer_timeout);
}
Result Usb::Finished() const {
return SendCommand(0, 0);
}
Result Usb::InternalRead(void* _buf, s64 off, s64 size) const {
u8* buf = (u8*)_buf;
alignas(0x1000) u8 aligned[0x1000]{};
const auto stored_size = size;
s64 total = 0;
while (size) {
auto read_size = size;
auto read_buf = buf;
if (u64(buf) & 0xFFF) {
read_size = std::min<u64>(size, sizeof(aligned) - (u64(buf) & 0xFFF));
read_buf = aligned;
log_write("unaligned read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
} else if (read_size & 0xFFF) {
if (read_size <= 0xFFF) {
log_write("unaligned small read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
read_buf = aligned;
} else {
log_write("unaligned big read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
// read as much as possible into buffer, the rest will
// be handled in a second read which will be aligned size aligned.
read_size = read_size & ~0xFFF;
}
out_names.clear();
for (const auto& name : std::views::split(names, '\n')) {
if (!name.empty()) {
out_names.emplace_back(name.data(), name.size());
}
R_TRY(SendCommand(off, read_size));
u32 transferredSize{};
R_TRY(TransferPacketImpl(true, read_buf, read_size, &transferredSize, m_transfer_timeout));
R_UNLESS(transferredSize <= read_size, Result_BadTransferSize);
if (read_buf == aligned) {
std::memcpy(buf, aligned, transferredSize);
}
if (transferredSize < read_size) {
log_write("reading less than expected! %u vs %zd stored: %zd\n", transferredSize, read_size, stored_size);
}
off += transferredSize;
buf += transferredSize;
size -= transferredSize;
total += transferredSize;
}
R_UNLESS(total == stored_size, Result_BadTotalSize);
for (auto& name : out_names) {
log_write("got name: %s\n", name.c_str());
}
R_UNLESS(!out_names.empty(), Result_BadCount);
log_write("USB SUCCESS\n");
R_SUCCEED();
}
void Usb::SetFileNameForTranfser(const std::string& name) {
m_transfer_file_name = name;
}
Result Usb::SendCmdHeader(u32 cmdId, size_t dataSize, u64 timeout) {
tinfoil::USBCmdHeader header{
.magic = tinfoil::Magic_Command0,
.type = tinfoil::USBCmdType::REQUEST,
.cmdId = cmdId,
.dataSize = dataSize,
};
return m_usb->TransferAll(false, &header, sizeof(header), timeout);
}
Result Usb::SendFileRangeCmd(u64 off, u64 size, u64 timeout) {
tinfoil::FileRangeCmdHeader fRangeHeader;
fRangeHeader.size = size;
fRangeHeader.offset = off;
fRangeHeader.nspNameLen = m_transfer_file_name.size();
fRangeHeader.padding = 0;
R_TRY(SendCmdHeader(tinfoil::USBCmdId::FILE_RANGE, sizeof(fRangeHeader) + fRangeHeader.nspNameLen, timeout));
R_TRY(m_usb->TransferAll(false, &fRangeHeader, sizeof(fRangeHeader), timeout));
R_TRY(m_usb->TransferAll(false, m_transfer_file_name.data(), m_transfer_file_name.size(), timeout));
tinfoil::USBCmdHeader responseHeader;
R_TRY(m_usb->TransferAll(true, &responseHeader, sizeof(responseHeader), timeout));
R_SUCCEED();
}
Result Usb::Finished(u64 timeout) {
return SendCmdHeader(tinfoil::USBCmdId::EXIT, 0, timeout);
}
Result Usb::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
R_TRY(GetOpenResult());
R_TRY(InternalRead(buf, off, size));
R_TRY(SendFileRangeCmd(off, size, m_usb->GetTransferTimeout()));
R_TRY(m_usb->TransferAll(true, buf, size));
*bytes_read = size;
R_SUCCEED();
}

View File

@@ -18,9 +18,9 @@
#include "i18n.hpp"
#include "log.hpp"
#include <new>
#include <zstd.h>
#include <minIni.h>
#include <algorithm>
namespace sphaira::yati {
namespace {
@@ -30,40 +30,10 @@ constexpr NcmStorageId NCM_STORAGE_IDS[]{
NcmStorageId_SdCard,
};
// custom allocator for std::vector that respects alignment.
// https://en.cppreference.com/w/cpp/named_req/Allocator
template <typename T, std::size_t Align>
struct CustomVectorAllocator {
public:
// https://en.cppreference.com/w/cpp/memory/new/operator_new
auto allocate(std::size_t n) -> T* {
// log_write("allocating ptr size: %zu\n", n);
return new(align) T[n];
}
// https://en.cppreference.com/w/cpp/memory/new/operator_delete
auto deallocate(T* p, std::size_t n) noexcept -> void {
// log_write("deleting ptr size: %zu\n", n);
::operator delete[] (p, n, align);
}
private:
static constexpr inline std::align_val_t align{Align};
};
template <typename T>
struct PageAllocator : CustomVectorAllocator<T, 0x1000> {
using value_type = T; // used by std::vector
};
template<class T, class U>
bool operator==(const PageAllocator <T>&, const PageAllocator <U>&) { return true; }
using PageAlignedVector = std::vector<u8, PageAllocator<u8>>;
constexpr u32 KEYGEN_LIMIT = 0x20;
struct NcaCollection : container::CollectionEntry {
nca::Header header{};
// NcmContentType
u8 type{};
NcmContentId content_id{};
@@ -72,6 +42,8 @@ struct NcaCollection : container::CollectionEntry {
u8 hash[SHA256_HASH_SIZE]{};
// set true if nca has been modified.
bool modified{};
// set if the nca was not installed.
bool skipped{};
};
struct CnmtCollection : NcaCollection {
@@ -81,7 +53,7 @@ struct CnmtCollection : NcaCollection {
// if set, the ticket / cert will be installed once all nca's have installed.
std::vector<FsRightsId> rights_id{};
NcmContentMetaHeader header{};
NcmContentMetaHeader meta_header{};
NcmContentMetaKey key{};
NcmContentInfo content_info{};
std::vector<u8> extended_header{};
@@ -107,7 +79,7 @@ struct ThreadBuffer {
buf.reserve(INFLATE_BUFFER_MAX);
}
PageAlignedVector buf;
std::vector<u8> buf;
s64 off;
};
@@ -137,7 +109,7 @@ public:
return ringbuf_capacity() - ringbuf_size();
}
void ringbuf_push(PageAlignedVector& buf_in, s64 off_in) {
void ringbuf_push(std::vector<u8>& buf_in, s64 off_in) {
auto& value = this->buf[this->w_index % ringbuf_capacity()];
value.off = off_in;
std::swap(value.buf, buf_in);
@@ -145,7 +117,7 @@ public:
this->w_index = (this->w_index + 1U) % (ringbuf_capacity() * 2U);
}
void ringbuf_pop(PageAlignedVector& buf_out, s64& off_out) {
void ringbuf_pop(std::vector<u8>& buf_out, s64& off_out) {
auto& value = this->buf[this->r_index % ringbuf_capacity()];
off_out = value.off;
std::swap(value.buf, buf_out);
@@ -178,7 +150,7 @@ struct ThreadData {
Result Read(void* buf, s64 size, u64* bytes_read);
Result SetDecompressBuf(PageAlignedVector& buf, s64 off, s64 size) {
Result SetDecompressBuf(std::vector<u8>& buf, s64 off, s64 size) {
buf.resize(size);
mutexLock(std::addressof(read_mutex));
@@ -192,7 +164,7 @@ struct ThreadData {
return condvarWakeOne(std::addressof(can_decompress));
}
Result GetDecompressBuf(PageAlignedVector& buf_out, s64& off_out) {
Result GetDecompressBuf(std::vector<u8>& buf_out, s64& off_out) {
mutexLock(std::addressof(read_mutex));
if (!read_buffers.ringbuf_size()) {
R_TRY(condvarWait(std::addressof(can_decompress), std::addressof(read_mutex)));
@@ -204,7 +176,7 @@ struct ThreadData {
return condvarWakeOne(std::addressof(can_read));
}
Result SetWriteBuf(PageAlignedVector& buf, s64 size, bool skip_verify) {
Result SetWriteBuf(std::vector<u8>& buf, s64 size, bool skip_verify) {
buf.resize(size);
if (!skip_verify) {
sha256ContextUpdate(std::addressof(sha256), buf.data(), buf.size());
@@ -221,7 +193,7 @@ struct ThreadData {
return condvarWakeOne(std::addressof(can_write));
}
Result GetWriteBuf(PageAlignedVector& buf_out, s64& off_out) {
Result GetWriteBuf(std::vector<u8>& buf_out, s64& off_out) {
mutexLock(std::addressof(write_mutex));
if (!write_buffers.ringbuf_size()) {
R_TRY(condvarWait(std::addressof(can_write), std::addressof(write_mutex)));
@@ -276,8 +248,8 @@ struct Yati {
Result Setup(const ConfigOverride& override);
Result InstallNca(std::span<TikCollection> tickets, NcaCollection& nca);
Result InstallNcaInternal(std::span<TikCollection> tickets, NcaCollection& nca);
Result InstallCnmtNca(std::span<TikCollection> tickets, CnmtCollection& cnmt, const container::Collections& collections);
Result InstallControlNca(std::span<TikCollection> tickets, const CnmtCollection& cnmt, NcaCollection& nca);
Result readFuncInternal(ThreadData* t);
Result decompressFuncInternal(ThreadData* t);
@@ -303,7 +275,6 @@ struct Yati {
NcmContentMetaDatabase db{};
NcmStorageId storage_id{};
Service es{};
Service ns_app{};
std::unique_ptr<container::Base> container{};
Config config{};
@@ -361,11 +332,11 @@ HashStr hexIdToStr(auto id) {
// parsing ncz headers, sections and reading ncz blocks
Result Yati::readFuncInternal(ThreadData* t) {
// the main buffer which data is read into.
PageAlignedVector buf;
std::vector<u8> buf;
// workaround ncz block reading ahead. if block isn't found, we usually
// would seek back to the offset, however this is not possible in stream
// mode, so we instead store the data to the temp buffer and pre-pend it.
PageAlignedVector temp_buf;
std::vector<u8> temp_buf;
buf.reserve(t->max_buffer_size);
temp_buf.reserve(t->max_buffer_size);
@@ -453,12 +424,12 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
s64 inflate_offset{};
Aes128CtrContext ctx{};
PageAlignedVector inflate_buf{};
std::vector<u8> inflate_buf{};
inflate_buf.reserve(t->max_buffer_size);
s64 written{};
s64 decompress_buf_off{};
PageAlignedVector buf{};
std::vector<u8> buf{};
buf.reserve(t->max_buffer_size);
// encrypts the nca and passes the buffer to the write thread.
@@ -471,7 +442,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
// the remaining data.
// rather that copying the entire vector to the write thread,
// only copy (store) the remaining amount.
PageAlignedVector temp_vector{};
std::vector<u8> temp_vector{};
if (size < inflate_offset) {
temp_vector.resize(inflate_offset - size);
std::memcpy(temp_vector.data(), inflate_buf.data() + size, temp_vector.size());
@@ -480,7 +451,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
for (s64 off = 0; off < size;) {
// log_write("looking for section\n");
if (!ncz_section || !ncz_section->InRange(written)) {
auto it = std::find_if(t->ncz_sections.cbegin(), t->ncz_sections.cend(), [written](auto& e){
auto it = std::ranges::find_if(t->ncz_sections, [written](auto& e){
return e.InRange(written);
});
@@ -532,12 +503,17 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
if (!is_ncz || !decompress_buf_off) {
// check nca header
if (!decompress_buf_off) {
log_write("reading nca header\n");
nca::Header header{};
crypto::cryptoAes128Xts(buf.data(), std::addressof(header), keys.header_key, 0, 0x200, sizeof(header), false);
log_write("verifying nca header magic\n");
R_UNLESS(header.magic == 0x3341434E, Result_InvalidNcaMagic);
log_write("nca magic is ok! type: %u\n", header.content_type);
// store the unmodified header.
t->nca->header = header;
if (!config.skip_rsa_header_fixed_key_verify) {
log_write("verifying nca fixed key\n");
R_TRY(nca::VerifyFixedKey(header));
@@ -547,6 +523,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
}
t->write_size = header.size;
log_write("setting placeholder size: %zu\n", header.size);
R_TRY(ncmContentStorageSetPlaceHolderSize(std::addressof(cs), std::addressof(t->nca->placeholder_id), header.size));
if (!config.ignore_distribution_bit && header.distribution_type == nca::DistributionType_GameCard) {
@@ -556,11 +533,13 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
TikCollection* ticket = nullptr;
if (isRightsIdValid(header.rights_id)) {
auto it = std::find_if(t->tik.begin(), t->tik.end(), [header](auto& e){
auto it = std::ranges::find_if(t->tik, [&header](auto& e){
return !std::memcmp(&header.rights_id, &e.rights_id, sizeof(e.rights_id));
});
log_write("looking for ticket %s\n", hexIdToStr(header.rights_id).str);
R_UNLESS(it != t->tik.end(), Result_TicketNotFound);
log_write("ticket found\n");
it->required = true;
ticket = &(*it);
}
@@ -632,7 +611,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
// todo: blocks need to use read offset, as the offset + size is compressed range.
if (t->ncz_blocks.size()) {
if (!ncz_block || !ncz_block->InRange(decompress_buf_off)) {
auto it = std::find_if(t->ncz_blocks.cbegin(), t->ncz_blocks.cend(), [decompress_buf_off](auto& e){
auto it = std::ranges::find_if(t->ncz_blocks, [decompress_buf_off](auto& e){
return e.InRange(decompress_buf_off);
});
@@ -709,7 +688,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
// write thread writes data to the nca placeholder.
Result Yati::writeFuncInternal(ThreadData* t) {
PageAlignedVector buf;
std::vector<u8> buf;
buf.reserve(t->max_buffer_size);
while (t->write_offset < t->write_size && R_SUCCEEDED(t->GetResults())) {
@@ -782,18 +761,23 @@ Yati::~Yati() {
splCryptoExit();
serviceClose(std::addressof(ns_app));
nsExit();
es::Exit();
for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) {
ncmContentMetaDatabaseClose(std::addressof(ncm_db[i]));
ncmContentStorageClose(std::addressof(ncm_cs[i]));
}
serviceClose(std::addressof(es));
appletSetMediaPlaybackState(false);
if (config.boost_mode) {
appletSetCpuBoostMode(ApmCpuBoostMode_Normal);
}
}
Result Yati::Setup(const ConfigOverride& override) {
config.sd_card_install = override.sd_card_install.value_or(App::GetApp()->m_install_sd.Get());
config.boost_mode = App::GetApp()->m_boost_mode.Get();
config.allow_downgrade = App::GetApp()->m_allow_downgrade.Get();
config.skip_if_already_installed = App::GetApp()->m_skip_if_already_installed.Get();
config.ticket_only = App::GetApp()->m_ticket_only.Get();
@@ -811,11 +795,15 @@ Result Yati::Setup(const ConfigOverride& override) {
config.lower_system_version = override.lower_system_version.value_or(App::GetApp()->m_lower_system_version.Get());
storage_id = config.sd_card_install ? NcmStorageId_SdCard : NcmStorageId_BuiltInUser;
if (config.boost_mode) {
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
}
R_TRY(source->GetOpenResult());
R_TRY(splCryptoInitialize());
R_TRY(nsInitialize());
R_TRY(nsGetApplicationManagerInterface(std::addressof(ns_app)));
R_TRY(smGetService(std::addressof(es), "es"));
R_TRY(es::Initialize());
for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) {
R_TRY(ncmOpenContentMetaDatabase(std::addressof(ncm_db[i]), NCM_STORAGE_IDS[i]));
@@ -829,10 +817,17 @@ Result Yati::Setup(const ConfigOverride& override) {
R_SUCCEED();
}
Result Yati::InstallNca(std::span<TikCollection> tickets, NcaCollection& nca) {
log_write("in install nca\n");
pbox->NewTransfer(nca.name);
keys::parse_hex_key(std::addressof(nca.content_id), nca.name.c_str());
Result Yati::InstallNcaInternal(std::span<TikCollection> tickets, NcaCollection& nca) {
if (config.skip_if_already_installed) {
R_TRY(ncmContentStorageHas(std::addressof(cs), std::addressof(nca.skipped), std::addressof(nca.content_id)));
if (nca.skipped) {
log_write("\tskipped nca as it's already installed ncmContentStorageHas()\n");
R_TRY(ncmContentStorageReadContentIdFile(std::addressof(cs), std::addressof(nca.header), sizeof(nca.header), std::addressof(nca.content_id), 0));
crypto::cryptoAes128Xts(std::addressof(nca.header), std::addressof(nca.header), keys.header_key, 0, 0x200, sizeof(nca.header), false);
R_SUCCEED();
}
}
log_write("generateing placeholder\n");
R_TRY(ncmContentStorageGeneratePlaceHolderId(std::addressof(cs), std::addressof(nca.placeholder_id)));
log_write("creating placeholder\n");
@@ -918,39 +913,73 @@ Result Yati::InstallNca(std::span<TikCollection> tickets, NcaCollection& nca) {
R_SUCCEED();
}
Result Yati::InstallNca(std::span<TikCollection> tickets, NcaCollection& nca) {
log_write("in install nca\n");
pbox->NewTransfer(nca.name);
keys::parse_hex_key(std::addressof(nca.content_id), nca.name.c_str());
R_TRY(InstallNcaInternal(tickets, nca));
fs::FsPath path;
if (nca.skipped) {
R_TRY(ncmContentStorageGetPath(std::addressof(cs), path, sizeof(path), std::addressof(nca.content_id)));
} else {
R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs)));
R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(nca.placeholder_id)));
}
if (nca.header.content_type == nca::ContentType_Program) {
// todo: verify npdm key.
} else if (nca.header.content_type == nca::ContentType_Control) {
NacpLanguageEntry entry;
std::vector<u8> icon;
// this may fail if tickets aren't installed and the nca uses title key crypto.
if (R_SUCCEEDED(nca::ParseControl(path, nca.header.program_id, &entry, sizeof(entry), &icon))) {
pbox->SetTitle(entry.name).SetImageData(icon);
}
}
R_SUCCEED();
}
Result Yati::InstallCnmtNca(std::span<TikCollection> tickets, CnmtCollection& cnmt, const container::Collections& collections) {
R_TRY(InstallNca(tickets, cnmt));
fs::FsPath path;
R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs)));
R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(cnmt.placeholder_id)));
if (cnmt.skipped) {
R_TRY(ncmContentStorageGetPath(std::addressof(cs), path, sizeof(path), std::addressof(cnmt.content_id)));
} else {
R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs)));
R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(cnmt.placeholder_id)));
}
ncm::PackagedContentMeta header;
std::vector<NcmPackagedContentInfo> infos;
R_TRY(ParseCnmtNca(path, header, cnmt.extended_header, infos));
R_TRY(nca::ParseCnmt(path, cnmt.header.program_id, header, cnmt.extended_header, infos));
for (const auto& info : infos) {
if (info.info.content_type == NcmContentType_DeltaFragment) {
for (const auto& packed_info : infos) {
const auto& info = packed_info.info;
if (info.content_type == NcmContentType_DeltaFragment) {
continue;
}
const auto str = hexIdToStr(info.info.content_id);
const auto it = std::find_if(collections.cbegin(), collections.cend(), [str](auto& e){
const auto str = hexIdToStr(info.content_id);
const auto it = std::ranges::find_if(collections, [&str](auto& e){
return e.name.find(str.str) != e.name.npos;
});
R_UNLESS(it != collections.cend(), Result_NcaNotFound);
log_write("found: %s\n", str.str);
cnmt.infos.emplace_back(info);
cnmt.infos.emplace_back(packed_info);
auto& nca = cnmt.ncas.emplace_back(*it);
nca.type = info.info.content_type;
nca.type = info.content_type;
}
// update header
cnmt.header = header.meta_header;
cnmt.header.content_count = cnmt.infos.size() + 1;
cnmt.header.storage_id = 0;
cnmt.meta_header = header.meta_header;
cnmt.meta_header.content_count = cnmt.infos.size() + 1;
cnmt.meta_header.storage_id = 0;
cnmt.key.id = header.title_id;
cnmt.key.version = header.title_version;
@@ -979,32 +1008,12 @@ Result Yati::InstallCnmtNca(std::span<TikCollection> tickets, CnmtCollection& cn
return lhs.type > rhs.type;
};
std::sort(cnmt.ncas.begin(), cnmt.ncas.end(), sorter);
std::ranges::sort(cnmt.ncas, sorter);
log_write("found all cnmts\n");
R_SUCCEED();
}
Result Yati::InstallControlNca(std::span<TikCollection> tickets, const CnmtCollection& cnmt, NcaCollection& nca) {
R_TRY(InstallNca(tickets, nca));
fs::FsPath path;
R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs)));
R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(nca.placeholder_id)));
// this can fail if it's not a valid control nca, examples are mario 3d all stars.
// there are 4 control ncas, only 1 is valid (InvalidNcaId 0x235E02).
NacpLanguageEntry entry;
std::vector<u8> icon;
if (R_SUCCEEDED(yati::ParseControlNca(path, ncm::GetAppId(cnmt.key), &entry, sizeof(entry), &icon))) {
pbox->SetTitle(entry.name).SetImageData(icon);
} else {
log_write("\tWARNING: failed to parse control nca!\n");
}
R_SUCCEED();
}
Result Yati::ParseTicketsIntoCollection(std::vector<TikCollection>& tickets, const container::Collections& collections, bool read_data) {
for (const auto& collection : collections) {
if (collection.name.ends_with(".tik")) {
@@ -1012,7 +1021,7 @@ Result Yati::ParseTicketsIntoCollection(std::vector<TikCollection>& tickets, con
keys::parse_hex_key(entry.rights_id.c, collection.name.c_str());
const auto str = collection.name.substr(0, collection.name.length() - 4) + ".cert";
const auto cert = std::find_if(collections.cbegin(), collections.cend(), [str](auto& e){
const auto cert = std::ranges::find_if(collections, [&str](auto& e){
return e.name.find(str) != e.name.npos;
});
@@ -1091,6 +1100,13 @@ Result Yati::ShouldSkip(const CnmtCollection& cnmt, bool& skip) {
} else if (config.skip_data_patch && cnmt.key.type == NcmContentMetaType_DataPatch) {
log_write("\tskipping: [NcmContentMetaType_DataPatch]\n");
skip = true;
} else if (config.skip_if_already_installed) {
bool has;
R_TRY(ncmContentMetaDatabaseHas(std::addressof(db), std::addressof(has), std::addressof(cnmt.key)));
if (has) {
log_write("\tskipping: [ncmContentMetaDatabaseHas()]\n");
skip = true;
}
}
R_SUCCEED();
@@ -1105,7 +1121,7 @@ Result Yati::ImportTickets(std::span<TikCollection> tickets) {
log_write("patching ticket\n");
R_TRY(es::PatchTicket(ticket.ticket, keys));
log_write("installing ticket\n");
R_TRY(es::ImportTicket(std::addressof(es), ticket.ticket.data(), ticket.ticket.size(), ticket.cert.data(), ticket.cert.size()));
R_TRY(es::ImportTicket(ticket.ticket.data(), ticket.ticket.size(), ticket.cert.data(), ticket.cert.size()));
ticket.required = false;
}
}
@@ -1183,7 +1199,7 @@ Result Yati::RegisterNcasAndPushRecord(const CnmtCollection& cnmt, u32 latest_ve
log_write("registered cnmt nca\n");
for (auto& nca : cnmt.ncas) {
if (nca.type != NcmContentType_DeltaFragment) {
if (!nca.skipped && nca.type != NcmContentType_DeltaFragment) {
log_write("registering nca: %s\n", nca.name.c_str());
R_TRY(ncm::Register(std::addressof(cs), std::addressof(nca.content_id), std::addressof(nca.placeholder_id)));
log_write("registered nca: %s\n", nca.name.c_str());
@@ -1194,7 +1210,7 @@ Result Yati::RegisterNcasAndPushRecord(const CnmtCollection& cnmt, u32 latest_ve
// build ncm meta and push to the database.
BufHelper buf{};
buf.write(std::addressof(cnmt.header), sizeof(cnmt.header));
buf.write(std::addressof(cnmt.meta_header), sizeof(cnmt.meta_header));
buf.write(cnmt.extended_header.data(), cnmt.extended_header.size());
buf.write(std::addressof(cnmt.content_info), sizeof(cnmt.content_info));
@@ -1262,12 +1278,7 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr<source::Base> sour
log_write("installing nca's\n");
for (auto& nca : cnmt.ncas) {
if (nca.type == NcmContentType_Control) {
log_write("installing control nca\n");
R_TRY(yati->InstallControlNca(tickets, cnmt, nca));
} else {
R_TRY(yati->InstallNca(tickets, nca));
}
R_TRY(yati->InstallNca(tickets, nca));
}
R_TRY(yati->ImportTickets(tickets));
@@ -1284,6 +1295,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
R_TRY(yati->Setup(override));
// not supported with stream installs (yet).
yati->config.skip_if_already_installed = false;
yati->config.convert_to_standard_crypto = false;
yati->config.lower_master_key = false;
@@ -1309,7 +1321,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
return lhs.offset < rhs.offset;
};
std::sort(collections.begin(), collections.end(), sorter);
std::ranges::sort(collections, sorter);
for (const auto& collection : collections) {
if (collection.name.ends_with(".nca") || collection.name.ends_with(".ncz")) {
@@ -1326,7 +1338,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
keys::parse_hex_key(rights_id.c, collection.name.c_str());
const auto str = collection.name.substr(0, collection.name.length() - 4) + ".cert";
auto entry = std::find_if(tickets.begin(), tickets.end(), [rights_id](auto& e){
auto entry = std::ranges::find_if(tickets, [&rights_id](auto& e){
return !std::memcmp(&rights_id, &e.rights_id, sizeof(rights_id));
});
@@ -1345,7 +1357,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
for (auto& cnmt : cnmts) {
// copy nca structs into cnmt.
for (auto& cnmt_nca : cnmt.ncas) {
auto it = std::find_if(ncas.cbegin(), ncas.cend(), [cnmt_nca](auto& e){
auto it = std::ranges::find_if(ncas, [&cnmt_nca](auto& e){
return e.name == cnmt_nca.name;
});
@@ -1412,71 +1424,4 @@ Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr<source::Bas
}
}
Result ParseCnmtNca(const fs::FsPath& path, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos) {
FsFileSystem fs;
R_TRY(fsOpenFileSystem(std::addressof(fs), FsFileSystemType_ContentMeta, path));
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
FsDir dir;
R_TRY(fsFsOpenDirectory(std::addressof(fs), fs::FsPath{"/"}, FsDirOpenMode_ReadFiles, std::addressof(dir)));
ON_SCOPE_EXIT(fsDirClose(std::addressof(dir)));
s64 total_entries;
FsDirectoryEntry buf;
R_TRY(fsDirRead(std::addressof(dir), std::addressof(total_entries), 1, std::addressof(buf)));
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::AppendPath("/", buf.name), FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
s64 offset{};
u64 bytes_read;
R_TRY(fsFileRead(std::addressof(file), offset, std::addressof(header), sizeof(header), 0, std::addressof(bytes_read)));
offset += bytes_read;
// read extended header
extended_header.resize(header.meta_header.extended_header_size);
R_TRY(fsFileRead(std::addressof(file), offset, extended_header.data(), extended_header.size(), 0, std::addressof(bytes_read)));
offset += bytes_read;
// read infos.
infos.resize(header.meta_header.content_count);
R_TRY(fsFileRead(std::addressof(file), offset, infos.data(), infos.size() * sizeof(NcmPackagedContentInfo), 0, std::addressof(bytes_read)));
offset += bytes_read;
R_SUCCEED();
}
Result ParseControlNca(const fs::FsPath& path, u64 id, void* nacp_out, s64 nacp_size, std::vector<u8>* icon_out) {
FsFileSystem fs;
R_TRY(fsOpenFileSystemWithId(std::addressof(fs), id, FsFileSystemType_ContentControl, path, FsContentAttributes_All));
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
// read nacp.
if (nacp_out) {
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/control.nacp"}, FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
u64 bytes_read;
R_TRY(fsFileRead(&file, 0, nacp_out, nacp_size, 0, &bytes_read));
}
// read icon.
if (icon_out) {
FsFile file;
R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/icon_AmericanEnglish.dat"}, FsOpenMode_Read, std::addressof(file)));
ON_SCOPE_EXIT(fsFileClose(std::addressof(file)));
s64 size;
R_TRY(fsFileGetSize(std::addressof(file), std::addressof(size)));
icon_out->resize(size);
u64 bytes_read;
R_TRY(fsFileRead(&file, 0, icon_out->data(), icon_out->size(), 0, &bytes_read));
}
R_SUCCEED();
}
} // namespace sphaira::yati

171
tools/usb_install_pc.py Normal file
View File

@@ -0,0 +1,171 @@
# This script depends on PyUSB. You can get it with pip install pyusb.
# You will also need libusb installed
# My sincere apologies for this process being overly complicated. Apparently Python and Windows
# aren't very friendly :(
# Windows Instructions:
# 1. Download Zadig from https://zadig.akeo.ie/.
# 2. With your switch plugged in and on the Tinfoil USB install menu,
# choose "List All Devices" under the options menu in Zadig, and select libnx USB comms.
# 3. Choose libusbK from the driver list and click the "Replace Driver" button.
# 4. Run this script
# macOS Instructions:
# 1. Install Homebrew https://brew.sh
# 2. Install Python 3
# sudo mkdir /usr/local/Frameworks
# sudo chown $(whoami) /usr/local/Frameworks
# brew install python
# 3. Install PyUSB
# pip3 install pyusb
# 4. Install libusb
# brew install libusb
# 5. Plug in your Switch and go to Tinfoil > Title Management > USB Install NSP
# 6. Run this script
# python3 usb_install_pc.py <path/to/nsp_folder>
import usb.core
import usb.util
import struct
import sys
from pathlib import Path
import time
CMD_ID_EXIT = 0
CMD_ID_FILE_RANGE = 1
CMD_TYPE_RESPONSE = 1
# list of supported extensions.
EXTS = (".nsp", ".xci", ".nsz", ".xcz")
def send_response_header(out_ep, cmd_id, data_size):
out_ep.write(b'TUC0') # Tinfoil USB Command 0
out_ep.write(struct.pack('<B', CMD_TYPE_RESPONSE))
out_ep.write(b'\x00' * 3)
out_ep.write(struct.pack('<I', cmd_id))
out_ep.write(struct.pack('<Q', data_size))
out_ep.write(b'\x00' * 0xC)
def file_range_cmd(nsp_dir, in_ep, out_ep, data_size):
file_range_header = in_ep.read(0x20)
range_size = struct.unpack('<Q', file_range_header[:8])[0]
range_offset = struct.unpack('<Q', file_range_header[8:16])[0]
nsp_name_len = struct.unpack('<Q', file_range_header[16:24])[0]
#in_ep.read(0x8) # Reserved
nsp_name = bytes(in_ep.read(nsp_name_len)).decode('utf-8')
print('Range Size: {}, Range Offset: {}, Name len: {}, Name: {}'.format(range_size, range_offset, nsp_name_len, nsp_name))
send_response_header(out_ep, CMD_ID_FILE_RANGE, range_size)
with open(nsp_name, 'rb') as f:
f.seek(range_offset)
curr_off = 0x0
end_off = range_size
read_size = 0x800000
while curr_off < end_off:
if curr_off + read_size >= end_off:
read_size = end_off - curr_off
buf = f.read(read_size)
out_ep.write(data=buf, timeout=0)
curr_off += read_size
def poll_commands(nsp_dir, in_ep, out_ep):
while True:
cmd_header = bytes(in_ep.read(0x20, timeout=0))
magic = cmd_header[:4]
print('Magic: {}'.format(magic), flush=True)
if magic != b'TUC0': # Tinfoil USB Command 0
continue
cmd_type = struct.unpack('<B', cmd_header[4:5])[0]
cmd_id = struct.unpack('<I', cmd_header[8:12])[0]
data_size = struct.unpack('<Q', cmd_header[12:20])[0]
print('Cmd Type: {}, Command id: {}, Data size: {}'.format(cmd_type, cmd_id, data_size), flush=True)
if cmd_id == CMD_ID_EXIT:
print('Exiting...')
break
elif cmd_id == CMD_ID_FILE_RANGE:
file_range_cmd(nsp_dir, in_ep, out_ep, data_size)
def send_nsp_list(nsp_dir, out_ep):
nsp_path_list = list()
nsp_path_list_len = 0
# Add all files with the extension .nsp in the provided dir
for nsp_path in [f for f in nsp_dir.iterdir() if f.is_file() and (f.suffix in EXTS)]:
nsp_path_list.append(nsp_path.__str__() + '\n')
nsp_path_list_len += len(nsp_path.__str__()) + 1
print('Sending header...')
out_ep.write(b'TUL0') # Tinfoil USB List 0
out_ep.write(struct.pack('<I', nsp_path_list_len))
out_ep.write(b'\x00' * 0x8) # Padding
print('Sending NSP list: {}'.format(nsp_path_list))
for nsp_path in nsp_path_list:
out_ep.write(nsp_path)
def print_usage():
print("""\
usb_install_pc.py
Used for the installation of NSPs over USB.
Usage: usb_install_pc.py <nsp folder>""")
if __name__ == '__main__':
if len(sys.argv) != 2:
print_usage()
sys.exit(1)
nsp_dir = Path(sys.argv[1])
if not nsp_dir.is_dir():
raise ValueError('1st argument must be a directory')
print("waiting for switch...\n")
dev = None
while (dev is None):
dev = usb.core.find(idVendor=0x057E, idProduct=0x3000)
time.sleep(0.5)
print("found the switch!\n")
cfg = None
try:
cfg = dev.get_active_configuration()
print("found active config")
except usb.core.USBError:
print("no currently active config")
cfg = None
if cfg is None:
dev.reset()
dev.set_configuration()
cfg = dev.get_active_configuration()
is_out_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_OUT
is_in_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_IN
out_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_out_ep)
in_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_in_ep)
assert out_ep is not None
assert in_ep is not None
print("iManufacturer: {} iProduct: {} iSerialNumber: {}".format(dev.manufacturer, dev.product, dev.serial_number))
print("bcdUSB: {} bMaxPacketSize0: {}".format(hex(dev.bcdUSB), dev.bMaxPacketSize0))
send_nsp_list(nsp_dir, out_ep)
poll_commands(nsp_dir, in_ep, out_ep)

View File

@@ -1,141 +0,0 @@
# based on usb.py from Tinfoil, by Adubbz.
import struct
import sys
import os
import usb.core
import usb.util
import time
import glob
from pathlib import Path
# magic number (SPHA) for the script and switch.
MAGIC = 0x53504841
# version of the usb script.
VERSION = 2
# list of supported extensions.
EXTS = (".nsp", ".xci", ".nsz", ".xcz")
def verify_switch(bcdUSB, count, in_ep, out_ep):
header = in_ep.read(8, timeout=0)
switch_magic = struct.unpack('<I', header[0:4])[0]
switch_version = struct.unpack('<I', header[4:8])[0]
if switch_magic != MAGIC:
raise Exception("Unexpected magic {}".format(switch_magic))
if switch_version != VERSION:
raise Exception("Unexpected version {}".format(switch_version))
send_data = struct.pack('<IIII', MAGIC, VERSION, bcdUSB, count)
out_ep.write(data=send_data, timeout=0)
def send_file_info(path, in_ep, out_ep):
file_name = Path(path).name
file_size = Path(path).stat().st_size
file_name_len = len(file_name)
send_data = struct.pack('<QQ', file_size, file_name_len)
out_ep.write(data=send_data, timeout=0)
out_ep.write(data=file_name, timeout=0)
def wait_for_input(path, in_ep, out_ep):
buf = None
predicted_off = 0
print("now waiting for intput\n")
with open(path, "rb") as file:
while True:
header = in_ep.read(24, timeout=0)
range_offset = struct.unpack('<Q', header[8:16])[0]
range_size = struct.unpack('<Q', header[16:24])[0]
if (range_offset == 0 and range_size == 0):
break
if (buf != None and range_offset == predicted_off and range_size == len(buf)):
# print("predicted the read off {} size {}".format(predicted_off, len(buf)))
pass
else:
file.seek(range_offset)
buf = file.read(range_size)
if (len(buf) != range_size):
# print("off: {} size: {}".format(range_offset, range_size))
raise ValueError('bad buf size!!!!!')
result = out_ep.write(data=buf, timeout=0)
if (len(buf) != result):
print("off: {} size: {}".format(range_offset, range_size))
raise ValueError('bad result!!!!!')
predicted_off = range_offset + range_size
buf = file.read(range_size)
if __name__ == '__main__':
print("hello world")
# check which mode the user has selected.
args = len(sys.argv)
if (args != 2):
print("either run python usb_total.py game.nsp OR drag and drop the game onto the python file (if python is in your path)")
sys.exit(1)
path = sys.argv[1]
files = []
if os.path.isfile(path) and path.endswith(EXTS):
files.append(path)
elif os.path.isdir(path):
for f in glob.glob(path + "/**/*.*", recursive=True):
if os.path.isfile(f) and f.endswith(EXTS):
files.append(f)
else:
raise ValueError('must be a file!')
# for file in files:
# print("found file: {}".format(file))
# Find the switch
print("waiting for switch...\n")
dev = None
while (dev is None):
dev = usb.core.find(idVendor=0x057E, idProduct=0x3000)
time.sleep(0.5)
print("found the switch!\n")
cfg = None
try:
cfg = dev.get_active_configuration()
print("found active config")
except usb.core.USBError:
print("no currently active config")
cfg = None
if cfg is None:
dev.set_configuration()
cfg = dev.get_active_configuration()
is_out_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_OUT
is_in_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_IN
out_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_out_ep)
in_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_in_ep)
assert out_ep is not None
assert in_ep is not None
print("iManufacturer: {} iProduct: {} iSerialNumber: {}".format(dev.manufacturer, dev.product, dev.serial_number))
print("bcdUSB: {} bMaxPacketSize0: {}".format(hex(dev.bcdUSB), dev.bMaxPacketSize0))
try:
verify_switch(dev.bcdUSB, len(files), in_ep, out_ep)
for file in files:
print("installing file: {}".format(file))
send_file_info(file, in_ep, out_ep)
wait_for_input(file, in_ep, out_ep)
dev.reset()
except Exception as inst:
print("An exception occurred " + str(inst))