34 Commits
0.8.1 ... 0.9.1

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

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

github and appstore support sending gzip json files, themezer doesn't seem to.
2025-04-30 00:40:04 +01:00
ITotalJustice
ec4b96b95d remove stale etag if the server stops sending etags back
workaround for appstore images which stopped sending etags back.
2025-04-29 22:41:33 +01:00
ITotalJustice
a2e343daa7 improve popup_list to highlight the currently selected item. 2025-04-29 22:40:32 +01:00
BIGBIGSUI
b811c9e3cd Update zh.json (#129)
A latest zh.json. Hope it will be helpful
2025-04-29 20:35:05 +01:00
49 changed files with 2368 additions and 393 deletions

View File

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

View File

@@ -66,7 +66,7 @@
"Select Theme": "选择主题", "Select Theme": "选择主题",
"Shuffle": "随机播放", "Shuffle": "随机播放",
"Music": "音乐", "Music": "音乐",
"12 Hour Time": "", "12 Hour Time": "12小时制时间",
"Network": "网络", "Network": "网络",
"Network Options": "网络选项", "Network Options": "网络选项",
"Ftp": "FTP", "Ftp": "FTP",
@@ -170,7 +170,7 @@
"Controller": "控制器", "Controller": "控制器",
"Pad ": "手柄 ", "Pad ": "手柄 ",
" (Available)": " (可用的)", " (Available)": " (可用的)",
" (Unsupported)": "", " (Unsupported)": " (不支持的)",
" (Unconnected)": " (未连接)", " (Unconnected)": " (未连接)",
"HandHeld": "掌机模式", "HandHeld": "掌机模式",
"Rotation": "旋转", "Rotation": "旋转",
@@ -239,7 +239,7 @@
"Update avaliable: ": "有可用更新!", "Update avaliable: ": "有可用更新!",
"Download update: ": "下载更新:", "Download update: ": "下载更新:",
"Updated to ": "更新至 ", "Updated to ": "更新至 ",
"Press OK to restart Sphaira": "", "Press OK to restart Sphaira": "按OK键以重启shphaira菜单",
"Restart Sphaira?": "重启 Sphaira", "Restart Sphaira?": "重启 Sphaira",
"Failed to download update": "更新下载失败", "Failed to download update": "更新下载失败",
"Restore hbmenu?": "恢复 hbmenu", "Restore hbmenu?": "恢复 hbmenu",

View File

@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.13) cmake_minimum_required(VERSION 3.13)
set(sphaira_VERSION 0.8.1) set(sphaira_VERSION 0.9.1)
project(sphaira project(sphaira
VERSION ${sphaira_VERSION} VERSION ${sphaira_VERSION}
@@ -41,6 +41,7 @@ add_executable(sphaira
source/ui/menus/file_viewer.cpp source/ui/menus/file_viewer.cpp
source/ui/menus/filebrowser.cpp source/ui/menus/filebrowser.cpp
source/ui/menus/homebrew.cpp source/ui/menus/homebrew.cpp
source/ui/menus/irs_menu.cpp
source/ui/menus/main_menu.cpp source/ui/menus/main_menu.cpp
source/ui/menus/menu_base.cpp source/ui/menus/menu_base.cpp
source/ui/menus/themezer.cpp source/ui/menus/themezer.cpp
@@ -48,6 +49,7 @@ add_executable(sphaira
source/ui/menus/usb_menu.cpp source/ui/menus/usb_menu.cpp
source/ui/menus/ftp_menu.cpp source/ui/menus/ftp_menu.cpp
source/ui/menus/gc_menu.cpp source/ui/menus/gc_menu.cpp
source/ui/menus/game_menu.cpp
source/ui/error_box.cpp source/ui/error_box.cpp
source/ui/notification.cpp source/ui/notification.cpp
@@ -60,6 +62,7 @@ add_executable(sphaira
source/ui/widget.cpp source/ui/widget.cpp
source/ui/list.cpp source/ui/list.cpp
source/ui/bubbles.cpp source/ui/bubbles.cpp
source/ui/scrolling_text.cpp
source/app.cpp source/app.cpp
source/download.cpp source/download.cpp

View File

@@ -184,6 +184,7 @@ public:
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true}; option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
option::OptionBool m_12hour_time{INI_SECTION, "12hour_time", false}; option::OptionBool m_12hour_time{INI_SECTION, "12hour_time", false};
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto option::OptionLong m_language{INI_SECTION, "language", 0}; // auto
option::OptionString m_right_side_menu{INI_SECTION, "right_side_menu", "Appstore"};
// install options // install options
option::OptionBool m_install{INI_SECTION, "install", false}; option::OptionBool m_install{INI_SECTION, "install", false};

View File

@@ -1,13 +1,14 @@
#pragma once #pragma once
#include <string> #include <string>
#include <string_view>
namespace sphaira::i18n { namespace sphaira::i18n {
bool init(long index); bool init(long index);
void exit(); void exit();
std::string get(const char* str); std::string get(std::string_view str);
} // namespace sphaira::i18n } // namespace sphaira::i18n

View File

@@ -2,8 +2,8 @@
#include "ui/menus/menu_base.hpp" #include "ui/menus/menu_base.hpp"
#include "ui/scrollable_text.hpp" #include "ui/scrollable_text.hpp"
#include "ui/scrolling_text.hpp"
#include "ui/list.hpp" #include "ui/list.hpp"
#include "nro.hpp"
#include "fs.hpp" #include "fs.hpp"
#include <span> #include <span>
@@ -73,6 +73,7 @@ struct EntryMenu final : MenuBase {
EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu); EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu);
~EntryMenu(); ~EntryMenu();
auto GetShortTitle() const -> const char* override { return "Entry"; };
void Update(Controller* controller, TouchInfo* touch) override; void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override; void Draw(NVGcontext* vg, Theme* theme) override;
// void OnFocusGained() override; // void OnFocusGained() override;
@@ -135,9 +136,10 @@ enum OrderType {
}; };
struct Menu final : MenuBase { struct Menu final : MenuBase {
Menu(const std::vector<NroEntry>& nro_entries); Menu();
~Menu(); ~Menu();
auto GetShortTitle() const -> const char* override { return "Store"; };
void Update(Controller* controller, TouchInfo* touch) override; void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override; void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override; void OnFocusGained() override;
@@ -162,13 +164,16 @@ struct Menu final : MenuBase {
} }
private: private:
const std::vector<NroEntry>& m_nro_entries;
std::vector<Entry> m_entries{}; std::vector<Entry> m_entries{};
std::vector<EntryMini> m_entries_index[Filter_MAX]{}; std::vector<EntryMini> m_entries_index[Filter_MAX]{};
std::vector<EntryMini> m_entries_index_author{}; std::vector<EntryMini> m_entries_index_author{};
std::vector<EntryMini> m_entries_index_search{}; std::vector<EntryMini> m_entries_index_search{};
std::span<EntryMini> m_entries_current{}; std::span<EntryMini> m_entries_current{};
ScrollingText m_scroll_name{};
ScrollingText m_scroll_author{};
ScrollingText m_scroll_version{};
Filter m_filter{Filter::Filter_All}; Filter m_filter{Filter::Filter_All};
SortType m_sort{SortType::SortType_Updated}; SortType m_sort{SortType::SortType_Updated};
OrderType m_order{OrderType::OrderType_Descending}; OrderType m_order{OrderType::OrderType_Descending};

View File

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

View File

@@ -124,6 +124,7 @@ struct Menu final : MenuBase {
Menu(const std::vector<NroEntry>& nro_entries); Menu(const std::vector<NroEntry>& nro_entries);
~Menu(); ~Menu();
auto GetShortTitle() const -> const char* override { return "Files"; };
void Update(Controller* controller, TouchInfo* touch) override; void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override; void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override; void OnFocusGained() override;
@@ -137,6 +138,13 @@ private:
void InstallForwarder(); void InstallForwarder();
void InstallFile(const FileEntry& target); void InstallFile(const FileEntry& target);
void InstallFiles(const std::vector<FileEntry>& targets); 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; auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result;
void LoadAssocEntriesPath(const fs::FsPath& path); void LoadAssocEntriesPath(const fs::FsPath& path);

View File

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

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

@@ -43,8 +43,10 @@ struct Menu final : MenuBase {
Menu(); Menu();
~Menu(); ~Menu();
auto GetShortTitle() const -> const char* override { return "GC"; };
void Update(Controller* controller, TouchInfo* touch) override; void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override; void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
private: private:
Result GcMount(); Result GcMount();

View File

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

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "ui/menus/menu_base.hpp" #include "ui/menus/menu_base.hpp"
#include "ui/scrolling_text.hpp"
#include "ui/list.hpp" #include "ui/list.hpp"
#include "nro.hpp" #include "nro.hpp"
#include "fs.hpp" #include "fs.hpp"
@@ -26,27 +27,30 @@ struct Menu final : MenuBase {
Menu(); Menu();
~Menu(); ~Menu();
auto GetShortTitle() const -> const char* override { return "Apps"; };
void Update(Controller* controller, TouchInfo* touch) override; void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override; void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override; void OnFocusGained() override;
void SetIndex(s64 index);
void InstallHomebrew();
void ScanHomebrew();
void Sort();
void SortAndFindLastFile();
auto GetHomebrewList() const -> const std::vector<NroEntry>& { auto GetHomebrewList() const -> const std::vector<NroEntry>& {
return m_entries; 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 { auto IsStarEnabled() -> bool {
return m_sort.Get() >= SortType_UpdatedStar; 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: private:
static constexpr inline const char* INI_SECTION = "homebrew"; static constexpr inline const char* INI_SECTION = "homebrew";
@@ -54,6 +58,10 @@ private:
s64 m_index{}; // where i am in the array s64 m_index{}; // where i am in the array
std::unique_ptr<List> m_list{}; 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_sort{INI_SECTION, "sort", SortType::SortType_AlphabeticalStar};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending}; option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
option::OptionBool m_hide_sphaira{INI_SECTION, "hide_sphaira", false}; option::OptionBool m_hide_sphaira{INI_SECTION, "hide_sphaira", false};

View File

@@ -0,0 +1,70 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include <span>
namespace sphaira::ui::menu::irs {
enum Rotation {
Rotation_0,
Rotation_90,
Rotation_180,
Rotation_270,
};
enum Colour {
Colour_Grey,
Colour_Ironbow,
Colour_Green,
Colour_Red,
Colour_Blue,
};
struct Entry {
IrsIrCameraHandle m_handle{};
IrsIrCameraStatus status{};
bool m_update_needed{};
};
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{};
IrsImageTransferProcessorExConfig m_config{};
IrsMomentProcessorConfig m_moment_config{};
IrsClusteringProcessorConfig m_clustering_config{};
IrsTeraPluginProcessorConfig m_tera_config{};
IrsIrLedProcessorConfig m_led_config{};
IrsAdaptiveClusteringProcessorConfig m_adaptive_config{};
IrsHandAnalysisConfig m_hand_config{};
Entry m_entries[IRS_MAX_CAMERAS]{};
u32 m_irs_width{};
u32 m_irs_height{};
std::vector<u32> m_rgba{};
std::vector<u8> m_irs_buffer{};
IrsImageTransferProcessorState m_prev_state{};
Rotation m_rotation{Rotation_90};
Colour m_colour{Colour_Grey};
int m_image{};
s64 m_index{};
};
} // namespace sphaira::ui::menu::irs

View File

@@ -3,7 +3,6 @@
#include "ui/widget.hpp" #include "ui/widget.hpp"
#include "ui/menus/homebrew.hpp" #include "ui/menus/homebrew.hpp"
#include "ui/menus/filebrowser.hpp" #include "ui/menus/filebrowser.hpp"
#include "ui/menus/appstore.hpp"
namespace sphaira::ui::menu::main { namespace sphaira::ui::menu::main {
@@ -18,6 +17,32 @@ enum class UpdateState {
Error, 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 // this holds 2 menus and allows for switching between them
struct MainMenu final : Widget { struct MainMenu final : Widget {
MainMenu(); MainMenu();
@@ -39,7 +64,7 @@ private:
private: private:
std::shared_ptr<homebrew::Menu> m_homebrew_menu{}; std::shared_ptr<homebrew::Menu> m_homebrew_menu{};
std::shared_ptr<filebrowser::Menu> m_filebrowser_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::shared_ptr<MenuBase> m_current_menu{};
std::string m_update_url{}; std::string m_update_url{};

View File

@@ -10,6 +10,7 @@ struct MenuBase : Widget {
MenuBase(std::string title); MenuBase(std::string title);
virtual ~MenuBase(); virtual ~MenuBase();
virtual auto GetShortTitle() const -> const char* = 0;
virtual void Update(Controller* controller, TouchInfo* touch); virtual void Update(Controller* controller, TouchInfo* touch);
virtual void Draw(NVGcontext* vg, Theme* theme); virtual void Draw(NVGcontext* vg, Theme* theme);
@@ -21,6 +22,10 @@ struct MenuBase : Widget {
void SetTitleSubHeading(std::string sub_heading); void SetTitleSubHeading(std::string sub_heading);
void SetSubHeading(std::string sub_heading); void SetSubHeading(std::string sub_heading);
auto GetTitle() const {
return m_title;
}
private: private:
void UpdateVars(); void UpdateVars();

View File

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

View File

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

View File

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

View File

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

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

@@ -41,6 +41,7 @@ enum KeyGeneration {
KeyGeneration_1700 = 0x11, KeyGeneration_1700 = 0x11,
KeyGeneration_1800 = 0x12, KeyGeneration_1800 = 0x12,
KeyGeneration_1900 = 0x13, KeyGeneration_1900 = 0x13,
KeyGeneration_2000 = 0x14,
KeyGeneration_Invalid = 0xFF, KeyGeneration_Invalid = 0xFF,
}; };

View File

@@ -31,6 +31,9 @@ union ExtendedHeader {
NcmDataPatchMetaExtendedHeader data_patch; 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(u8 meta_type, u64 id) -> u64;
auto GetAppId(const NcmContentMetaKey& key) -> u64; auto GetAppId(const NcmContentMetaKey& key) -> u64;
auto GetAppId(const PackagedContentMeta& meta) -> u64; auto GetAppId(const PackagedContentMeta& meta) -> u64;

View File

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

View File

@@ -19,6 +19,7 @@ struct Usb final : Base {
Result_BadCount = MAKERESULT(USBModule, 2), Result_BadCount = MAKERESULT(USBModule, 2),
Result_BadTransferSize = MAKERESULT(USBModule, 3), Result_BadTransferSize = MAKERESULT(USBModule, 3),
Result_BadTotalSize = MAKERESULT(USBModule, 4), Result_BadTotalSize = MAKERESULT(USBModule, 4),
Result_Cancelled = MAKERESULT(USBModule, 11),
}; };
Usb(u64 transfer_timeout); Usb(u64 transfer_timeout);
@@ -28,10 +29,18 @@ struct Usb final : Base {
Result Finished(); Result Finished();
Result Init(); Result Init();
Result IsUsbConnected(u64 timeout) const; Result IsUsbConnected(u64 timeout);
Result WaitForConnection(u64 timeout, std::vector<std::string>& out_names); Result WaitForConnection(u64 timeout, std::vector<std::string>& out_names);
void SetFileNameForTranfser(const std::string& name); void SetFileNameForTranfser(const std::string& name);
auto GetCancelEvent() {
return &m_uevent;
}
void SignalCancel() override {
ueventSignal(GetCancelEvent());
}
public: public:
// custom allocator for std::vector that respects alignment. // custom allocator for std::vector that respects alignment.
// https://en.cppreference.com/w/cpp/named_req/Allocator // https://en.cppreference.com/w/cpp/named_req/Allocator
@@ -69,16 +78,18 @@ private:
Result SendFileRangeCmd(u64 offset, u64 size); Result SendFileRangeCmd(u64 offset, u64 size);
Event *GetCompletionEvent(UsbSessionEndpoint ep) 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 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 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); Result TransferAll(bool read, void *data, u32 size, u64 timeout);
private: private:
UsbDsInterface* m_interface{}; UsbDsInterface* m_interface{};
UsbDsEndpoint* m_endpoints[2]{}; UsbDsEndpoint* m_endpoints[2]{};
u64 m_transfer_timeout{}; u64 m_transfer_timeout{};
UEvent m_uevent{};
// std::vector<UEvent*> m_cancel_events{};
// aligned buffer that transfer data is copied to and from. // aligned buffer that transfer data is copied to and from.
// a vector is used to avoid multiple alloc within the transfer loop. // a vector is used to avoid multiple alloc within the transfer loop.
PageAlignedVector m_aligned{}; PageAlignedVector m_aligned{};

View File

@@ -7,11 +7,6 @@
#include "ui/error_box.hpp" #include "ui/error_box.hpp"
#include "ui/menus/main_menu.hpp" #include "ui/menus/main_menu.hpp"
#include "ui/menus/themezer.hpp"
#include "ui/menus/ghdl.hpp"
#include "ui/menus/usb_menu.hpp"
#include "ui/menus/ftp_menu.hpp"
#include "ui/menus/gc_menu.hpp"
#include "app.hpp" #include "app.hpp"
#include "log.hpp" #include "log.hpp"
@@ -1485,11 +1480,11 @@ void App::DisplayThemeOptions(bool left_side) {
options->Add(std::make_shared<ui::SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [](bool& enable){ options->Add(std::make_shared<ui::SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [](bool& enable){
App::SetThemeMusicEnable(enable); App::SetThemeMusicEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [](bool& enable){ options->Add(std::make_shared<ui::SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [](bool& enable){
App::Set12HourTimeEnable(enable); App::Set12HourTimeEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryCallback>("Download Default Music"_i18n, [](){ options->Add(std::make_shared<ui::SidebarEntryCallback>("Download Default Music"_i18n, [](){
// check if we already have music // check if we already have music
@@ -1517,27 +1512,17 @@ void App::DisplayMiscOptions(bool left_side) {
auto options = std::make_shared<ui::Sidebar>("Misc Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT); 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)); ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<ui::SidebarEntryCallback>("Themezer"_i18n, [](){ for (auto& e : ui::menu::main::GetMiscMenuEntries()) {
App::Push(std::make_shared<ui::menu::themezer::Menu>()); if (e.name == g_app->m_right_side_menu.Get()) {
})); continue;
options->Add(std::make_shared<ui::SidebarEntryCallback>("GitHub"_i18n, [](){
App::Push(std::make_shared<ui::menu::gh::Menu>());
}));
if (App::GetApp()->m_install.Get()) {
if (App::GetFtpEnable()) {
options->Add(std::make_shared<ui::SidebarEntryCallback>("Ftp Install"_i18n, [](){
App::Push(std::make_shared<ui::menu::ftp::Menu>());
}));
} }
options->Add(std::make_shared<ui::SidebarEntryCallback>("Usb Install"_i18n, [](){ if (e.IsInstall() && !App::GetInstallEnable()) {
App::Push(std::make_shared<ui::menu::usb::Menu>()); continue;
})); }
options->Add(std::make_shared<ui::SidebarEntryCallback>("GameCard Install"_i18n, [](){ options->Add(std::make_shared<ui::SidebarEntryCallback>(i18n::get(e.title), [e](){
App::Push(std::make_shared<ui::menu::gc::Menu>()); App::Push(e.func());
})); }));
} }
} }
@@ -1551,17 +1536,52 @@ void App::DisplayAdvancedOptions(bool left_side) {
text_scroll_speed_items.push_back("Normal"_i18n); text_scroll_speed_items.push_back("Normal"_i18n);
text_scroll_speed_items.push_back("Fast"_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){ options->Add(std::make_shared<ui::SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [](bool& enable){
App::SetLogEnable(enable); App::SetLogEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryBool>("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [](bool& enable){ options->Add(std::make_shared<ui::SidebarEntryBool>("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [](bool& enable){
App::SetReplaceHbmenuEnable(enable); App::SetReplaceHbmenuEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [](s64& index_out){ options->Add(std::make_shared<ui::SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [](s64& index_out){
App::SetTextScrollSpeed(index_out); App::SetTextScrollSpeed(index_out);
}, (s64)App::GetTextScrollSpeed())); }, App::GetTextScrollSpeed()));
options->Add(std::make_shared<ui::SidebarEntryArray>("Set right-side menu"_i18n, right_side_menu_items, [menu_names](s64& index_out){
const auto e = menu_names[index_out];
if (g_app->m_right_side_menu.Get() != e) {
g_app->m_right_side_menu.Set(e);
App::Push(std::make_shared<ui::OptionBox>(
"Press OK to restart Sphaira"_i18n, "OK"_i18n, [](auto){
App::ExitRestart();
}
));
}
}, i18n::get(g_app->m_right_side_menu.Get())));
options->Add(std::make_shared<ui::SidebarEntryCallback>("Install options"_i18n, [left_side](){ options->Add(std::make_shared<ui::SidebarEntryCallback>("Install options"_i18n, [left_side](){
App::DisplayInstallOptions(left_side); App::DisplayInstallOptions(left_side);
@@ -1578,11 +1598,11 @@ void App::DisplayInstallOptions(bool left_side) {
options->Add(std::make_shared<ui::SidebarEntryBool>("Enable"_i18n, App::GetApp()->m_install.Get(), [](bool& enable){ options->Add(std::make_shared<ui::SidebarEntryBool>("Enable"_i18n, App::GetApp()->m_install.Get(), [](bool& enable){
App::GetApp()->m_install.Set(enable); App::GetApp()->m_install.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryBool>("Show install warning"_i18n, App::GetApp()->m_install_prompt.Get(), [](bool& 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); App::GetApp()->m_install_prompt.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryArray>("Install location"_i18n, install_items, [](s64& index_out){ options->Add(std::make_shared<ui::SidebarEntryArray>("Install location"_i18n, install_items, [](s64& index_out){
App::SetInstallSdEnable(index_out); App::SetInstallSdEnable(index_out);
@@ -1590,67 +1610,67 @@ void App::DisplayInstallOptions(bool left_side) {
options->Add(std::make_shared<ui::SidebarEntryBool>("Boost CPU clock"_i18n, App::GetApp()->m_boost_mode.Get(), [](bool& enable){ 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); App::GetApp()->m_boost_mode.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryBool>("Allow downgrade"_i18n, App::GetApp()->m_allow_downgrade.Get(), [](bool& 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); App::GetApp()->m_allow_downgrade.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip if already installed"_i18n, App::GetApp()->m_skip_if_already_installed.Get(), [](bool& enable){ 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); App::GetApp()->m_skip_if_already_installed.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryBool>("Ticket only"_i18n, App::GetApp()->m_ticket_only.Get(), [](bool& enable){ 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); App::GetApp()->m_ticket_only.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip base"_i18n, App::GetApp()->m_skip_base.Get(), [](bool& enable){ 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); App::GetApp()->m_skip_base.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip patch"_i18n, App::GetApp()->m_skip_patch.Get(), [](bool& enable){ options->Add(std::make_shared<ui::SidebarEntryBool>("Skip patch"_i18n, App::GetApp()->m_skip_patch.Get(), [](bool& enable){
App::GetApp()->m_skip_patch.Set(enable); App::GetApp()->m_skip_patch.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip dlc"_i18n, App::GetApp()->m_skip_addon.Get(), [](bool& enable){ options->Add(std::make_shared<ui::SidebarEntryBool>("Skip dlc"_i18n, App::GetApp()->m_skip_addon.Get(), [](bool& enable){
App::GetApp()->m_skip_addon.Set(enable); App::GetApp()->m_skip_addon.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip data patch"_i18n, App::GetApp()->m_skip_data_patch.Get(), [](bool& enable){ 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); App::GetApp()->m_skip_data_patch.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip ticket"_i18n, App::GetApp()->m_skip_ticket.Get(), [](bool& enable){ 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); App::GetApp()->m_skip_ticket.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryBool>("skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [](bool& enable){ 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); App::GetApp()->m_skip_nca_hash_verify.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip RSA header verify"_i18n, App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(), [](bool& enable){ 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); App::GetApp()->m_skip_rsa_header_fixed_key_verify.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip RSA NPDM verify"_i18n, App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(), [](bool& enable){ 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); App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryBool>("Ignore distribution bit"_i18n, App::GetApp()->m_ignore_distribution_bit.Get(), [](bool& enable){ 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); App::GetApp()->m_ignore_distribution_bit.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryBool>("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [](bool& enable){ 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); App::GetApp()->m_convert_to_standard_crypto.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryBool>("Lower master key"_i18n, App::GetApp()->m_lower_master_key.Get(), [](bool& enable){ 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); App::GetApp()->m_lower_master_key.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<ui::SidebarEntryBool>("Lower system version"_i18n, App::GetApp()->m_lower_system_version.Get(), [](bool& enable){ 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::GetApp()->m_lower_system_version.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
} }
App::~App() { App::~App() {

View File

@@ -195,6 +195,8 @@ private:
} else { } else {
const auto update_entry = [this, &hash_key](const char* tag, const std::string& value) { const auto update_entry = [this, &hash_key](const char* tag, const std::string& value) {
if (value.empty()) { if (value.empty()) {
// workaround for appstore accepting etags but not returning them.
yyjson_mut_obj_remove_str(hash_key, tag);
return true; return true;
} else { } else {
auto key = yyjson_mut_obj_get(hash_key, tag); auto key = yyjson_mut_obj_get(hash_key, tag);
@@ -471,7 +473,8 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
return {}; 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); 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_BUFFERSIZE, 1024*512);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERFUNCTION, header_callback); CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERFUNCTION, header_callback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERDATA, &header_out); 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) { if (has_post) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_POSTFIELDS, e.GetFields().c_str()); 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); 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.DeleteFile(e.GetPath());
fs.CreateDirectoryRecursivelyWithPath(e.GetPath()); fs.CreateDirectoryRecursivelyWithPath(e.GetPath());
if (R_FAILED(fs.RenameFile(tmp_buf, e.GetPath()))) { if (R_FAILED(fs.RenameFile(tmp_buf, e.GetPath()))) {

View File

@@ -13,8 +13,8 @@ yyjson_doc* json;
yyjson_val* root; yyjson_val* root;
std::unordered_map<std::string, std::string> g_tr_cache; std::unordered_map<std::string, std::string> g_tr_cache;
std::string get_internal(const char* str, size_t len) { std::string get_internal(std::string_view str) {
const std::string kkey = {str, len}; const std::string kkey = {str.data(), str.length()};
if (auto it = g_tr_cache.find(kkey); it != g_tr_cache.end()) { if (auto it = g_tr_cache.find(kkey); it != g_tr_cache.end()) {
return it->second; return it->second;
@@ -28,7 +28,7 @@ std::string get_internal(const char* str, size_t len) {
return kkey; return kkey;
} }
auto key = yyjson_obj_getn(root, str, len); auto key = yyjson_obj_getn(root, str.data(), str.length());
if (!key) { if (!key) {
log_write("\tfailed to find key: [%s]\n", kkey.c_str()); log_write("\tfailed to find key: [%s]\n", kkey.c_str());
return kkey; return kkey;
@@ -134,8 +134,8 @@ void exit() {
g_i18n_data.clear(); g_i18n_data.clear();
} }
std::string get(const char* str) { std::string get(std::string_view str) {
return get_internal(str, std::strlen(str)); return get_internal(str);
} }
} // namespace sphaira::i18n } // namespace sphaira::i18n
@@ -143,7 +143,7 @@ std::string get(const char* str) {
namespace literals { namespace literals {
std::string operator"" _i18n(const char* str, size_t len) { 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 } // namespace literals

View File

@@ -857,7 +857,8 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
u64 hash_data[SHA256_HASH_SIZE / sizeof(u64)]; u64 hash_data[SHA256_HASH_SIZE / sizeof(u64)];
const auto hash_path = config.nro_path + config.args; const auto hash_path = config.nro_path + config.args;
sha256CalculateHash(hash_data, hash_path.data(), hash_path.length()); 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; 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)); 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 // remove previous application record
if (already_installed || hosversionBefore(2,0,0)) { if (already_installed || hosversionBefore(2,0,0)) {
const auto rc = ns::DeleteApplicationRecord(srv_ptr, tid); const auto rc = ns::DeleteApplicationRecord(srv_ptr, tid);

View File

@@ -37,7 +37,7 @@ auto ErrorBox::Draw(NVGcontext* vg, Theme* theme) -> void {
gfx::drawTextArgs(vg, center_x, 180, 63, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_ERROR), "\uE140"); gfx::drawTextArgs(vg, center_x, 180, 63, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_ERROR), "\uE140");
if (m_code.has_value()) { if (m_code.has_value()) {
const auto code = m_code.value(); const auto code = m_code.value();
gfx::drawTextArgs(vg, center_x, 270, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Error code: 0x%X Module: %u Description: 0x%X Value: 0x%X", code, R_MODULE(code), R_DESCRIPTION(code), R_VALUE(code)); gfx::drawTextArgs(vg, center_x, 270, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Code: 0x%X Module: %u Description: 0x%X Value: 0x%X", code, R_MODULE(code), R_DESCRIPTION(code), R_VALUE(code));
} else { } else {
gfx::drawTextArgs(vg, center_x, 270, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "An error occurred"_i18n.c_str()); gfx::drawTextArgs(vg, center_x, 270, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "An error occurred"_i18n.c_str());
} }

View File

@@ -236,7 +236,7 @@ void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, float x, f
bool crop = false; bool crop = false;
if (iw < w || ih < h) { if (iw < w || ih < h) {
rounded_image = false; rounded_image = false;
gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded ? 15 : 0); gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded ? 5 : 0);
} }
if (iw > w || ih > h) { if (iw > w || ih > h) {
crop = true; crop = true;
@@ -244,7 +244,7 @@ void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, float x, f
nvgIntersectScissor(vg, x, y, w, h); nvgIntersectScissor(vg, x, y, w, h);
} }
gfx::drawImage(vg, ix, iy, iw, ih, i.image, rounded_image ? 15 : 0); gfx::drawImage(vg, ix, iy, iw, ih, i.image, rounded_image ? 5 : 0);
if (crop) { if (crop) {
nvgRestore(vg); nvgRestore(vg);
} }
@@ -846,13 +846,42 @@ void EntryMenu::SetIndex(s64 index) {
} }
} }
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::FsNativeSd fs;
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/icons"); fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/icons");
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/banners"); fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/banners");
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/screens"); fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/screens");
this->SetActions( this->SetActions(
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
if (m_is_author) {
m_is_author = false;
if (m_is_search) {
SetSearch(m_search_term);
} else {
SetFilter(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);
}
} 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](){ std::make_pair(Button::A, Action{"Info"_i18n, [this](){
if (m_entries_current.empty()) { if (m_entries_current.empty()) {
// log_write("pushing A when empty: size: %zu count: %zu\n", repo_json.size(), m_entries_current.size()); // log_write("pushing A when empty: size: %zu count: %zu\n", repo_json.size(), m_entries_current.size());
@@ -1027,7 +1056,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
} }
auto text_id = ThemeEntryID_TEXT; auto text_id = ThemeEntryID_TEXT;
if (pos == m_index) { const auto selected = pos == m_index;
if (selected) {
text_id = ThemeEntryID_TEXT_SELECTED; text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, theme, 4.f, v); gfx::drawRectOutline(vg, theme, 4.f, v);
} else { } else {
@@ -1040,15 +1070,13 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
DrawIcon(vg, e.image, m_default_image, x + 20, y + 20, 115, 115, true, image_scale); 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); // gfx::drawImage(vg, x + 20, y + 20, image_size, image_size_h, image.image ? image.image : m_default_image);
nvgSave(vg); const auto text_off = 148;
nvgIntersectScissor(vg, v.x, v.y, w - 30.f, h); // clip const auto text_x = x + text_off;
{ const auto text_clip_w = w - 30.f - text_off;
const float font_size = 18; 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()); 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());
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.author.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());
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.version.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());
}
nvgRestore(vg);
float i_size = 22; float i_size = 22;
switch (e.status) { switch (e.status) {
@@ -1265,7 +1293,6 @@ void Menu::Sort() {
void Menu::SetFilter(Filter filter) { void Menu::SetFilter(Filter filter) {
m_is_search = false; m_is_search = false;
m_is_author = false; m_is_author = false;
RemoveAction(Button::B);
m_filter = filter; m_filter = filter;
m_entries_current = m_entries_index[m_filter]; m_entries_current = m_entries_index[m_filter];
@@ -1304,17 +1331,6 @@ void Menu::SetSearch(const std::string& term) {
} }
} }
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_is_search = true;
m_entries_current = m_entries_index_search; m_entries_current = m_entries_index_search;
SetIndex(0); SetIndex(0);
@@ -1337,21 +1353,6 @@ void Menu::SetAuthor() {
} }
} }
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_is_author = true;
m_entries_current = m_entries_index_author; m_entries_current = m_entries_index_author;
SetIndex(0); SetIndex(0);

View File

@@ -22,6 +22,7 @@
#include "yati/source/file.hpp" #include "yati/source/file.hpp"
#include <minIni.h> #include <minIni.h>
#include <minizip/zip.h>
#include <minizip/unzip.h> #include <minizip/unzip.h>
#include <dirent.h> #include <dirent.h>
#include <cstring> #include <cstring>
@@ -57,6 +58,14 @@ constexpr std::string_view IMAGE_EXTENSIONS[] = {
constexpr std::string_view INSTALL_EXTENSIONS[] = { constexpr std::string_view INSTALL_EXTENSIONS[] = {
"nsp", "xci", "nsz", "xcz", "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 { struct RomDatabaseEntry {
@@ -267,6 +276,25 @@ auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, const
Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i18n}, m_nro_entries{nro_entries} { Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i18n}, m_nro_entries{nro_entries} {
this->SetActions( 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](){ std::make_pair(Button::R2, Action{[this](){
if (!m_selected_files.empty()) { if (!m_selected_files.empty()) {
ResetSelection(); ResetSelection();
@@ -384,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){ options->Add(std::make_shared<SidebarEntryBool>("Show Hidden"_i18n, m_show_hidden.Get(), [this](bool& v_out){
m_show_hidden.Set(v_out); m_show_hidden.Set(v_out);
SortAndFindLastFile(); SortAndFindLastFile();
}, "Yes"_i18n, "No"_i18n)); }));
options->Add(std::make_shared<SidebarEntryBool>("Folders First"_i18n, m_folders_first.Get(), [this](bool& v_out){ options->Add(std::make_shared<SidebarEntryBool>("Folders First"_i18n, m_folders_first.Get(), [this](bool& v_out){
m_folders_first.Set(v_out); m_folders_first.Set(v_out);
SortAndFindLastFile(); SortAndFindLastFile();
}, "Yes"_i18n, "No"_i18n)); }));
options->Add(std::make_shared<SidebarEntryBool>("Hidden Last"_i18n, m_hidden_last.Get(), [this](bool& v_out){ options->Add(std::make_shared<SidebarEntryBool>("Hidden Last"_i18n, m_hidden_last.Get(), [this](bool& v_out){
m_hidden_last.Set(v_out); m_hidden_last.Set(v_out);
SortAndFindLastFile(); SortAndFindLastFile();
}, "Yes"_i18n, "No"_i18n)); }));
})); }));
if (m_entries_current.size()) { if (m_entries_current.size()) {
@@ -475,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. // returns true if all entries match the ext array.
if (m_entries_current.size() && App::GetInstallEnable()) { const auto check_all_ext = [this](auto& exts){
bool should_install = true;
if (!m_selected_count) { if (!m_selected_count) {
should_install = IsExtension(GetEntry().GetExtension(), INSTALL_EXTENSIONS); return IsExtension(GetEntry().GetExtension(), exts);
} else { } else {
const auto entries = GetSelectedEntries(); const auto entries = GetSelectedEntries();
for (auto&e : entries) { for (auto&e : entries) {
if (!IsExtension(e.GetExtension(), INSTALL_EXTENSIONS)) { if (!IsExtension(e.GetExtension(), exts)) {
should_install = false; return false;
break;
} }
} }
} }
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](){ options->Add(std::make_shared<SidebarEntryCallback>("Install"_i18n, [this](){
if (!m_selected_count) { if (!m_selected_count) {
InstallFile(GetEntry()); InstallFile(GetEntry());
@@ -501,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](){ options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::RIGHT); auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options)); ON_SCOPE_EXIT(App::Push(options));
@@ -529,7 +645,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
options->Add(std::make_shared<SidebarEntryCallback>("Create Folder"_i18n, [this](){ options->Add(std::make_shared<SidebarEntryCallback>("Create Folder"_i18n, [this](){
std::string out; 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(); App::PopToMenu();
fs::FsPath full_path; fs::FsPath full_path;
@@ -554,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){ 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_ignore_read_only.Set(v_out);
m_fs->SetIgnoreReadOnly(v_out); m_fs->SetIgnoreReadOnly(v_out);
}, "Yes"_i18n, "No"_i18n)); }));
SidebarEntryArray::Items mount_items; SidebarEntryArray::Items mount_items;
mount_items.push_back("Sd"_i18n); mount_items.push_back("Sd"_i18n);
@@ -650,7 +747,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
} }
auto text_id = ThemeEntryID_TEXT; auto text_id = ThemeEntryID_TEXT;
if (m_index == i) { const auto selected = m_index == i;
if (selected) {
text_id = ThemeEntryID_TEXT_SELECTED; text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, theme, 4.f, v); gfx::drawRectOutline(vg, theme, 4.f, v);
} else { } else {
@@ -673,7 +771,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
} else if (IsExtension(ext, INSTALL_EXTENSIONS)) { } else if (IsExtension(ext, INSTALL_EXTENSIONS)) {
// todo: maybe replace this icon with something else? // todo: maybe replace this icon with something else?
icon = ThemeEntryID_ICON_NRO; icon = ThemeEntryID_ICON_NRO;
} else if (IsExtension(ext, "zip")) { } else if (IsExtension(ext, ZIP_EXTENSIONS)) {
icon = ThemeEntryID_ICON_ZIP; icon = ThemeEntryID_ICON_ZIP;
} else if (IsExtension(ext, "nro")) { } else if (IsExtension(ext, "nro")) {
icon = ThemeEntryID_ICON_NRO; icon = ThemeEntryID_ICON_NRO;
@@ -848,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 { auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
log_write("new scan path: %s\n", new_path.s); log_write("new scan path: %s\n", new_path.s);
if (!is_walk_up && !m_path.empty() && !m_entries_current.empty()) { if (!is_walk_up && !m_path.empty() && !m_entries_current.empty()) {

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

@@ -184,8 +184,6 @@ Menu::Menu() : MenuBase{"GameCard"_i18n} {
fsOpenDeviceOperator(std::addressof(m_dev_op)); fsOpenDeviceOperator(std::addressof(m_dev_op));
fsOpenGameCardDetectionEventNotifier(std::addressof(m_event_notifier)); fsOpenGameCardDetectionEventNotifier(std::addressof(m_event_notifier));
fsEventNotifierGetEventHandle(std::addressof(m_event_notifier), std::addressof(m_event), true); fsEventNotifierGetEventHandle(std::addressof(m_event_notifier), std::addressof(m_event), true);
GcOnEvent();
UpdateStorageSize();
} }
Menu::~Menu() { Menu::~Menu() {
@@ -269,6 +267,13 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
}); });
} }
void Menu::OnFocusGained() {
MenuBase::OnFocusGained();
GcOnEvent();
UpdateStorageSize();
}
Result Menu::GcMount() { Result Menu::GcMount() {
GcUnmount(); GcUnmount();
@@ -391,20 +396,18 @@ Result Menu::GcMount() {
} else { } else {
App::Notify("Gc install failed!"_i18n); App::Notify("Gc install failed!"_i18n);
} }
UpdateStorageSize();
})); }));
} }
} }
}}); }});
if (m_entries.size() > 1) { if (m_entries.size() > 1) {
SetAction(Button::L, Action{"Prev"_i18n, [this](){ SetAction(Button::L2, Action{"Prev"_i18n, [this](){
if (m_entry_index != 0) { if (m_entry_index != 0) {
OnChangeIndex(m_entry_index - 1); OnChangeIndex(m_entry_index - 1);
} }
}}); }});
SetAction(Button::R, Action{"Next"_i18n, [this](){ SetAction(Button::R2, Action{"Next"_i18n, [this](){
if (m_entry_index < m_entries.size()) { if (m_entry_index < m_entries.size()) {
OnChangeIndex(m_entry_index + 1); OnChangeIndex(m_entry_index + 1);
} }
@@ -424,8 +427,8 @@ void Menu::GcUnmount() {
m_lang_entry = {}; m_lang_entry = {};
FreeImage(); FreeImage();
RemoveAction(Button::L); RemoveAction(Button::L2);
RemoveAction(Button::R); RemoveAction(Button::R2);
} }
Result Menu::GcPoll(bool* inserted) { Result Menu::GcPoll(bool* inserted) {

View File

@@ -24,6 +24,11 @@ auto GenerateStarPath(const fs::FsPath& nro_path) -> fs::FsPath {
return out; return out;
} }
void FreeEntry(NVGcontext* vg, NroEntry& e) {
nvgDeleteImage(vg, e.image);
e.image = 0;
}
} // namespace } // namespace
Menu::Menu() : MenuBase{"Homebrew"_i18n} { Menu::Menu() : MenuBase{"Homebrew"_i18n} {
@@ -64,7 +69,7 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
options->Add(std::make_shared<SidebarEntryBool>("Hide Sphaira"_i18n, m_hide_sphaira.Get(), [this](bool& enable){ options->Add(std::make_shared<SidebarEntryBool>("Hide Sphaira"_i18n, m_hide_sphaira.Get(), [this](bool& enable){
m_hide_sphaira.Set(enable); m_hide_sphaira.Set(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
})); }));
#if 0 #if 0
@@ -80,11 +85,12 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
"Back"_i18n, "Delete"_i18n, 1, [this](auto op_index){ "Back"_i18n, "Delete"_i18n, 1, [this](auto op_index){
if (op_index && *op_index) { if (op_index && *op_index) {
if (R_SUCCEEDED(fs::FsNativeSd().DeleteFile(m_entries[m_index].path))) { 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); m_entries.erase(m_entries.begin() + m_index);
SetIndex(m_index ? m_index - 1 : 0); SetIndex(m_index ? m_index - 1 : 0);
} }
} }
} }, m_entries[m_index].image
)); ));
}, true)); }, true));
@@ -97,7 +103,7 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
if (op_index && *op_index) { if (op_index && *op_index) {
InstallHomebrew(); InstallHomebrew();
} }
} }, m_entries[m_index].image
)); ));
} else { } else {
InstallHomebrew(); InstallHomebrew();
@@ -114,11 +120,7 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
} }
Menu::~Menu() { Menu::~Menu() {
auto vg = App::GetVg(); FreeEntries();
for (auto&p : m_entries) {
nvgDeleteImage(vg, p.image);
}
} }
void Menu::Update(Controller* controller, TouchInfo* touch) { void Menu::Update(Controller* controller, TouchInfo* touch) {
@@ -160,7 +162,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
} }
auto text_id = ThemeEntryID_TEXT; auto text_id = ThemeEntryID_TEXT;
if (pos == m_index) { const auto selected = pos == m_index;
if (selected) {
text_id = ThemeEntryID_TEXT_SELECTED; text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, theme, 4.f, v); gfx::drawRectOutline(vg, theme, 4.f, v);
} else { } else {
@@ -168,11 +171,11 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
} }
const float image_size = 115; const float image_size = 115;
gfx::drawImage(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage(), 15); gfx::drawImage(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage(), 5);
nvgSave(vg); const auto text_off = 148;
nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip const auto text_x = x + text_off;
{ const auto text_clip_w = w - 30.f - text_off;
bool has_star = false; bool has_star = false;
if (IsStarEnabled()) { if (IsStarEnabled()) {
if (!e.has_star.has_value()) { if (!e.has_star.has_value()) {
@@ -182,11 +185,9 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
} }
const float font_size = 18; 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()); 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());
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetAuthor()); m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, 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()); m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetDisplayVersion());
}
nvgRestore(vg);
}); });
} }
@@ -238,6 +239,7 @@ void Menu::InstallHomebrew() {
void Menu::ScanHomebrew() { void Menu::ScanHomebrew() {
TimeStamp ts; TimeStamp ts;
FreeEntries();
nro_scan("/switch", m_entries, m_hide_sphaira.Get()); nro_scan("/switch", m_entries, m_hide_sphaira.Get());
log_write("nros found: %zu time_taken: %.2f\n", m_entries.size(), ts.GetSecondsD()); log_write("nros found: %zu time_taken: %.2f\n", m_entries.size(), ts.GetSecondsD());
@@ -394,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) { Result Menu::InstallHomebrew(const fs::FsPath& path, const NacpStruct& nacp, const std::vector<u8>& icon) {
OwoConfig config{}; OwoConfig config{};
config.nro_path = path.toString(); config.nro_path = path.toString();

View File

@@ -0,0 +1,552 @@
#include "ui/menus/irs_menu.hpp"
#include "ui/sidebar.hpp"
#include "ui/popup_list.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "ui/nvg_util.hpp"
#include "i18n.hpp"
#include <cstring>
#include <array>
namespace sphaira::ui::menu::irs {
namespace {
// from trial and error
constexpr u32 GAIN_MIN = 1;
constexpr u32 GAIN_MAX = 16;
consteval auto generte_iron_palette_table() {
std::array<u32, 256> array{};
const u32 iron_palette[] = {
0xff000014, 0xff000025, 0xff00002a, 0xff000032, 0xff000036, 0xff00003e, 0xff000042, 0xff00004f,
0xff010055, 0xff010057, 0xff02005c, 0xff03005e, 0xff040063, 0xff050065, 0xff070069, 0xff0a0070,
0xff0b0073, 0xff0d0075, 0xff0d0076, 0xff100078, 0xff120079, 0xff15007c, 0xff17007d, 0xff1c0081,
0xff200084, 0xff220085, 0xff260087, 0xff280089, 0xff2c008a, 0xff2e008b, 0xff32008d, 0xff38008f,
0xff390090, 0xff3c0092, 0xff3e0093, 0xff410094, 0xff420095, 0xff450096, 0xff470096, 0xff4c0097,
0xff4f0097, 0xff510097, 0xff540098, 0xff560098, 0xff5a0099, 0xff5c0099, 0xff5f009a, 0xff64009b,
0xff66009b, 0xff6a009b, 0xff6c009c, 0xff6f009c, 0xff70009c, 0xff73009d, 0xff75009d, 0xff7a009d,
0xff7e009d, 0xff7f009d, 0xff83009d, 0xff84009d, 0xff87009d, 0xff89009d, 0xff8b009d, 0xff91009c,
0xff93009c, 0xff96009b, 0xff98009b, 0xff9b009b, 0xff9c009b, 0xff9f009b, 0xffa0009b, 0xffa4009b,
0xffa7009a, 0xffa8009a, 0xffaa0099, 0xffab0099, 0xffae0198, 0xffaf0198, 0xffb00198, 0xffb30196,
0xffb40296, 0xffb60295, 0xffb70395, 0xffb90495, 0xffba0495, 0xffbb0593, 0xffbc0593, 0xffbf0692,
0xffc00791, 0xffc00791, 0xffc10990, 0xffc20a8f, 0xffc30b8e, 0xffc40c8d, 0xffc60d8b, 0xffc81088,
0xffc91187, 0xffca1385, 0xffcb1385, 0xffcc1582, 0xffcd1681, 0xffce187e, 0xffcf187c, 0xffd11b78,
0xffd21c75, 0xffd21d74, 0xffd32071, 0xffd4216f, 0xffd5236b, 0xffd52469, 0xffd72665, 0xffd92a60,
0xffda2b5e, 0xffdb2e5a, 0xffdb2f57, 0xffdd3051, 0xffdd314e, 0xffde3347, 0xffdf3444, 0xffe0373a,
0xffe03933, 0xffe13a30, 0xffe23c2a, 0xffe33d26, 0xffe43f20, 0xffe4411d, 0xffe5431b, 0xffe64616,
0xffe74715, 0xffe74913, 0xffe84a12, 0xffe84c0f, 0xffe94d0e, 0xffea4e0c, 0xffea4f0c, 0xffeb520a,
0xffec5409, 0xffec5608, 0xffec5808, 0xffed5907, 0xffed5b06, 0xffee5c06, 0xffee5d05, 0xffef6004,
0xffef6104, 0xfff06303, 0xfff06403, 0xfff16603, 0xfff16603, 0xfff16803, 0xfff16902, 0xfff16b02,
0xfff26d01, 0xfff26e01, 0xfff37001, 0xfff37101, 0xfff47300, 0xfff47400, 0xfff47600, 0xfff47a00,
0xfff57b00, 0xfff57e00, 0xfff57f00, 0xfff68100, 0xfff68200, 0xfff78400, 0xfff78500, 0xfff88800,
0xfff88900, 0xfff88a00, 0xfff88c00, 0xfff98d00, 0xfff98e00, 0xfff98f00, 0xfff99100, 0xfffa9400,
0xfffa9500, 0xfffb9800, 0xfffb9900, 0xfffb9c00, 0xfffc9d00, 0xfffca000, 0xfffca100, 0xfffda400,
0xfffda700, 0xfffda800, 0xfffdab00, 0xfffdac00, 0xfffdae00, 0xfffeaf00, 0xfffeb100, 0xfffeb400,
0xfffeb500, 0xfffeb800, 0xfffeb900, 0xfffeba00, 0xfffebb00, 0xfffebd00, 0xfffebe00, 0xfffec200,
0xfffec400, 0xfffec500, 0xfffec700, 0xfffec800, 0xfffeca01, 0xfffeca01, 0xfffecc02, 0xfffecf04,
0xfffecf04, 0xfffed106, 0xfffed308, 0xfffed50a, 0xfffed60a, 0xfffed80c, 0xfffed90d, 0xffffdb10,
0xffffdc14, 0xffffdd16, 0xffffde1b, 0xffffdf1e, 0xffffe122, 0xffffe224, 0xffffe328, 0xffffe531,
0xffffe635, 0xffffe73c, 0xffffe83f, 0xffffea46, 0xffffeb49, 0xffffec50, 0xffffed54, 0xffffee5f,
0xffffef67, 0xfffff06a, 0xfffff172, 0xfffff177, 0xfffff280, 0xfffff285, 0xfffff38e, 0xfffff49a,
0xfffff59e, 0xfffff5a6, 0xfffff6aa, 0xfffff7b3, 0xfffff7b6, 0xfffff8bd, 0xfffff8c1, 0xfffff9ca,
0xfffffad1, 0xfffffad4, 0xfffffcdb, 0xfffffcdf, 0xfffffde5, 0xfffffde8, 0xfffffeee, 0xfffffff6
};
for (u32 i = 0; i < 256; i++) {
const auto c = iron_palette[i];
array[i] = RGBA8_MAXALPHA((c >> 16) & 0xFF, (c >> 8) & 0xFF, (c >> 0) & 0xFF);
}
return array;
}
// ARGB Ironbow palette
constexpr auto iron_palette = generte_iron_palette_table();
void irsConvertConfigExToNormal(const IrsImageTransferProcessorExConfig* ex, IrsImageTransferProcessorConfig* nor) {
std::memcpy(nor, ex, sizeof(*nor));
}
void irsConvertConfigNormalToEx(const IrsImageTransferProcessorConfig* nor, IrsImageTransferProcessorExConfig* ex) {
std::memcpy(ex, nor, sizeof(*nor));
}
} // namespace
Menu::Menu() : MenuBase{"Irs"_i18n} {
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}});
SetAction(Button::X, Action{"Options"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
SidebarEntryArray::Items controller_str;
for (u32 i = 0; i < IRS_MAX_CAMERAS; i++) {
controller_str.emplace_back(GetEntryName(i));
}
SidebarEntryArray::Items rotation_str;
rotation_str.emplace_back("0 (Sideways)"_i18n);
rotation_str.emplace_back("90 (Flat)"_i18n);
rotation_str.emplace_back("180 (-Sideways)"_i18n);
rotation_str.emplace_back("270 (Upside down)"_i18n);
SidebarEntryArray::Items colour_str;
colour_str.emplace_back("Grey"_i18n);
colour_str.emplace_back("Ironbow"_i18n);
colour_str.emplace_back("Green"_i18n);
colour_str.emplace_back("Red"_i18n);
colour_str.emplace_back("Blue"_i18n);
SidebarEntryArray::Items light_target_str;
light_target_str.emplace_back("All leds"_i18n);
light_target_str.emplace_back("Bright group"_i18n);
light_target_str.emplace_back("Dim group"_i18n);
light_target_str.emplace_back("None"_i18n);
SidebarEntryArray::Items gain_str;
for (u32 i = GAIN_MIN; i <= GAIN_MAX; i++) {
gain_str.emplace_back(std::to_string(i));
}
SidebarEntryArray::Items is_negative_image_used_str;
is_negative_image_used_str.emplace_back("Normal image"_i18n);
is_negative_image_used_str.emplace_back("Negative image"_i18n);
SidebarEntryArray::Items format_str;
format_str.emplace_back("320x240"_i18n);
format_str.emplace_back("160x120"_i18n);
format_str.emplace_back("80x60"_i18n);
if (hosversionAtLeast(4,0,0)) {
format_str.emplace_back("40x30"_i18n);
format_str.emplace_back("20x15"_i18n);
}
options->Add(std::make_shared<SidebarEntryArray>("Controller"_i18n, controller_str, [this](s64& index){
irsStopImageProcessor(m_entries[m_index].m_handle);
m_index = index;
UpdateConfig(&m_config);
}, m_index));
options->Add(std::make_shared<SidebarEntryArray>("Rotation"_i18n, rotation_str, [this](s64& index){
m_rotation = (Rotation)index;
}, m_rotation));
options->Add(std::make_shared<SidebarEntryArray>("Colour"_i18n, colour_str, [this](s64& index){
m_colour = (Colour)index;
updateColourArray();
}, m_colour));
options->Add(std::make_shared<SidebarEntryArray>("Light Target"_i18n, light_target_str, [this](s64& index){
m_config.light_target = index;
UpdateConfig(&m_config);
}, m_config.light_target));
options->Add(std::make_shared<SidebarEntryArray>("Gain"_i18n, gain_str, [this](s64& index){
m_config.gain = GAIN_MIN + index;
UpdateConfig(&m_config);
}, m_config.gain - GAIN_MIN));
options->Add(std::make_shared<SidebarEntryArray>("Negative Image"_i18n, is_negative_image_used_str, [this](s64& index){
m_config.is_negative_image_used = index;
UpdateConfig(&m_config);
}, m_config.is_negative_image_used));
options->Add(std::make_shared<SidebarEntryArray>("Format"_i18n, format_str, [this](s64& index){
m_config.orig_format = index;
m_config.trimming_format = index;
UpdateConfig(&m_config);
}, m_config.orig_format));
if (hosversionAtLeast(4,0,0)) {
options->Add(std::make_shared<SidebarEntryArray>("Trimming Format"_i18n, format_str, [this](s64& index){
// you cannot set trim a larger region than the source
if (index < m_config.orig_format) {
index = m_config.orig_format;
} else {
m_config.trimming_format = index;
UpdateConfig(&m_config);
}
}, m_config.orig_format));
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);
}));
}
options->Add(std::make_shared<SidebarEntryCallback>("Load Default"_i18n, [this](){
LoadDefaultConfig();
}, true));
}});
if (R_FAILED(m_init_rc = irsInitialize())) {
return;
}
static_assert(IRS_MAX_CAMERAS >= 9, "max camaeras has gotten smaller!");
// open all handles
irsGetIrCameraHandle(&m_entries[0].m_handle, HidNpadIdType_No1);
irsGetIrCameraHandle(&m_entries[1].m_handle, HidNpadIdType_No2);
irsGetIrCameraHandle(&m_entries[2].m_handle, HidNpadIdType_No3);
irsGetIrCameraHandle(&m_entries[3].m_handle, HidNpadIdType_No4);
irsGetIrCameraHandle(&m_entries[4].m_handle, HidNpadIdType_No5);
irsGetIrCameraHandle(&m_entries[5].m_handle, HidNpadIdType_No6);
irsGetIrCameraHandle(&m_entries[6].m_handle, HidNpadIdType_No7);
irsGetIrCameraHandle(&m_entries[7].m_handle, HidNpadIdType_No8);
irsGetIrCameraHandle(&m_entries[8].m_handle, HidNpadIdType_Handheld);
// get status of all handles
PollCameraStatus(true);
// load default config
LoadDefaultConfig();
}
Menu::~Menu() {
ResetImage();
for (auto& e : m_entries) {
irsStopImageProcessor(e.m_handle);
}
// this closes all handles
irsExit();
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
PollCameraStatus();
SetTitleSubHeading(GetEntryName(m_index));
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
IrsImageTransferProcessorState state;
const auto rc = irsGetImageTransferProcessorState(m_entries[m_index].m_handle, m_irs_buffer.data(), m_irs_buffer.size(), &state);
if (R_SUCCEEDED(rc) && state.sampling_number != m_prev_state.sampling_number) {
m_prev_state = state;
SetSubHeading("Ambient Noise Level: "_i18n + std::to_string(m_prev_state.ambient_noise_level));
updateColourArray();
}
if (m_image) {
float cx{}, cy{};
float w{}, h{};
float angle{};
switch (m_rotation) {
case Rotation_0: {
const auto scale_x = m_pos.w / float(m_irs_width);
const auto scale_y = m_pos.h / float(m_irs_height);
const auto scale = std::min(scale_x, scale_y);
w = m_irs_width * scale;
h = m_irs_height * scale;
cx = (m_pos.x + m_pos.w / 2.F) - w / 2.F;
cy = (m_pos.y + m_pos.h / 2.F) - h / 2.F;
angle = 0;
} break;
case Rotation_90: {
const auto scale_x = m_pos.w / float(m_irs_height);
const auto scale_y = m_pos.h / float(m_irs_width);
const auto scale = std::min(scale_x, scale_y);
w = m_irs_width * scale;
h = m_irs_height * scale;
cx = (m_pos.x + m_pos.w / 2.F) + h / 2.F;
cy = (m_pos.y + m_pos.h / 2.F) - w / 2.F;
angle = 90;
} break;
case Rotation_180: {
const auto scale_x = m_pos.w / float(m_irs_width);
const auto scale_y = m_pos.h / float(m_irs_height);
const auto scale = std::min(scale_x, scale_y);
w = m_irs_width * scale;
h = m_irs_height * scale;
cx = (m_pos.x + m_pos.w / 2.F) + w / 2.F;
cy = (m_pos.y + m_pos.h / 2.F) + h / 2.F;
angle = 180;
} break;
case Rotation_270: {
const auto scale_x = m_pos.w / float(m_irs_height);
const auto scale_y = m_pos.h / float(m_irs_width);
const auto scale = std::min(scale_x, scale_y);
w = m_irs_width * scale;
h = m_irs_height * scale;
cx = (m_pos.x + m_pos.w / 2.F) - h / 2.F;
cy = (m_pos.y + m_pos.h / 2.F) + w / 2.F;
angle = 270;
} break;
}
nvgSave(vg);
nvgTranslate(vg, cx, cy);
const auto paint = nvgImagePattern(vg, 0, 0, w, h, 0, m_image, 1.f);
nvgRotate(vg, nvgDegToRad(angle));
nvgBeginPath(vg);
nvgRect(vg, 0, 0, w, h);
nvgFillPaint(vg, paint);
nvgFill(vg);
nvgRestore(vg);
}
}
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) {
int index = 0;
for (auto& e : m_entries) {
IrsIrCameraStatus status;
if (R_FAILED(irsGetIrCameraStatus(e.m_handle, &status))) {
log_write("failed to get ir status\n");
continue;
}
if (e.status != status || statup) {
e.status = status;
e.m_update_needed = false;
log_write("status changed\n");
switch (e.status) {
case IrsIrCameraStatus_Available:
if (hosversionAtLeast(4,0,0)) {
// calling this breaks the handle, kinda
#if 0
if (R_FAILED(irsCheckFirmwareUpdateNecessity(e.m_handle, &e.m_update_needed))) {
log_write("failed to check if update needed: %u\n", e.m_update_needed);
} else {
if (e.m_update_needed) {
log_write("update needed\n");
} else {
log_write("no update needed\n");
}
}
#endif
}
log_write("irs index: %d status: IrsIrCameraStatus_Available\n", index);
break;
case IrsIrCameraStatus_Unsupported:
log_write("irs index: %d status: IrsIrCameraStatus_Unsupported\n", index);
break;
case IrsIrCameraStatus_Unconnected:
log_write("irs index: %d status: IrsIrCameraStatus_Unconnected\n", index);
break;
}
}
index++;
}
}
void Menu::ResetImage() {
if (m_image) {
nvgDeleteImage(App::GetVg(), m_image);
m_image = 0;
}
}
void Menu::UpdateImage() {
ResetImage();
m_image = nvgCreateImageRGBA(App::GetVg(), m_irs_width, m_irs_height, NVG_IMAGE_NEAREST, (const unsigned char*)m_rgba.data());
}
void Menu::LoadDefaultConfig() {
IrsImageTransferProcessorExConfig ex_config;
if (hosversionAtLeast(4,0,0)) {
irsGetDefaultImageTransferProcessorExConfig(&ex_config);
} else {
IrsImageTransferProcessorConfig nor_config;
irsGetDefaultImageTransferProcessorConfig(&nor_config);
irsConvertConfigNormalToEx(&nor_config, &ex_config);
}
irsGetMomentProcessorDefaultConfig(&m_moment_config);
irsGetClusteringProcessorDefaultConfig(&m_clustering_config);
irsGetIrLedProcessorDefaultConfig(&m_led_config);
m_tera_config = {};
m_adaptive_config = {};
m_hand_config = {};
UpdateConfig(&ex_config);
}
void Menu::UpdateConfig(const IrsImageTransferProcessorExConfig* config) {
m_config = *config;
irsStopImageProcessor(m_entries[m_index].m_handle);
if (R_FAILED(irsRunMomentProcessor(m_entries[m_index].m_handle, &m_moment_config))) {
log_write("failed to irsRunMomentProcessor\n");
} else {
log_write("did irsRunMomentProcessor\n");
}
if (R_FAILED(irsRunClusteringProcessor(m_entries[m_index].m_handle, &m_clustering_config))) {
log_write("failed to irsRunClusteringProcessor\n");
} else {
log_write("did irsRunClusteringProcessor\n");
}
if (R_FAILED(irsRunPointingProcessor(m_entries[m_index].m_handle))) {
log_write("failed to irsRunPointingProcessor\n");
} else {
log_write("did irsRunPointingProcessor\n");
}
if (R_FAILED(irsRunTeraPluginProcessor(m_entries[m_index].m_handle, &m_tera_config))) {
log_write("failed to irsRunTeraPluginProcessor\n");
} else {
log_write("did irsRunTeraPluginProcessor\n");
}
if (R_FAILED(irsRunIrLedProcessor(m_entries[m_index].m_handle, &m_led_config))) {
log_write("failed to irsRunIrLedProcessor\n");
} else {
log_write("did irsRunIrLedProcessor\n");
}
if (R_FAILED(irsRunAdaptiveClusteringProcessor(m_entries[m_index].m_handle, &m_adaptive_config))) {
log_write("failed to irsRunAdaptiveClusteringProcessor\n");
} else {
log_write("did irsRunAdaptiveClusteringProcessor\n");
}
if (R_FAILED(irsRunHandAnalysis(m_entries[m_index].m_handle, &m_hand_config))) {
log_write("failed to irsRunHandAnalysis\n");
} else {
log_write("did irsRunHandAnalysis\n");
}
if (hosversionAtLeast(4,0,0)) {
m_init_rc = irsRunImageTransferExProcessor(m_entries[m_index].m_handle, &m_config, 0x10000000);
} else {
IrsImageTransferProcessorConfig nor;
irsConvertConfigExToNormal(&m_config, &nor);
m_init_rc = irsRunImageTransferProcessor(m_entries[m_index].m_handle, &nor, 0x10000000);
}
if (R_FAILED(m_init_rc)) {
log_write("irs failed to set config!\n");
}
auto format = m_config.orig_format;
log_write("IRS CONFIG\n");
log_write("\texposure_time: %lu\n", m_config.exposure_time);
log_write("\tlight_target: %u\n", m_config.light_target);
log_write("\tgain: %u\n", m_config.gain);
log_write("\tis_negative_image_used: %u\n", m_config.is_negative_image_used);
log_write("\tlight_target: %u\n", m_config.light_target);
if (hosversionAtLeast(4,0,0)) {
format = m_config.trimming_format;
log_write("\ttrimming_format: %u\n", m_config.trimming_format);
log_write("\ttrimming_start_x: %u\n", m_config.trimming_start_x);
log_write("\ttrimming_start_y: %u\n", m_config.trimming_start_y);
log_write("\tis_external_light_filter_enabled: %u\n", m_config.is_external_light_filter_enabled);
}
switch (format) {
case IrsImageTransferProcessorFormat_320x240:
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_320x240");
m_irs_width = 320;
m_irs_height = 240;
break;
case IrsImageTransferProcessorFormat_160x120:
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_160x120");
m_irs_width = 160;
m_irs_height = 120;
break;
case IrsImageTransferProcessorFormat_80x60:
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_80x60");
m_irs_width = 80;
m_irs_height = 60;
break;
case IrsImageTransferProcessorFormat_40x30:
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_40x30");
m_irs_width = 40;
m_irs_height = 30;
break;
case IrsImageTransferProcessorFormat_20x15:
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_20x15");
m_irs_width = 20;
m_irs_height = 15;
break;
}
m_rgba.resize(m_irs_width * m_irs_height);
m_irs_buffer.resize(m_irs_width * m_irs_height);
m_prev_state.sampling_number = UINT64_MAX;
std::fill(m_irs_buffer.begin(), m_irs_buffer.end(), 0);
updateColourArray();
}
void Menu::updateColourArray() {
const auto ir_width = m_irs_width;
const auto ir_height = m_irs_height;
const auto colour = m_colour;
for (u32 y = 0; y < ir_height; y++) {
for (u32 x = 0; x < ir_width; x++) {
const u32 pos = y * ir_width + x;
const u32 pos2 = y * ir_width + x;
switch (colour) {
case Colour_Grey:
m_rgba[pos] = RGBA8_MAXALPHA(m_irs_buffer[pos2], m_irs_buffer[pos2], m_irs_buffer[pos2]);
break;
case Colour_Ironbow:
m_rgba[pos] = iron_palette[m_irs_buffer[pos2]];
break;
case Colour_Green:
m_rgba[pos] = RGBA8_MAXALPHA(0, m_irs_buffer[pos2], 0);
break;
case Colour_Red:
m_rgba[pos] = RGBA8_MAXALPHA(m_irs_buffer[pos2], 0, 0);
break;
case Colour_Blue:
m_rgba[pos] = RGBA8_MAXALPHA(0, 0, m_irs_buffer[pos2]);
break;
}
}
}
UpdateImage();
}
auto Menu::GetEntryName(s64 i) -> std::string {
const auto& e = m_entries[i];
std::string text = "Pad "_i18n + (i == 8 ? "HandHeld"_i18n : std::to_string(i));
switch (e.status) {
case IrsIrCameraStatus_Available:
text += " (Available)"_i18n;
break;
case IrsIrCameraStatus_Unsupported:
text += " (Unsupported)"_i18n;
break;
case IrsIrCameraStatus_Unconnected:
text += " (Unconnected)"_i18n;
break;
}
return text;
}
} // namespace sphaira::ui::menu::irs

View File

@@ -6,6 +6,15 @@
#include "ui/progress_box.hpp" #include "ui/progress_box.hpp"
#include "ui/error_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 "app.hpp"
#include "log.hpp" #include "log.hpp"
#include "download.hpp" #include "download.hpp"
@@ -22,6 +31,22 @@ namespace {
constexpr const char* GITHUB_URL{"https://api.github.com/repos/ITotalJustice/sphaira/releases/latest"}; 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"}; 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 { auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string version) -> bool {
static fs::FsPath zip_out{"/switch/sphaira/cache/update.zip"}; static fs::FsPath zip_out{"/switch/sphaira/cache/update.zip"};
constexpr auto chunk_size = 1024 * 512; // 512KiB constexpr auto chunk_size = 1024 * 512; // 512KiB
@@ -139,8 +164,24 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
return true; 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 } // namespace
auto GetMiscMenuEntries() -> std::span<const MiscMenuEntry> {
return MISC_MENU_ENTRIES;
}
MainMenu::MainMenu() { MainMenu::MainMenu() {
curl::Api().ToFileAsync( curl::Api().ToFileAsync(
curl::Url{GITHUB_URL}, curl::Url{GITHUB_URL},
@@ -208,9 +249,7 @@ MainMenu::MainMenu() {
this->SetActions( this->SetActions(
std::make_pair(Button::START, Action{App::Exit}), std::make_pair(Button::START, Action{App::Exit}),
std::make_pair(Button::SELECT, Action{"Misc"_i18n, [this](){ std::make_pair(Button::SELECT, Action{App::DisplayMiscOptions}),
App::DisplayMiscOptions();
}}),
std::make_pair(Button::Y, Action{"Menu"_i18n, [this](){ 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); auto options = std::make_shared<Sidebar>("Menu Options"_i18n, "v" APP_VERSION_HASH, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(options)); ON_SCOPE_EXIT(App::Push(options));
@@ -241,15 +280,15 @@ MainMenu::MainMenu() {
options->Add(std::make_shared<SidebarEntryBool>("Ftp"_i18n, App::GetFtpEnable(), [](bool& enable){ options->Add(std::make_shared<SidebarEntryBool>("Ftp"_i18n, App::GetFtpEnable(), [](bool& enable){
App::SetFtpEnable(enable); App::SetFtpEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<SidebarEntryBool>("Mtp"_i18n, App::GetMtpEnable(), [](bool& enable){ options->Add(std::make_shared<SidebarEntryBool>("Mtp"_i18n, App::GetMtpEnable(), [](bool& enable){
App::SetMtpEnable(enable); App::SetMtpEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
options->Add(std::make_shared<SidebarEntryBool>("Nxlink"_i18n, App::GetNxlinkEnable(), [](bool& enable){ options->Add(std::make_shared<SidebarEntryBool>("Nxlink"_i18n, App::GetNxlinkEnable(), [](bool& enable){
App::SetNxlinkEnable(enable); App::SetNxlinkEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }));
if (m_update_state == UpdateState::Update) { if (m_update_state == UpdateState::Update) {
options->Add(std::make_shared<SidebarEntryCallback>("Download update: "_i18n + m_update_version, [this](){ options->Add(std::make_shared<SidebarEntryCallback>("Download update: "_i18n + m_update_version, [this](){
@@ -288,7 +327,7 @@ MainMenu::MainMenu() {
m_homebrew_menu = std::make_shared<homebrew::Menu>(); m_homebrew_menu = std::make_shared<homebrew::Menu>();
m_filebrowser_menu = std::make_shared<filebrowser::Menu>(m_homebrew_menu->GetHomebrewList()); 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; m_current_menu = m_homebrew_menu;
AddOnLRPress(); AddOnLRPress();
@@ -339,16 +378,16 @@ void MainMenu::OnLRPress(std::shared_ptr<MenuBase> menu, Button b) {
void MainMenu::AddOnLRPress() { void MainMenu::AddOnLRPress() {
if (m_current_menu != m_filebrowser_menu) { 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]{ SetAction(Button::L, Action{i18n::get(label), [this]{
OnLRPress(m_filebrowser_menu, Button::L); OnLRPress(m_filebrowser_menu, Button::L);
}}); }});
} }
if (m_current_menu != m_app_store_menu) { if (m_current_menu != m_right_side_menu) {
const auto label = m_current_menu == m_homebrew_menu ? "Store" : "Apps"; 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]{ SetAction(Button::R, Action{i18n::get(label), [this]{
OnLRPress(m_app_store_menu, Button::R); OnLRPress(m_right_side_menu, Button::R);
}}); }});
} }
} }

View File

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

View File

@@ -10,8 +10,8 @@
namespace sphaira::ui::menu::usb { namespace sphaira::ui::menu::usb {
namespace { namespace {
constexpr u64 CONNECTION_TIMEOUT = 1e+9 * 1; // 1 second constexpr u64 CONNECTION_TIMEOUT = UINT64_MAX;
constexpr u64 TRANSFER_TIMEOUT = 1e+9 * 5; // 5 seconds constexpr u64 TRANSFER_TIMEOUT = UINT64_MAX;
void thread_func(void* user) { void thread_func(void* user) {
auto app = static_cast<Menu*>(user); auto app = static_cast<Menu*>(user);
@@ -22,6 +22,9 @@ void thread_func(void* user) {
} }
const auto rc = app->m_usb_source->IsUsbConnected(CONNECTION_TIMEOUT); const auto rc = app->m_usb_source->IsUsbConnected(CONNECTION_TIMEOUT);
if (rc == app->m_usb_source->Result_Cancelled) {
break;
}
// set connected status // set connected status
mutexLock(&app->m_mutex); mutexLock(&app->m_mutex);
@@ -76,6 +79,7 @@ Menu::Menu() : MenuBase{"USB"_i18n} {
} }
mutexInit(&m_mutex); mutexInit(&m_mutex);
if (m_state != State::Failed) { if (m_state != State::Failed) {
threadCreate(&m_thread, thread_func, this, nullptr, 1024*32, 0x2C, 1); threadCreate(&m_thread, thread_func, this, nullptr, 1024*32, 0x2C, 1);
threadStart(&m_thread); threadStart(&m_thread);
@@ -85,6 +89,7 @@ Menu::Menu() : MenuBase{"USB"_i18n} {
Menu::~Menu() { Menu::~Menu() {
// signal for thread to exit and wait. // signal for thread to exit and wait.
m_stop_source.request_stop(); m_stop_source.request_stop();
m_usb_source->SignalCancel();
threadWaitForExit(&m_thread); threadWaitForExit(&m_thread);
threadClose(&m_thread); threadClose(&m_thread);
@@ -117,6 +122,8 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
const auto rc = yati::InstallFromSource(pbox, m_usb_source, file_name); const auto rc = yati::InstallFromSource(pbox, m_usb_source, file_name);
if (R_FAILED(rc)) { if (R_FAILED(rc)) {
m_usb_source->SignalCancel();
log_write("exiting usb install\n");
return false; return false;
} }

View File

@@ -1,4 +1,5 @@
#include "ui/nvg_util.hpp" #include "ui/nvg_util.hpp"
#include "log.hpp"
#include <cstddef> #include <cstddef>
#include <cstdio> #include <cstdio>
#include <cstdarg> #include <cstdarg>
@@ -10,6 +11,9 @@
namespace sphaira::ui::gfx { namespace sphaira::ui::gfx {
namespace { 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 = { constexpr std::array buttons = {
std::pair{Button::A, "\uE0E0"}, std::pair{Button::A, "\uE0E0"},
std::pair{Button::B, "\uE0E1"}, std::pair{Button::B, "\uE0E1"},
@@ -17,10 +21,8 @@ constexpr std::array buttons = {
std::pair{Button::Y, "\uE0E3"}, std::pair{Button::Y, "\uE0E3"},
std::pair{Button::L, "\uE0E4"}, std::pair{Button::L, "\uE0E4"},
std::pair{Button::R, "\uE0E5"}, std::pair{Button::R, "\uE0E5"},
std::pair{Button::L, "\uE0E6"}, std::pair{Button::L2, "\uE0E6"},
std::pair{Button::R, "\uE0E7"}, std::pair{Button::R2, "\uE0E7"},
std::pair{Button::L2, "\uE0E8"},
std::pair{Button::R2, "\uE0E9"},
std::pair{Button::UP, "\uE0EB"}, std::pair{Button::UP, "\uE0EB"},
std::pair{Button::DOWN, "\uE0EC"}, std::pair{Button::DOWN, "\uE0EC"},
std::pair{Button::LEFT, "\uE0ED"}, std::pair{Button::LEFT, "\uE0ED"},
@@ -33,8 +35,29 @@ constexpr std::array buttons = {
std::pair{Button::R3, "\uE105"}, std::pair{Button::R3, "\uE105"},
}; };
// software based clipping, saves a few cpu cycles.
bool ClipRect(float x, float y) {
return x >= SCREEN_WIDTH || y >= SCREEN_HEIGHT;
}
bool ClipText(float x, float y, int align) {
if ((!(align & ALIGN_HOR) || (align & NVG_ALIGN_LEFT)) && x >= SCREEN_WIDTH) {
return true;
}
if ((!(align & ALIGN_VER) || (align & NVG_ALIGN_TOP)) && y >= SCREEN_HEIGHT) {
return true;
}
return false;
}
// NEW --------------------- // NEW ---------------------
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) { void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) {
if (ClipRect(v.x, v.y)) {
return;
}
nvgBeginPath(vg); nvgBeginPath(vg);
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded); nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
nvgFillColor(vg, c); nvgFillColor(vg, c);
@@ -42,6 +65,10 @@ void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rou
} }
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) { void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) {
if (ClipRect(v.x, v.y)) {
return;
}
nvgBeginPath(vg); nvgBeginPath(vg);
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded); nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
nvgFillPaint(vg, p); nvgFillPaint(vg, p);
@@ -120,6 +147,10 @@ void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, con
} }
void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, const Vec4& v, const NVGcolor& c) { 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; const auto corner_radius = 0.5;
drawRectOutlineInternal(vg, theme, size, v); drawRectOutlineInternal(vg, theme, size, v);
nvgBeginPath(vg); nvgBeginPath(vg);
@@ -129,6 +160,10 @@ void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, con
} }
void drawTextIntenal(NVGcontext* vg, const Vec2& v, float size, const char* str, const char* end, int align, const NVGcolor& c) { 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); nvgBeginPath(vg);
nvgFontSize(vg, size); nvgFontSize(vg, size);
nvgTextAlign(vg, align); nvgTextAlign(vg, align);
@@ -166,6 +201,10 @@ void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture,
} }
void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, const NVGcolor& c, const char* str, int align, const char* end) { 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); nvgBeginPath(vg);
nvgFontSize(vg, size); nvgFontSize(vg, size);
nvgTextAlign(vg, align); nvgTextAlign(vg, align);

View File

@@ -23,7 +23,7 @@ auto OptionBoxEntry::Selected(bool enable) -> void {
m_selected = enable; 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_message{message}
, m_callback{cb} { , m_callback{cb} {
@@ -40,14 +40,15 @@ OptionBox::OptionBox(const std::string& message, const Option& a, Callback cb)
Setup(0); Setup(0);
} }
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb) OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb, int image)
: OptionBox{message, a, b, 0, cb} { : 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_message{message}
, m_callback{cb} { , m_callback{cb}
, m_image{image} {
m_pos.w = 770.f; m_pos.w = 770.f;
m_pos.h = 295.f; m_pos.h = 295.f;
@@ -65,17 +66,6 @@ OptionBox::OptionBox(const std::string& message, const Option& a, const Option&
Setup(index); 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 { auto OptionBox::Update(Controller* controller, TouchInfo* touch) -> void {
Widget::Update(controller, touch); Widget::Update(controller, touch);
@@ -92,13 +82,25 @@ auto OptionBox::Update(Controller* controller, TouchInfo* touch) -> void {
} }
auto OptionBox::Draw(NVGcontext* vg, Theme* theme) -> void { auto OptionBox::Draw(NVGcontext* vg, Theme* theme) -> void {
const float padding = 15;
gfx::dimBackground(vg); gfx::dimBackground(vg);
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP)); gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP), 5);
nvgSave(vg); nvgSave(vg);
nvgTextLineHeight(vg, 1.5); 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); nvgRestore(vg);
gfx::drawRect(vg, m_spacer_line, theme->GetColour(ThemeEntryID_LINE_SEPARATOR)); gfx::drawRect(vg, m_spacer_line, theme->GetColour(ThemeEntryID_LINE_SEPARATOR));

View File

@@ -73,6 +73,8 @@ PopupList::PopupList(std::string title, Items items, Callback cb, s64 index)
}}) }})
); );
m_starting_index = m_index;
m_pos.w = 1280.f; m_pos.w = 1280.f;
const float a = std::min(370.f, (60.f * static_cast<float>(m_items.size()))); const float a = std::min(370.f, (60.f * static_cast<float>(m_items.size())));
m_pos.h = 80.f + 140.f + a; m_pos.h = 80.f + 140.f + a;
@@ -110,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) { m_list->Draw(vg, theme, m_items.size(), [this](auto* vg, auto* theme, auto v, auto i) {
const auto& [x, y, w, h] = v; 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::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 { } else {
if (i != m_items.size() - 1) { if (i != m_items.size() - 1) {
gfx::drawRect(vg, x, y + h, w, 1.f, theme->GetColour(ThemeEntryID_LINE_SEPARATOR)); 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); Widget::Draw(vg, theme);

View File

@@ -95,7 +95,7 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
} }
gfx::dimBackground(vg); 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. // The pop up shape.
// const Vec4 box = { 255, 145, 770, 430 }; // const Vec4 box = { 255, 145, 770, 430 };

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_true_str{std::move(true_str)}
, m_false_str{std::move(false_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](){ SetAction(Button::A, Action{"OK"_i18n, [this](){
m_option ^= 1; m_option ^= 1;
m_callback(m_option); m_callback(m_option);

View File

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

View File

@@ -172,6 +172,7 @@ auto GetKeyGenStr(u8 key_gen) -> const char* {
case KeyGeneration_1700: return "17.0.0"; case KeyGeneration_1700: return "17.0.0";
case KeyGeneration_1800: return "18.0.0"; case KeyGeneration_1800: return "18.0.0";
case KeyGeneration_1900: return "19.0.0"; case KeyGeneration_1900: return "19.0.0";
case KeyGeneration_2000: return "20.0.0";
} }
return "Unknown"; return "Unknown";

View File

@@ -7,6 +7,38 @@ namespace {
} // namespace } // namespace
auto GetMetaTypeStr(u8 meta_type) -> const char* {
switch (meta_type) {
case NcmContentMetaType_Unknown: return "Unknown";
case NcmContentMetaType_SystemProgram: return "SystemProgram";
case NcmContentMetaType_SystemData: return "SystemData";
case NcmContentMetaType_SystemUpdate: return "SystemUpdate";
case NcmContentMetaType_BootImagePackage: return "BootImagePackage";
case NcmContentMetaType_BootImagePackageSafe: return "BootImagePackageSafe";
case NcmContentMetaType_Application: return "Application";
case NcmContentMetaType_Patch: return "Patch";
case NcmContentMetaType_AddOnContent: return "AddOnContent";
case NcmContentMetaType_Delta: return "Delta";
case NcmContentMetaType_DataPatch: return "DataPatch";
}
return "Unknown";
}
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 { auto GetAppId(u8 meta_type, u64 id) -> u64 {
if (meta_type == NcmContentMetaType_Patch) { if (meta_type == NcmContentMetaType_Patch) {
return id ^ 0x800; return id ^ 0x800;

View File

@@ -64,6 +64,7 @@ static_assert(sizeof(USBCmdHeader) == 0x20, "USBCmdHeader must be 0x20!");
Usb::Usb(u64 transfer_timeout) { Usb::Usb(u64 transfer_timeout) {
m_open_result = usbDsInitialize(); m_open_result = usbDsInitialize();
m_transfer_timeout = transfer_timeout; m_transfer_timeout = transfer_timeout;
ueventCreate(GetCancelEvent(), true);
// this avoids allocations during transfers. // this avoids allocations during transfers.
m_aligned.reserve(1024 * 1024 * 16); m_aligned.reserve(1024 * 1024 * 16);
} }
@@ -223,8 +224,49 @@ Result Usb::Init() {
R_SUCCEED(); R_SUCCEED();
} }
Result Usb::IsUsbConnected(u64 timeout) const { // the blow code is taken from libnx, with the addition of a uevent to cancel.
return usbDsWaitReady(timeout); Result Usb::IsUsbConnected(u64 timeout) {
Result rc;
UsbState state = UsbState_Detached;
rc = usbDsGetState(&state);
if (R_FAILED(rc)) return rc;
if (state == UsbState_Configured) return 0;
bool has_timeout = timeout != UINT64_MAX;
u64 deadline = 0;
const std::array waiters{
waiterForEvent(usbDsGetStateChangeEvent()),
waiterForUEvent(GetCancelEvent()),
};
if (has_timeout)
deadline = armGetSystemTick() + armNsToTicks(timeout);
do {
if (has_timeout) {
s64 remaining = deadline - armGetSystemTick();
timeout = remaining > 0 ? armTicksToNs(remaining) : 0;
}
s32 idx;
rc = waitObjects(&idx, waiters.data(), waiters.size(), timeout);
eventClear(usbDsGetStateChangeEvent());
// check if we got one of the cancel events.
if (R_SUCCEEDED(rc) && idx != 0) {
rc = Result_Cancelled; // cancelled.
break;
}
rc = usbDsGetState(&state);
} while (R_SUCCEEDED(rc) && state != UsbState_Configured && timeout > 0);
if (R_SUCCEEDED(rc) && state != UsbState_Configured && timeout == 0)
return KERNELRESULT(TimedOut);
return rc;
} }
Result Usb::WaitForConnection(u64 timeout, std::vector<std::string>& out_names) { Result Usb::WaitForConnection(u64 timeout, std::vector<std::string>& out_names) {
@@ -261,9 +303,22 @@ Event *Usb::GetCompletionEvent(UsbSessionEndpoint ep) const {
return std::addressof(m_endpoints[ep]->CompletionEvent); 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); 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 = Result_Cancelled; // cancelled.
}
if (R_FAILED(rc)) { if (R_FAILED(rc)) {
R_TRY(usbDsEndpoint_Cancel(m_endpoints[ep])); R_TRY(usbDsEndpoint_Cancel(m_endpoints[ep]));
@@ -287,7 +342,7 @@ Result Usb::GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_reques
R_SUCCEED(); 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; u32 urb_id;
/* If we're not configured yet, wait to become configured first. */ /* If we're not configured yet, wait to become configured first. */