59 Commits
0.7.0 ... 0.9.0

Author SHA1 Message Date
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
ITotalJustice
282c6e5493 bump version for release 0.7.0 -> 0.8.0 2025-04-27 21:01:45 +01:00
ITotalJustice
2c2f602d14 add gc_menu, add progress, icon, time remaining to progress bar (see full commit message).
- fix ignore distribution bit doing nothing.
- fix yati failing to parse control nca causing the transfer to abort.
- yati now uses ncm rather than ns to get the latest app version.
- improve ui::list input handling (it handles directional buttons now).
- progress bar displays speed and time remaining.
- added gc menu (taken from my gc installer nx and gci).
2025-04-27 20:01:13 +01:00
ITotalJustice
f7f1254699 Merge pull request #128 from ITotalJustice/stream_installs
Stream installs (FTP), and many fixes
2025-04-23 01:02:26 +01:00
ITotalJustice
90f8a62823 display useful info in ftp menu (ip, port, user, pass, ssid, passphrase) 2025-04-23 01:00:36 +01:00
ITotalJustice
e2a1c8b5e3 fix yati not setting correct version, add support for using zip name when creating forwarder, remove some dead code.
fixes #126
fixes #127
2025-04-22 23:15:16 +01:00
ITotalJustice
21f6f4b74d [skip ci] fix file assoc always using internal name, fix menu showing wrong time
fixes #126
2025-04-22 00:08:26 +01:00
ITotalJustice
75d3b3ee0d [skip-ci] initial support for stream installs, add ftp installs.
do NOT build or release binaries of this version, it is not complete and there will be dragons.
2025-04-21 23:23:59 +01:00
ITotalJustice
0dde379932 don't return from usb menu on error, wait until the user presses B 2025-04-21 13:33:36 +01:00
ITotalJustice
9800bbecdf add basic support for gamecard installing 2025-04-21 13:30:46 +01:00
ITotalJustice
60e915c255 enable screenshot permissions in applet mode. 2025-04-21 12:40:37 +01:00
ITotalJustice
786f8a42fa send file name and size via usb, add requirements.txt for usb.py 2025-04-21 01:41:20 +01:00
ITotalJustice
5a4a0f75f2 add support for mame and neogeo, as well as alias for rom folder names 2025-04-20 22:03:53 +01:00
89 changed files with 4770 additions and 3048 deletions

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

@@ -2,3 +2,4 @@
path=/retroarch/cores/fbneo_libretro_libnx.nro
supported_extensions=zip|7z|cue|ccd
database=FBNeo - Arcade Games
use_base_name=true

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/mame2000_libretro_libnx.nro
supported_extensions=zip|7z
database=MAME 2000
use_base_name=true

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/mame2003_libretro_libnx.nro
supported_extensions=zip
database=MAME 2003
use_base_name=true

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/mame2003_plus_libretro_libnx.nro
supported_extensions=zip
database=MAME 2003-Plus
use_base_name=true

View File

@@ -2,3 +2,4 @@
path=/retroarch/cores/xrick_libretro_libnx.nro
supported_extensions=zip
database=Rick Dangerous
use_base_name=true

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

@@ -66,7 +66,7 @@
"Select Theme": "选择主题",
"Shuffle": "随机播放",
"Music": "音乐",
"12 Hour Time": "",
"12 Hour Time": "12小时制时间",
"Network": "网络",
"Network Options": "网络选项",
"Ftp": "FTP",
@@ -170,7 +170,7 @@
"Controller": "控制器",
"Pad ": "手柄 ",
" (Available)": " (可用的)",
" (Unsupported)": "",
" (Unsupported)": " (不支持的)",
" (Unconnected)": " (未连接)",
"HandHeld": "掌机模式",
"Rotation": "旋转",
@@ -239,7 +239,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",

View File

@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.13)
set(sphaira_VERSION 0.7.0)
set(sphaira_VERSION 0.9.0)
project(sphaira
VERSION ${sphaira_VERSION}
@@ -47,6 +47,9 @@ add_executable(sphaira
source/ui/menus/themezer.cpp
source/ui/menus/ghdl.cpp
source/ui/menus/usb_menu.cpp
source/ui/menus/ftp_menu.cpp
source/ui/menus/gc_menu.cpp
source/ui/menus/game_menu.cpp
source/ui/error_box.cpp
source/ui/notification.cpp
@@ -59,6 +62,7 @@ add_executable(sphaira
source/ui/widget.cpp
source/ui/list.cpp
source/ui/bubbles.cpp
source/ui/scrolling_text.cpp
source/app.cpp
source/download.cpp
@@ -72,7 +76,6 @@ add_executable(sphaira
source/nxlink.cpp
source/owo.cpp
source/swkbd.cpp
source/web.cpp
source/i18n.cpp
source/ftpsrv_helper.cpp
@@ -82,6 +85,8 @@ add_executable(sphaira
source/yati/source/file.cpp
source/yati/source/stdio.cpp
source/yati/source/usb.cpp
source/yati/source/stream.cpp
source/yati/source/stream_file.cpp
source/yati/nx/es.cpp
source/yati/nx/keys.cpp
@@ -142,7 +147,8 @@ set(FETCHCONTENT_QUIET FALSE)
FetchContent_Declare(ftpsrv
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
GIT_TAG 1.2.2
# GIT_TAG 1.2.2
GIT_TAG f8a30fd
SOURCE_SUBDIR NONE
)

View File

@@ -64,7 +64,7 @@ public:
static void SetTheme(s64 theme_index);
static auto GetThemeIndex() -> s64;
static auto GetDefaultImage(int* w = nullptr, int* h = nullptr) -> int;
static auto GetDefaultImage() -> int;
// returns argv[0]
static auto GetExePath() -> fs::FsPath;
@@ -102,6 +102,13 @@ public:
static void PlaySoundEffect(SoundEffect effect);
static void DisplayThemeOptions(bool left_side = true);
// todo:
static void DisplayNetworkOptions(bool left_side = true);
static void DisplayMiscOptions(bool left_side = true);
static void DisplayAdvancedOptions(bool left_side = true);
static void DisplayInstallOptions(bool left_side = true);
void Draw();
void Update();
void Poll();
@@ -177,15 +184,16 @@ 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_sd{INI_SECTION, "install_sd", true};
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
option::OptionLong m_boost_mode{INI_SECTION, "boost_mode", false};
option::OptionBool m_allow_downgrade{INI_SECTION, "allow_downgrade", false};
option::OptionBool m_skip_if_already_installed{INI_SECTION, "skip_if_already_installed", true};
option::OptionBool m_ticket_only{INI_SECTION, "ticket_only", false};
option::OptionBool m_patch_ticket{INI_SECTION, "patch_ticket", true};
option::OptionBool m_skip_base{INI_SECTION, "skip_base", false};
option::OptionBool m_skip_patch{INI_SECTION, "skip_patch", false};
option::OptionBool m_skip_addon{INI_SECTION, "skip_addon", false};

View File

@@ -454,4 +454,10 @@ struct FsNativeContentStorage final : FsNative {
}
};
struct FsNativeGameCard final : FsNative {
FsNativeGameCard(const FsGameCardHandle* handle, FsGameCardPartition partition, bool ignore_read_only = true) : FsNative{ignore_read_only} {
m_open_result = fsOpenGameCardFileSystem(&m_fs, handle, partition);
}
};
} // namespace fs

View File

@@ -1,8 +1,22 @@
#pragma once
#include <functional>
namespace sphaira::ftpsrv {
bool Init();
void Exit();
using OnInstallStart = std::function<bool(void* user, const char* path)>;
using OnInstallWrite = std::function<bool(void* user, const void* buf, size_t size)>;
using OnInstallClose = std::function<void(void* user)>;
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close);
void DisableInstallMode();
unsigned GetPort();
bool IsAnon();
const char* GetUser();
const char* GetPass();
} // namespace sphaira::ftpsrv

View File

@@ -1,13 +1,14 @@
#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

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

@@ -6,11 +6,11 @@ namespace sphaira::ui {
struct List final : Object {
using Callback = std::function<void(NVGcontext* vg, Theme* theme, Vec4 v, s64 index)>;
using TouchCallback = std::function<void(s64 index)>;
using TouchCallback = std::function<void(bool touch, s64 index)>;
List(s64 row, s64 page, const Vec4& pos, const Vec4& v, const Vec2& pad = {});
void OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCallback callback);
void OnUpdate(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback);
void Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const;

View File

@@ -2,8 +2,8 @@
#include "ui/menus/menu_base.hpp"
#include "ui/scrollable_text.hpp"
#include "ui/scrolling_text.hpp"
#include "ui/list.hpp"
#include "nro.hpp"
#include "fs.hpp"
#include <span>
@@ -73,6 +73,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;
@@ -135,9 +136,10 @@ enum OrderType {
};
struct Menu final : MenuBase {
Menu(const std::vector<NroEntry>& nro_entries);
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;
@@ -162,13 +164,16 @@ struct Menu final : MenuBase {
}
private:
const std::vector<NroEntry>& m_nro_entries;
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{};
ScrollingText m_scroll_name{};
ScrollingText m_scroll_author{};
ScrollingText m_scroll_version{};
Filter m_filter{Filter::Filter_All};
SortType m_sort{SortType::SortType_Updated};
OrderType m_order{OrderType::OrderType_Descending};

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

@@ -89,6 +89,19 @@ struct FileAssocEntry {
std::string name{}; // ini name
std::vector<std::string> ext{}; // list of ext
std::vector<std::string> database{}; // list of systems
bool use_base_name{}; // if set, uses base name (rom.zip) otherwise uses internal name (rom.gba)
auto IsExtension(std::string_view extension, std::string_view internal_extension) const -> bool {
for (const auto& assoc_ext : ext) {
if (extension.length() == assoc_ext.length() && !strncasecmp(assoc_ext.data(), extension.data(), assoc_ext.length())) {
return true;
}
if (internal_extension.length() == assoc_ext.length() && !strncasecmp(assoc_ext.data(), internal_extension.data(), assoc_ext.length())) {
return true;
}
}
return false;
}
};
struct LastFile {
@@ -111,6 +124,7 @@ struct Menu final : MenuBase {
Menu(const std::vector<NroEntry>& nro_entries);
~Menu();
auto GetShortTitle() const -> const char* override { return "Files"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
@@ -124,6 +138,13 @@ private:
void InstallForwarder();
void InstallFile(const FileEntry& target);
void InstallFiles(const std::vector<FileEntry>& targets);
void UnzipFile(const fs::FsPath& folder, const FileEntry& target);
void UnzipFiles(fs::FsPath folder, const std::vector<FileEntry>& targets);
void ZipFile(const fs::FsPath& zip_path, const FileEntry& target);
void ZipFiles(fs::FsPath zip_path, const std::vector<FileEntry>& targets);
auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result;
void LoadAssocEntriesPath(const fs::FsPath& path);

View File

@@ -0,0 +1,60 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "yati/source/stream.hpp"
namespace sphaira::ui::menu::ftp {
enum class State {
// not connected.
None,
// just connected, starts the transfer.
Connected,
// set whilst transfer is in progress.
Progress,
// set when the transfer is finished.
Done,
// failed to connect.
Failed,
};
struct StreamFtp final : yati::source::Stream {
StreamFtp(const fs::FsPath& path, std::stop_token token);
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
bool Push(const void* buf, s64 size);
void Disable();
// private:
fs::FsPath m_path{};
std::stop_token m_token{};
std::vector<u8> m_buffer{};
Mutex m_mutex{};
bool m_active{};
// bool m_push_exit{};
};
struct Menu final : MenuBase {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "FTP"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
// this should be private
// private:
std::shared_ptr<StreamFtp> m_source;
Thread m_thread{};
Mutex m_mutex{};
// the below are shared across threads, lock with the above mutex!
State m_state{State::None};
const char* m_user{};
const char* m_pass{};
unsigned m_port{};
bool m_anon{};
};
} // namespace sphaira::ui::menu::ftp

View File

@@ -0,0 +1,88 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "ui/scrolling_text.hpp"
#include "ui/list.hpp"
#include "fs.hpp"
#include "option.hpp"
#include <memory>
namespace sphaira::ui::menu::game {
enum class NacpLoadStatus {
// not yet attempted to be loaded.
None,
// loaded, ready to parse.
Loaded,
// failed to load, do not attempt to load again!
Error,
};
struct Entry {
u64 app_id{};
s64 size{};
char display_version[0x10]{};
NacpLanguageEntry lang{};
int image{};
std::unique_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;
}
};
enum SortType {
SortType_Updated,
};
enum OrderType {
OrderType_Descending,
OrderType_Ascending,
};
struct Menu final : MenuBase {
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();
void FreeEntries();
private:
static constexpr inline const char* INI_SECTION = "games";
std::vector<Entry> m_entries{};
s64 m_index{}; // where i am in the array
std::unique_ptr<List> m_list{};
bool m_is_reversed{};
bool m_dirty{};
ScrollingText m_scroll_name{};
ScrollingText m_scroll_author{};
ScrollingText m_scroll_version{};
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Updated};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
option::OptionBool m_hide_forwarders{INI_SECTION, "hide_forwarders", false};
};
} // namespace sphaira::ui::menu::game

View File

@@ -0,0 +1,82 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "yati/container/base.hpp"
#include "yati/source/base.hpp"
#include "ui/list.hpp"
#include <span>
#include <memory>
namespace sphaira::ui::menu::gc {
struct GcCollection : yati::container::CollectionEntry {
GcCollection(const char* _name, s64 _size, u8 _type, u8 _id_offset) {
name = _name;
size = _size;
type = _type;
id_offset = _id_offset;
}
// NcmContentType
u8 type{};
u8 id_offset{};
};
using GcCollections = std::vector<GcCollection>;
struct ApplicationEntry {
u64 app_id{};
u32 version{};
u8 key_gen{};
std::vector<GcCollections> application{};
std::vector<GcCollections> patch{};
std::vector<GcCollections> add_on{};
std::vector<GcCollections> data_patch{};
yati::container::Collections tickets{};
auto GetSize() const -> s64;
auto GetSize(const std::vector<GcCollections>& entries) const -> s64;
};
struct Menu final : MenuBase {
Menu();
~Menu();
auto GetShortTitle() const -> const char* override { return "GC"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
private:
Result GcMount();
void GcUnmount();
Result GcPoll(bool* inserted);
Result GcOnEvent();
Result UpdateStorageSize();
void FreeImage();
void OnChangeIndex(s64 new_index);
private:
FsDeviceOperator m_dev_op{};
FsGameCardHandle m_handle{};
std::unique_ptr<fs::FsNativeGameCard> m_fs{};
FsEventNotifier m_event_notifier{};
Event m_event{};
std::vector<ApplicationEntry> m_entries{};
std::unique_ptr<List> m_list{};
s64 m_entry_index{};
s64 m_option_index{};
s64 m_size_free_sd{};
s64 m_size_total_sd{};
s64 m_size_free_nand{};
s64 m_size_total_nand{};
NacpLanguageEntry m_lang_entry{};
int m_icon{};
bool m_mounted{};
};
} // namespace sphaira::ui::menu::gc

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

@@ -1,6 +1,7 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "ui/scrolling_text.hpp"
#include "ui/list.hpp"
#include "nro.hpp"
#include "fs.hpp"
@@ -26,27 +27,30 @@ struct Menu final : MenuBase {
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();
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";
@@ -54,6 +58,10 @@ private:
s64 m_index{}; // where i am in the array
std::unique_ptr<List> m_list{};
ScrollingText m_scroll_name{};
ScrollingText m_scroll_author{};
ScrollingText m_scroll_version{};
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_AlphabeticalStar};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
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

@@ -10,6 +10,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 +22,10 @@ struct MenuBase : Widget {
void SetTitleSubHeading(std::string sub_heading);
void SetSubHeading(std::string sub_heading);
auto GetTitle() const {
return m_title;
}
private:
void UpdateVars();
@@ -29,6 +34,7 @@ private:
std::string m_title_sub_heading{};
std::string m_sub_heading{};
protected:
struct tm m_tm{};
TimeStamp m_poll_timestamp{};
u32 m_battery_percetange{};

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

@@ -5,17 +5,15 @@
namespace sphaira::ui::gfx {
void drawImage(NVGcontext*, float x, float y, float w, float h, int texture);
void drawImage(NVGcontext*, const Vec4& v, int texture);
void drawImageRounded(NVGcontext*, float x, float y, float w, float h, int texture);
void drawImageRounded(NVGcontext*, const Vec4& v, int texture);
void drawImage(NVGcontext*, float x, float y, float w, float h, int texture, float rounded = 0.F);
void drawImage(NVGcontext*, const Vec4& v, int texture, float rounded = 0.F);
void dimBackground(NVGcontext*);
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGcolor& c, bool rounded = false);
void drawRect(NVGcontext*, const Vec4& v, const NVGcolor& c, bool rounded = false);
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGpaint& p, bool rounded = false);
void drawRect(NVGcontext*, const Vec4& v, const NVGpaint& p, bool rounded = false);
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGcolor& c, float rounding = 0.F);
void drawRect(NVGcontext*, const Vec4& v, const NVGcolor& c, float rounding = 0.F);
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGpaint& p, float rounding = 0.F);
void drawRect(NVGcontext*, const Vec4& v, const NVGpaint& p, float rounding = 0.F);
void drawRectOutline(NVGcontext*, const Theme*, float size, float x, float y, float w, float h);
void drawRectOutline(NVGcontext*, const Theme*, float size, const Vec4& v);

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

@@ -3,6 +3,7 @@
#include "widget.hpp"
#include "fs.hpp"
#include <functional>
#include <span>
namespace sphaira::ui {
@@ -12,6 +13,8 @@ using ProgressBoxDoneCallback = std::function<void(bool success)>;
struct ProgressBox final : Widget {
ProgressBox(
int image,
const std::string& action,
const std::string& title,
ProgressBoxCallback callback, ProgressBoxDoneCallback done = [](bool success){},
int cpuid = 1, int prio = 0x2C, int stack_size = 1024*1024
@@ -24,6 +27,9 @@ struct ProgressBox final : Widget {
auto SetTitle(const std::string& title) -> ProgressBox&;
auto NewTransfer(const std::string& transfer) -> ProgressBox&;
auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
// not const in order to avoid copy by using std::swap
auto SetImageData(std::vector<u8>& data) -> ProgressBox&;
auto SetImageDataConst(std::span<const u8> data) -> ProgressBox&;
void RequestExit();
auto ShouldExit() -> bool;
@@ -41,6 +47,9 @@ struct ProgressBox final : Widget {
};
}
private:
void FreeImage();
public:
struct ThreadData {
ProgressBox* pbox{};
@@ -52,12 +61,22 @@ private:
Mutex m_mutex{};
Thread m_thread{};
ThreadData m_thread_data{};
ProgressBoxDoneCallback m_done{};
// shared data start.
std::string m_action{};
std::string m_title{};
std::string m_transfer{};
s64 m_size{};
s64 m_offset{};
s64 m_last_offset{};
s64 m_speed{};
TimeStamp m_timestamp{};
std::vector<u8> m_image_data{};
// shared data end.
int m_image{};
bool m_own_image{};
};
// this is a helper function that does many things.

View File

@@ -0,0 +1,19 @@
#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)));
private:
std::string m_str;
s64 m_tick;
float m_text_xoff;
};
} // namespace sphaira::ui

View File

@@ -226,7 +226,6 @@ struct ThemeMeta {
struct Theme {
ThemeMeta meta;
PLSR_BFSTM music;
ElementEntry elements[ThemeEntryID_MAX];
auto GetColour(ThemeEntryID id) const {

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

@@ -3,6 +3,7 @@
#include "yati/source/base.hpp"
#include <vector>
#include <string>
#include <memory>
#include <switch.h>
namespace sphaira::yati::container {
@@ -28,12 +29,15 @@ using Collections = std::vector<CollectionEntry>;
struct Base {
using Source = source::Base;
Base(Source* source) : m_source{source} { }
Base(std::shared_ptr<Source> source) : m_source{source} { }
virtual ~Base() = default;
virtual Result GetCollections(Collections& out) = 0;
auto GetSource() const {
return m_source;
}
protected:
Source* m_source;
std::shared_ptr<Source> m_source;
};
} // namespace sphaira::yati::container

View File

@@ -8,7 +8,6 @@ namespace sphaira::yati::container {
struct Nsp final : Base {
using Base::Base;
Result GetCollections(Collections& out) override;
static Result Validate(source::Base* source);
};
} // namespace sphaira::yati::container

View File

@@ -10,7 +10,6 @@ namespace sphaira::yati::container {
struct Xci final : Base {
using Base::Base;
Result GetCollections(Collections& out) override;
static Result Validate(source::Base* source);
};
} // namespace sphaira::yati::container

View File

@@ -78,6 +78,6 @@ Result SetTicketData(std::span<u8> ticket, const es::TicketData* in);
Result GetTitleKey(keys::KeyEntry& out, const TicketData& data, const keys::Keys& keys);
Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys);
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys, bool convert_personalised);
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys);
} // namespace sphaira::es

View File

@@ -41,6 +41,7 @@ enum KeyGeneration {
KeyGeneration_1700 = 0x11,
KeyGeneration_1800 = 0x12,
KeyGeneration_1900 = 0x13,
KeyGeneration_2000 = 0x14,
KeyGeneration_Invalid = 0xFF,
};
@@ -175,7 +176,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,4 +216,6 @@ Result DecryptKeak(const keys::Keys& keys, Header& header);
Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation);
Result VerifyFixedKey(const Header& header);
auto GetKeyGenStr(u8 key_gen) -> const char*;
} // namespace sphaira::nca

View File

@@ -19,7 +19,7 @@ static_assert(sizeof(PackagedContentMeta) == 0x20);
struct ContentStorageRecord {
NcmContentMetaKey key;
u8 storage_id;
u8 storage_id; // NcmStorageId
u8 padding[0x7];
};
@@ -31,7 +31,12 @@ union ExtendedHeader {
NcmDataPatchMetaExtendedHeader data_patch;
};
auto GetMetaTypeStr(u8 meta_type) -> const char*;
auto GetStorageIdStr(u8 storage_id) -> const char*;
auto GetAppId(u8 meta_type, u64 id) -> u64;
auto GetAppId(const NcmContentMetaKey& key) -> u64;
auto GetAppId(const PackagedContentMeta& meta) -> u64;
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id);
Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id);

View File

@@ -1,265 +0,0 @@
/*
* tik.h
*
* Copyright (c) 2020-2024, DarkMatterCore <pabloacurielz@gmail.com>.
*
* This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool).
*
* nxdumptool is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nxdumptool is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#ifndef __TIK_H__
#define __TIK_H__
#include "signature.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SIGNED_TIK_MIN_SIZE sizeof(TikSigHmac160) /* Assuming no ESV1/ESV2 records are available. */
#define SIGNED_TIK_MAX_SIZE 0x400 /* Max ticket entry size in the ES ticket system savedata file. */
#define TIK_FORMAT_VERSION 2
#define GENERATE_TIK_STRUCT(sigtype, tiksize) \
typedef struct { \
SignatureBlock##sigtype sig_block; \
TikCommonBlock tik_common_block; \
u8 es_section_record_data[]; \
} TikSig##sigtype; \
NXDT_ASSERT(TikSig##sigtype, tiksize);
typedef enum {
TikTitleKeyType_Common = 0,
TikTitleKeyType_Personalized = 1,
TikTitleKeyType_Count = 2 ///< Total values supported by this enum.
} TikTitleKeyType;
typedef enum {
TikLicenseType_Permanent = 0,
TikLicenseType_Demo = 1,
TikLicenseType_Trial = 2,
TikLicenseType_Rental = 3,
TikLicenseType_Subscription = 4,
TikLicenseType_Service = 5,
TikLicenseType_Count = 6 ///< Total values supported by this enum.
} TikLicenseType;
typedef enum {
TikPropertyMask_None = 0,
TikPropertyMask_PreInstallation = BIT(0), ///< Determines if the title comes pre-installed on the device. Most likely unused -- a remnant from previous ticket formats.
TikPropertyMask_SharedTitle = BIT(1), ///< Determines if the title holds shared contents only. Most likely unused -- a remnant from previous ticket formats.
TikPropertyMask_AllContents = BIT(2), ///< Determines if the content index mask shall be bypassed. Most likely unused -- a remnant from previous ticket formats.
TikPropertyMask_DeviceLinkIndepedent = BIT(3), ///< Determines if the console should *not* connect to the Internet to verify if the title's being used by the primary console.
TikPropertyMask_Volatile = BIT(4), ///< Determines if the ticket copy inside ticket.bin is available after reboot. Can be encrypted.
TikPropertyMask_ELicenseRequired = BIT(5), ///< Determines if the console should connect to the Internet to perform license verification.
TikPropertyMask_Count = 6 ///< Total values supported by this enum.
} TikPropertyMask;
/// Placed after the ticket signature block.
typedef struct {
char issuer[0x40];
u8 titlekey_block[0x100];
u8 format_version; ///< Always matches TIK_FORMAT_VERSION.
u8 titlekey_type; ///< TikTitleKeyType.
u16 ticket_version;
u8 license_type; ///< TikLicenseType.
u8 key_generation; ///< NcaKeyGeneration.
u16 property_mask; ///< TikPropertyMask.
u8 reserved[0x8];
u64 ticket_id;
u64 device_id;
FsRightsId rights_id;
u32 account_id;
u32 sect_total_size;
u32 sect_hdr_offset;
u16 sect_hdr_count;
u16 sect_hdr_entry_size;
} TikCommonBlock;
NXDT_ASSERT(TikCommonBlock, 0x180);
/// ESV1/ESV2 section records are placed right after the ticket data. These aren't available in TikTitleKeyType_Common tickets.
/// These are only used if the sect_* fields from the common block are non-zero (other than 'sect_hdr_offset').
/// Each ESV2 section record is followed by a 'record_count' number of ESV1 records, each one of 'record_size' size.
typedef enum {
TikSectionType_None = 0,
TikSectionType_Permanent = 1,
TikSectionType_Subscription = 2,
TikSectionType_Content = 3,
TikSectionType_ContentConsumption = 4,
TikSectionType_AccessTitle = 5,
TikSectionType_LimitedResource = 6,
TikSectionType_Count = 7 ///< Total values supported by this enum.
} TikSectionType;
typedef struct {
u32 sect_offset;
u32 record_size;
u32 section_size;
u16 record_count;
u16 section_type; ///< TikSectionType.
} TikESV2SectionRecord;
/// Used with TikSectionType_Permanent.
typedef struct {
u8 ref_id[0x10];
u32 ref_id_attr;
} TikESV1PermanentRecord;
/// Used with TikSectionType_Subscription.
typedef struct {
u32 limit;
u8 ref_id[0x10];
u32 ref_id_attr;
} TikESV1SubscriptionRecord;
/// Used with TikSectionType_Content.
typedef struct {
u32 offset;
u8 access_mask[0x80];
} TikESV1ContentRecord;
/// Used with TikSectionType_ContentConsumption.
typedef struct {
u16 index;
u16 code;
u32 limit;
} TikESV1ContentConsumptionRecord;
/// Used with TikSectionType_AccessTitle.
typedef struct {
u64 access_title_id;
u64 access_title_mask;
} TikESV1AccessTitleRecord;
/// Used with TikSectionType_LimitedResource.
typedef struct {
u32 limit;
u8 ref_id[0x10];
u32 ref_id_attr;
} TikESV1LimitedResourceRecord;
/// All tickets generated below use a little endian sig_type field.
GENERATE_TIK_STRUCT(Rsa4096, 0x3C0); /// RSA-4096 signature.
GENERATE_TIK_STRUCT(Rsa2048, 0x2C0); /// RSA-2048 signature.
GENERATE_TIK_STRUCT(Ecc480, 0x200); /// ECC signature.
GENERATE_TIK_STRUCT(Hmac160, 0x1C0); /// HMAC signature.
/// Ticket type.
typedef enum {
TikType_None = 0,
TikType_SigRsa4096 = 1,
TikType_SigRsa2048 = 2,
TikType_SigEcc480 = 3,
TikType_SigHmac160 = 4,
TikType_Count = 5 ///< Total values supported by this enum.
} TikType;
/// Used to store ticket type, size and raw data, as well as titlekey data.
typedef struct {
u8 type; ///< TikType.
u64 size; ///< Raw ticket size.
u8 data[SIGNED_TIK_MAX_SIZE]; ///< Raw ticket data.
u8 key_generation; ///< NcaKeyGeneration.
u8 enc_titlekey[0x10]; ///< Titlekey with titlekek crypto (RSA-OAEP unwrapped if dealing with a TikTitleKeyType_Personalized ticket).
char enc_titlekey_str[0x21]; ///< Character string representation of enc_titlekey.
u8 dec_titlekey[0x10]; ///< Titlekey without titlekek crypto. Ready to use for NCA FS section decryption.
char dec_titlekey_str[0x21]; ///< Character string representation of dec_titlekey.
char rights_id_str[0x21]; ///< Character string representation of the rights ID from the ticket.
} Ticket;
/// Retrieves a ticket from either the ES ticket system savedata file (eMMC BIS System partition) or the secure Hash FS partition from an inserted gamecard.
/// Both the input rights ID and key generation values must have been retrieved from a NCA that depends on the desired ticket.
/// Titlekey is also RSA-OAEP unwrapped (if needed) and titlekek-decrypted right away.
bool tikRetrieveTicketByRightsId(Ticket *dst, const FsRightsId *id, u8 key_generation, bool use_gamecard);
/// Converts a TikTitleKeyType_Personalized ticket into a TikTitleKeyType_Common ticket and optionally generates a raw certificate chain for the new signature issuer.
/// Bear in mind the 'size' member from the Ticket parameter will be updated by this function to remove any possible references to ESV1/ESV2 records.
/// If both 'out_raw_cert_chain' and 'out_raw_cert_chain_size' pointers are provided, raw certificate chain data will be saved to them.
/// certGenerateRawCertificateChainBySignatureIssuer() is used internally, so the output buffer must be freed by the user.
bool tikConvertPersonalizedTicketToCommonTicket(Ticket *tik, u8 **out_raw_cert_chain, u64 *out_raw_cert_chain_size);
/// Helper inline functions for signed ticket blobs.
NX_INLINE TikCommonBlock *tikGetCommonBlockFromSignedTicketBlob(void *buf)
{
return (TikCommonBlock*)signatureGetPayloadFromSignedBlob(buf, false);
}
NX_INLINE u64 tikGetSectionRecordsSizeFromSignedTicketBlob(void *buf)
{
TikCommonBlock *tik_common_block = tikGetCommonBlockFromSignedTicketBlob(buf);
if (!tik_common_block) return 0;
u64 offset = sizeof(TikCommonBlock), out_size = 0;
for(u32 i = 0; i < tik_common_block->sect_hdr_count; i++)
{
TikESV2SectionRecord *rec = (TikESV2SectionRecord*)((u8*)tik_common_block + offset);
offset += (sizeof(TikESV2SectionRecord) + ((u64)rec->record_count * (u64)rec->record_size));
out_size += offset;
}
return out_size;
}
NX_INLINE bool tikIsValidSignedTicketBlob(void *buf)
{
u64 ticket_size = (signatureGetBlockSizeFromSignedBlob(buf, false) + sizeof(TikCommonBlock));
return (ticket_size > sizeof(TikCommonBlock) && (ticket_size + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) <= SIGNED_TIK_MAX_SIZE);
}
NX_INLINE u64 tikGetSignedTicketBlobSize(void *buf)
{
return (tikIsValidSignedTicketBlob(buf) ? (signatureGetBlockSizeFromSignedBlob(buf, false) + sizeof(TikCommonBlock) + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) : 0);
}
NX_INLINE u64 tikGetSignedTicketBlobHashAreaSize(void *buf)
{
return (tikIsValidSignedTicketBlob(buf) ? (sizeof(TikCommonBlock) + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) : 0);
}
/// Helper inline functions for Ticket elements.
NX_INLINE bool tikIsValidTicket(Ticket *tik)
{
return (tik && tik->type > TikType_None && tik->type < TikType_Count && tik->size >= SIGNED_TIK_MIN_SIZE && tik->size <= SIGNED_TIK_MAX_SIZE && tikIsValidSignedTicketBlob(tik->data));
}
NX_INLINE TikCommonBlock *tikGetCommonBlockFromTicket(Ticket *tik)
{
return (tikIsValidTicket(tik) ? tikGetCommonBlockFromSignedTicketBlob(tik->data) : NULL);
}
NX_INLINE bool tikIsPersonalizedTicket(Ticket *tik)
{
TikCommonBlock *tik_common_block = tikGetCommonBlockFromTicket(tik);
return (tik_common_block ? (tik_common_block->titlekey_type == TikTitleKeyType_Personalized) : false);
}
NX_INLINE u64 tikGetHashAreaSizeFromTicket(Ticket *tik)
{
return (tikIsValidTicket(tik) ? tikGetSignedTicketBlobHashAreaSize(tik->data) : 0);
}
#ifdef __cplusplus
}
#endif
#endif /* __TIK_H__ */

View File

@@ -10,6 +10,14 @@ struct Base {
// virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
virtual bool IsStream() const {
return false;
}
virtual void SignalCancel() {
}
Result GetOpenResult() const {
return m_open_result;
}

View File

@@ -8,7 +8,7 @@
namespace sphaira::yati::source {
struct Stdio final : Base {
Stdio(const char* path);
Stdio(const fs::FsPath& path);
~Stdio();
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;

View File

@@ -0,0 +1,28 @@
#pragma once
#include "base.hpp"
#include <vector>
#include <switch.h>
namespace sphaira::yati::source {
// streams are for data that do not allow for random access,
// such as FTP or MTP.
struct Stream : Base {
virtual ~Stream() = default;
virtual Result ReadChunk(void* buf, s64 size, u64* bytes_read) = 0;
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
bool IsStream() const override {
return true;
}
protected:
Result m_open_result{};
private:
s64 m_offset{};
};
} // namespace sphaira::yati::source

View File

@@ -0,0 +1,22 @@
// this is used for testing that streams work, this code isn't used in normal
// release builds as it is slower / less feature complete than normal.
#pragma once
#include "stream.hpp"
#include "fs.hpp"
#include <switch.h>
namespace sphaira::yati::source {
struct StreamFile final : Stream {
StreamFile(FsFileSystem* fs, const fs::FsPath& path);
~StreamFile();
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
private:
FsFile m_file{};
s64 m_offset{};
};
} // namespace sphaira::yati::source

View File

@@ -2,6 +2,10 @@
#include "base.hpp"
#include "fs.hpp"
#include <vector>
#include <string>
#include <new>
#include <switch.h>
namespace sphaira::yati::source {
@@ -21,10 +25,47 @@ struct Usb final : Base {
~Usb();
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
Result Finished() const;
Result Finished();
Result Init();
Result WaitForConnection(u64 timeout, u32& speed, u32& count);
Result IsUsbConnected(u64 timeout) const;
Result WaitForConnection(u64 timeout, std::vector<std::string>& out_names);
void SetFileNameForTranfser(const std::string& name);
auto GetCancelEvent() {
return &m_uevent;
}
void SignalCancel() override {
ueventSignal(GetCancelEvent());
}
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* {
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);
}
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>>;
private:
enum UsbSessionEndpoint {
@@ -32,20 +73,26 @@ private:
UsbSessionEndpoint_Out = 1,
};
Result SendCommand(s64 off, s64 size) const;
Result InternalRead(void* buf, s64 off, s64 size) const;
Result SendCmdHeader(u32 cmdId, size_t dataSize);
Result SendFileRangeCmd(u64 offset, u64 size);
bool GetConfigured() const;
Event *GetCompletionEvent(UsbSessionEndpoint ep) const;
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) const;
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout);
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 TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout);
Result TransferAll(bool read, void *data, u32 size, u64 timeout);
private:
UsbDsInterface* m_interface{};
UsbDsEndpoint* m_endpoints[2]{};
u64 m_transfer_timeout{};
UEvent m_uevent{};
// std::vector<UEvent*> m_cancel_events{};
// aligned buffer that transfer data is copied to and from.
// a vector is used to avoid multiple alloc within the transfer loop.
PageAlignedVector m_aligned{};
std::string m_transfer_file_name{};
};
} // namespace sphaira::yati::source

View File

@@ -9,8 +9,11 @@
#include "fs.hpp"
#include "source/base.hpp"
#include "container/base.hpp"
#include "nx/ncm.hpp"
#include "ui/progress_box.hpp"
#include <memory>
#include <optional>
namespace sphaira::yati {
@@ -68,6 +71,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{};
@@ -78,11 +85,6 @@ struct Config {
// installs tickets only.
bool ticket_only{};
// converts personalised tickets to common tickets, allows for offline play.
// this breaks ticket signature so es needs to be patched.
// modified common tickets are patched regardless of this setting.
bool patch_ticket{};
// flags to enable / disable install of specific types.
bool skip_base{};
bool skip_patch{};
@@ -116,12 +118,25 @@ struct Config {
bool lower_system_version{};
};
Result InstallFromFile(FsFileSystem* fs, const fs::FsPath& path);
Result InstallFromStdioFile(const char* path);
Result InstallFromSource(std::shared_ptr<source::Base> source);
// overridable options, set to avoid
struct ConfigOverride {
std::optional<bool> sd_card_install{};
std::optional<bool> skip_nca_hash_verify{};
std::optional<bool> skip_rsa_header_fixed_key_verify{};
std::optional<bool> skip_rsa_npdm_fixed_key_verify{};
std::optional<bool> ignore_distribution_bit{};
std::optional<bool> convert_to_standard_crypto{};
std::optional<bool> lower_master_key{};
std::optional<bool> lower_system_version{};
};
Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path);
Result InstallFromStdioFile(ui::ProgressBox* pbox, const char* path);
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source);
Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path, const ConfigOverride& override = {});
Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path, const ConfigOverride& override = {});
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const fs::FsPath& path, const ConfigOverride& override = {});
Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr<container::Base> container, const ConfigOverride& override = {});
Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const container::Collections& collections, const ConfigOverride& override = {});
Result ParseCnmtNca(const fs::FsPath& path, u64 program_id, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos);
Result ParseControlNca(const fs::FsPath& path, u64 program_id, void* nacp_out = nullptr, s64 nacp_size = 0, std::vector<u8>* icon_out = nullptr);
} // namespace sphaira::yati

View File

@@ -1,7 +1,12 @@
#include "ui/menus/main_menu.hpp"
#include "ui/error_box.hpp"
#include "ui/option_box.hpp"
#include "ui/bubbles.hpp"
#include "ui/sidebar.hpp"
#include "ui/popup_list.hpp"
#include "ui/option_box.hpp"
#include "ui/progress_box.hpp"
#include "ui/error_box.hpp"
#include "ui/menus/main_menu.hpp"
#include "app.hpp"
#include "log.hpp"
@@ -34,8 +39,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]{};
};
@@ -544,7 +574,7 @@ auto App::GetThemeIndex() -> s64 {
return g_app->m_theme_index;
}
auto App::GetDefaultImage(int* w, int* h) -> int {
auto App::GetDefaultImage() -> int {
return g_app->m_default_image;
}
@@ -1104,7 +1134,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) {
@@ -1134,10 +1163,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);
}
}
}
@@ -1300,16 +1331,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");
@@ -1374,15 +1410,6 @@ App::App(const char* argv0) {
const long old_launch_count = ini_getl(GetExePath(), "launch_count", 0, App::PLAYLOG_PATH);
ini_putl(GetExePath(), "launch_count", old_launch_count + 1, App::PLAYLOG_PATH);
s64 sd_free_space;
if (R_SUCCEEDED(fs.GetFreeSpace("/", &sd_free_space))) {
log_write("sd_free_space: %zd\n", sd_free_space);
}
s64 sd_total_space;
if (R_SUCCEEDED(fs.GetTotalSpace("/", &sd_total_space))) {
log_write("sd_total_space: %zd\n", sd_total_space);
}
// load default image
if (R_SUCCEEDED(romfsInit())) {
ON_SCOPE_EXIT(romfsExit());
@@ -1392,8 +1419,33 @@ App::App(const char* argv0) {
}
}
// soon (tm)
// ui::bubble::Init();
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");
@@ -1412,6 +1464,215 @@ void App::PlaySoundEffect(SoundEffect effect) {
plsrPlayerPlay(id);
}
void App::DisplayThemeOptions(bool left_side) {
ui::SidebarEntryArray::Items theme_items{};
const auto theme_meta = App::GetThemeMetaList();
for (auto& p : theme_meta) {
theme_items.emplace_back(p.name);
}
auto options = std::make_shared<ui::Sidebar>("Theme Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<ui::SidebarEntryArray>("Select Theme"_i18n, theme_items, [](s64& index_out){
App::SetTheme(index_out);
}, App::GetThemeIndex()));
options->Add(std::make_shared<ui::SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [](bool& enable){
App::SetThemeMusicEnable(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [](bool& enable){
App::Set12HourTimeEnable(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryCallback>("Download Default Music"_i18n, [](){
// check if we already have music
if (fs::FileExists(DEFAULT_MUSIC_PATH)) {
App::Push(std::make_shared<ui::OptionBox>(
"Overwrite current default music?"_i18n,
"No"_i18n, "Yes"_i18n, 0, [](auto op_index){
if (op_index && *op_index) {
download_default_music();
}
}
));
} else {
download_default_music();
}
}));
}
void App::DisplayNetworkOptions(bool left_side) {
}
void App::DisplayMiscOptions(bool left_side) {
auto options = std::make_shared<ui::Sidebar>("Misc Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
for (auto& e : ui::menu::main::GetMiscMenuEntries()) {
if (e.name == g_app->m_right_side_menu.Get()) {
continue;
}
if (e.IsInstall() && !App::GetInstallEnable()) {
continue;
}
options->Add(std::make_shared<ui::SidebarEntryCallback>(i18n::get(e.title), [e](){
App::Push(e.func());
}));
}
}
void App::DisplayAdvancedOptions(bool left_side) {
auto options = std::make_shared<ui::Sidebar>("Advanced Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
ui::SidebarEntryArray::Items text_scroll_speed_items;
text_scroll_speed_items.push_back("Slow"_i18n);
text_scroll_speed_items.push_back("Normal"_i18n);
text_scroll_speed_items.push_back("Fast"_i18n);
std::vector<const char*> menu_names;
for (auto& e : ui::menu::main::GetMiscMenuEntries()) {
if (!e.IsShortcut()) {
continue;
}
if (e.IsInstall() && !App::GetInstallEnable()) {
continue;
}
menu_names.emplace_back(e.name);
}
ui::SidebarEntryArray::Items right_side_menu_items;
for (auto& str : menu_names) {
right_side_menu_items.push_back(i18n::get(str));
}
const auto it = std::find(menu_names.cbegin(), menu_names.cend(), g_app->m_right_side_menu.Get());
if (it == menu_names.cend()) {
g_app->m_right_side_menu.Set(menu_names[0]);
}
options->Add(std::make_shared<ui::SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [](bool& enable){
App::SetLogEnable(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [](bool& enable){
App::SetReplaceHbmenuEnable(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [](s64& index_out){
App::SetTextScrollSpeed(index_out);
}, App::GetTextScrollSpeed()));
options->Add(std::make_shared<ui::SidebarEntryArray>("Set right-side menu"_i18n, right_side_menu_items, [menu_names](s64& index_out){
const auto e = menu_names[index_out];
if (g_app->m_right_side_menu.Get() != e) {
g_app->m_right_side_menu.Set(e);
App::Push(std::make_shared<ui::OptionBox>(
"Press OK to restart Sphaira"_i18n, "OK"_i18n, [](auto){
App::ExitRestart();
}
));
}
}, i18n::get(g_app->m_right_side_menu.Get())));
options->Add(std::make_shared<ui::SidebarEntryCallback>("Install options"_i18n, [left_side](){
App::DisplayInstallOptions(left_side);
}));
}
void App::DisplayInstallOptions(bool left_side) {
auto options = std::make_shared<ui::Sidebar>("Install Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
ui::SidebarEntryArray::Items install_items;
install_items.push_back("System memory"_i18n);
install_items.push_back("microSD card"_i18n);
options->Add(std::make_shared<ui::SidebarEntryBool>("Enable"_i18n, App::GetApp()->m_install.Get(), [](bool& enable){
App::GetApp()->m_install.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Show install warning"_i18n, App::GetApp()->m_install_prompt.Get(), [](bool& enable){
App::GetApp()->m_install_prompt.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryArray>("Install location"_i18n, install_items, [](s64& index_out){
App::SetInstallSdEnable(index_out);
}, (s64)App::GetInstallSdEnable()));
options->Add(std::make_shared<ui::SidebarEntryBool>("Boost CPU clock"_i18n, App::GetApp()->m_boost_mode.Get(), [](bool& enable){
App::GetApp()->m_boost_mode.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Allow downgrade"_i18n, App::GetApp()->m_allow_downgrade.Get(), [](bool& enable){
App::GetApp()->m_allow_downgrade.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip if already installed"_i18n, App::GetApp()->m_skip_if_already_installed.Get(), [](bool& enable){
App::GetApp()->m_skip_if_already_installed.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Ticket only"_i18n, App::GetApp()->m_ticket_only.Get(), [](bool& enable){
App::GetApp()->m_ticket_only.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip base"_i18n, App::GetApp()->m_skip_base.Get(), [](bool& enable){
App::GetApp()->m_skip_base.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip patch"_i18n, App::GetApp()->m_skip_patch.Get(), [](bool& enable){
App::GetApp()->m_skip_patch.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip dlc"_i18n, App::GetApp()->m_skip_addon.Get(), [](bool& enable){
App::GetApp()->m_skip_addon.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip data patch"_i18n, App::GetApp()->m_skip_data_patch.Get(), [](bool& enable){
App::GetApp()->m_skip_data_patch.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip ticket"_i18n, App::GetApp()->m_skip_ticket.Get(), [](bool& enable){
App::GetApp()->m_skip_ticket.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [](bool& enable){
App::GetApp()->m_skip_nca_hash_verify.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip RSA header verify"_i18n, App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(), [](bool& enable){
App::GetApp()->m_skip_rsa_header_fixed_key_verify.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip RSA NPDM verify"_i18n, App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(), [](bool& enable){
App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Ignore distribution bit"_i18n, App::GetApp()->m_ignore_distribution_bit.Get(), [](bool& enable){
App::GetApp()->m_ignore_distribution_bit.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [](bool& enable){
App::GetApp()->m_convert_to_standard_crypto.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Lower master key"_i18n, App::GetApp()->m_lower_master_key.Get(), [](bool& enable){
App::GetApp()->m_lower_master_key.Set(enable);
}));
options->Add(std::make_shared<ui::SidebarEntryBool>("Lower system version"_i18n, App::GetApp()->m_lower_system_version.Get(), [](bool& enable){
App::GetApp()->m_lower_system_version.Set(enable);
}));
}
App::~App() {
log_write("starting to exit\n");

View File

@@ -195,6 +195,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);
@@ -471,7 +473,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);
}
}
@@ -490,6 +493,8 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_BUFFERSIZE, 1024*512);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERFUNCTION, header_callback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERDATA, &header_out);
// enable all forms of compression supported by libcurl.
CURL_EASY_SETOPT_LOG(curl, CURLOPT_ACCEPT_ENCODING, "");
if (has_post) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_POSTFIELDS, e.GetFields().c_str());
@@ -558,6 +563,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()))) {

View File

@@ -12,14 +12,30 @@
#include <nx/vfs_nx.h>
#include <nx/utils.h>
namespace sphaira::ftpsrv {
namespace {
struct InstallSharedData {
std::mutex mutex;
std::deque<std::string> queued_files;
void* user;
OnInstallStart on_start;
OnInstallWrite on_write;
OnInstallClose on_close;
bool in_progress;
bool enabled;
};
const char* INI_PATH = "/config/ftpsrv/config.ini";
FtpSrvConfig g_ftpsrv_config = {0};
volatile bool g_should_exit = false;
bool g_is_running{false};
Thread g_thread;
std::mutex g_mutex{};
InstallSharedData g_shared_data{};
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
sphaira::App::NotifyFlashLed();
@@ -29,6 +45,235 @@ void ftp_progress_callback(void) {
sphaira::App::NotifyFlashLed();
}
const char* SUPPORTED_EXT[] = {
".nsp", ".xci", ".nsz", ".xcz",
};
struct VfsUserData {
char* path;
int valid;
};
// ive given up with good names.
void on_thing() {
log_write("[FTP] doing on_thing\n");
std::scoped_lock lock{g_shared_data.mutex};
log_write("[FTP] locked on_thing\n");
if (!g_shared_data.in_progress) {
if (!g_shared_data.queued_files.empty()) {
log_write("[FTP] pushing new file data\n");
if (!g_shared_data.on_start || !g_shared_data.on_start(g_shared_data.user, g_shared_data.queued_files[0].c_str())) {
g_shared_data.queued_files.clear();
} else {
log_write("[FTP] success on new file push\n");
g_shared_data.in_progress = true;
}
}
}
}
int vfs_install_open(void* user, const char* path, enum FtpVfsOpenMode mode) {
{
std::scoped_lock lock{g_shared_data.mutex};
auto data = static_cast<VfsUserData*>(user);
data->valid = 0;
if (mode != FtpVfsOpenMode_WRITE) {
errno = EACCES;
return -1;
}
if (!g_shared_data.enabled) {
errno = EACCES;
return -1;
}
const char* ext = strrchr(path, '.');
if (!ext) {
errno = EACCES;
return -1;
}
bool found = false;
for (size_t i = 0; i < std::size(SUPPORTED_EXT); i++) {
if (!strcasecmp(ext, SUPPORTED_EXT[i])) {
found = true;
break;
}
}
if (!found) {
errno = EINVAL;
return -1;
}
// check if we already have this file queued.
auto it = std::find(g_shared_data.queued_files.cbegin(), g_shared_data.queued_files.cend(), path);
if (it != g_shared_data.queued_files.cend()) {
errno = EEXIST;
return -1;
}
g_shared_data.queued_files.push_back(path);
data->path = strdup(path);
data->valid = true;
}
on_thing();
log_write("[FTP] got file: %s\n", path);
return 0;
}
int vfs_install_read(void* user, void* buf, size_t size) {
errno = EACCES;
return -1;
}
int vfs_install_write(void* user, const void* buf, size_t size) {
std::scoped_lock lock{g_shared_data.mutex};
if (!g_shared_data.enabled) {
errno = EACCES;
return -1;
}
auto data = static_cast<VfsUserData*>(user);
if (!data->valid) {
errno = EACCES;
return -1;
}
if (!g_shared_data.on_write || !g_shared_data.on_write(g_shared_data.user, buf, size)) {
errno = EIO;
return -1;
}
return size;
}
int vfs_install_seek(void* user, const void* buf, size_t size, size_t off) {
errno = ESPIPE;
return -1;
}
int vfs_install_isfile_open(void* user) {
std::scoped_lock lock{g_shared_data.mutex};
auto data = static_cast<VfsUserData*>(user);
return data->valid;
}
int vfs_install_isfile_ready(void* user) {
std::scoped_lock lock{g_shared_data.mutex};
auto data = static_cast<VfsUserData*>(user);
const auto ready = !g_shared_data.queued_files.empty() && data->path == g_shared_data.queued_files[0];
return ready;
}
int vfs_install_close(void* user) {
{
log_write("[FTP] closing file\n");
std::scoped_lock lock{g_shared_data.mutex};
auto data = static_cast<VfsUserData*>(user);
if (data->valid) {
log_write("[FTP] closing valid file\n");
auto it = std::find(g_shared_data.queued_files.cbegin(), g_shared_data.queued_files.cend(), data->path);
if (it != g_shared_data.queued_files.cend()) {
if (it == g_shared_data.queued_files.cbegin()) {
log_write("[FTP] closing current file\n");
if (g_shared_data.on_close) {
g_shared_data.on_close(g_shared_data.user);
}
g_shared_data.in_progress = false;
} else {
log_write("[FTP] closing other file...\n");
}
g_shared_data.queued_files.erase(it);
} else {
log_write("[FTP] could not find file in queue...\n");
}
if (data->path) {
free(data->path);
}
data->valid = 0;
}
memset(data, 0, sizeof(*data));
}
on_thing();
return 0;
}
int vfs_install_opendir(void* user, const char* path) {
return 0;
}
const char* vfs_install_readdir(void* user, void* user_entry) {
return NULL;
}
int vfs_install_dirlstat(void* user, const void* user_entry, const char* path, struct stat* st) {
st->st_nlink = 1;
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
return 0;
}
int vfs_install_isdir_open(void* user) {
return 1;
}
int vfs_install_closedir(void* user) {
return 0;
}
int vfs_install_stat(const char* path, struct stat* st) {
st->st_nlink = 1;
st->st_mode = S_IFDIR | S_IWUSR | S_IWGRP | S_IWOTH;
return 0;
}
int vfs_install_mkdir(const char* path) {
return -1;
}
int vfs_install_unlink(const char* path) {
return -1;
}
int vfs_install_rmdir(const char* path) {
return -1;
}
int vfs_install_rename(const char* src, const char* dst) {
return -1;
}
FtpVfs g_vfs_install = {
.open = vfs_install_open,
.read = vfs_install_read,
.write = vfs_install_write,
.seek = vfs_install_seek,
.close = vfs_install_close,
.isfile_open = vfs_install_isfile_open,
.isfile_ready = vfs_install_isfile_ready,
.opendir = vfs_install_opendir,
.readdir = vfs_install_readdir,
.dirlstat = vfs_install_dirlstat,
.closedir = vfs_install_closedir,
.isdir_open = vfs_install_isdir_open,
.stat = vfs_install_stat,
.lstat = vfs_install_stat,
.mkdir = vfs_install_mkdir,
.unlink = vfs_install_unlink,
.rmdir = vfs_install_rmdir,
.rename = vfs_install_rename,
};
void loop(void* arg) {
while (!g_should_exit) {
ftpsrv_init(&g_ftpsrv_config);
@@ -44,8 +289,6 @@ void loop(void* arg) {
} // namespace
namespace sphaira::ftpsrv {
bool Init() {
std::scoped_lock lock{g_mutex};
if (g_is_running) {
@@ -84,6 +327,9 @@ bool Init() {
mount_bis = ini_getbool("Nx-App", "mount_bis", mount_bis, INI_PATH);
save_writable = ini_getbool("Nx-App", "save_writable", save_writable, INI_PATH);
mount_devices = true;
g_ftpsrv_config.timeout = 0;
if (!g_ftpsrv_config.port) {
return false;
}
@@ -93,7 +339,13 @@ bool Init() {
g_ftpsrv_config.anon = true;
}
vfs_nx_init(mount_devices, save_writable, mount_bis);
const VfsNxCustomPath custom = {
.name = "install",
.user = NULL,
.func = &g_vfs_install,
};
vfs_nx_init(&custom, mount_devices, save_writable, mount_bis);
Result rc;
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*16, 0x2C, 2))) {
@@ -123,6 +375,40 @@ void Exit() {
fsdev_wrapUnmountAll();
}
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close) {
std::scoped_lock lock{g_shared_data.mutex};
g_shared_data.user = user;
g_shared_data.on_start = on_start;
g_shared_data.on_write = on_write;
g_shared_data.on_close = on_close;
g_shared_data.enabled = true;
}
void DisableInstallMode() {
std::scoped_lock lock{g_shared_data.mutex};
g_shared_data.enabled = false;
}
unsigned GetPort() {
std::scoped_lock lock{g_mutex};
return g_ftpsrv_config.port;
}
bool IsAnon() {
std::scoped_lock lock{g_mutex};
return g_ftpsrv_config.anon;
}
const char* GetUser() {
std::scoped_lock lock{g_mutex};
return g_ftpsrv_config.user;
}
const char* GetPass() {
std::scoped_lock lock{g_mutex};
return g_ftpsrv_config.pass;
}
} // namespace sphaira::ftpsrv
extern "C" {

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;
@@ -134,8 +134,8 @@ 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
@@ -143,7 +143,7 @@ std::string get(const char* str) {
namespace literals {
std::string operator"" _i18n(const char* str, size_t len) {
return sphaira::i18n::get_internal(str, len);
return sphaira::i18n::get_internal({str, len});
}
} // namespace literals

View File

@@ -66,6 +66,9 @@ void userAppInit(void) {
if (R_FAILED(rc = ncmInitialize()))
diagAbortWithResult(rc);
// it doesn't matter if this fails.
appletSetScreenShotPermission(AppletScreenShotPermission_Enable);
log_nxlink_init();
}

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);
@@ -1005,7 +1012,7 @@ auto install_forwarder(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId st
}
auto install_forwarder(OwoConfig& config, NcmStorageId storage_id) -> Result {
App::Push(std::make_shared<ui::ProgressBox>("Installing Forwarder"_i18n, [config, storage_id](auto pbox) mutable -> bool {
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing Forwarder"_i18n, config.name, [config, storage_id](auto pbox) mutable -> bool {
return R_SUCCEEDED(install_forwarder(pbox, config, storage_id));
}));
R_SUCCEED();

File diff suppressed because it is too large Load Diff

View File

@@ -34,8 +34,35 @@ auto List::ClampY(float y, s64 count) const -> float {
return y;
}
void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCallback callback) {
if (touch->is_clicked && touch->in_range(GetPos())) {
void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback) {
const auto page_up_button = m_row == 1 ? Button::DPAD_LEFT : Button::L2;
const auto page_down_button = m_row == 1 ? Button::DPAD_RIGHT : Button::R2;
if (controller->GotDown(Button::DOWN)) {
if (ScrollDown(index, m_row, count)) {
callback(false, index);
}
} else if (controller->GotDown(Button::UP)) {
if (ScrollUp(index, m_row, count)) {
callback(false, index);
}
} else if (controller->GotDown(page_down_button)) {
if (ScrollDown(index, m_page, count)) {
callback(false, index);
}
} else if (controller->GotDown(page_up_button)) {
if (ScrollUp(index, m_page, count)) {
callback(false, index);
}
} else if (m_row > 1 && controller->GotDown(Button::RIGHT)) {
if (count && index < (count - 1) && (index + 1) % m_row != 0) {
callback(false, index + 1);
}
} else if (m_row > 1 && controller->GotDown(Button::LEFT)) {
if (count && index != 0 && (index % m_row) != 0) {
callback(false, index - 1);
}
} else if (touch->is_clicked && touch->in_range(GetPos())) {
auto v = m_v;
v.y -= ClampY(m_yoff + m_y_prog, count);
@@ -63,7 +90,7 @@ void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCa
vv.h = std::min(v.y + v.h, m_pos.y + m_pos.h) - v.y;
if (touch->in_range(vv)) {
callback(i);
callback(true, i);
return;
}
}

View File

@@ -236,18 +236,15 @@ void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, float x, f
bool crop = false;
if (iw < w || ih < h) {
rounded_image = false;
gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded);
gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded ? 5 : 0);
}
if (iw > w || ih > h) {
crop = true;
nvgSave(vg);
nvgIntersectScissor(vg, x, y, w, h);
}
if (rounded_image) {
gfx::drawImageRounded(vg, ix, iy, iw, ih, i.image);
} else {
gfx::drawImage(vg, ix, iy, iw, ih, i.image);
}
gfx::drawImage(vg, ix, iy, iw, ih, i.image, rounded_image ? 5 : 0);
if (crop) {
nvgRestore(vg);
}
@@ -593,6 +590,14 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
return true;
}
// case-insensitive version of str.find()
auto FindCaseInsensitive(std::string_view base, std::string_view term) -> bool {
const auto it = std::search(base.cbegin(), base.cend(), term.cbegin(), term.cend(), [](char a, char b){
return std::toupper(a) == std::toupper(b);
});
return it != base.cend();
}
} // namespace
EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
@@ -769,7 +774,7 @@ void EntryMenu::UpdateOptions() {
};
const auto install = [this](){
App::Push(std::make_shared<ProgressBox>("Installing "_i18n + m_entry.title, [this](auto pbox){
App::Push(std::make_shared<ProgressBox>(m_entry.image.image, "Downloading "_i18n, m_entry.title, [this](auto pbox){
return InstallApp(pbox, m_entry);
}, [this](bool success){
if (success) {
@@ -782,7 +787,7 @@ void EntryMenu::UpdateOptions() {
};
const auto uninstall = [this](){
App::Push(std::make_shared<ProgressBox>("Uninstalling "_i18n + m_entry.title, [this](auto pbox){
App::Push(std::make_shared<ProgressBox>(m_entry.image.image, "Uninstalling "_i18n, m_entry.title, [this](auto pbox){
return UninstallApp(pbox, m_entry);
}, [this](bool success){
if (success) {
@@ -841,59 +846,40 @@ void EntryMenu::SetIndex(s64 index) {
}
}
auto toLower(const std::string& str) -> std::string {
std::string lower;
std::transform(str.cbegin(), str.cend(), std::back_inserter(lower), tolower);
return lower;
}
Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}, m_nro_entries{nro_entries} {
Menu::Menu() : MenuBase{"AppStore"_i18n} {
fs::FsNativeSd fs;
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/icons");
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/banners");
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/screens");
this->SetActions(
std::make_pair(Button::RIGHT, Action{[this](){
if (m_entries_current.empty()) {
return;
}
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
if (m_is_author) {
m_is_author = false;
if (m_is_search) {
SetSearch(m_search_term);
} else {
SetFilter(m_filter);
}
if (m_index < (m_entries_current.size() - 1) && (m_index + 1) % 3 != 0) {
SetIndex(m_index + 1);
App::PlaySoundEffect(SoundEffect_Scroll);
log_write("moved right\n");
}
}}),
std::make_pair(Button::LEFT, Action{[this](){
if (m_entries_current.empty()) {
return;
}
if (m_index != 0 && (m_index % 3) != 0) {
SetIndex(m_index - 1);
App::PlaySoundEffect(SoundEffect_Scroll);
log_write("moved left\n");
}
}}),
std::make_pair(Button::DOWN, Action{[this](){
if (m_list->ScrollDown(m_index, 3, m_entries_current.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
if (m_list->ScrollUp(m_index, 3, m_entries_current.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::R2, Action{[this](){
if (m_list->ScrollDown(m_index, 9, m_entries_current.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::L2, Action{[this](){
if (m_list->ScrollUp(m_index, 9, m_entries_current.size())) {
SetIndex(m_index);
SetIndex(m_entry_author_jump_back);
if (m_entry_author_jump_back >= 9) {
m_list->SetYoff((((m_entry_author_jump_back - 9) + 3) / 3) * m_list->GetMaxY());
} else {
m_list->SetYoff(0);
}
} else if (m_is_search) {
m_is_search = false;
SetFilter(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);
}
} else {
SetPop();
}
}}),
std::make_pair(Button::A, Action{"Info"_i18n, [this](){
@@ -983,8 +969,8 @@ Menu::~Menu() {
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_entries_current.size(), [this](auto i) {
if (m_index == i) {
m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this](bool touch, auto i) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
@@ -1070,7 +1056,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
}
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 {
@@ -1083,29 +1070,27 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
DrawIcon(vg, e.image, m_default_image, x + 20, y + 20, 115, 115, 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);
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), e.title.c_str());
m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.author.c_str());
m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.version.c_str());
float i_size = 22;
switch (e.status) {
case EntryStatus::Get:
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image);
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image, 15);
break;
case EntryStatus::Installed:
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image);
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image, 15);
break;
case EntryStatus::Local:
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image);
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image, 15);
break;
case EntryStatus::Update:
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image);
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image, 15);
break;
}
});
@@ -1308,7 +1293,6 @@ void Menu::Sort() {
void Menu::SetFilter(Filter filter) {
m_is_search = false;
m_is_author = false;
RemoveAction(Button::B);
m_filter = filter;
m_entries_current = m_entries_index[m_filter];
@@ -1338,27 +1322,15 @@ void Menu::SetSearch(const std::string& term) {
m_search_term = term;
m_entries_index_search.clear();
const auto query = toLower(m_search_term);
const auto npos = std::string::npos;
const auto query = m_search_term;
for (u64 i = 0; i < m_entries.size(); i++) {
const auto& e = m_entries[i];
if (toLower(e.title).find(query) != npos || toLower(e.author).find(query) != npos || toLower(e.details).find(query) != npos || toLower(e.description).find(query) != npos) {
if (FindCaseInsensitive(e.title, query) || FindCaseInsensitive(e.author, query) || FindCaseInsensitive(e.description, query)) {
m_entries_index_search.emplace_back(i);
}
}
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetFilter(m_filter);
SetIndex(m_entry_search_jump_back);
if (m_entry_search_jump_back >= 9) {
m_list->SetYoff(0);
m_list->SetYoff((((m_entry_search_jump_back - 9) + 3) / 3) * m_list->GetMaxY());
} else {
m_list->SetYoff(0);
}
}});
m_is_search = true;
m_entries_current = m_entries_index_search;
SetIndex(0);
@@ -1372,29 +1344,15 @@ 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);

View File

@@ -22,6 +22,7 @@
#include "yati/source/file.hpp"
#include <minIni.h>
#include <minizip/zip.h>
#include <minizip/unzip.h>
#include <dirent.h>
#include <cstring>
@@ -57,14 +58,40 @@ 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 {
std::string_view folder;
std::string_view database;
// uses the naming scheme from retropie.
std::string_view folder{};
// uses the naming scheme from Retroarch.
std::string_view database{};
// custom alias, to make everyone else happy.
std::array<std::string_view, 4> alias{};
// compares against all of the above strings.
auto IsDatabase(std::string_view name) const {
if (name == folder || name == database) {
return true;
}
for (const auto& str : alias) {
if (!str.empty() && name == str) {
return true;
}
}
return false;
}
};
// using PathPair = std::pair<std::string_view, std::string_view>;
constexpr RomDatabaseEntry PATHS[]{
{ "3do", "The 3DO Company - 3DO"},
{ "atari800", "Atari - 8-bit"},
@@ -100,6 +127,14 @@ constexpr RomDatabaseEntry PATHS[]{
{ "pico8", "Sega - PICO"},
{ "wonderswan", "Bandai - WonderSwan"},
{ "wonderswancolor", "Bandai - WonderSwan Color"},
{ "mame", "MAME 2000", { "MAME", "mame-libretro", } },
{ "mame", "MAME 2003", { "MAME", "mame-libretro", } },
{ "mame", "MAME 2003-Plus", { "MAME", "mame-libretro", } },
{ "neogeo", "SNK - Neo Geo Pocket" },
{ "neogeo", "SNK - Neo Geo Pocket Color" },
{ "neogeo", "SNK - Neo Geo CD" },
};
constexpr fs::FsPath DAYBREAK_PATH{"/switch/daybreak.nro"};
@@ -120,49 +155,49 @@ auto IsExtension(std::string_view ext1, std::string_view ext2) -> bool {
// tries to find database path using folder name
// names are taken from retropie
// retroarch database names can also be used
auto GetRomDatabaseFromPath(std::string_view path) -> int {
using RomDatabaseIndexs = std::vector<size_t>;
auto GetRomDatabaseFromPath(std::string_view path) -> RomDatabaseIndexs {
if (path.length() <= 1) {
return -1;
return {};
}
// this won't fail :)
RomDatabaseIndexs indexs;
const auto db_name = path.substr(path.find_last_of('/') + 1);
// log_write("new path: %s\n", db_name.c_str());
for (int i = 0; i < std::size(PATHS); i++) {
auto& p = PATHS[i];
if ((
p.folder.length() == db_name.length() && !strncasecmp(p.folder.data(), db_name.data(), p.folder.length())) ||
(p.database.length() == db_name.length() && !strncasecmp(p.database.data(), db_name.data(), p.database.length()))) {
const auto& p = PATHS[i];
if (p.IsDatabase(db_name)) {
log_write("found it :) %.*s\n", (int)p.database.length(), p.database.data());
return i;
indexs.emplace_back(i);
}
}
// if we failed, try again but with the folder about
// "/roms/psx/scooby-doo/scooby-doo.bin", this will check psx
const auto last_off = path.substr(0, path.find_last_of('/'));
if (const auto off = last_off.find_last_of('/'); off != std::string_view::npos) {
const auto db_name2 = last_off.substr(off + 1);
// printf("got db: %s\n", db_name2.c_str());
for (int i = 0; i < std::size(PATHS); i++) {
auto& p = PATHS[i];
if ((
p.folder.length() == db_name2.length() && !strcasecmp(p.folder.data(), db_name2.data())) ||
(p.database.length() == db_name2.length() && !strcasecmp(p.database.data(), db_name2.data()))) {
log_write("found it :) %.*s\n", (int)p.database.length(), p.database.data());
return i;
if (indexs.empty()) {
const auto last_off = path.substr(0, path.find_last_of('/'));
if (const auto off = last_off.find_last_of('/'); off != std::string_view::npos) {
const auto db_name2 = last_off.substr(off + 1);
// printf("got db: %s\n", db_name2.c_str());
for (int i = 0; i < std::size(PATHS); i++) {
const auto& p = PATHS[i];
if (p.IsDatabase(db_name2)) {
log_write("found it :) %.*s\n", (int)p.database.length(), p.database.data());
indexs.emplace_back(i);
}
}
}
}
return -1;
return indexs;
}
//
auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, std::string extension, int db_idx, const NroEntry& nro) {
auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, const RomDatabaseIndexs& db_indexs, const NroEntry& nro) {
// if no db entries, use nro icon
if (db_idx < 0) {
if (db_indexs.empty()) {
log_write("using nro image\n");
return nro_get_icon(nro.path, nro.icon_size, nro.icon_offset);
}
@@ -184,49 +219,51 @@ auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, std::
#define RA_THUMBNAIL_PATH "/retroarch/thumbnails/"
#define RA_BOXART_EXT ".png"
const auto system_name = std::string{PATHS[db_idx].database.data(), PATHS[db_idx].database.length()};//GetDatabaseFromExt(database, extension);
auto system_name_gh = system_name + "/master";
for (auto& c : system_name_gh) {
if (c == ' ') {
c = '_';
for (auto db_idx : db_indexs) {
const auto system_name = std::string{PATHS[db_idx].database.data(), PATHS[db_idx].database.length()};//GetDatabaseFromExt(database, extension);
auto system_name_gh = system_name + "/master";
for (auto& c : system_name_gh) {
if (c == ' ') {
c = '_';
}
}
}
std::string filename_gh;
filename_gh.reserve(filename.size());
for (auto c : filename) {
if (c == ' ') {
filename_gh += "%20";
} else {
filename_gh.push_back(c);
std::string filename_gh;
filename_gh.reserve(filename.size());
for (auto c : filename) {
if (c == ' ') {
filename_gh += "%20";
} else {
filename_gh.push_back(c);
}
}
}
const std::string thumbnail_path = system_name + RA_BOXART_NAME + filename + RA_BOXART_EXT;
const std::string ra_thumbnail_path = RA_THUMBNAIL_PATH + thumbnail_path;
const std::string ra_thumbnail_url = RA_BOXART_URL + thumbnail_path;
const std::string gh_thumbnail_url = GH_BOXART_URL + system_name_gh + RA_BOXART_NAME + filename_gh + RA_BOXART_EXT;
const std::string thumbnail_path = system_name + RA_BOXART_NAME + filename + RA_BOXART_EXT;
const std::string ra_thumbnail_path = RA_THUMBNAIL_PATH + thumbnail_path;
const std::string ra_thumbnail_url = RA_BOXART_URL + thumbnail_path;
const std::string gh_thumbnail_url = GH_BOXART_URL + system_name_gh + RA_BOXART_NAME + filename_gh + RA_BOXART_EXT;
log_write("starting image convert on: %s\n", ra_thumbnail_path.c_str());
// try and find icon locally
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Trying to load "_i18n + ra_thumbnail_path);
std::vector<u8> image_file;
if (R_SUCCEEDED(fs->read_entire_file(ra_thumbnail_path.c_str(), image_file))) {
return image_file;
log_write("starting image convert on: %s\n", ra_thumbnail_path.c_str());
// try and find icon locally
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Trying to load "_i18n + ra_thumbnail_path);
std::vector<u8> image_file;
if (R_SUCCEEDED(fs->read_entire_file(ra_thumbnail_path.c_str(), image_file))) {
return image_file;
}
}
}
// try and download icon
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Downloading "_i18n + gh_thumbnail_url);
const auto result = curl::Api().ToMemory(
curl::Url{gh_thumbnail_url},
curl::OnProgress{pbox->OnDownloadProgressCallback()}
);
// try and download icon
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Downloading "_i18n + gh_thumbnail_url);
const auto result = curl::Api().ToMemory(
curl::Url{gh_thumbnail_url},
curl::OnProgress{pbox->OnDownloadProgressCallback()}
);
if (result.success && !result.data.empty()) {
return result.data;
if (result.success && !result.data.empty()) {
return result.data;
}
}
}
@@ -239,6 +276,25 @@ auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, std::
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();
@@ -251,26 +307,6 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
m_selected_count--;
}
}}),
std::make_pair(Button::DOWN, Action{[this](){
if (m_list->ScrollDown(m_index, 1, m_entries_current.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
if (m_list->ScrollUp(m_index, 1, m_entries_current.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::DPAD_RIGHT, Action{[this](){
if (m_list->ScrollDown(m_index, 8, m_entries_current.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::DPAD_LEFT, Action{[this](){
if (m_list->ScrollUp(m_index, 8, m_entries_current.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::A, Action{"Open"_i18n, [this](){
if (m_entries_current.empty()) {
return;
@@ -376,17 +412,17 @@ 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()) {
@@ -467,22 +503,24 @@ 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());
@@ -493,6 +531,92 @@ 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();
}
}));
}
}
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](){
if (!m_selected_count) {
UnzipFile("", GetEntry());
} else {
UnzipFiles("", GetSelectedEntries());
}
}));
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) {
if (!m_selected_count) {
UnzipFile("/", GetEntry());
} else {
UnzipFiles("/", GetSelectedEntries());
}
}
}));
}));
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()) {
if (!m_selected_count) {
UnzipFile(out, GetEntry());
} else {
UnzipFiles(out, GetSelectedEntries());
}
}
}));
}));
}
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](){
if (!m_selected_count) {
ZipFile("", GetEntry());
} else {
ZipFiles("", GetSelectedEntries());
}
}));
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()) {
if (!m_selected_count) {
ZipFile(out, GetEntry());
} else {
ZipFiles(out, GetSelectedEntries());
}
}
}));
}));
}
}
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));
@@ -521,7 +645,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;
@@ -546,29 +670,10 @@ 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<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);
@@ -598,8 +703,8 @@ Menu::~Menu() {
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_entries_current.size(), [this](auto i) {
if (m_index == i) {
m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this](bool touch, auto i) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
@@ -642,7 +747,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 {
@@ -665,7 +771,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;
@@ -774,7 +880,7 @@ void Menu::InstallForwarder() {
if (op_index) {
const auto assoc = assoc_list[*op_index];
log_write("pushing it\n");
App::Push(std::make_shared<ProgressBox>("Installing Forwarder"_i18n, [assoc, this](auto pbox) -> bool {
App::Push(std::make_shared<ProgressBox>(0, "Installing Forwarder"_i18n, GetEntry().name, [assoc, this](auto pbox) -> bool {
log_write("inside callback\n");
NroEntry nro{};
@@ -784,8 +890,7 @@ void Menu::InstallForwarder() {
return false;
}
log_write("got nro data\n");
std::string file_name = GetEntry().GetInternalName();
std::string extension = GetEntry().GetInternalExtension();
auto file_name = assoc.use_base_name ? GetEntry().GetName() : GetEntry().GetInternalName();
if (auto pos = file_name.find_last_of('.'); pos != std::string::npos) {
log_write("got filename\n");
@@ -793,7 +898,7 @@ void Menu::InstallForwarder() {
log_write("got filename2: %s\n\n", file_name.c_str());
}
const auto db_idx = GetRomDatabaseFromPath(m_path);
const auto db_indexs = GetRomDatabaseFromPath(m_path);
OwoConfig config{};
config.nro_path = assoc.path.toString();
@@ -801,7 +906,8 @@ void Menu::InstallForwarder() {
config.name = nro.nacp.lang[0].name + std::string{" | "} + file_name;
// config.name = file_name;
config.nacp = nro.nacp;
config.icon = GetRomIcon(m_fs.get(), pbox, file_name, extension, db_idx, nro);
config.icon = GetRomIcon(m_fs.get(), pbox, file_name, db_indexs, nro);
pbox->SetImageDataConst(config.icon);
return R_SUCCEEDED(App::Install(pbox, config));
}));
@@ -822,7 +928,7 @@ void Menu::InstallFiles(const std::vector<FileEntry>& targets) {
if (op_index && *op_index) {
App::PopToMenu();
App::Push(std::make_shared<ui::ProgressBox>("Installing App"_i18n, [this, targets](auto pbox) mutable -> bool {
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this, targets](auto pbox) mutable -> bool {
for (auto& e : targets) {
const auto rc = yati::InstallFromFile(pbox, &m_fs->m_fs, GetNewPath(e));
if (rc == yati::Result_Cancelled) {
@@ -840,6 +946,272 @@ void Menu::InstallFiles(const std::vector<FileEntry>& targets) {
}));
}
void Menu::UnzipFile(const fs::FsPath& dir_path, const FileEntry& target) {
std::vector<FileEntry> targets{target};
UnzipFiles(dir_path, targets);
}
void Menu::UnzipFiles(fs::FsPath dir_path, const std::vector<FileEntry>& targets) {
// 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::ZipFile(const fs::FsPath& zip_path, const FileEntry& target) {
std::vector<FileEntry> targets{target};
ZipFiles(zip_path, targets);
}
void Menu::ZipFiles(fs::FsPath zip_out, const std::vector<FileEntry>& targets) {
// 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");
}));
}
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()) {
@@ -912,32 +1284,32 @@ auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
auto Menu::FindFileAssocFor() -> std::vector<FileAssocEntry> {
// only support roms in correctly named folders, sorry!
const auto db_idx = GetRomDatabaseFromPath(m_path);
const auto db_indexs = GetRomDatabaseFromPath(m_path);
const auto& entry = GetEntry();
const auto extension = entry.internal_extension.empty() ? entry.extension : entry.internal_extension;
if (extension.empty()) {
const auto extension = entry.extension;
const auto internal_extension = entry.internal_extension.empty() ? entry.extension : entry.internal_extension;
if (extension.empty() && internal_extension.empty()) {
// log_write("failed to get extension for db: %s path: %s\n", database_entry.c_str(), m_path);
return {};
}
// log_write("got extension for db: %s path: %s\n", database_entry.c_str(), m_path);
std::vector<FileAssocEntry> out_entries;
if (db_idx >= 0) {
if (!db_indexs.empty()) {
// if database isn't empty, then we are in a valid folder
// search for an entry that matches the db and ext
for (const auto& assoc : m_assoc_entries) {
for (const auto& assoc_db : assoc.database) {
if (assoc_db == PATHS[db_idx].folder || assoc_db == PATHS[db_idx].database) {
for (const auto& assoc_ext : assoc.ext) {
if (assoc_ext == extension) {
log_write("found ext: %s assoc_ext: %s assoc.ext: %s\n", assoc.path.s, assoc_ext.c_str(), extension.c_str());
// if (assoc_db == PATHS[db_idx].folder || assoc_db == PATHS[db_idx].database) {
for (auto db_idx : db_indexs) {
if (PATHS[db_idx].IsDatabase(assoc_db)) {
if (assoc.IsExtension(extension, internal_extension)) {
out_entries.emplace_back(assoc);
goto jump;
}
}
}
}
jump:
}
} else {
// otherwise, if not in a valid folder, find an entry that doesn't
@@ -948,11 +1320,9 @@ auto Menu::FindFileAssocFor() -> std::vector<FileAssocEntry> {
// to be in the correct folder, ie psx, to know what system that .iso is for.
for (const auto& assoc : m_assoc_entries) {
if (assoc.database.empty()) {
for (const auto& assoc_ext : assoc.ext) {
if (assoc_ext == extension) {
log_write("found ext: %s\n", assoc.path.s);
out_entries.emplace_back(assoc);
}
if (assoc.IsExtension(extension, internal_extension)) {
log_write("found ext: %s\n", assoc.path.s);
out_entries.emplace_back(assoc);
}
}
}
@@ -1009,6 +1379,10 @@ void Menu::LoadAssocEntriesPath(const fs::FsPath& path) {
}
}
}
} else if (!strcmp(Key, "use_base_name")) {
if (!strcmp(Value, "true") || !strcmp(Value, "1")) {
assoc->use_base_name = true;
}
}
return 1;
}, &assoc, full_path);
@@ -1044,7 +1418,7 @@ void Menu::LoadAssocEntriesPath(const fs::FsPath& path) {
continue;
}
// log_write("\tpath: %s\n", assoc.path.c_str());
// log_write("\tpath: %s\n", assoc.path.s);
// log_write("\tname: %s\n", assoc.name.c_str());
// for (const auto& ext : assoc.ext) {
// log_write("\t\text: %s\n", ext.c_str());
@@ -1197,7 +1571,7 @@ void Menu::OnDeleteCallback() {
Scan(m_path);
log_write("did delete\n");
} else {
App::Push(std::make_shared<ProgressBox>("Deleting"_i18n, [this](auto pbox){
App::Push(std::make_shared<ProgressBox>(0, "Deleting"_i18n, "", [this](auto pbox){
FsDirCollections collections;
// build list of dirs / files
@@ -1290,7 +1664,7 @@ void Menu::OnPasteCallback() {
Scan(m_path);
log_write("did paste\n");
} else {
App::Push(std::make_shared<ProgressBox>("Pasting"_i18n, [this](auto pbox){
App::Push(std::make_shared<ProgressBox>(0, "Pasting"_i18n, "", [this](auto pbox){
if (m_selected_type == SelectedType::Cut) {
for (const auto& p : m_selected_files) {

View File

@@ -0,0 +1,300 @@
#include "ui/menus/ftp_menu.hpp"
#include "yati/yati.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
#include "ftpsrv_helper.hpp"
#include <cstring>
namespace sphaira::ui::menu::ftp {
namespace {
constexpr u64 MAX_BUFFER_SIZE = 1024*1024*32;
constexpr u64 SLEEPNS = 1000;
volatile bool IN_PUSH_THREAD{};
bool OnInstallStart(void* user, const char* path) {
auto menu = (Menu*)user;
log_write("[INSTALL] inside OnInstallStart()\n");
for (;;) {
mutexLock(&menu->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex));
if (menu->m_state != State::Progress) {
break;
}
if (menu->GetToken().stop_requested()) {
return false;
}
svcSleepThread(1e+6);
}
log_write("[INSTALL] OnInstallStart() got state: %u\n", (u8)menu->m_state);
if (menu->m_source) {
log_write("[INSTALL] OnInstallStart() we have source\n");
for (;;) {
mutexLock(&menu->m_source->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&menu->m_source->m_mutex));
if (!IN_PUSH_THREAD) {
break;
}
if (menu->GetToken().stop_requested()) {
return false;
}
svcSleepThread(1e+6);
}
log_write("[INSTALL] OnInstallStart() stopped polling source\n");
}
log_write("[INSTALL] OnInstallStart() doing make_shared\n");
menu->m_source = std::make_shared<StreamFtp>(path, menu->GetToken());
mutexLock(&menu->m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex));
menu->m_state = State::Connected;
log_write("[INSTALL] OnInstallStart() done make shared\n");
return true;
}
bool OnInstallWrite(void* user, const void* buf, size_t size) {
auto menu = (Menu*)user;
return menu->m_source->Push(buf, size);
}
void OnInstallClose(void* user) {
auto menu = (Menu*)user;
menu->m_source->Disable();
}
} // namespace
StreamFtp::StreamFtp(const fs::FsPath& path, std::stop_token token) {
m_path = path;
m_token = token;
m_buffer.reserve(MAX_BUFFER_SIZE);
m_active = true;
}
Result StreamFtp::ReadChunk(void* buf, s64 size, u64* bytes_read) {
while (!m_token.stop_requested()) {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (m_buffer.empty()) {
if (!m_active) {
break;
}
svcSleepThread(SLEEPNS);
} else {
size = std::min<s64>(size, m_buffer.size());
std::memcpy(buf, m_buffer.data(), size);
m_buffer.erase(m_buffer.begin(), m_buffer.begin() + size);
*bytes_read = size;
R_SUCCEED();
}
}
return 0x1;
}
bool StreamFtp::Push(const void* buf, s64 size) {
IN_PUSH_THREAD = true;
ON_SCOPE_EXIT(IN_PUSH_THREAD = false);
while (!m_token.stop_requested()) {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (!m_active) {
break;
}
if (m_buffer.size() + size >= MAX_BUFFER_SIZE) {
svcSleepThread(SLEEPNS);
} else {
const auto offset = m_buffer.size();
m_buffer.resize(offset + size);
std::memcpy(m_buffer.data() + offset, buf, size);
return true;
}
}
return false;
}
void StreamFtp::Disable() {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
m_active = false;
}
Menu::Menu() : MenuBase{"FTP Install (EXPERIMENTAL)"_i18n} {
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}});
SetAction(Button::X, Action{"Options"_i18n, [this](){
App::DisplayInstallOptions(false);
}});
mutexInit(&m_mutex);
ftpsrv::InitInstallMode(this, OnInstallStart, OnInstallWrite, OnInstallClose);
m_port = ftpsrv::GetPort();
m_anon = ftpsrv::IsAnon();
if (!m_anon) {
m_user = ftpsrv::GetUser();
m_pass = ftpsrv::GetPass();
}
}
Menu::~Menu() {
// signal for thread to exit and wait.
ftpsrv::DisableInstallMode();
m_stop_source.request_stop();
if (m_source) {
m_source->Disable();
}
log_write("closing data!!!!\n");
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
switch (m_state) {
case State::None:
break;
case State::Connected:
log_write("set to progress\n");
m_state = State::Progress;
log_write("got connection\n");
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool {
log_write("inside progress box\n");
const auto rc = yati::InstallFromSource(pbox, m_source, m_source->m_path);
if (R_FAILED(rc)) {
m_source->Disable();
return false;
}
log_write("progress box is done\n");
return true;
}, [this](bool result){
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (result) {
App::Notify("Ftp install success!"_i18n);
m_state = State::Done;
} else {
App::Notify("Ftp install failed!"_i18n);
m_state = State::Failed;
}
}));
break;
case State::Progress:
case State::Done:
case State::Failed:
break;
}
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (m_ip) {
if (m_type == NifmInternetConnectionType_WiFi) {
SetSubHeading("Connection Type: WiFi | Strength: "_i18n + std::to_string(m_strength));
} else {
SetSubHeading("Connection Type: Ethernet"_i18n);
}
} else {
SetSubHeading("Connection Type: None"_i18n);
}
const float start_x = 80;
const float font_size = 22;
const float spacing = 33;
float start_y = 125;
float bounds[4];
nvgFontSize(vg, font_size);
// note: textbounds strips spaces...todo: use nvgTextGlyphPositions() instead.
#define draw(key, ...) \
gfx::textBounds(vg, start_x, start_y, bounds, key.c_str()); \
gfx::drawTextArgs(vg, start_x, start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT), key.c_str()); \
gfx::drawTextArgs(vg, bounds[2], start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED), __VA_ARGS__); \
start_y += spacing;
if (m_ip) {
draw("Host:"_i18n, " %u.%u.%u.%u", m_ip&0xFF, (m_ip>>8)&0xFF, (m_ip>>16)&0xFF, (m_ip>>24)&0xFF);
draw("Port:"_i18n, " %u", m_port);
if (!m_anon) {
draw("Username:"_i18n, " %s", m_user);
draw("Password:"_i18n, " %s", m_pass);
}
if (m_type == NifmInternetConnectionType_WiFi) {
NifmNetworkProfileData profile{};
if (R_SUCCEEDED(nifmGetCurrentNetworkProfile(&profile))) {
const auto& settings = profile.wireless_setting_data;
std::string passphrase;
std::transform(std::cbegin(settings.passphrase), std::cend(settings.passphrase), passphrase.begin(), toascii);
draw("SSID:"_i18n, " %.*s", settings.ssid_len, settings.ssid);
draw("Passphrase:"_i18n, " %s", passphrase.c_str());
}
}
}
#undef draw
switch (m_state) {
case State::None:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str());
break;
case State::Connected:
break;
case State::Progress:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Transferring data..."_i18n.c_str());
break;
case State::Done:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Press B to exit..."_i18n.c_str());
break;
case State::Failed:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to install via FTP, press B to exit..."_i18n.c_str());
break;
}
}
void Menu::OnFocusGained() {
MenuBase::OnFocusGained();
}
} // namespace sphaira::ui::menu::ftp

View File

@@ -0,0 +1,452 @@
#include "app.hpp"
#include "log.hpp"
#include "fs.hpp"
#include "ui/menus/game_menu.hpp"
#include "ui/sidebar.hpp"
#include "ui/error_box.hpp"
#include "ui/option_box.hpp"
#include "ui/progress_box.hpp"
#include "ui/popup_list.hpp"
#include "ui/nvg_util.hpp"
#include "defines.hpp"
#include "i18n.hpp"
#include "yati/nx/ncm.hpp"
#include <utility>
#include <cstring>
namespace sphaira::ui::menu::game {
namespace {
// thank you Shchmue ^^
struct ApplicationOccupiedSizeEntry {
u8 storageId;
u8 padding[0x7];
u64 sizeApplication;
u64 sizePatch;
u64 sizeAddOnContent;
};
struct ApplicationOccupiedSize {
ApplicationOccupiedSizeEntry entry[4];
};
static_assert(sizeof(ApplicationOccupiedSize) == sizeof(NsApplicationOccupiedSize));
using MetaEntries = std::vector<NsApplicationContentMetaStatus>;
Result Notify(Result rc, const std::string& error_message) {
if (R_FAILED(rc)) {
App::Push(std::make_shared<ui::ErrorBox>(rc,
i18n::get(error_message)
));
} else {
App::Notify("Success");
}
return rc;
}
Result GetMetaEntries(const Entry& e, MetaEntries& out) {
s32 count;
R_TRY(nsCountApplicationContentMeta(e.app_id, &count));
out.resize(count);
R_TRY(nsListApplicationContentMetaStatus(e.app_id, 0, out.data(), out.size(), &count));
out.resize(count);
R_SUCCEED();
}
// also sets the status to error.
void FakeNacpEntry(Entry& e) {
e.status = NacpLoadStatus::Error;
// fake the nacp entry
std::strcpy(e.lang.name, "Corrupted");
std::strcpy(e.lang.author, "Corrupted");
std::strcpy(e.display_version, "0.0.0");
e.control.reset();
}
bool LoadControlImage(Entry& e) {
if (!e.image && e.control) {
const auto jpeg_size = e.control_size - sizeof(NacpStruct);
e.image = nvgCreateImageMem(App::GetVg(), 0, e.control->icon, jpeg_size);
e.control.reset();
return true;
}
return false;
}
void LoadControlEntry(Entry& e, bool force_image_load = false) {
if (e.status == NacpLoadStatus::None) {
e.control = std::make_unique<NsApplicationControlData>();
if (R_FAILED(nsGetApplicationControlData(NsApplicationControlSource_Storage, e.app_id, e.control.get(), sizeof(NsApplicationControlData), &e.control_size))) {
FakeNacpEntry(e);
} else {
NacpLanguageEntry* lang{};
if (R_FAILED(nsGetApplicationDesiredLanguage(&e.control->nacp, &lang)) || !lang) {
FakeNacpEntry(e);
} else {
e.lang = *lang;
std::memcpy(e.display_version, e.control->nacp.display_version, sizeof(e.display_version));
e.status = NacpLoadStatus::Loaded;
if (force_image_load) {
LoadControlImage(e);
}
}
}
}
}
void FreeEntry(NVGcontext* vg, Entry& e) {
nvgDeleteImage(vg, e.image);
e.image = 0;
}
void LaunchEntry(const Entry& e) {
const auto rc = appletRequestLaunchApplication(e.app_id, nullptr);
Notify(rc, "Failed to launch application");
}
} // namespace
Menu::Menu() : MenuBase{"Games"_i18n} {
this->SetActions(
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}}),
std::make_pair(Button::A, Action{"Launch"_i18n, [this](){
if (m_entries.empty()) {
return;
}
LaunchEntry(m_entries[m_index]);
}}),
std::make_pair(Button::X, Action{"Options"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Game Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
if (m_entries.size()) {
options->Add(std::make_shared<SidebarEntryCallback>("Sort By"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Sort Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
SidebarEntryArray::Items sort_items;
sort_items.push_back("Updated"_i18n);
SidebarEntryArray::Items order_items;
order_items.push_back("Descending"_i18n);
order_items.push_back("Ascending"_i18n);
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
m_sort.Set(index_out);
SortAndFindLastFile();
}, m_sort.Get()));
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](s64& index_out){
m_order.Set(index_out);
SortAndFindLastFile();
}, m_order.Get()));
options->Add(std::make_shared<SidebarEntryBool>("Hide forwarders"_i18n, m_hide_forwarders.Get(), [this](bool& v_out){
m_hide_forwarders.Set(v_out);
m_dirty = true;
}));
}));
#if 0
options->Add(std::make_shared<SidebarEntryCallback>("Info"_i18n, [this](){
}));
#endif
options->Add(std::make_shared<SidebarEntryCallback>("Launch random game"_i18n, [this](){
const auto random_index = randomGet64() % std::size(m_entries);
auto& e = m_entries[random_index];
LoadControlEntry(e, true);
App::Push(std::make_shared<OptionBox>(
"Launch "_i18n + e.GetName(),
"Back"_i18n, "Launch"_i18n, 1, [this, &e](auto op_index){
if (op_index && *op_index) {
LaunchEntry(e);
}
}, e.image
));
}));
options->Add(std::make_shared<SidebarEntryCallback>("List meta records"_i18n, [this](){
MetaEntries meta_entries;
const auto rc = GetMetaEntries(m_entries[m_index], meta_entries);
if (R_FAILED(rc)) {
App::Push(std::make_shared<ui::ErrorBox>(rc,
i18n::get("Failed to list application meta entries")
));
return;
}
if (meta_entries.empty()) {
App::Notify("No meta entries found...\n");
return;
}
PopupList::Items items;
for (auto& e : meta_entries) {
char buf[256];
std::snprintf(buf, sizeof(buf), "Type: %s Storage: %s [%016lX][v%u]", ncm::GetMetaTypeStr(e.meta_type), ncm::GetStorageIdStr(e.storageID), e.application_id, e.version);
items.emplace_back(buf);
}
App::Push(std::make_shared<PopupList>(
"Entries", items, [this, meta_entries](auto op_index){
#if 0
if (op_index) {
const auto& e = meta_entries[*op_index];
}
#endif
}
));
}));
// completely deletes the application record and all data.
options->Add(std::make_shared<SidebarEntryCallback>("Delete"_i18n, [this](){
const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].GetName() + "?";
App::Push(std::make_shared<OptionBox>(
buf,
"Back"_i18n, "Delete"_i18n, 0, [this](auto op_index){
if (op_index && *op_index) {
const auto rc = nsDeleteApplicationCompletely(m_entries[m_index].app_id);
if (R_SUCCEEDED(Notify(rc, "Failed to delete application"))) {
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));
// removes installed data but keeps the record, basically archiving.
options->Add(std::make_shared<SidebarEntryCallback>("Delete entity"_i18n, [this](){
const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].GetName() + "?";
App::Push(std::make_shared<OptionBox>(
buf,
"Back"_i18n, "Delete"_i18n, 0, [this](auto op_index){
if (op_index && *op_index) {
const auto rc = nsDeleteApplicationEntity(m_entries[m_index].app_id);
Notify(rc, "Failed to delete application");
}
}, m_entries[m_index].image
));
}, true));
}
}})
);
const Vec4 v{75, 110, 370, 155};
const Vec2 pad{10, 10};
m_list = std::make_unique<List>(3, 9, m_pos, v, pad);
nsInitialize();
}
Menu::~Menu() {
FreeEntries();
nsExit();
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
SetIndex(i);
}
});
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
// max images per frame, in order to not hit io / gpu too hard.
const int image_load_max = 2;
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];
if (e.status == NacpLoadStatus::None) {
LoadControlEntry(e);
}
// lazy load image
if (image_load_count < image_load_max) {
if (LoadControlImage(e)) {
image_load_count++;
}
}
auto text_id = ThemeEntryID_TEXT;
const auto selected = pos == m_index;
if (selected) {
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(), 5);
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), e.GetName());
m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetAuthor());
m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetDisplayVersion());
});
}
void Menu::OnFocusGained() {
MenuBase::OnFocusGained();
if (m_dirty || m_entries.empty()) {
ScanHomebrew();
}
}
void Menu::SetIndex(s64 index) {
m_index = index;
if (!m_index) {
m_list->SetYoff(0);
}
char title_id[33];
std::snprintf(title_id, sizeof(title_id), "%016lX", m_entries[m_index].app_id);
SetTitleSubHeading(title_id);
this->SetSubHeading(std::to_string(m_index + 1) + " / " + std::to_string(m_entries.size()));
}
void Menu::ScanHomebrew() {
constexpr auto ENTRY_CHUNK_COUNT = 1000;
const auto hide_forwarders = m_hide_forwarders.Get();
TimeStamp ts;
FreeEntries();
m_entries.reserve(ENTRY_CHUNK_COUNT);
std::vector<NsApplicationRecord> record_list(ENTRY_CHUNK_COUNT);
s32 offset{};
while (true) {
s32 record_count{};
if (R_FAILED(nsListApplicationRecord(record_list.data(), record_list.size(), offset, &record_count))) {
log_write("failed to list application records at offset: %d\n", offset);
}
// finished parsing all entries.
if (!record_count) {
break;
}
for (s32 i = 0; i < record_count; i++) {
const auto& e = record_list[i];
#if 0
u8 unk_x09 = e.unk_x09;
u64 unk_x0a;// = e.unk_x0a;
u8 unk_x10 = e.unk_x10;
u64 unk_x11;// = e.unk_x11;
memcpy(&unk_x0a, e.unk_x0a, sizeof(e.unk_x0a));
memcpy(&unk_x11, e.unk_x11, sizeof(e.unk_x11));
log_write("ID: %016lx got type: %u unk_x09: %u unk_x0a: %zu unk_x10: %u unk_x11: %zu\n", e.application_id, e.type,
unk_x09,
unk_x0a,
unk_x10,
unk_x11
);
#endif
if (hide_forwarders && (e.application_id & 0x0500000000000000) == 0x0500000000000000) {
continue;
}
s64 size{};
// code for sorting by size, it's too slow however...
#if 0
ApplicationOccupiedSize occupied_size;
if (R_SUCCEEDED(nsCalculateApplicationOccupiedSize(e.application_id, (NsApplicationOccupiedSize*)&occupied_size))) {
for (auto& s : occupied_size.entry) {
size += s.sizeApplication;
size += s.sizePatch;
size += s.sizeAddOnContent;
}
}
#endif
m_entries.emplace_back(e.application_id, size);
}
offset += record_count;
}
m_is_reversed = false;
m_dirty = false;
log_write("games found: %zu time_taken: %.2f seconds %zu ms %zu ns\n", m_entries.size(), ts.GetSecondsD(), ts.GetMs(), ts.GetNs());
this->Sort();
SetIndex(0);
}
void Menu::Sort() {
// const auto sort = m_sort.Get();
const auto order = m_order.Get();
if (order == OrderType_Ascending) {
if (!m_is_reversed) {
std::reverse(m_entries.begin(), m_entries.end());
m_is_reversed = true;
}
} else {
if (m_is_reversed) {
std::reverse(m_entries.begin(), m_entries.end());
m_is_reversed = false;
}
}
}
void Menu::SortAndFindLastFile() {
const auto app_id = m_entries[m_index].app_id;
Sort();
SetIndex(0);
s64 index = -1;
for (u64 i = 0; i < m_entries.size(); i++) {
if (app_id == m_entries[i].app_id) {
index = i;
break;
}
}
if (index >= 0) {
// guesstimate where the position is
if (index >= 9) {
m_list->SetYoff((((index - 9) + 3) / 3) * m_list->GetMaxY());
} else {
m_list->SetYoff(0);
}
SetIndex(index);
}
}
void Menu::FreeEntries() {
auto vg = App::GetVg();
for (auto&p : m_entries) {
FreeEntry(vg, p);
}
m_entries.clear();
}
} // namespace sphaira::ui::menu::game

View File

@@ -0,0 +1,534 @@
#include "ui/menus/gc_menu.hpp"
#include "yati/yati.hpp"
#include "yati/nx/nca.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
#include <cstring>
namespace sphaira::ui::menu::gc {
namespace {
const char *g_option_list[] = {
"Nand Install",
"SD Card Install",
"Exit",
};
struct HashStr {
char str[0x21];
};
HashStr hexIdToStr(auto id) {
HashStr str{};
const auto id_lower = std::byteswap(*(u64*)id.c);
const auto id_upper = std::byteswap(*(u64*)(id.c + 0x8));
std::snprintf(str.str, 0x21, "%016lx%016lx", id_lower, id_upper);
return str;
}
// @Gc is the mount point, S is for secure partion, the remaining is the
// the gamecard handle value in lower-case hex.
auto BuildGcPath(const char* name, const FsGameCardHandle* handle, FsGameCardPartition partiton = FsGameCardPartition_Secure) -> fs::FsPath {
static const char mount_parition[] = {
[FsGameCardPartition_Update] = 'U',
[FsGameCardPartition_Normal] = 'N',
[FsGameCardPartition_Secure] = 'S',
[FsGameCardPartition_Logo] = 'L',
};
fs::FsPath path;
std::snprintf(path, sizeof(path), "@Gc%c%08x://%s", mount_parition[partiton], handle->value, name);
return path;
}
Result fsOpenGameCardDetectionEventNotifier(FsEventNotifier* out) {
return serviceDispatch(fsGetServiceSession(), 501,
.out_num_objects = 1,
.out_objects = &out->s
);
}
auto InRange(u64 off, u64 offset, u64 size) -> bool {
return off < offset + size && off >= offset;
}
struct GcSource final : yati::source::Base {
GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs, bool sd_install);
~GcSource();
Result Read(void* buf, s64 off, s64 size, u64* bytes_read);
yati::container::Collections m_collections{};
yati::ConfigOverride m_config{};
fs::FsNativeGameCard* m_fs{};
FsFile m_file{};
s64 m_offset{};
s64 m_size{};
};
GcSource::GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs, bool sd_install)
: m_fs{fs} {
m_offset = -1;
s64 offset{};
const auto add_collections = [&](const auto& collections) {
for (auto collection : collections) {
collection.offset = offset;
m_collections.emplace_back(collection);
offset += collection.size;
}
};
const auto add_entries = [&](const auto& entries) {
for (auto& e : entries) {
add_collections(e);
}
};
// yati can handle all of this for use, however, yati lacks information
// for ncas until it installs the cnmt and parses it.
// as we already have this info, we can only send yati what we want to install.
if (App::GetApp()->m_ticket_only.Get()) {
add_collections(entry.tickets);
} else {
if (!App::GetApp()->m_skip_base.Get()) {
add_entries(entry.application);
}
if (!App::GetApp()->m_skip_patch.Get()) {
add_entries(entry.patch);
}
if (!App::GetApp()->m_skip_addon.Get()) {
add_entries(entry.add_on);
}
if (!App::GetApp()->m_skip_data_patch.Get()) {
add_entries(entry.data_patch);
}
if (!App::GetApp()->m_skip_ticket.Get()) {
add_collections(entry.tickets);
}
}
// we don't need to verify the nca's, this speeds up installs.
m_config.sd_card_install = sd_install;
m_config.skip_nca_hash_verify = true;
m_config.skip_rsa_header_fixed_key_verify = true;
m_config.skip_rsa_npdm_fixed_key_verify = true;
}
GcSource::~GcSource() {
fsFileClose(&m_file);
}
Result GcSource::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
// check is we need to open a new file.
if (!InRange(off, m_offset, m_size)) {
fsFileClose(&m_file);
m_file = {};
// find new file based on the offset.
bool found = false;
for (auto& collection : m_collections) {
if (InRange(off, collection.offset, collection.size)) {
found = true;
m_offset = collection.offset;
m_size = collection.size;
R_TRY(m_fs->OpenFile(fs::AppendPath("/", collection.name), FsOpenMode_Read, &m_file));
break;
}
}
// this will never fail, unless i break something in yati.
R_UNLESS(found, 0x1);
}
return fsFileRead(&m_file, off - m_offset, buf, size, 0, bytes_read);
}
} // namespace
auto ApplicationEntry::GetSize(const std::vector<GcCollections>& entries) const -> s64 {
s64 size{};
for (auto& e : entries) {
for (auto& collection : e) {
size += collection.size;
}
}
return size;
}
auto ApplicationEntry::GetSize() const -> s64 {
s64 size{};
size += GetSize(application);
size += GetSize(patch);
size += GetSize(add_on);
size += GetSize(data_patch);
return size;
}
Menu::Menu() : MenuBase{"GameCard"_i18n} {
this->SetActions(
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}}),
std::make_pair(Button::X, Action{"Options"_i18n, [this](){
App::DisplayInstallOptions(false);
}})
);
const Vec4 v{485, 275, 720, 70};
const Vec2 pad{0, 125 - v.h};
m_list = std::make_unique<List>(1, 3, m_pos, v, pad);
fsOpenDeviceOperator(std::addressof(m_dev_op));
fsOpenGameCardDetectionEventNotifier(std::addressof(m_event_notifier));
fsEventNotifierGetEventHandle(std::addressof(m_event_notifier), std::addressof(m_event), true);
}
Menu::~Menu() {
GcUnmount();
eventClose(std::addressof(m_event));
fsEventNotifierClose(std::addressof(m_event_notifier));
fsDeviceOperatorClose(std::addressof(m_dev_op));
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
// poll for the gamecard first before handling inputs as the gamecard
// may have been removed, thus pressing A would fail.
if (R_SUCCEEDED(eventWait(std::addressof(m_event), 0))) {
GcOnEvent();
}
MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_option_index, std::size(g_option_list), [this](bool touch, auto i) {
if (touch && m_option_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
m_option_index = i;
}
});
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
#define STORAGE_BAR_W 325
#define STORAGE_BAR_H 14
const auto size_sd_gb = (double)m_size_free_sd / 0x40000000;
const auto size_nand_gb = (double)m_size_free_nand / 0x40000000;
gfx::drawTextArgs(vg, 490, 135, 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "System memory %.1f GB", size_nand_gb);
gfx::drawRect(vg, 480, 170, STORAGE_BAR_W, STORAGE_BAR_H, theme->GetColour(ThemeEntryID_TEXT));
gfx::drawRect(vg, 480 + 1, 170 + 1, STORAGE_BAR_W - 2, STORAGE_BAR_H - 2, theme->GetColour(ThemeEntryID_BACKGROUND));
gfx::drawRect(vg, 480 + 2, 170 + 2, STORAGE_BAR_W - (((double)m_size_free_nand / (double)m_size_total_nand) * STORAGE_BAR_W) - 4, STORAGE_BAR_H - 4, theme->GetColour(ThemeEntryID_TEXT));
gfx::drawTextArgs(vg, 870, 135, 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "microSD card %.1f GB", size_sd_gb);
gfx::drawRect(vg, 860, 170, STORAGE_BAR_W, STORAGE_BAR_H, theme->GetColour(ThemeEntryID_TEXT));
gfx::drawRect(vg, 860 + 1, 170 + 1, STORAGE_BAR_W - 2, STORAGE_BAR_H - 2, theme->GetColour(ThemeEntryID_BACKGROUND));
gfx::drawRect(vg, 860 + 2, 170 + 2, STORAGE_BAR_W - (((double)m_size_free_sd / (double)m_size_total_sd) * STORAGE_BAR_W) - 4, STORAGE_BAR_H - 4, theme->GetColour(ThemeEntryID_TEXT));
gfx::drawRect(vg, 30, 90, 375, 555, theme->GetColour(ThemeEntryID_GRID));
if (!m_entries.empty()) {
const auto& e = m_entries[m_entry_index];
const auto size = e.GetSize();
gfx::drawImage(vg, 90, 130, 256, 256, m_icon ? m_icon : App::GetDefaultImage());
nvgSave(vg);
nvgIntersectScissor(vg, 50, 90, 325, 555);
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", m_lang_entry.name);
gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", m_lang_entry.author);
gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "App-ID: 0%lX", e.app_id);
gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key-Gen: %u (%s)", e.key_gen, nca::GetKeyGenStr(e.key_gen));
gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Size: %.2f GB", (double)size / 0x40000000);
gfx::drawTextArgs(vg, 50, 615, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Base: %zu Patch: %zu Addon: %zu Data: %zu", e.application.size(), e.patch.size(), e.add_on.size(), e.data_patch.size());
nvgRestore(vg);
}
m_list->Draw(vg, theme, std::size(g_option_list), [this](auto* vg, auto* theme, auto v, auto i) {
const auto& [x, y, w, h] = v;
const auto text_y = y + (h / 2.f);
auto colour = ThemeEntryID_TEXT;
if (i == m_option_index) {
gfx::drawRectOutline(vg, theme, 4.f, v);
// g_background.selected_bar = create_shape(Colour_Nintendo_Cyan, 90, 230, 4, 45, true);
// draw_shape_position(&g_background.selected_bar, 485, g_options[i].text->rect.y - 10);
gfx::drawRect(vg, 490, text_y - 45.f / 2.f, 2, 45, theme->GetColour(ThemeEntryID_TEXT_SELECTED));
colour = ThemeEntryID_TEXT_SELECTED;
}
if (i != 2 && !m_mounted) {
colour = ThemeEntryID_TEXT_INFO;
}
gfx::drawTextArgs(vg, x + 15, y + (h / 2.f), 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(colour), "%s", g_option_list[i]);
});
}
void Menu::OnFocusGained() {
MenuBase::OnFocusGained();
GcOnEvent();
UpdateStorageSize();
}
Result Menu::GcMount() {
GcUnmount();
R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(m_handle)));
m_fs = std::make_unique<fs::FsNativeGameCard>(std::addressof(m_handle), FsGameCardPartition_Secure, false);
R_TRY(m_fs->GetFsOpenResult());
FsDir dir;
R_TRY(m_fs->OpenDirectory("/", FsDirOpenMode_ReadFiles, std::addressof(dir)));
ON_SCOPE_EXIT(fsDirClose(std::addressof(dir)));
s64 count;
R_TRY(m_fs->DirGetEntryCount(std::addressof(dir), std::addressof(count)));
std::vector<FsDirectoryEntry> buf(count);
s64 total_entries;
R_TRY(m_fs->DirRead(std::addressof(dir), std::addressof(total_entries), buf.size(), buf.data()));
R_UNLESS(buf.size() == total_entries, 0x1);
yati::container::Collections ticket_collections;
for (const auto& e : buf) {
if (!std::string_view(e.name).ends_with(".tik") && !std::string_view(e.name).ends_with(".cert")) {
continue;
}
ticket_collections.emplace_back(e.name, 0, e.file_size);
}
for (const auto& e : buf) {
// we could use ncm to handle finding all the ncas for us
// however, we can parse faster than ncm.
// not only that, the first few calls trying to mount ncm db for
// the gamecard will fail as it has not yet been parsed (or it's locked?).
// we could, of course, just wait until ncm is ready, which is about
// 32ms, but i already have code for manually parsing cnmt so lets re-use it.
if (!std::string_view(e.name).ends_with(".cnmt.nca")) {
continue;
}
// we don't yet use the header or extended header.
ncm::PackagedContentMeta header;
std::vector<u8> extended_header;
std::vector<NcmPackagedContentInfo> infos;
const auto path = BuildGcPath(e.name, &m_handle);
R_TRY(yati::ParseCnmtNca(path, 0, header, extended_header, infos));
u8 key_gen;
FsRightsId rights_id;
R_TRY(fsGetRightsIdAndKeyGenerationByPath(path, FsContentAttributes_All, &key_gen, &rights_id));
// always add tickets, yati will ignore them if not needed.
GcCollections collections;
// add cnmt file.
collections.emplace_back(e.name, e.file_size, NcmContentType_Meta, 0);
for (const auto& packed_info : infos) {
const auto& info = packed_info.info;
// these don't exist for gamecards, however i may copy/paste this code
// somewhere so i'm future proofing against myself.
if (info.content_type == NcmContentType_DeltaFragment) {
continue;
}
// find the nca file, this will never fail for gamecards, see above comment.
const auto str = hexIdToStr(info.content_id);
const auto it = std::find_if(buf.cbegin(), buf.cend(), [str](auto& e){
return !std::strncmp(str.str, e.name, std::strlen(str.str));
});
R_UNLESS(it != buf.cend(), yati::Result_NcaNotFound);
collections.emplace_back(it->name, it->file_size, info.content_type, info.id_offset);
}
const auto app_id = ncm::GetAppId(header);
ApplicationEntry* app_entry{};
for (auto& app : m_entries) {
if (app.app_id == app_id) {
app_entry = &app;
break;
}
}
if (!app_entry) {
app_entry = &m_entries.emplace_back(app_id, header.title_version);
}
app_entry->version = std::max(app_entry->version, header.title_version);
app_entry->key_gen = std::max(app_entry->key_gen, key_gen);
if (header.meta_type == NcmContentMetaType_Application) {
app_entry->application.emplace_back(collections);
} else if (header.meta_type == NcmContentMetaType_Patch) {
app_entry->patch.emplace_back(collections);
} else if (header.meta_type == NcmContentMetaType_AddOnContent) {
app_entry->add_on.emplace_back(collections);
} else if (header.meta_type == NcmContentMetaType_DataPatch) {
app_entry->data_patch.emplace_back(collections);
}
}
R_UNLESS(m_entries.size(), 0x1);
// append tickets to every application, yati will ignore if undeeded.
for (auto& e : m_entries) {
e.tickets = ticket_collections;
}
SetAction(Button::A, Action{"OK"_i18n, [this](){
if (m_option_index == 2) {
SetPop();
} else {
if (m_mounted) {
App::Push(std::make_shared<ui::ProgressBox>(m_icon, "Installing "_i18n, m_lang_entry.name, [this](auto pbox) mutable -> bool {
auto source = std::make_shared<GcSource>(m_entries[m_entry_index], m_fs.get(), m_option_index == 1);
return R_SUCCEEDED(yati::InstallFromCollections(pbox, source, source->m_collections, source->m_config));
}, [this](bool result){
if (result) {
App::Notify("Gc install success!"_i18n);
} else {
App::Notify("Gc install failed!"_i18n);
}
}));
}
}
}});
if (m_entries.size() > 1) {
SetAction(Button::L2, Action{"Prev"_i18n, [this](){
if (m_entry_index != 0) {
OnChangeIndex(m_entry_index - 1);
}
}});
SetAction(Button::R2, Action{"Next"_i18n, [this](){
if (m_entry_index < m_entries.size()) {
OnChangeIndex(m_entry_index + 1);
}
}});
}
OnChangeIndex(0);
m_mounted = true;
R_SUCCEED();
}
void Menu::GcUnmount() {
m_fs.reset();
m_entries.clear();
m_entry_index = 0;
m_mounted = false;
m_lang_entry = {};
FreeImage();
RemoveAction(Button::L2);
RemoveAction(Button::R2);
}
Result Menu::GcPoll(bool* inserted) {
R_TRY(fsDeviceOperatorIsGameCardInserted(&m_dev_op, inserted));
// if the handle changed, re-mount the game card.
if (*inserted && m_mounted) {
FsGameCardHandle handle;
R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(handle)));
if (handle.value != m_handle.value) {
R_TRY(GcMount());
}
}
R_SUCCEED();
}
Result Menu::GcOnEvent() {
bool inserted{};
R_TRY(GcPoll(&inserted));
if (m_mounted != inserted) {
log_write("gc state changed\n");
m_mounted = inserted;
if (m_mounted) {
log_write("trying to mount\n");
m_mounted = R_SUCCEEDED(GcMount());
if (m_mounted) {
App::PlaySoundEffect(SoundEffect::SoundEffect_Startup);
}
} else {
log_write("trying to unmount\n");
GcUnmount();
}
}
R_SUCCEED();
}
Result Menu::UpdateStorageSize() {
fs::FsNativeContentStorage fs_nand{FsContentStorageId_User};
fs::FsNativeContentStorage fs_sd{FsContentStorageId_SdCard};
R_TRY(fs_sd.GetFreeSpace("/", &m_size_free_sd));
R_TRY(fs_sd.GetTotalSpace("/", &m_size_total_sd));
R_TRY(fs_nand.GetFreeSpace("/", &m_size_free_nand));
R_TRY(fs_nand.GetTotalSpace("/", &m_size_total_nand));
R_SUCCEED();
}
void Menu::FreeImage() {
if (m_icon) {
nvgDeleteImage(App::GetVg(), m_icon);
m_icon = 0;
}
}
void Menu::OnChangeIndex(s64 new_index) {
FreeImage();
m_entry_index = new_index;
const auto index = m_entries.empty() ? 0 : m_entry_index + 1;
this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size()));
// nsGetApplicationControlData() will fail if it's the first time
// mounting a gamecard if the image is not already cached.
// waiting 1-2s after mount, then calling seems to work.
// however, we can just manually parse the nca to get the data we need,
// which always works and *is* faster too ;)
for (auto& e : m_entries[m_entry_index].application) {
for (auto& collection : e) {
if (collection.type == NcmContentType_Control) {
NacpStruct nacp;
std::vector<u8> icon;
const auto path = BuildGcPath(collection.name.c_str(), &m_handle);
u64 program_id = m_entries[m_entry_index].app_id | collection.id_offset;
if (hosversionAtLeast(17, 0, 0)) {
fsGetProgramId(&program_id, path, FsContentAttributes_All);
}
if (R_SUCCEEDED(yati::ParseControlNca(path, program_id, &nacp, sizeof(nacp), &icon))) {
log_write("managed to parse control nca %s\n", path.s);
NacpLanguageEntry* lang_entry{};
nacpGetLanguageEntry(&nacp, &lang_entry);
if (lang_entry) {
m_lang_entry = *lang_entry;
}
m_icon = nvgCreateImageMem(App::GetVg(), 0, icon.data(), icon.size());
if (m_icon > 0) {
return;
}
} else {
log_write("\tFAILED to parse control nca %s\n", path.s);
}
}
}
}
}
} // namespace sphaira::ui::menu::gc

View File

@@ -247,27 +247,6 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
fs::FsNativeSd().CreateDirectoryRecursively(CACHE_PATH);
this->SetActions(
std::make_pair(Button::DOWN, Action{[this](){
if (m_list->ScrollDown(m_index, 1, m_entries.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
if (m_list->ScrollUp(m_index, 1, m_entries.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::DPAD_RIGHT, Action{[this](){
if (m_list->ScrollDown(m_index, 8, m_entries.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::DPAD_LEFT, Action{[this](){
if (m_list->ScrollUp(m_index, 8, m_entries.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::A, Action{"Download"_i18n, [this](){
if (m_entries.empty()) {
return;
@@ -277,7 +256,7 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
static GhApiEntry gh_entry;
gh_entry = {};
App::Push(std::make_shared<ProgressBox>("Downloading "_i18n + GetEntry().repo, [this](auto pbox){
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, GetEntry().repo, [this](auto pbox){
return DownloadAssetJson(pbox, GenerateApiUrl(GetEntry()), gh_entry);
}, [this](bool success){
if (success) {
@@ -325,7 +304,7 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
}
const auto func = [this, &asset_entry, ptr](){
App::Push(std::make_shared<ProgressBox>("Downloading "_i18n + GetEntry().repo, [this, &asset_entry, ptr](auto pbox){
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, GetEntry().repo, [this, &asset_entry, ptr](auto pbox){
return DownloadApp(pbox, asset_entry, ptr);
}, [this, ptr](bool success){
if (success) {
@@ -373,8 +352,8 @@ Menu::~Menu() {
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_entries.size(), [this](auto i) {
if (m_index == i) {
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);

View File

@@ -24,44 +24,15 @@ auto GenerateStarPath(const fs::FsPath& nro_path) -> fs::FsPath {
return out;
}
void FreeEntry(NVGcontext* vg, NroEntry& e) {
nvgDeleteImage(vg, e.image);
e.image = 0;
}
} // namespace
Menu::Menu() : MenuBase{"Homebrew"_i18n} {
this->SetActions(
std::make_pair(Button::RIGHT, Action{[this](){
if (m_index < (m_entries.size() - 1) && (m_index + 1) % 3 != 0) {
SetIndex(m_index + 1);
App::PlaySoundEffect(SoundEffect_Scroll);
log_write("moved right\n");
}
}}),
std::make_pair(Button::LEFT, Action{[this](){
if (m_index != 0 && (m_index % 3) != 0) {
SetIndex(m_index - 1);
App::PlaySoundEffect(SoundEffect_Scroll);
log_write("moved left\n");
}
}}),
std::make_pair(Button::DOWN, Action{[this](){
if (m_list->ScrollDown(m_index, 3, m_entries.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
if (m_list->ScrollUp(m_index, 3, m_entries.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::R2, Action{[this](){
if (m_list->ScrollDown(m_index, 9, m_entries.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::L2, Action{[this](){
if (m_list->ScrollUp(m_index, 9, m_entries.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::A, Action{"Launch"_i18n, [this](){
nro_launch(m_entries[m_index].path);
}}),
@@ -98,7 +69,7 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
options->Add(std::make_shared<SidebarEntryBool>("Hide Sphaira"_i18n, m_hide_sphaira.Get(), [this](bool& enable){
m_hide_sphaira.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
}));
#if 0
@@ -114,11 +85,12 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
"Back"_i18n, "Delete"_i18n, 1, [this](auto op_index){
if (op_index && *op_index) {
if (R_SUCCEEDED(fs::FsNativeSd().DeleteFile(m_entries[m_index].path))) {
FreeEntry(App::GetVg(), m_entries[m_index]);
m_entries.erase(m_entries.begin() + m_index);
SetIndex(m_index ? m_index - 1 : 0);
}
}
}
}, m_entries[m_index].image
));
}, true));
@@ -131,7 +103,7 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
if (op_index && *op_index) {
InstallHomebrew();
}
}
}, m_entries[m_index].image
));
} else {
InstallHomebrew();
@@ -148,17 +120,13 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
}
Menu::~Menu() {
auto vg = App::GetVg();
for (auto&p : m_entries) {
nvgDeleteImage(vg, p.image);
}
FreeEntries();
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_entries.size(), [this](auto i) {
if (m_index == i) {
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
@@ -194,7 +162,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
}
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 {
@@ -202,25 +171,23 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
}
const float image_size = 115;
gfx::drawImageRounded(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage());
gfx::drawImage(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage(), 5);
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();
const auto text_off = 148;
const auto text_x = x + text_off;
const auto text_clip_w = w - 30.f - text_off;
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);
const float font_size = 18;
m_scroll_name.DrawArgs(vg, selected, text_x, y + 45, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), "%s%s", has_star ? "\u2605 " : "", e.GetName());
m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetAuthor());
m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetDisplayVersion());
});
}
@@ -272,6 +239,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());
@@ -428,6 +396,16 @@ void Menu::SortAndFindLastFile() {
}
}
void Menu::FreeEntries() {
auto vg = App::GetVg();
for (auto&p : m_entries) {
FreeEntry(vg, p);
}
m_entries.clear();
}
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

@@ -1,8 +1,4 @@
#include "ui/menus/main_menu.hpp"
#include "ui/menus/irs_menu.hpp"
#include "ui/menus/themezer.hpp"
#include "ui/menus/ghdl.hpp"
#include "ui/menus/usb_menu.hpp"
#include "ui/sidebar.hpp"
#include "ui/popup_list.hpp"
@@ -10,11 +6,19 @@
#include "ui/progress_box.hpp"
#include "ui/error_box.hpp"
#include "ui/menus/irs_menu.hpp"
#include "ui/menus/themezer.hpp"
#include "ui/menus/ghdl.hpp"
#include "ui/menus/usb_menu.hpp"
#include "ui/menus/ftp_menu.hpp"
#include "ui/menus/gc_menu.hpp"
#include "ui/menus/game_menu.hpp"
#include "ui/menus/appstore.hpp"
#include "app.hpp"
#include "log.hpp"
#include "download.hpp"
#include "defines.hpp"
#include "web.hpp"
#include "i18n.hpp"
#include <cstring>
@@ -27,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
@@ -144,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},
@@ -213,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));
@@ -233,48 +270,29 @@ MainMenu::MainMenu() {
language_items.push_back("Swedish"_i18n);
language_items.push_back("Vietnamese"_i18n);
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [this](){
SidebarEntryArray::Items theme_items{};
const auto theme_meta = App::GetThemeMetaList();
for (auto& p : theme_meta) {
theme_items.emplace_back(p.name);
}
auto options = std::make_shared<Sidebar>("Theme Options"_i18n, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<SidebarEntryArray>("Select Theme"_i18n, theme_items, [this, theme_items](s64& index_out){
App::SetTheme(index_out);
}, App::GetThemeIndex()));
options->Add(std::make_shared<SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [this](bool& enable){
App::SetThemeMusicEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [this](bool& enable){
App::Set12HourTimeEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [](){
App::DisplayThemeOptions();
}));
options->Add(std::make_shared<SidebarEntryCallback>("Network"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Network Options"_i18n, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<SidebarEntryBool>("Ftp"_i18n, App::GetFtpEnable(), [this](bool& enable){
options->Add(std::make_shared<SidebarEntryBool>("Ftp"_i18n, App::GetFtpEnable(), [](bool& enable){
App::SetFtpEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<SidebarEntryBool>("Mtp"_i18n, App::GetMtpEnable(), [this](bool& enable){
options->Add(std::make_shared<SidebarEntryBool>("Mtp"_i18n, App::GetMtpEnable(), [](bool& enable){
App::SetMtpEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<SidebarEntryBool>("Nxlink"_i18n, App::GetNxlinkEnable(), [this](bool& enable){
options->Add(std::make_shared<SidebarEntryBool>("Nxlink"_i18n, App::GetNxlinkEnable(), [](bool& enable){
App::SetNxlinkEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
if (m_update_state == UpdateState::Update) {
options->Add(std::make_shared<SidebarEntryCallback>("Download update: "_i18n + m_update_version, [this](){
App::Push(std::make_shared<ProgressBox>("Downloading "_i18n + m_update_version, [this](auto pbox){
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, "Sphaira v" + m_update_version, [this](auto pbox){
return InstallUpdate(pbox, m_update_url, m_update_version);
}, [this](bool success){
if (success) {
@@ -293,151 +311,23 @@ MainMenu::MainMenu() {
}
}));
options->Add(std::make_shared<SidebarEntryArray>("Language"_i18n, language_items, [this](s64& index_out){
options->Add(std::make_shared<SidebarEntryArray>("Language"_i18n, language_items, [](s64& index_out){
App::SetLanguage(index_out);
}, (s64)App::GetLanguage()));
options->Add(std::make_shared<SidebarEntryCallback>("Misc"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Misc Options"_i18n, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<SidebarEntryCallback>("Themezer"_i18n, [](){
App::Push(std::make_shared<menu::themezer::Menu>());
}));
options->Add(std::make_shared<SidebarEntryCallback>("GitHub"_i18n, [](){
App::Push(std::make_shared<menu::gh::Menu>());
}));
options->Add(std::make_shared<SidebarEntryCallback>("Irs"_i18n, [](){
App::Push(std::make_shared<menu::irs::Menu>());
}));
if (App::IsApplication()) {
options->Add(std::make_shared<SidebarEntryCallback>("Web"_i18n, [](){
WebShow("https://lite.duckduckgo.com/lite");
}));
}
if (App::GetApp()->m_install.Get()) {
options->Add(std::make_shared<SidebarEntryCallback>("Usb Install"_i18n, [](){
App::Push(std::make_shared<menu::usb::Menu>());
}));
}
options->Add(std::make_shared<SidebarEntryCallback>("Misc"_i18n, [](){
App::DisplayMiscOptions();
}));
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(options));
SidebarEntryArray::Items text_scroll_speed_items;
text_scroll_speed_items.push_back("Slow"_i18n);
text_scroll_speed_items.push_back("Normal"_i18n);
text_scroll_speed_items.push_back("Fast"_i18n);
options->Add(std::make_shared<SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [this](bool& enable){
App::SetLogEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [this](bool& enable){
App::SetReplaceHbmenuEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [this](s64& index_out){
App::SetTextScrollSpeed(index_out);
}, (s64)App::GetTextScrollSpeed()));
options->Add(std::make_shared<SidebarEntryCallback>("Install options"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Install Options"_i18n, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(options));
SidebarEntryArray::Items install_items;
install_items.push_back("System memory"_i18n);
install_items.push_back("microSD card"_i18n);
options->Add(std::make_shared<SidebarEntryBool>("Enable"_i18n, App::GetInstallEnable(), [this](bool& enable){
App::SetInstallEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [this](bool& enable){
App::SetInstallPrompt(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryArray>("Install location"_i18n, install_items, [this](s64& index_out){
App::SetInstallSdEnable(index_out);
}, (s64)App::GetInstallSdEnable()));
options->Add(std::make_shared<SidebarEntryBool>("Allow downgrade"_i18n, App::GetApp()->m_allow_downgrade.Get(), [this](bool& enable){
App::GetApp()->m_allow_downgrade.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip if already installed"_i18n, App::GetApp()->m_skip_if_already_installed.Get(), [this](bool& enable){
App::GetApp()->m_skip_if_already_installed.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Ticket only"_i18n, App::GetApp()->m_ticket_only.Get(), [this](bool& enable){
App::GetApp()->m_ticket_only.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Patch ticket"_i18n, App::GetApp()->m_patch_ticket.Get(), [this](bool& enable){
App::GetApp()->m_patch_ticket.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip base"_i18n, App::GetApp()->m_skip_base.Get(), [this](bool& enable){
App::GetApp()->m_skip_base.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip Patch"_i18n, App::GetApp()->m_skip_patch.Get(), [this](bool& enable){
App::GetApp()->m_skip_patch.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip addon"_i18n, App::GetApp()->m_skip_addon.Get(), [this](bool& enable){
App::GetApp()->m_skip_addon.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip data patch"_i18n, App::GetApp()->m_skip_data_patch.Get(), [this](bool& enable){
App::GetApp()->m_skip_data_patch.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip ticket"_i18n, App::GetApp()->m_skip_ticket.Get(), [this](bool& enable){
App::GetApp()->m_skip_ticket.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [this](bool& enable){
App::GetApp()->m_skip_nca_hash_verify.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip RSA header verify"_i18n, App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(), [this](bool& enable){
App::GetApp()->m_skip_rsa_header_fixed_key_verify.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Skip RSA NPDM verify"_i18n, App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(), [this](bool& enable){
App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Ignore distribution bit"_i18n, App::GetApp()->m_ignore_distribution_bit.Get(), [this](bool& enable){
App::GetApp()->m_ignore_distribution_bit.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [this](bool& enable){
App::GetApp()->m_convert_to_standard_crypto.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Lower master key"_i18n, App::GetApp()->m_lower_master_key.Get(), [this](bool& enable){
App::GetApp()->m_lower_master_key.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryBool>("Lower system version"_i18n, App::GetApp()->m_lower_system_version.Get(), [this](bool& enable){
App::GetApp()->m_lower_system_version.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n));
}));
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [](){
App::DisplayAdvancedOptions();
}));
}})
);
m_homebrew_menu = std::make_shared<homebrew::Menu>();
m_filebrowser_menu = std::make_shared<filebrowser::Menu>(m_homebrew_menu->GetHomebrewList());
m_app_store_menu = std::make_shared<appstore::Menu>(m_homebrew_menu->GetHomebrewList());
m_right_side_menu = CreateRightSideMenu();
m_current_menu = m_homebrew_menu;
AddOnLRPress();
@@ -488,16 +378,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

@@ -18,17 +18,18 @@ MenuBase::~MenuBase() {
void MenuBase::Update(Controller* controller, TouchInfo* touch) {
Widget::Update(controller, touch);
// update every second.
if (m_poll_timestamp.GetSeconds() >= 1) {
UpdateVars();
}
}
void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
DrawElement(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, ThemeEntryID_BACKGROUND);
Widget::Draw(vg, theme);
// update every second, do this in Draw because Update() isn't called if it
// doesn't have focus.
if (m_poll_timestamp.GetSeconds() >= 1) {
UpdateVars();
}
const float start_y = 70;
const float font_size = 22;
const float spacing = 30;

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) {
@@ -375,45 +376,6 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
}});
this->SetActions(
std::make_pair(Button::RIGHT, Action{[this](){
const auto& page = m_pages[m_page_index];
if (m_index < (page.m_packList.size() - 1) && (m_index + 1) % 3 != 0) {
SetIndex(m_index + 1);
App::PlaySoundEffect(SoundEffect_Scroll);
log_write("moved right\n");
}
}}),
std::make_pair(Button::LEFT, Action{[this](){
if (m_index != 0 && (m_index % 3) != 0) {
SetIndex(m_index - 1);
App::PlaySoundEffect(SoundEffect_Scroll);
log_write("moved left\n");
}
}}),
std::make_pair(Button::DOWN, Action{[this](){
const auto& page = m_pages[m_page_index];
if (m_list->ScrollDown(m_index, 3, page.m_packList.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
const auto& page = m_pages[m_page_index];
if (m_list->ScrollUp(m_index, 3, page.m_packList.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::R2, Action{[this](){
const auto& page = m_pages[m_page_index];
if (m_list->ScrollDown(m_index, 6, page.m_packList.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::L2, Action{[this](){
const auto& page = m_pages[m_page_index];
if (m_list->ScrollUp(m_index, 6, page.m_packList.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::A, Action{"Download"_i18n, [this](){
App::Push(std::make_shared<OptionBox>(
"Download theme?"_i18n,
@@ -424,7 +386,7 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
const auto& entry = page.m_packList[m_index];
const auto url = apiBuildUrlDownloadPack(entry);
App::Push(std::make_shared<ProgressBox>("Installing "_i18n + entry.details.name, [this, &entry](auto pbox){
App::Push(std::make_shared<ProgressBox>(entry.themes[0].preview.lazy_image.image, "Downloading "_i18n, entry.details.name, [this, &entry](auto pbox){
return InstallTheme(pbox, entry);
}, [this, &entry](bool success){
if (success) {
@@ -453,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) {
@@ -491,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;
@@ -499,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();
@@ -532,8 +494,8 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
return;
}
m_list->OnUpdate(controller, touch, page.m_packList.size(), [this](auto i) {
if (m_index == i) {
m_list->OnUpdate(controller, touch, m_index, page.m_packList.size(), [this](bool touch, auto i) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
@@ -575,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 {
@@ -642,16 +605,14 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
}
}
gfx::drawImageRounded(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage());
gfx::drawImage(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage(), 5);
}
nvgSave(vg);
nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip
{
gfx::drawTextArgs(vg, x + xoff, y + 180 + 20, 18, NVG_ALIGN_LEFT, theme->GetColour(text_id), "%s", e.details.name.c_str());
gfx::drawTextArgs(vg, x + xoff, y + 180 + 55, 18, NVG_ALIGN_LEFT, theme->GetColour(text_id), "%s", e.creator.display_name.c_str());
}
nvgRestore(vg);
const auto text_x = x + xoff;
const auto text_clip_w = w - 30.f - xoff;
const float font_size = 18;
m_scroll_name.Draw(vg, selected, text_x, y + 180 + 20, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.details.name.c_str());
m_scroll_author.Draw(vg, selected, text_x, y + 180 + 55, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.creator.display_name.c_str());
});
}

View File

@@ -10,8 +10,8 @@
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;
void thread_func(void* user) {
auto app = static_cast<Menu*>(user);
@@ -21,17 +21,26 @@ 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);
// 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;
}
}
}
}
@@ -43,6 +52,10 @@ Menu::Menu() : MenuBase{"USB"_i18n} {
SetPop();
}});
SetAction(Button::X, Action{"Options"_i18n, [this](){
App::DisplayInstallOptions(false);
}});
// if mtp is enabled, disable it for now.
m_was_mtp_enabled = App::GetMtpEnable();
if (m_was_mtp_enabled) {
@@ -63,6 +76,7 @@ Menu::Menu() : MenuBase{"USB"_i18n} {
}
mutexInit(&m_mutex);
if (m_state != State::Failed) {
threadCreate(&m_thread, thread_func, this, nullptr, 1024*32, 0x2C, 1);
threadStart(&m_thread);
@@ -72,6 +86,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);
@@ -91,46 +106,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());
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>("Installing App"_i18n, [this](auto pbox) mutable -> bool {
log_write("inside progress box\n");
for (u32 i = 0; i < m_usb_count; i++) {
const auto rc = yati::InstallFromSource(pbox, m_usb_source);
if (R_FAILED(rc)) {
return false;
}
log_write("inside progress box\n");
for (const auto& file_name : m_names) {
m_usb_source->SetFileNameForTranfser(file_name);
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);
} else {
App::Notify("Usb install failed!"_i18n);
}
App::Notify("Installed via usb"_i18n);
}
return true;
}, [this](bool result){
if (result) {
App::Notify("Usb install success!"_i18n);
m_state = State::Done;
this->SetPop();
}));
break;
case State::Progress:
break;
case State::Done:
break;
case State::Failed:
break;
SetPop();
} else {
App::Notify("Usb install failed!"_i18n);
m_state = State::Failed;
}
}));
}
}
@@ -145,7 +152,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:
@@ -153,12 +165,11 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
break;
case State::Done:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Press B to Exit..."_i18n.c_str());
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Press B to exit..."_i18n.c_str());
break;
case State::Failed:
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to init usb..."_i18n.c_str());
this->SetPop();
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to init usb, press B to exit..."_i18n.c_str());
break;
}
}

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,25 +35,42 @@ constexpr std::array buttons = {
std::pair{Button::R3, "\uE105"},
};
// NEW ---------------------
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, bool rounded) {
nvgBeginPath(vg);
if (rounded) {
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
} else {
nvgRect(vg, v.x, v.y, v.w, v.h);
// software based clipping, saves a few cpu cycles.
bool ClipRect(float x, float y) {
return x >= SCREEN_WIDTH || y >= SCREEN_HEIGHT;
}
bool ClipText(float x, float y, int align) {
if ((!(align & ALIGN_HOR) || (align & NVG_ALIGN_LEFT)) && x >= SCREEN_WIDTH) {
return true;
}
if ((!(align & ALIGN_VER) || (align & NVG_ALIGN_TOP)) && y >= SCREEN_HEIGHT) {
return true;
}
return false;
}
// NEW ---------------------
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) {
if (ClipRect(v.x, v.y)) {
return;
}
nvgBeginPath(vg);
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
nvgFillColor(vg, c);
nvgFill(vg);
}
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGpaint& p, bool rounded) {
nvgBeginPath(vg);
if (rounded) {
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
} else {
nvgRect(vg, v.x, v.y, v.w, v.h);
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) {
if (ClipRect(v.x, v.y)) {
return;
}
nvgBeginPath(vg);
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
nvgFillPaint(vg, p);
nvgFill(vg);
}
@@ -128,6 +147,10 @@ void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, con
}
void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, const Vec4& v, const NVGcolor& c) {
if (ClipRect(v.x, v.y)) {
return;
}
const auto corner_radius = 0.5;
drawRectOutlineInternal(vg, theme, size, v);
nvgBeginPath(vg);
@@ -137,6 +160,10 @@ void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, con
}
void drawTextIntenal(NVGcontext* vg, const Vec2& v, float size, const char* str, const char* end, int align, const NVGcolor& c) {
if (ClipText(v.x, v.y, align)) {
return;
}
nvgBeginPath(vg);
nvgFontSize(vg, size);
nvgTextAlign(vg, align);
@@ -164,28 +191,20 @@ void drawTextArgs(NVGcontext* vg, float x, float y, float size, int align, const
drawText(vg, x, y, size, buffer, nullptr, align, c);
}
void drawImage(NVGcontext* vg, const Vec4& v, int texture) {
void drawImage(NVGcontext* vg, const Vec4& v, int texture, float rounded) {
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
drawRect(vg, v, paint, false);
drawRect(vg, v, paint, rounded);
}
void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture) {
drawImage(vg, Vec4(x, y, w, h), texture);
}
void drawImageRounded(NVGcontext* vg, const Vec4& v, int texture) {
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
nvgBeginPath(vg);
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
nvgFillPaint(vg, paint);
nvgFill(vg);
}
void drawImageRounded(NVGcontext* vg, float x, float y, float w, float h, int texture) {
drawImageRounded(vg, Vec4(x, y, w, h), texture);
void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture, float rounded) {
drawImage(vg, Vec4(x, y, w, h), texture, rounded);
}
void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, const NVGcolor& c, const char* str, int align, const char* end) {
if (ClipText(x, y, align)) {
return;
}
nvgBeginPath(vg);
nvgFontSize(vg, size);
nvgTextAlign(vg, align);
@@ -208,19 +227,19 @@ void dimBackground(NVGcontext* vg) {
drawRectIntenal(vg, {0.f,0.f,SCREEN_WIDTH,SCREEN_HEIGHT}, nvgRGBA(0, 0, 0, 180), false);
}
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, bool rounded) {
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, float rounded) {
drawRectIntenal(vg, {x,y,w,h}, c, rounded);
}
void drawRect(NVGcontext* vg, const Vec4& v, const NVGcolor& c, bool rounded) {
void drawRect(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) {
drawRectIntenal(vg, v, c, rounded);
}
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint& p, bool rounded) {
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint& p, float rounded) {
drawRectIntenal(vg, {x,y,w,h}, p, rounded);
}
void drawRect(NVGcontext* vg, const Vec4& v, const NVGpaint& p, bool rounded) {
void drawRect(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) {
drawRectIntenal(vg, v, p, rounded);
}

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

@@ -62,16 +62,6 @@ PopupList::PopupList(std::string title, Items items, Callback cb, s64 index)
, m_callback{cb}
, m_index{index} {
this->SetActions(
std::make_pair(Button::DOWN, Action{[this](){
if (m_list->ScrollDown(m_index, 1, m_items.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
if (m_list->ScrollUp(m_index, 1, m_items.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::A, Action{"Select"_i18n, [this](){
if (m_callback) {
m_callback(m_index);
@@ -83,6 +73,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;
@@ -103,9 +95,11 @@ PopupList::PopupList(std::string title, Items items, Callback cb, s64 index)
auto PopupList::Update(Controller* controller, TouchInfo* touch) -> void {
Widget::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_items.size(), [this](auto i) {
m_list->OnUpdate(controller, touch, m_index, m_items.size(), [this](bool touch, auto i) {
SetIndex(i);
FireAction(Button::A);
if (touch) {
FireAction(Button::A);
}
});
}
@@ -118,15 +112,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

@@ -5,6 +5,7 @@
#include "defines.hpp"
#include "log.hpp"
#include "i18n.hpp"
#include <cstring>
namespace sphaira::ui {
namespace {
@@ -17,7 +18,7 @@ void threadFunc(void* arg) {
} // namespace
ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback, ProgressBoxDoneCallback done, int cpuid, int prio, int stack_size) {
ProgressBox::ProgressBox(int image, const std::string& action, const std::string& title, ProgressBoxCallback callback, ProgressBoxDoneCallback done, int cpuid, int prio, int stack_size) {
SetAction(Button::B, Action{"Back"_i18n, [this](){
App::Push(std::make_shared<OptionBox>("Are you sure you wish to cancel?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){
if (op_index && *op_index) {
@@ -27,11 +28,6 @@ ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback,
}));
}});
m_pos.w = 770.f;
m_pos.h = 430.f;
m_pos.x = 255;
m_pos.y = 145;
m_pos.w = 770.f;
m_pos.h = 295.f;
m_pos.x = (SCREEN_WIDTH / 2.f) - (m_pos.w / 2.f);
@@ -39,6 +35,8 @@ ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback,
m_done = done;
m_title = title;
m_action = action;
m_image = image;
m_thread_data.pbox = this;
m_thread_data.callback = callback;
@@ -60,6 +58,7 @@ ProgressBox::~ProgressBox() {
log_write("failed to close thread\n");
}
FreeImage();
m_done(m_thread_data.result);
}
@@ -73,33 +72,91 @@ auto ProgressBox::Update(Controller* controller, TouchInfo* touch) -> void {
auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
mutexLock(&m_mutex);
std::vector<u8> image_data{};
std::swap(m_image_data, image_data);
if (m_timestamp.GetSeconds()) {
m_timestamp.Update();
m_speed = m_offset - m_last_offset;
m_last_offset = m_offset;
}
const auto title = m_title;
const auto transfer = m_transfer;
const auto size = m_size;
const auto offset = m_offset;
const auto speed = m_speed;
const auto last_offset = m_last_offset;
mutexUnlock(&m_mutex);
if (!image_data.empty()) {
FreeImage();
m_image = nvgCreateImageMem(vg, 0, image_data.data(), image_data.size());
m_own_image = true;
}
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 };
const auto center_x = m_pos.x + m_pos.w/2;
const auto end_y = m_pos.y + m_pos.h;
const Vec4 prog_bar = { 400, end_y - 80, 480, 12 };
const auto progress_bar_w = m_pos.w - 230;
const Vec4 prog_bar = { center_x - progress_bar_w / 2, end_y - 100, progress_bar_w, 12 };
nvgSave(vg);
nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH());
if (m_image) {
gfx::drawImage(vg, GetX() + 30, GetY() + 30, 128, 128, m_image, 10);
}
// shapes.
if (offset && size) {
gfx::drawRect(vg, prog_bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND));
const auto font_size = 18.F;
const auto pad = 15.F;
const float rounding = 5;
gfx::drawRect(vg, prog_bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND), rounding);
const u32 percentage = ((double)offset / (double)size) * 100.0;
gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((float)offset / (float)size) * prog_bar.w, prog_bar.h, theme->GetColour(ThemeEntryID_PROGRESSBAR));
gfx::drawTextArgs(vg, prog_bar.x + prog_bar.w + 10, prog_bar.y, 20, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%u%%", percentage);
gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((float)offset / (float)size) * prog_bar.w, prog_bar.h, theme->GetColour(ThemeEntryID_PROGRESSBAR), rounding);
gfx::drawTextArgs(vg, prog_bar.x + prog_bar.w + pad, prog_bar.y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%u%%", percentage);
const double speed_mb = (double)speed / (1024.0 * 1024.0);
const double speed_kb = (double)speed / (1024.0);
char speed_str[32];
if (speed_mb >= 0.01) {
std::snprintf(speed_str, sizeof(speed_str), "%.2f MiB/s", speed_mb);
} else {
std::snprintf(speed_str, sizeof(speed_str), "%.2f KiB/s", speed_kb);
}
const auto left = size - last_offset;
const auto left_seconds = left / speed;
const auto hours = left_seconds / (60 * 60);
const auto minutes = left_seconds % (60 * 60) / 60;
const auto seconds = left_seconds % 60;
char time_str[64];
if (hours) {
std::snprintf(time_str, sizeof(time_str), "%zu hours %zu minutes remaining", hours, minutes);
} else if (minutes) {
std::snprintf(time_str, sizeof(time_str), "%zu minutes %zu seconds remaining", minutes, seconds);
} else {
std::snprintf(time_str, sizeof(time_str), "%zu seconds remaining", seconds);
}
gfx::drawTextArgs(vg, center_x, prog_bar.y + prog_bar.h + 30, 18, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s (%s)", time_str, speed_str);
}
gfx::drawTextArgs(vg, center_x, m_pos.y + 60, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), title.c_str());
gfx::drawTextArgs(vg, center_x, m_pos.y + 40, 24, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), m_action.c_str());
gfx::drawTextArgs(vg, center_x, m_pos.y + 100, 22, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), title.c_str());
if (!transfer.empty()) {
gfx::drawTextArgs(vg, center_x, prog_bar.y - 15 - 20 * 1.5F, 20, NVG_ALIGN_CENTER, theme->GetColour(ThemeEntryID_TEXT), "%s", transfer.c_str());
gfx::drawTextArgs(vg, center_x, m_pos.y + 150, 18, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", transfer.c_str());
}
nvgRestore(vg);
}
auto ProgressBox::SetTitle(const std::string& title) -> ProgressBox& {
@@ -115,6 +172,8 @@ auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
m_transfer = transfer;
m_size = 0;
m_offset = 0;
m_last_offset = 0;
m_timestamp.Update();
mutexUnlock(&m_mutex);
Yield();
return *this;
@@ -129,6 +188,21 @@ auto ProgressBox::UpdateTransfer(s64 offset, s64 size) -> ProgressBox& {
return *this;
}
auto ProgressBox::SetImageData(std::vector<u8>& data) -> ProgressBox& {
mutexLock(&m_mutex);
std::swap(m_image_data, data);
mutexUnlock(&m_mutex);
return *this;
}
auto ProgressBox::SetImageDataConst(std::span<const u8> data) -> ProgressBox& {
mutexLock(&m_mutex);
m_image_data.resize(data.size());
std::memcpy(m_image_data.data(), data.data(), m_image_data.size());
mutexUnlock(&m_mutex);
return *this;
}
void ProgressBox::RequestExit() {
m_stop_source.request_stop();
}
@@ -184,4 +258,13 @@ void ProgressBox::Yield() {
svcSleepThread(YieldType_WithoutCoreMigration);
}
void ProgressBox::FreeImage() {
if (m_image && m_own_image) {
nvgDeleteImage(App::GetVg(), m_image);
}
m_image = 0;
m_own_image = false;
}
} // namespace sphaira::ui

View File

@@ -0,0 +1,81 @@
#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) {
m_str = text_entry;
m_tick = 0;
m_text_xoff = 0;
}
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);
}
} // namespace sphaira::ui

View File

@@ -43,6 +43,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);
@@ -259,9 +266,11 @@ auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void {
if (touch->is_clicked && !touch->in_range(GetPos())) {
App::PopToMenu();
} else {
m_list->OnUpdate(controller, touch, m_items.size(), [this](auto i) {
m_list->OnUpdate(controller, touch, m_index, m_items.size(), [this](bool touch, auto i) {
SetIndex(i);
FireAction(Button::A);
if (touch) {
FireAction(Button::A);
}
});
}
@@ -334,18 +343,6 @@ void Sidebar::SetupButtons() {
// add default actions, overriding if needed.
this->SetActions(
std::make_pair(Button::DOWN, Action{[this](){
auto index = m_index;
if (m_list->ScrollDown(index, 1, m_items.size())) {
SetIndex(index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
auto index = m_index;
if (m_list->ScrollUp(index, 1, m_items.size())) {
SetIndex(index);
}
}}),
// each item has it's own Action, but we take over B
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
SetPop();

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

@@ -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

@@ -24,14 +24,6 @@ struct Pfs0FileTableEntry {
} // namespace
Result Nsp::Validate(source::Base* source) {
u32 magic;
u64 bytes_read;
R_TRY(source->Read(std::addressof(magic), 0, sizeof(magic), std::addressof(bytes_read)));
R_UNLESS(magic == PFS0_MAGIC, 0x1);
R_SUCCEED();
}
Result Nsp::GetCollections(Collections& out) {
u64 bytes_read;
s64 off = 0;

View File

@@ -60,22 +60,14 @@ Result Hfs0GetPartition(source::Base* source, s64 off, Hfs0& out) {
} // namespace
Result Xci::Validate(source::Base* source) {
u32 magic;
u64 bytes_read;
R_TRY(source->Read(std::addressof(magic), 0x100, sizeof(magic), std::addressof(bytes_read)));
R_UNLESS(magic == XCI_MAGIC, 0x1);
R_SUCCEED();
}
Result Xci::GetCollections(Collections& out) {
Hfs0 root{};
R_TRY(Hfs0GetPartition(m_source, HFS0_HEADER_OFFSET, root));
R_TRY(Hfs0GetPartition(m_source.get(), HFS0_HEADER_OFFSET, root));
for (u32 i = 0; i < root.header.total_files; i++) {
if (root.string_table[i] == "secure") {
Hfs0 secure{};
R_TRY(Hfs0GetPartition(m_source, root.data_offset + root.file_table[i].data_offset, secure));
R_TRY(Hfs0GetPartition(m_source.get(), root.data_offset + root.file_table[i].data_offset, secure));
for (u32 i = 0; i < secure.header.total_files; i++) {
CollectionEntry entry;

View File

@@ -106,13 +106,13 @@ Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys)
// todo: i thought i already wrote the code for this??
// todo: patch the ticket.
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys, bool convert_personalised) {
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys) {
TicketData data;
R_TRY(GetTicketData(ticket, &data));
if (data.title_key_type == es::TicketTitleKeyType_Common) {
// todo: verify common signature
} else if (data.title_key_type == es::TicketTitleKeyType_Personalized && convert_personalised) {
} else if (data.title_key_type == es::TicketTitleKeyType_Personalized) {
}

View File

@@ -151,4 +151,31 @@ Result VerifyFixedKey(const Header& header) {
R_SUCCEED();
}
auto GetKeyGenStr(u8 key_gen) -> const char* {
switch (key_gen) {
case KeyGenerationOld_100: return "1.0.0";
case KeyGenerationOld_300: return "3.0.0";
case KeyGeneration_301: return "3.0.1";
case KeyGeneration_400: return "4.0.0";
case KeyGeneration_500: return "5.0.0";
case KeyGeneration_600: return "6.0.0";
case KeyGeneration_620: return "6.2.0";
case KeyGeneration_700: return "7.0.0";
case KeyGeneration_810: return "8.1.0";
case KeyGeneration_900: return "9.0.0";
case KeyGeneration_910: return "9.1.0";
case KeyGeneration_1210: return "12.1.0";
case KeyGeneration_1300: return "13.0.0";
case KeyGeneration_1400: return "14.0.0";
case KeyGeneration_1500: return "15.0.0";
case KeyGeneration_1600: return "16.0.0";
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";
}
} // namespace sphaira::nca

View File

@@ -7,14 +7,54 @@ namespace {
} // namespace
auto GetAppId(const NcmContentMetaKey& key) -> u64 {
if (key.type == NcmContentMetaType_Patch) {
return key.id ^ 0x800;
} else if (key.type == NcmContentMetaType_AddOnContent) {
return (key.id ^ 0x1000) & ~0xFFF;
} else {
return key.id;
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";
}
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;
} else if (meta_type == NcmContentMetaType_AddOnContent) {
return (id ^ 0x1000) & ~0xFFF;
} else {
return id;
}
}
auto GetAppId(const NcmContentMetaKey& key) -> u64 {
return GetAppId(key.type, key.id);
}
auto GetAppId(const PackagedContentMeta& meta) -> u64 {
return GetAppId(meta.meta_type, meta.title_id);
}
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id) {

View File

@@ -2,7 +2,7 @@
namespace sphaira::yati::source {
Stdio::Stdio(const char* path) {
Stdio::Stdio(const fs::FsPath& path) {
m_file = std::fopen(path, "rb");
if (!m_file) {
m_open_result = fsdevGetLastResult();

View File

@@ -0,0 +1,41 @@
#include "yati/source/stream.hpp"
#include "defines.hpp"
#include "log.hpp"
namespace sphaira::yati::source {
Result Stream::Read(void* _buf, s64 off, s64 size, u64* bytes_read_out) {
// streams don't allow for random access (seeking backwards).
R_UNLESS(off >= m_offset, 0x1);
auto buf = static_cast<u8*>(_buf);
*bytes_read_out = 0;
// check if we already have some data in the buffer.
while (size) {
// while it is invalid to seek backwards, it is valid to seek forwards.
// this can be done to skip padding, skip undeeded files etc.
// to handle this, simply read the data into a buffer and discard it.
if (off > m_offset) {
const auto skip_size = off - m_offset;
std::vector<u8> temp_buf(skip_size);
u64 bytes_read;
R_TRY(ReadChunk(temp_buf.data(), temp_buf.size(), &bytes_read));
m_offset += bytes_read;
} else {
u64 bytes_read;
R_TRY(ReadChunk(buf, size, &bytes_read));
*bytes_read_out += bytes_read;
buf += bytes_read;
off += bytes_read;
m_offset += bytes_read;
size -= bytes_read;
}
}
R_SUCCEED();
}
} // namespace sphaira::yati::source

View File

@@ -0,0 +1,23 @@
#include "yati/source/stream_file.hpp"
#include "log.hpp"
namespace sphaira::yati::source {
StreamFile::StreamFile(FsFileSystem* fs, const fs::FsPath& path) {
m_open_result = fsFsOpenFile(fs, path, FsOpenMode_Read, std::addressof(m_file));
}
StreamFile::~StreamFile() {
if (R_SUCCEEDED(GetOpenResult())) {
fsFileClose(std::addressof(m_file));
}
}
Result StreamFile::ReadChunk(void* buf, s64 size, u64* bytes_read) {
R_TRY(GetOpenResult());
const auto rc = fsFileRead(std::addressof(m_file), m_offset, buf, size, 0, bytes_read);
m_offset += *bytes_read;
return rc;
}
} // namespace sphaira::yati::source

View File

@@ -14,33 +14,59 @@
* 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 "log.hpp"
#include <ranges>
namespace sphaira::yati::source {
namespace {
constexpr u32 MAGIC = 0x53504841;
constexpr u32 VERSION = 1;
struct SendHeader {
u32 magic;
u32 version;
enum USBCmdType : u8 {
REQUEST = 0,
RESPONSE = 1
};
struct RecvHeader {
u32 magic;
u32 version;
u32 bcdUSB;
u32 count;
enum USBCmdId : u32 {
EXIT = 0,
FILE_RANGE = 1
};
struct NX_PACKED USBCmdHeader {
u32 magic;
USBCmdType type;
u8 padding[0x3] = {0};
u32 cmdId;
u64 dataSize;
u8 reserved[0xC] = {0};
};
struct FileRangeCmdHeader {
u64 size;
u64 offset;
u64 nspNameLen;
u64 padding;
};
struct TUSHeader {
u32 magic; // TUL0 (Tinfoil Usb List 0)
u32 nspListSize;
u64 padding;
};
static_assert(sizeof(TUSHeader) == 0x10, "TUSHeader must be 0x10!");
static_assert(sizeof(USBCmdHeader) == 0x20, "USBCmdHeader must be 0x20!");
} // namespace
Usb::Usb(u64 transfer_timeout) {
m_open_result = usbDsInitialize();
m_transfer_timeout = transfer_timeout;
ueventCreate(GetCancelEvent(), true);
// this avoids allocations during transfers.
m_aligned.reserve(1024 * 1024 * 16);
}
Usb::~Usb() {
@@ -53,6 +79,11 @@ Result Usb::Init() {
log_write("doing USB init\n");
R_TRY(m_open_result);
SetSysSerialNumber serial_number;
R_TRY(setsysInitialize());
ON_SCOPE_EXIT(setsysExit());
R_TRY(setsysGetSerialNumber(&serial_number));
u8 iManufacturer, iProduct, iSerialNumber;
static const u16 supported_langs[1] = {0x0409};
// Send language descriptor
@@ -62,7 +93,7 @@ Result Usb::Init() {
// Send product
R_TRY(usbDsAddUsbStringDescriptor(&iProduct, "Nintendo Switch"));
// Send serial number
R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, "SerialNumber"));
R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, serial_number.number));
// Send device descriptors
struct usb_device_descriptor device_descriptor = {
@@ -131,13 +162,11 @@ Result Usb::Init() {
.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 = {
@@ -145,7 +174,6 @@ Result Usb::Init() {
.bDescriptorType = USB_DT_ENDPOINT,
.bEndpointAddress = USB_ENDPOINT_OUT,
.bmAttributes = USB_TRANSFER_TYPE_BULK,
.wMaxPacketSize = 0x40,
};
const struct usb_ss_endpoint_companion_descriptor endpoint_companion = {
@@ -163,6 +191,8 @@ Result Usb::Init() {
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));
@@ -194,49 +224,60 @@ Result Usb::Init() {
R_SUCCEED();
}
Result Usb::WaitForConnection(u64 timeout, u32& speed, u32& count) {
const SendHeader send_header{
.magic = MAGIC,
.version = VERSION,
};
Result Usb::IsUsbConnected(u64 timeout) const {
return usbDsWaitReady(timeout);
}
alignas(0x1000) u8 aligned[0x1000]{};
std::memcpy(aligned, std::addressof(send_header), sizeof(send_header));
Result Usb::WaitForConnection(u64 timeout, std::vector<std::string>& out_names) {
TUSHeader header;
R_TRY(TransferAll(true, &header, sizeof(header), timeout));
R_UNLESS(header.magic == 0x304C5554, Result_BadMagic);
R_UNLESS(header.nspListSize > 0, Result_BadCount);
log_write("USB got header\n");
// send header.
u32 transferredSize;
R_TRY(TransferPacketImpl(false, aligned, sizeof(send_header), &transferredSize, timeout));
std::vector<char> names(header.nspListSize);
R_TRY(TransferAll(true, names.data(), names.size(), timeout));
// receive header.
struct RecvHeader recv_header{};
R_TRY(TransferPacketImpl(true, aligned, sizeof(recv_header), &transferredSize, timeout));
out_names.clear();
for (const auto& name : std::views::split(names, '\n')) {
if (!name.empty()) {
out_names.emplace_back(name.data(), name.size());
}
}
// copy data into header struct.
std::memcpy(&recv_header, aligned, sizeof(recv_header));
for (auto& name : out_names) {
log_write("got name: %s\n", name.c_str());
}
// 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_UNLESS(!out_names.empty(), Result_BadCount);
log_write("USB SUCCESS\n");
R_SUCCEED();
}
bool Usb::GetConfigured() const {
UsbState usb_state;
usbDsGetState(std::addressof(usb_state));
return usb_state == UsbState_Configured;
void Usb::SetFileNameForTranfser(const std::string& name) {
m_transfer_file_name = name;
}
Event *Usb::GetCompletionEvent(UsbSessionEndpoint ep) const {
return std::addressof(m_endpoints[ep]->CompletionEvent);
}
Result Usb::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) const {
Result Usb::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) {
auto event = GetCompletionEvent(ep);
const auto rc = eventWait(event, timeout);
const std::array waiters{
waiterForEvent(event),
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 != 0) {
log_write("got usb cancel event\n");
rc = 0xEC01; // cancelled.
}
if (R_FAILED(rc)) {
R_TRY(usbDsEndpoint_Cancel(m_endpoints[ep]));
@@ -260,16 +301,11 @@ Result Usb::GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_reques
R_SUCCEED();
}
Result Usb::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) const {
Result Usb::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) {
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);
}
R_TRY(IsUsbConnected(timeout));
/* Select the appropriate endpoint and begin a transfer. */
const auto ep = read ? UsbSessionEndpoint_Out : UsbSessionEndpoint_In;
@@ -282,78 +318,73 @@ Result Usb::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_tr
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 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.
Result Usb::TransferAll(bool read, void *data, u32 size, u64 timeout) {
auto buf = static_cast<u8*>(data);
m_aligned.resize((size + 0xFFF) & ~0xFFF);
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;
}
if (!read) {
std::memcpy(m_aligned.data(), buf, size);
}
R_TRY(SendCommand(off, read_size));
u32 out_size_transferred;
R_TRY(TransferPacketImpl(read, m_aligned.data(), size, &out_size_transferred, timeout));
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 (read) {
std::memcpy(buf, m_aligned.data(), out_size_transferred);
}
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;
buf += out_size_transferred;
size -= out_size_transferred;
}
R_UNLESS(total == stored_size, Result_BadTotalSize);
R_SUCCEED();
}
Result Usb::SendCmdHeader(u32 cmdId, size_t dataSize) {
USBCmdHeader header{
.magic = 0x30435554, // TUC0 (Tinfoil USB Command 0)
.type = USBCmdType::REQUEST,
.cmdId = cmdId,
.dataSize = dataSize,
};
return TransferAll(false, &header, sizeof(header), m_transfer_timeout);
}
Result Usb::SendFileRangeCmd(u64 off, u64 size) {
FileRangeCmdHeader fRangeHeader;
fRangeHeader.size = size;
fRangeHeader.offset = off;
fRangeHeader.nspNameLen = m_transfer_file_name.size();
fRangeHeader.padding = 0;
R_TRY(SendCmdHeader(USBCmdId::FILE_RANGE, sizeof(fRangeHeader) + fRangeHeader.nspNameLen));
R_TRY(TransferAll(false, &fRangeHeader, sizeof(fRangeHeader), m_transfer_timeout));
R_TRY(TransferAll(false, m_transfer_file_name.data(), m_transfer_file_name.size(), m_transfer_timeout));
USBCmdHeader responseHeader;
R_TRY(TransferAll(true, &responseHeader, sizeof(responseHeader), m_transfer_timeout));
R_SUCCEED();
}
Result Usb::Finished() {
return SendCmdHeader(USBCmdId::EXIT, 0);
}
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));
R_TRY(TransferAll(true, buf, size, m_transfer_timeout));
*bytes_read = size;
R_SUCCEED();
}

File diff suppressed because it is too large Load Diff

1
tools/requirements.txt Normal file
View File

@@ -0,0 +1 @@
pyusb

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,130 +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
# magic number (SPHA) for the script and switch.
MAGIC = 0x53504841
# version of the usb script.
VERSION = 1
# 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 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))
wait_for_input(file, in_ep, out_ep)
dev.reset()
except Exception as inst:
print("An exception occurred " + str(inst))