Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b4e81c935 | ||
|
|
e243d5b64e | ||
|
|
252cd0cee6 | ||
|
|
14abcc50b5 | ||
|
|
134aadad5a | ||
|
|
a56bc9e4fa | ||
|
|
5bd466a9b6 | ||
|
|
16c58512ec | ||
|
|
b1b0b13f2a | ||
|
|
03e77faf06 | ||
|
|
7e381924ab | ||
|
|
5763610e54 | ||
|
|
49956a3f84 | ||
|
|
b2915a8142 | ||
|
|
e002aa9ec2 | ||
|
|
0aaf460dbf | ||
|
|
76c8b806d0 | ||
|
|
61783bc530 | ||
|
|
a3a2a04991 | ||
|
|
b6304fca75 | ||
|
|
5612ae5691 | ||
|
|
657c160599 | ||
|
|
f66494aeb5 | ||
|
|
650e7812e5 | ||
|
|
cca54340a2 | ||
|
|
8161b52e7b | ||
|
|
9390bd3865 | ||
|
|
483be133a5 | ||
|
|
e2022eac4c | ||
|
|
977331c3b2 | ||
|
|
64a40ae672 | ||
|
|
4e5e1a801b | ||
|
|
01e06a79a5 | ||
|
|
c762dafc67 | ||
|
|
fd1d461ea8 | ||
|
|
2e14e4b09b | ||
|
|
fb7b37736b | ||
|
|
12e5069168 | ||
|
|
b81bc51b1c | ||
|
|
e3f846c9ec | ||
|
|
7d5876d881 | ||
|
|
990948b912 | ||
|
|
91a08d36b4 | ||
|
|
abc7a0799d | ||
|
|
ab973a3f99 | ||
|
|
d0179b8719 | ||
|
|
78ecdc014b | ||
|
|
0751fa9a2e | ||
|
|
f05230e870 | ||
|
|
9915307be0 | ||
|
|
62183f4524 | ||
|
|
ca1b31329d | ||
|
|
3a3b8008a1 | ||
|
|
e9f0d2349c | ||
|
|
26f195b54f | ||
|
|
aa48c1696d | ||
|
|
e526f376fe | ||
|
|
78bda75985 | ||
|
|
cf95128f0b | ||
|
|
6dbf48d73c | ||
|
|
1614c8e2e4 | ||
|
|
cdebcad4fe | ||
|
|
54c63d6f3b | ||
|
|
d840a8ddba | ||
|
|
c3b31d0fdd | ||
|
|
dd1a6eb25b | ||
|
|
271fab66f5 | ||
|
|
87642e914e | ||
|
|
45aa7c4e62 | ||
|
|
9b1788d1ec | ||
|
|
389a4cfef5 | ||
|
|
ac06631156 | ||
|
|
bc39e668eb | ||
|
|
e452615c77 | ||
|
|
588eb01379 | ||
|
|
4855a01f1a | ||
|
|
cb7fb0e506 | ||
|
|
cdb38f27a7 | ||
|
|
7804bbbcbc | ||
|
|
5db5f93af1 | ||
|
|
bab4bfce84 | ||
|
|
ec06763e50 | ||
|
|
5e315bd65f | ||
|
|
2edfe91ad6 | ||
|
|
7005118876 | ||
|
|
087d44fb40 | ||
|
|
e3722f2591 | ||
|
|
47855ce7b4 | ||
|
|
ec7caabdbd | ||
|
|
adf0a3b2cd | ||
|
|
f88e354ae8 | ||
|
|
df3d8d3990 |
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: ITotalJustice
|
||||||
|
patreon: totaljustice
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: totaljustice
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
polar: # Replace with a single Polar username
|
||||||
|
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||||
|
thanks_dev: # Replace with a single thanks.dev username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
10
.github/workflows/build_presets.yml
vendored
@@ -1,10 +1,6 @@
|
|||||||
name: build
|
name: build
|
||||||
|
|
||||||
on:
|
on: [push, pull_request]
|
||||||
push:
|
|
||||||
branches: [ "master" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "master" ]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -12,7 +8,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
preset: [Release, RelWithDebInfo, MinSizeRel, Debug]
|
preset: [MinSizeRel]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
container: devkitpro/devkita64:latest
|
container: devkitpro/devkita64:latest
|
||||||
|
|
||||||
@@ -24,7 +20,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Configure CMake
|
- name: Configure CMake
|
||||||
run: |
|
run: |
|
||||||
cmake --preset ${{ matrix.preset }}
|
cmake --preset ${{ matrix.preset }} -DUSE_VFS_GC=0
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cmake --build --preset ${{ matrix.preset }} --parallel 4
|
run: cmake --build --preset ${{ matrix.preset }} --parallel 4
|
||||||
|
|||||||
45
README.md
@@ -1,10 +1,12 @@
|
|||||||
# sphaira
|
# Sphaira
|
||||||
|
|
||||||
A homebrew menu for the switch.
|
A homebrew menu for the Nintendo Switch.
|
||||||
|
|
||||||
[See the gbatemp thread for more details / discussion](https://gbatemp.net/threads/sphaira-hbmenu-replacement.664523/).
|
[See the GBATemp thread for more details / discussion](https://gbatemp.net/threads/sphaira-hbmenu-replacement.664523/).
|
||||||
|
|
||||||
## showcase
|
[We have now have a Discord server!](https://discord.gg/8vZBsrprEc). Please use the issues tab to report bugs, as it is much easier for me to track.
|
||||||
|
|
||||||
|
## Showcase
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
:-------------------------:|:-------------------------:
|
:-------------------------:|:-------------------------:
|
||||||
@@ -13,28 +15,29 @@ A homebrew menu for the switch.
|
|||||||
 | 
|
 | 
|
||||||
 | 
|
 | 
|
||||||
|
|
||||||
## bug reports
|
## Bug reports
|
||||||
|
|
||||||
for any bug reports, please use the issues tab and explain in as much detail as possible!
|
For any bug reports, please use the issues tab and explain in as much detail as possible!
|
||||||
|
|
||||||
please include:
|
Please include:
|
||||||
|
|
||||||
- CFW type (i assume Atmosphere, but someone out there is still using Rajnx)
|
- CFW type (i assume Atmosphere, but someone out there is still using Rajnx);
|
||||||
- CFW version
|
- CFW version;
|
||||||
- FW version
|
- FW version;
|
||||||
- The bug itself and how to reproduce it
|
- The bug itself and how to reproduce it.
|
||||||
|
|
||||||
## ftp
|
## FTP
|
||||||
|
|
||||||
ftp can be enabled via the network menu and listens on port 5000, no username or password is required.
|
FTP can be enabled via the network menu. It uses the same config as ftpsrv `/config/ftpsrv/config.ini`. [See here for the full list
|
||||||
|
of all configs available](https://github.com/ITotalJustice/ftpsrv/blob/master/assets/config.ini.template).
|
||||||
|
|
||||||
## mtp
|
## MTP
|
||||||
|
|
||||||
mtp can be enabled via the network menu.
|
MTP can be enabled via the Network menu.
|
||||||
|
|
||||||
## file assoc
|
## File association
|
||||||
|
|
||||||
sphaira has file assoc support. lets say your app supports loading .png files, then you could write an assoc file, then when using the file browser, clicking on a .png file will launch your app along with the .png file as argv[1]. This was primarly added for rom loading support for emulators / frontends such as retroarch, melonds, mgba etc.
|
Sphaira has file association support. Let's say your app supports loading .png files, then you could write an association file, then when using the file browser, clicking on a .png file will launch your app along with the .png file as argv[1]. This was primarly added for rom loading support for emulators / frontends such as RetroArch, MelonDS, mGBA etc.
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[config]
|
[config]
|
||||||
@@ -42,9 +45,9 @@ path=/switch/your_app.nro
|
|||||||
supported_extensions=jpg|png|mp4|mp3
|
supported_extensions=jpg|png|mp4|mp3
|
||||||
```
|
```
|
||||||
|
|
||||||
the `path` field is optional. if left out, it will use the name of the ini to find the nro. For example, if the ini is called mgba.ini, it will try to find the nro in /switch/mgba.nro and /switch/folder/mgba.nro.
|
The `path` field is optional. If left out, it will use the name of the ini to find the nro. For example, if the ini is called mgba.ini, it will try to find the nro in /switch/mgba.nro and /switch/folder/mgba.nro.
|
||||||
|
|
||||||
see `assets/romfs/assoc/` for more examples of file assoc entries
|
See `assets/romfs/assoc/` for more examples of file assoc entries.
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
@@ -56,7 +59,7 @@ see `assets/romfs/assoc/` for more examples of file assoc entries
|
|||||||
- deko3d-nanovg
|
- deko3d-nanovg
|
||||||
- libpulsar
|
- libpulsar
|
||||||
- minIni
|
- minIni
|
||||||
- gbatemp
|
- GBATemp
|
||||||
- hb-appstore
|
- hb-appstore
|
||||||
- haze
|
- haze
|
||||||
- everyone who has contributed to this project!
|
- Everyone who has contributed to this project!
|
||||||
|
|||||||
8
assets/romfs/github/ftpsrv.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"url": "https://github.com/ITotalJustice/ftpsrv",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"name": "switch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
3
assets/romfs/github/sphaira.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"url": "https://github.com/ITotalJustice/sphaira"
|
||||||
|
}
|
||||||
3
assets/romfs/github/untitled.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"url": "https://github.com/ITotalJustice/untitled"
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"Details": "Details",
|
"Details": "Details",
|
||||||
"Update": "Update",
|
"Update": "Update",
|
||||||
"Remove": "Entfernen",
|
"Remove": "Entfernen",
|
||||||
|
"Restore": "Wiederherstellen",
|
||||||
"Download": "Download",
|
"Download": "Download",
|
||||||
"Next Page": "Nächste Seite",
|
"Next Page": "Nächste Seite",
|
||||||
"Prev Page": "Vorherige Seite",
|
"Prev Page": "Vorherige Seite",
|
||||||
@@ -26,6 +27,12 @@
|
|||||||
"Star": "Favorit",
|
"Star": "Favorit",
|
||||||
"System memory": "System-Speicher",
|
"System memory": "System-Speicher",
|
||||||
"microSD card": "microSD-Karte",
|
"microSD card": "microSD-Karte",
|
||||||
|
"Sd": "SD",
|
||||||
|
"Image System memory": "System-Speicher Bild",
|
||||||
|
"Image microSD card": "microSD-Karten Bild",
|
||||||
|
"Slow": "Langsam",
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Fast": "Schnell",
|
||||||
"Yes": "Ja",
|
"Yes": "Ja",
|
||||||
"No": "Nein",
|
"No": "Nein",
|
||||||
"Enabled": "Aktiviert",
|
"Enabled": "Aktiviert",
|
||||||
@@ -46,7 +53,7 @@
|
|||||||
"Alphabetical (Star)": "Alphabetisch (Favoriten)",
|
"Alphabetical (Star)": "Alphabetisch (Favoriten)",
|
||||||
"Likes": "Likes",
|
"Likes": "Likes",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"Decending": "Absteigend",
|
"Descending": "Absteigend",
|
||||||
"Descending (down)": "Absteigend",
|
"Descending (down)": "Absteigend",
|
||||||
"Desc": "Abst.",
|
"Desc": "Abst.",
|
||||||
"Ascending": "Aufsteigend",
|
"Ascending": "Aufsteigend",
|
||||||
@@ -54,14 +61,16 @@
|
|||||||
"Asc": "Aufst.",
|
"Asc": "Aufst.",
|
||||||
|
|
||||||
"Menu Options": "Menü-Optionen",
|
"Menu Options": "Menü-Optionen",
|
||||||
"Header": "Header",
|
|
||||||
"Theme": "Theme",
|
"Theme": "Theme",
|
||||||
"Theme Options": "Theme-Optionen",
|
"Theme Options": "Theme-Optionen",
|
||||||
"Select Theme": "Theme auswählen",
|
"Select Theme": "Theme auswählen",
|
||||||
"Shuffle": "Zufällig",
|
"Shuffle": "Zufällig",
|
||||||
"Music": "Musik",
|
"Music": "Musik",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "Netzwerk",
|
"Network": "Netzwerk",
|
||||||
"Network Options": "Netzwerk-Optionen",
|
"Network Options": "Netzwerk-Optionen",
|
||||||
|
"Ftp": "FTP",
|
||||||
|
"Mtp": "MTP",
|
||||||
"Nxlink": "Nxlink",
|
"Nxlink": "Nxlink",
|
||||||
"Nxlink Connected": "Nxlink verbunden",
|
"Nxlink Connected": "Nxlink verbunden",
|
||||||
"Nxlink Upload": "Nxlink Upload",
|
"Nxlink Upload": "Nxlink Upload",
|
||||||
@@ -82,6 +91,7 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "Logging",
|
"Logging": "Logging",
|
||||||
"Replace hbmenu on exit": "hbmenu beim Beenden ersetzen",
|
"Replace hbmenu on exit": "hbmenu beim Beenden ersetzen",
|
||||||
"Misc": "Sonstiges",
|
"Misc": "Sonstiges",
|
||||||
@@ -90,6 +100,7 @@
|
|||||||
"Install forwarders": "Forwarder installieren",
|
"Install forwarders": "Forwarder installieren",
|
||||||
"Install location": "Installationsort",
|
"Install location": "Installationsort",
|
||||||
"Show install warning": "Installationswarnung anzeigen",
|
"Show install warning": "Installationswarnung anzeigen",
|
||||||
|
"Text scroll speed": "Textlaufgeschwindigkeit",
|
||||||
|
|
||||||
"FileBrowser": "Datei-Browser",
|
"FileBrowser": "Datei-Browser",
|
||||||
"%zd files": "%zd Dateien",
|
"%zd files": "%zd Dateien",
|
||||||
@@ -112,6 +123,8 @@
|
|||||||
"Create Folder": "Ordner erstellen",
|
"Create Folder": "Ordner erstellen",
|
||||||
"Set Folder Name": "Ordnernamen eingeben",
|
"Set Folder Name": "Ordnernamen eingeben",
|
||||||
"View as text (unfinished)": "Als Text anzeigen (Beta)",
|
"View as text (unfinished)": "Als Text anzeigen (Beta)",
|
||||||
|
"Ignore read only": "Schreibschutz ignorieren",
|
||||||
|
"Mount": "Einbinden",
|
||||||
"Empty...": "Leer...",
|
"Empty...": "Leer...",
|
||||||
"Open with DayBreak?": "Mit DayBreak öffnen?",
|
"Open with DayBreak?": "Mit DayBreak öffnen?",
|
||||||
"Launch ": "Starten ",
|
"Launch ": "Starten ",
|
||||||
@@ -199,6 +212,10 @@
|
|||||||
"Bad Page": "Ungültige Seite",
|
"Bad Page": "Ungültige Seite",
|
||||||
"Download theme?": "Theme herunterladen?",
|
"Download theme?": "Theme herunterladen?",
|
||||||
|
|
||||||
|
"GitHub": "GitHub",
|
||||||
|
"Downloading json": "Lade JSON herunter",
|
||||||
|
"Select asset to download for ": "Wähle Asset zum Download für ",
|
||||||
|
|
||||||
"Installing ": "Installiere ",
|
"Installing ": "Installiere ",
|
||||||
"Uninstalling ": "Deinstalliere ",
|
"Uninstalling ": "Deinstalliere ",
|
||||||
"Deleting ": "Lösche ",
|
"Deleting ": "Lösche ",
|
||||||
@@ -211,6 +228,8 @@
|
|||||||
"Copying ": "Kopiere ",
|
"Copying ": "Kopiere ",
|
||||||
"Trying to load ": "Lade ",
|
"Trying to load ": "Lade ",
|
||||||
"Downloading ": "Lade herunter ",
|
"Downloading ": "Lade herunter ",
|
||||||
|
"Downloaded ": "Heruntergeladen ",
|
||||||
|
"Removed ": "Entfernt ",
|
||||||
"Checking MD5": "Prüfe MD5",
|
"Checking MD5": "Prüfe MD5",
|
||||||
"Loading...": "Lade...",
|
"Loading...": "Lade...",
|
||||||
"Loading": "Lade",
|
"Loading": "Lade",
|
||||||
@@ -220,11 +239,19 @@
|
|||||||
"Update avaliable: ": "Update verfügbar: ",
|
"Update avaliable: ": "Update verfügbar: ",
|
||||||
"Download update: ": "Update herunterladen: ",
|
"Download update: ": "Update herunterladen: ",
|
||||||
"Updated to ": "Aktualisiert auf ",
|
"Updated to ": "Aktualisiert auf ",
|
||||||
|
"Press OK to restart Sphaira": "OK drücken um Sphaira neuzustarten",
|
||||||
"Restart Sphaira?": "Sphaira neustarten?",
|
"Restart Sphaira?": "Sphaira neustarten?",
|
||||||
"Failed to download update": "Update-Download fehlgeschlagen",
|
"Failed to download update": "Update-Download fehlgeschlagen",
|
||||||
|
"Restore hbmenu?": "hbmenu wiederherstellen?",
|
||||||
|
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Konnte /switch/hbmenu.nro nicht finden\nBitte hbmenu über den AppStore neu installieren",
|
||||||
|
"Failed to restore hbmenu, please re-download hbmenu": "Wiederherstellung fehlgeschlagen, bitte hbmenu neu herunterladen",
|
||||||
|
"Failed to restore hbmenu, using sphaira instead": "Wiederherstellung fehlgeschlagen, verwende stattdessen Sphaira",
|
||||||
|
"Restored hbmenu, closing sphaira": "hbmenu wiederhergestellt, Sphaira wird beendet",
|
||||||
|
"Restored hbmenu": "hbmenu wiederhergestellt",
|
||||||
"Delete Selected files?": "Ausgewählte Dateien löschen?",
|
"Delete Selected files?": "Ausgewählte Dateien löschen?",
|
||||||
"Completely remove ": "Vollständig entfernen ",
|
"Completely remove ": "Vollständig entfernen ",
|
||||||
"Are you sure you want to delete ": "Wirklich löschen ",
|
"Are you sure you want to delete ": "Wirklich löschen ",
|
||||||
"Are you sure you wish to cancel?": "Wirklich abbrechen?",
|
"Are you sure you wish to cancel?": "Wirklich abbrechen?",
|
||||||
|
"Audio disabled due to suspended game": "",
|
||||||
"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."
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"Details": "Details",
|
"Details": "Details",
|
||||||
"Update": "Update",
|
"Update": "Update",
|
||||||
"Remove": "Remove",
|
"Remove": "Remove",
|
||||||
|
"Restore": "Restore",
|
||||||
"Download": "Download",
|
"Download": "Download",
|
||||||
"Next Page": "Next Page",
|
"Next Page": "Next Page",
|
||||||
"Prev Page": "Prev Page",
|
"Prev Page": "Prev Page",
|
||||||
@@ -26,6 +27,12 @@
|
|||||||
"Star": "Star",
|
"Star": "Star",
|
||||||
"System memory": "System memory",
|
"System memory": "System memory",
|
||||||
"microSD card": "microSD card",
|
"microSD card": "microSD card",
|
||||||
|
"Sd": "Sd",
|
||||||
|
"Image System memory": "Image System memory",
|
||||||
|
"Image microSD card": "Image microSD card",
|
||||||
|
"Slow": "Slow",
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Fast": "Fast",
|
||||||
"Yes": "Yes",
|
"Yes": "Yes",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
"Enabled": "Enabled",
|
"Enabled": "Enabled",
|
||||||
@@ -46,7 +53,7 @@
|
|||||||
"Alphabetical (Star)": "Alphabetical (Star)",
|
"Alphabetical (Star)": "Alphabetical (Star)",
|
||||||
"Likes": "Likes",
|
"Likes": "Likes",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"Decending": "Decending",
|
"Descending": "Descending",
|
||||||
"Descending (down)": "Descending (down)",
|
"Descending (down)": "Descending (down)",
|
||||||
"Desc": "Desc",
|
"Desc": "Desc",
|
||||||
"Ascending": "Ascending",
|
"Ascending": "Ascending",
|
||||||
@@ -54,14 +61,16 @@
|
|||||||
"Asc": "Asc",
|
"Asc": "Asc",
|
||||||
|
|
||||||
"Menu Options": "Menu Options",
|
"Menu Options": "Menu Options",
|
||||||
"Header": "Header",
|
|
||||||
"Theme": "Theme",
|
"Theme": "Theme",
|
||||||
"Theme Options": "Theme Options",
|
"Theme Options": "Theme Options",
|
||||||
"Select Theme": "Select Theme",
|
"Select Theme": "Select Theme",
|
||||||
"Shuffle": "Shuffle",
|
"Shuffle": "Shuffle",
|
||||||
"Music": "Music",
|
"Music": "Music",
|
||||||
|
"12 Hour Time": "12 Hour Time",
|
||||||
"Network": "Network",
|
"Network": "Network",
|
||||||
"Network Options": "Network Options",
|
"Network Options": "Network Options",
|
||||||
|
"Ftp": "FTP",
|
||||||
|
"Mtp": "MTP",
|
||||||
"Nxlink": "Nxlink",
|
"Nxlink": "Nxlink",
|
||||||
"Nxlink Connected": "Nxlink Connected",
|
"Nxlink Connected": "Nxlink Connected",
|
||||||
"Nxlink Upload": "Nxlink Upload",
|
"Nxlink Upload": "Nxlink Upload",
|
||||||
@@ -82,6 +91,7 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "Logging",
|
"Logging": "Logging",
|
||||||
"Replace hbmenu on exit": "Replace hbmenu on exit",
|
"Replace hbmenu on exit": "Replace hbmenu on exit",
|
||||||
"Misc": "Misc",
|
"Misc": "Misc",
|
||||||
@@ -90,6 +100,7 @@
|
|||||||
"Install forwarders": "Install forwarders",
|
"Install forwarders": "Install forwarders",
|
||||||
"Install location": "Install location",
|
"Install location": "Install location",
|
||||||
"Show install warning": "Show install warning",
|
"Show install warning": "Show install warning",
|
||||||
|
"Text scroll speed": "Text scroll speed",
|
||||||
|
|
||||||
"FileBrowser": "FileBrowser",
|
"FileBrowser": "FileBrowser",
|
||||||
"%zd files": "%zd files",
|
"%zd files": "%zd files",
|
||||||
@@ -112,6 +123,8 @@
|
|||||||
"Create Folder": "Create Folder",
|
"Create Folder": "Create Folder",
|
||||||
"Set Folder Name": "Set Folder Name",
|
"Set Folder Name": "Set Folder Name",
|
||||||
"View as text (unfinished)": "View as text (unfinished)",
|
"View as text (unfinished)": "View as text (unfinished)",
|
||||||
|
"Ignore read only": "Ignore read only",
|
||||||
|
"Mount": "Mount",
|
||||||
"Empty...": "Empty...",
|
"Empty...": "Empty...",
|
||||||
"Open with DayBreak?": "Open with DayBreak?",
|
"Open with DayBreak?": "Open with DayBreak?",
|
||||||
"Launch ": "Launch ",
|
"Launch ": "Launch ",
|
||||||
@@ -199,6 +212,10 @@
|
|||||||
"Bad Page": "Bad Page",
|
"Bad Page": "Bad Page",
|
||||||
"Download theme?": "Download theme?",
|
"Download theme?": "Download theme?",
|
||||||
|
|
||||||
|
"GitHub": "GitHub",
|
||||||
|
"Downloading json": "Downloading json",
|
||||||
|
"Select asset to download for ": "Select asset to download for ",
|
||||||
|
|
||||||
"Installing ": "Installing ",
|
"Installing ": "Installing ",
|
||||||
"Uninstalling ": "Uninstalling ",
|
"Uninstalling ": "Uninstalling ",
|
||||||
"Deleting ": "Deleting ",
|
"Deleting ": "Deleting ",
|
||||||
@@ -211,6 +228,8 @@
|
|||||||
"Copying ": "Copying ",
|
"Copying ": "Copying ",
|
||||||
"Trying to load ": "Trying to load ",
|
"Trying to load ": "Trying to load ",
|
||||||
"Downloading ": "Downloading ",
|
"Downloading ": "Downloading ",
|
||||||
|
"Downloaded ": "Downloaded ",
|
||||||
|
"Removed ": "Removed ",
|
||||||
"Checking MD5": "Checking MD5",
|
"Checking MD5": "Checking MD5",
|
||||||
"Loading...": "Loading...",
|
"Loading...": "Loading...",
|
||||||
"Loading": "Loading",
|
"Loading": "Loading",
|
||||||
@@ -220,11 +239,19 @@
|
|||||||
"Update avaliable: ": "Update avaliable: ",
|
"Update avaliable: ": "Update avaliable: ",
|
||||||
"Download update: ": "Download update: ",
|
"Download update: ": "Download update: ",
|
||||||
"Updated to ": "Updated to ",
|
"Updated to ": "Updated to ",
|
||||||
|
"Press OK to restart Sphaira": "Press OK to restart Sphaira",
|
||||||
"Restart Sphaira?": "Restart Sphaira?",
|
"Restart Sphaira?": "Restart Sphaira?",
|
||||||
"Failed to download update": "Failed to download update",
|
"Failed to download update": "Failed to download update",
|
||||||
|
"Restore hbmenu?": "Restore hbmenu?",
|
||||||
|
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu",
|
||||||
|
"Failed to restore hbmenu, please re-download hbmenu": "Failed to restore hbmenu, please re-download hbmenu",
|
||||||
|
"Failed to restore hbmenu, using sphaira instead": "Failed to restore hbmenu, using sphaira instead",
|
||||||
|
"Restored hbmenu, closing sphaira": "Restored hbmenu, closing sphaira",
|
||||||
|
"Restored hbmenu": "Restored hbmenu",
|
||||||
"Delete Selected files?": "Delete Selected files?",
|
"Delete Selected files?": "Delete Selected files?",
|
||||||
"Completely remove ": "Completely remove ",
|
"Completely remove ": "Completely remove ",
|
||||||
"Are you sure you want to delete ": "Are you sure you want to delete ",
|
"Are you sure you want to delete ": "Are you sure you want to delete ",
|
||||||
"Are you sure you wish to cancel?": "Are you sure you wish to cancel?",
|
"Are you sure you wish to cancel?": "Are you sure you wish to cancel?",
|
||||||
|
"Audio disabled due to suspended game": "Audio disabled due to suspended game",
|
||||||
"If this message appears repeatedly, please open an issue.": "If this message appears repeatedly, please open an issue."
|
"If this message appears repeatedly, please open an issue.": "If this message appears repeatedly, please open an issue."
|
||||||
}
|
}
|
||||||
@@ -9,23 +9,30 @@
|
|||||||
"OK": "OK",
|
"OK": "OK",
|
||||||
"Back": "Atrás",
|
"Back": "Atrás",
|
||||||
"Select": "Seleccionar",
|
"Select": "Seleccionar",
|
||||||
"Open": "Abierto",
|
"Open": "Abrir",
|
||||||
"Launch": "Ejecutar",
|
"Launch": "Ejecutar",
|
||||||
"Info": "Información",
|
"Info": "Información",
|
||||||
"Install": "Instalar",
|
"Install": "Instalar",
|
||||||
"Delete": "Borrar",
|
"Delete": "Borrar",
|
||||||
"Restart": "",
|
"Restart": "Reiniciar",
|
||||||
"Changelog": "Log de Cambios",
|
"Changelog": "Log de cambios",
|
||||||
"Details": "Detalles",
|
"Details": "Detalles",
|
||||||
"Update": "Actualizar",
|
"Update": "Actualizar",
|
||||||
"Remove": "Borrar",
|
"Remove": "Borrar",
|
||||||
|
"Restore": "Restaurar",
|
||||||
"Download": "Descargar",
|
"Download": "Descargar",
|
||||||
"Next Page": "Página siguiente",
|
"Next Page": "Página siguiente",
|
||||||
"Prev Page": "Página anterior",
|
"Prev Page": "Página anterior",
|
||||||
"Unstar": "",
|
"Unstar": "Quitar favorito",
|
||||||
"Star": "",
|
"Star": "Favorito",
|
||||||
"System memory": "",
|
"System memory": "Memoria de sistema",
|
||||||
"microSD card": "",
|
"microSD card": "microSD",
|
||||||
|
"Sd": "SD",
|
||||||
|
"Image System memory": "Imagen memoria interna",
|
||||||
|
"Image microSD card": "Imagen tarjeta microSD",
|
||||||
|
"Slow": "Lento",
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Fast": "Rápido",
|
||||||
"Yes": "Sí",
|
"Yes": "Sí",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
"Enabled": "Activado",
|
"Enabled": "Activado",
|
||||||
@@ -38,36 +45,38 @@
|
|||||||
"Order": "Orden",
|
"Order": "Orden",
|
||||||
"Search": "Buscar",
|
"Search": "Buscar",
|
||||||
"Updated": "Actualizado",
|
"Updated": "Actualizado",
|
||||||
"Updated (Star)": "Actualizado (Star)",
|
"Updated (Star)": "Actualizado (favorito)",
|
||||||
"Downloads": "Descargas",
|
"Downloads": "Descargas",
|
||||||
"Size": "Tamaño",
|
"Size": "Tamaño",
|
||||||
"Size (Star)": "Tamaño (Star)",
|
"Size (Star)": "Tamaño (favorito)",
|
||||||
"Alphabetical": "Alfabético",
|
"Alphabetical": "Alfabético",
|
||||||
"Alphabetical (Star)": "Alfabético (Star)",
|
"Alphabetical (Star)": "Alfabético (favorito)",
|
||||||
"Likes": "Me Gusta",
|
"Likes": "Me Gusta",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"Decending": "Descendente",
|
"Descending": "Descendente",
|
||||||
"Descending (down)": "Descendente (abajo)",
|
"Descending (down)": "Descendente (abajo)",
|
||||||
"Desc": "Descendente",
|
"Desc": "Descendente",
|
||||||
"Ascending": "Ascendente",
|
"Ascending": "Ascendente",
|
||||||
"Ascending (Up)": "Ascendente (arriba)",
|
"Ascending (Up)": "Ascendente (arriba)",
|
||||||
"Asc": "Ascendente",
|
"Asc": "Ascendente",
|
||||||
|
|
||||||
"Menu Options": "Opciones de Menú",
|
"Menu Options": "Opciones de menú",
|
||||||
"Header": "Encabezamiento",
|
|
||||||
"Theme": "Tema",
|
"Theme": "Tema",
|
||||||
"Theme Options": "Opciones de Tema",
|
"Theme Options": "Opciones de tema",
|
||||||
"Select Theme": "Seleccionar Tema",
|
"Select Theme": "Seleccionar tema",
|
||||||
"Shuffle": "Barajar",
|
"Shuffle": "Barajar",
|
||||||
"Music": "Música",
|
"Music": "Música",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "Red",
|
"Network": "Red",
|
||||||
"Network Options": "Opciones de Red",
|
"Network Options": "Opciones de red",
|
||||||
"Nxlink": "Nxlink",
|
"Ftp": "FTP",
|
||||||
"Nxlink Connected": "Nxlink Conectado",
|
"Mtp": "MTP",
|
||||||
"Nxlink Upload": "Nxlink Subida",
|
"Nxlink": "NXlink",
|
||||||
"Nxlink Finished": "Nxlink Finalizado",
|
"Nxlink Connected": "NXlink conectado",
|
||||||
"Switch-Handheld!": "",
|
"Nxlink Upload": "NXlink subida",
|
||||||
"Switch-Docked!": "",
|
"Nxlink Finished": "NXlink finalizado",
|
||||||
|
"Switch-Handheld!": "¡Switch-Modo-Portátil!",
|
||||||
|
"Switch-Docked!": "¡Switch-Modo-TV!",
|
||||||
"Language": "Idioma",
|
"Language": "Idioma",
|
||||||
"Auto": "Automático",
|
"Auto": "Automático",
|
||||||
"English": "English",
|
"English": "English",
|
||||||
@@ -82,43 +91,47 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
"Logging": "Explotación florestal",
|
"Vietnamese": "Vietnamese",
|
||||||
"Replace hbmenu on exit": "Reemplazar hbmenu al salir",
|
"Logging": "Registro",
|
||||||
|
"Replace hbmenu on exit": "Reemplazar hbmenu",
|
||||||
"Misc": "Varios",
|
"Misc": "Varios",
|
||||||
"Misc Options": "Opciones varias",
|
"Misc Options": "Opciones varias",
|
||||||
"Web": "Web",
|
"Web": "Web",
|
||||||
"Install forwarders": "",
|
"Install forwarders": "Instalar forwarders",
|
||||||
"Install location": "",
|
"Install location": "Dispositivo de instalación",
|
||||||
"Show install warning": "",
|
"Show install warning": "Precaución de instalación",
|
||||||
|
"Text scroll speed": "Velocidad de scroll",
|
||||||
|
|
||||||
"FileBrowser": "Explorador de Archivos",
|
"FileBrowser": "Explorador de archivos",
|
||||||
"%zd files": "%zd files",
|
"%zd files": "%zd archivos",
|
||||||
"%zd dirs": "%zd dirs",
|
"%zd dirs": "%zd carpetas",
|
||||||
"File Options": "Opciones de Tema",
|
"File Options": "Opciones de archivo",
|
||||||
"Show Hidden": "Mostrar Oculto",
|
"Show Hidden": "Mostrar archivos ocultos",
|
||||||
"Folders First": "Carpetas primero",
|
"Folders First": "Carpetas primero",
|
||||||
"Hidden Last": "Oculto último",
|
"Hidden Last": "Ocultos al final",
|
||||||
"Cut": "Cortar ",
|
"Cut": "Cortar",
|
||||||
"Copy": "Copiar",
|
"Copy": "Copiar",
|
||||||
"Paste": "Pegar",
|
"Paste": "Pegar",
|
||||||
"Paste ": "Pegar ",
|
"Paste ": "Pegar ",
|
||||||
" file(s)?": " ¿archivo(s)?",
|
" file(s)?": " ¿archivo(s)?",
|
||||||
"Rename": "Renombrar",
|
"Rename": "Renombrar",
|
||||||
"Set New File Name": "Establecer Nuevo Nombre de Archivo",
|
"Set New File Name": "Establecer nuevo nombre de archivo",
|
||||||
"Advanced": "Avanzado",
|
"Advanced": "Avanzado",
|
||||||
"Advanced Options": "Opciones Avanzadas",
|
"Advanced Options": "Opciones avanzadas",
|
||||||
"Create File": "Crear archivo",
|
"Create File": "Crear archivo",
|
||||||
"Set File Name": "Establecer Nombre de Archivo",
|
"Set File Name": "Establecer nombre de archivo",
|
||||||
"Create Folder": "Crear carpeta",
|
"Create Folder": "Crear carpeta",
|
||||||
"Set Folder Name": "Establecer Nombre de Carpeta",
|
"Set Folder Name": "Establecer nombre de carpeta",
|
||||||
"View as text (unfinished)": "Ver como texto (sin terminar)",
|
"View as text (unfinished)": "Ver como texto (sin terminar)",
|
||||||
|
"Ignore read only": "Ignorar sólo lectura",
|
||||||
|
"Mount": "Montar",
|
||||||
"Empty...": "Vacío...",
|
"Empty...": "Vacío...",
|
||||||
"Open with DayBreak?": "Abrir con DayBreak",
|
"Open with DayBreak?": "¿Abrir con DayBreak?",
|
||||||
"Launch ": "",
|
"Launch ": "Abrir ",
|
||||||
"Launch option for: ": "Opción de ejecución para: ",
|
"Launch option for: ": "Opción de abrir con: ",
|
||||||
"Select launcher for: ": "",
|
"Select launcher for: ": "Seleccionar abrir con: ",
|
||||||
|
|
||||||
"Homebrew": "Honebrew",
|
"Homebrew": "Homebrew",
|
||||||
"Homebrew Options": "Opciones de Homebrew",
|
"Homebrew Options": "Opciones de Homebrew",
|
||||||
"Hide Sphaira": "Ocultar Sphaira",
|
"Hide Sphaira": "Ocultar Sphaira",
|
||||||
"Install Forwarder": "Instalar Forwarder",
|
"Install Forwarder": "Instalar Forwarder",
|
||||||
@@ -128,16 +141,16 @@
|
|||||||
"Creating Control": "Creando Control",
|
"Creating Control": "Creando Control",
|
||||||
"Creating Meta": "Creando Meta",
|
"Creating Meta": "Creando Meta",
|
||||||
"Writing Nca": "Creando NCA",
|
"Writing Nca": "Creando NCA",
|
||||||
"Updating ncm databse": "Actualizando base de datos ncm ",
|
"Updating ncm databse": "Actualizando base de datos ncm",
|
||||||
"Pushing application record": "",
|
"Pushing application record": "Registro de aplicación",
|
||||||
"Installed!": "¡Instalado!",
|
"Installed!": "¡Instalado!",
|
||||||
"Failed to install forwarder": "Fallo al instalar forwarder",
|
"Failed to install forwarder": "Fallo al instalar forwarder",
|
||||||
"Unstarred ": "",
|
"Unstarred ": "Quitar Favorito",
|
||||||
"Starred ": "",
|
"Starred ": "Favorito",
|
||||||
|
|
||||||
"AppStore": "AppStore",
|
"AppStore": "Tienda",
|
||||||
"Filter: %s | Sort: %s | Order: %s": "Filtrar: %s | Clasificar: %s | Orden: %s",
|
"Filter: %s | Sort: %s | Order: %s": "Filtrar: %s | Clasificar: %s | Orden: %s",
|
||||||
"AppStore Options": "Opciones de la AppStore",
|
"AppStore Options": "Opciones de la Tienda",
|
||||||
"All": "Todo",
|
"All": "Todo",
|
||||||
"Games": "Juegos",
|
"Games": "Juegos",
|
||||||
"Emulators": "Emuladores",
|
"Emulators": "Emuladores",
|
||||||
@@ -145,60 +158,64 @@
|
|||||||
"Themes": "Temas",
|
"Themes": "Temas",
|
||||||
"Legacy": "Legado",
|
"Legacy": "Legado",
|
||||||
"version: %s": "version: %s",
|
"version: %s": "version: %s",
|
||||||
"updated: %s": "updated: %s",
|
"updated: %s": "actualizado: %s",
|
||||||
"category: %s": "category: %s",
|
"category: %s": "categoría: %s",
|
||||||
"extracted: %.2f MiB": "extracted: %.2f MiB",
|
"extracted: %.2f MiB": "extraído: %.2f MiB",
|
||||||
"app_dls: %s": "app_dls: %s",
|
"app_dls: %s": "app_dls: %s",
|
||||||
"More by Author": "Mostrar mas del Autor",
|
"More by Author": "Mostrar mas del Autor",
|
||||||
"Leave Feedback": "Dejar Mensaje",
|
"Leave Feedback": "Dejar Mensaje",
|
||||||
|
|
||||||
"Irs": "IRS",
|
"Irs": "IRS",
|
||||||
"Ambient Noise Level: ": "Nivel de Ruido",
|
"Ambient Noise Level: ": "Nivel de Ruido Ambiente",
|
||||||
"Controller": "Control",
|
"Controller": "Control",
|
||||||
"Pad ": "Almohadilla ",
|
"Pad ": "GamePad ",
|
||||||
" (Available)": " (Disponible)",
|
" (Available)": " (Disponible)",
|
||||||
" (Unsupported)": "",
|
" (Unsupported)": "(No Compatible)",
|
||||||
" (Unconnected)": " (Desconectado)",
|
" (Unconnected)": " (Desconectado)",
|
||||||
"HandHeld": "Portátil",
|
"HandHeld": "Portátil",
|
||||||
"Rotation": "Rotación",
|
"Rotation": "Rotación",
|
||||||
"0 (Sideways)": "0 (De Lado)",
|
"0 (Sideways)": "0° (De lado)",
|
||||||
"90 (Flat)": "90 (Plano)",
|
"90 (Flat)": "90° (Plano)",
|
||||||
"180 (-Sideways)": "180 (-De Lado)",
|
"180 (-Sideways)": "180° (De lado)",
|
||||||
"270 (Upside down)": "270 (Al Revés)",
|
"270 (Upside down)": "270° (Al revés)",
|
||||||
"Colour": "Color",
|
"Colour": "Color",
|
||||||
"Grey": "Gris",
|
"Grey": "Gris",
|
||||||
"Ironbow": "Paleta Térmica",
|
"Ironbow": "Paleta térmica",
|
||||||
"Green": "Verde",
|
"Green": "Verde",
|
||||||
"Red": "Rojo",
|
"Red": "Rojo",
|
||||||
"Blue": "Azul",
|
"Blue": "Azul",
|
||||||
"Light Target": "Objetivo de Luz",
|
"Light Target": "Objetivo de luz",
|
||||||
"All leds": "Todos los leds",
|
"All leds": "Todos los leds",
|
||||||
"Bright group": "Grupo brillante",
|
"Bright group": "Grupo brillo",
|
||||||
"Dim group": "Grupo tenue",
|
"Dim group": "Grupo tenue",
|
||||||
"None": "Ninguno",
|
"None": "Ninguno",
|
||||||
"Gain": "Ganancia",
|
"Gain": "Ganancia",
|
||||||
"Negative Image": "Imagen Negativa",
|
"Negative Image": "Imagen negativa",
|
||||||
"Normal image": "Imagen Normal",
|
"Normal image": "Imagen normal",
|
||||||
"Negative image": "Imagen Negativa",
|
"Negative image": "Imagen negativa",
|
||||||
"Format": "Formato",
|
"Format": "Formato",
|
||||||
"320x240": "320×240",
|
"320x240": "320×240",
|
||||||
"160x120": "160×120",
|
"160x120": "160×120",
|
||||||
"80x60": "80×60",
|
"80x60": "80×60",
|
||||||
"40x30": "40×30",
|
"40x30": "40×30",
|
||||||
"20x15": "20×15",
|
"20x15": "20×15",
|
||||||
"Trimming Format": "Formato de Recorte",
|
"Trimming Format": "Formato de recorte",
|
||||||
"External Light Filter": "Filtro de Luz Externa",
|
"External Light Filter": "Filtro de luz externa",
|
||||||
"Load Default": "Cargar Predeterminado",
|
"Load Default": "Cargar predeterminado",
|
||||||
|
|
||||||
"Themezer": "Themezer",
|
"Themezer": "Themezer",
|
||||||
"Themezer Options": "Opciones de Themezer",
|
"Themezer Options": "Opciones de Themezer",
|
||||||
"Nsfw": "NSFW",
|
"Nsfw": "NSFW",
|
||||||
"Page": "Página",
|
"Page": "Página",
|
||||||
"Page %zu / %zu": "Page %zu / %zu",
|
"Page %zu / %zu": "Pág. %zu / %zu",
|
||||||
"Enter Page Number": "Ingresar Número de Página",
|
"Enter Page Number": "Ingresar Número de Página",
|
||||||
"Bad Page": "Página Errónea",
|
"Bad Page": "Página Errónea",
|
||||||
"Download theme?": "¿Descargar Tema?",
|
"Download theme?": "¿Descargar Tema?",
|
||||||
|
|
||||||
|
"GitHub": "GitHub",
|
||||||
|
"Downloading json": "Descargando json",
|
||||||
|
"Select asset to download for ": "Seleccionar recurso a descargar para ",
|
||||||
|
|
||||||
"Installing ": "Instalando ",
|
"Installing ": "Instalando ",
|
||||||
"Uninstalling ": "Desinstalando ",
|
"Uninstalling ": "Desinstalando ",
|
||||||
"Deleting ": "Borrando ",
|
"Deleting ": "Borrando ",
|
||||||
@@ -209,22 +226,32 @@
|
|||||||
"Scanning ": "Escaneando ",
|
"Scanning ": "Escaneando ",
|
||||||
"Creating ": "Creando ",
|
"Creating ": "Creando ",
|
||||||
"Copying ": "Copiando ",
|
"Copying ": "Copiando ",
|
||||||
"Trying to load ": "",
|
"Trying to load ": "Intentando cargar ",
|
||||||
"Downloading ": "Descargando ",
|
"Downloading ": "Descargando ",
|
||||||
"Checking MD5": "Chqueando MD5",
|
"Downloaded ": "Descargado ",
|
||||||
|
"Removed ": "Removido ",
|
||||||
|
"Checking MD5": "Chequeando MD5",
|
||||||
"Loading...": "Cargando...",
|
"Loading...": "Cargando...",
|
||||||
"Loading": "Cargando",
|
"Loading": "Cargando",
|
||||||
"Empty!": "¡Vacío!",
|
"Empty!": "¡Vacío!",
|
||||||
"Not Ready...": "No Listo Aún...",
|
"Not Ready...": "No listo aún...",
|
||||||
"Error loading page!": "¡Error cargando la página!",
|
"Error loading page!": "¡Error cargando la página!",
|
||||||
"Update avaliable: ": "Actualización disponible: ",
|
"Update avaliable: ": "Actualización disponible: ",
|
||||||
"Download update: ": "Descargar actualización: ",
|
"Download update: ": "Descargar actualización: ",
|
||||||
"Updated to ": "",
|
"Updated to ": "Actualizado a ",
|
||||||
"Restart Sphaira?": "",
|
"Press OK to restart Sphaira": "Presiona OK para reiniciar sphaira",
|
||||||
|
"Restart Sphaira?": "¿Reiniciar sphaira?",
|
||||||
"Failed to download update": "Fallo al descargar actualización",
|
"Failed to download update": "Fallo al descargar actualización",
|
||||||
"Delete Selected files?": "¿Eliminar archivos Seleccionados?",
|
"Restore hbmenu?": "¿Restaurar hbmenu?",
|
||||||
|
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Fallo al encontrar /switch/hbmenu.nro\nUsar la Tienda para reinstalar hbmenu",
|
||||||
|
"Failed to restore hbmenu, please re-download hbmenu": "Fallo al restaurar hbmenu, por favor volver a descargar hbmenu",
|
||||||
|
"Failed to restore hbmenu, using sphaira instead": "Fallo al restaurar hbmenu, se usará sphaira",
|
||||||
|
"Restored hbmenu, closing sphaira": "hbmenu restaurado, cerrando sphaira",
|
||||||
|
"Restored hbmenu": "hbmenu restaurado",
|
||||||
|
"Delete Selected files?": "¿Eliminar archivos seleccionados?",
|
||||||
"Completely remove ": "Eliminar completamente",
|
"Completely remove ": "Eliminar completamente",
|
||||||
"Are you sure you want to delete ": "¿Estás seguro que quieres eliminar? ",
|
"Are you sure you want to delete ": "¿Estás seguro que quieres eliminar? ",
|
||||||
"Are you sure you wish to cancel?": "¿Estás seguro que deseas cancelar?",
|
"Are you sure you wish to cancel?": "¿Estás seguro que deseas cancelar?",
|
||||||
"If this message appears repeatedly, please open an issue.": ""
|
"Audio disabled due to suspended game": "",
|
||||||
|
"If this message appears repeatedly, please open an issue.": "Si este mensaje aparece repetidamente, por favor abrir un 'issue'."
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"Details": "Détails",
|
"Details": "Détails",
|
||||||
"Update": "Mise à jour",
|
"Update": "Mise à jour",
|
||||||
"Remove": "Supprimer",
|
"Remove": "Supprimer",
|
||||||
|
"Restore": "Restaurer",
|
||||||
"Download": "Télécharger",
|
"Download": "Télécharger",
|
||||||
"Next Page": "Page Suiv.",
|
"Next Page": "Page Suiv.",
|
||||||
"Prev Page": "Page Préc.",
|
"Prev Page": "Page Préc.",
|
||||||
@@ -26,6 +27,12 @@
|
|||||||
"Star": "Ajouter aux favories",
|
"Star": "Ajouter aux favories",
|
||||||
"System memory": "Mémoire système",
|
"System memory": "Mémoire système",
|
||||||
"microSD card": "Carte microSD",
|
"microSD card": "Carte microSD",
|
||||||
|
"Sd": "Sd",
|
||||||
|
"Image System memory": "Image de la mémoire System",
|
||||||
|
"Image microSD card": "Image de la Carte microSD",
|
||||||
|
"Slow": "Lent",
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Fast": "Rapide",
|
||||||
"Yes": "Oui",
|
"Yes": "Oui",
|
||||||
"No": "Non",
|
"No": "Non",
|
||||||
"Enabled": "Activé(e)",
|
"Enabled": "Activé(e)",
|
||||||
@@ -46,7 +53,7 @@
|
|||||||
"Alphabetical (Star)": "Alphabétique (Favories)",
|
"Alphabetical (Star)": "Alphabétique (Favories)",
|
||||||
"Likes": "Likes",
|
"Likes": "Likes",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"Decending": "Décroissant",
|
"Descending": "Décroissant",
|
||||||
"Descending (down)": "Décroissant",
|
"Descending (down)": "Décroissant",
|
||||||
"Desc": "Décroissant",
|
"Desc": "Décroissant",
|
||||||
"Ascending": "Croissant",
|
"Ascending": "Croissant",
|
||||||
@@ -54,14 +61,16 @@
|
|||||||
"Asc": "Croissant",
|
"Asc": "Croissant",
|
||||||
|
|
||||||
"Menu Options": "Options des Menus",
|
"Menu Options": "Options des Menus",
|
||||||
"Header": "En-tête",
|
|
||||||
"Theme": "Thème",
|
"Theme": "Thème",
|
||||||
"Theme Options": "Options de Thème",
|
"Theme Options": "Options de Thème",
|
||||||
"Select Theme": "Choisir un Thème",
|
"Select Theme": "Choisir un Thème",
|
||||||
"Shuffle": "Aléatoire",
|
"Shuffle": "Aléatoire",
|
||||||
"Music": "Musique",
|
"Music": "Musique",
|
||||||
|
"12 Hour Time": "Temps sur 12 heures",
|
||||||
"Network": "Réseau",
|
"Network": "Réseau",
|
||||||
"Network Options": "Options Réseau",
|
"Network Options": "Options Réseau",
|
||||||
|
"Ftp": "FTP",
|
||||||
|
"Mtp": "MTP",
|
||||||
"Nxlink": "Nxlink",
|
"Nxlink": "Nxlink",
|
||||||
"Nxlink Connected": "Nxlink Connecté",
|
"Nxlink Connected": "Nxlink Connecté",
|
||||||
"Nxlink Upload": "Nxlink téléversement",
|
"Nxlink Upload": "Nxlink téléversement",
|
||||||
@@ -82,6 +91,7 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "Journalisation",
|
"Logging": "Journalisation",
|
||||||
"Replace hbmenu on exit": "Remplacer hbmenu quand quitté",
|
"Replace hbmenu on exit": "Remplacer hbmenu quand quitté",
|
||||||
"Misc": "Divers",
|
"Misc": "Divers",
|
||||||
@@ -90,6 +100,7 @@
|
|||||||
"Install forwarders": "Installer les Forwarders",
|
"Install forwarders": "Installer les Forwarders",
|
||||||
"Install location": "Emplacement d'installation",
|
"Install location": "Emplacement d'installation",
|
||||||
"Show install warning": "Afficher l'avertissement d'installation",
|
"Show install warning": "Afficher l'avertissement d'installation",
|
||||||
|
"Text scroll speed": "Vitesse de défilement du texte",
|
||||||
|
|
||||||
"FileBrowser": "Explorateur de Fichiers",
|
"FileBrowser": "Explorateur de Fichiers",
|
||||||
"%zd files": "%zd fichiers",
|
"%zd files": "%zd fichiers",
|
||||||
@@ -112,6 +123,8 @@
|
|||||||
"Create Folder": "Créer un Dossier",
|
"Create Folder": "Créer un Dossier",
|
||||||
"Set Folder Name": "Nommer Le Dossier",
|
"Set Folder Name": "Nommer Le Dossier",
|
||||||
"View as text (unfinished)": "Afficher sous forme de texte (inachevé)",
|
"View as text (unfinished)": "Afficher sous forme de texte (inachevé)",
|
||||||
|
"Ignore read only": "Ignorer lecture seule",
|
||||||
|
"Mount": "Monter",
|
||||||
"Empty...": "Vide...",
|
"Empty...": "Vide...",
|
||||||
"Open with DayBreak?": "Ouvrir avec DayBreak?",
|
"Open with DayBreak?": "Ouvrir avec DayBreak?",
|
||||||
"Launch ": "Lancer ",
|
"Launch ": "Lancer ",
|
||||||
@@ -199,6 +212,10 @@
|
|||||||
"Bad Page": "Page inexistante",
|
"Bad Page": "Page inexistante",
|
||||||
"Download theme?": "Télécharger le thème?",
|
"Download theme?": "Télécharger le thème?",
|
||||||
|
|
||||||
|
"GitHub": "GitHub",
|
||||||
|
"Downloading json": "Téléchargement du json",
|
||||||
|
"Select asset to download for ": "Sélectionner l'asset pour télécharger ",
|
||||||
|
|
||||||
"Installing ": "Installation ",
|
"Installing ": "Installation ",
|
||||||
"Uninstalling ": "Désinstallation ",
|
"Uninstalling ": "Désinstallation ",
|
||||||
"Deleting ": "Suppression ",
|
"Deleting ": "Suppression ",
|
||||||
@@ -211,6 +228,8 @@
|
|||||||
"Copying ": "Copie ",
|
"Copying ": "Copie ",
|
||||||
"Trying to load ": "Tente de charger ",
|
"Trying to load ": "Tente de charger ",
|
||||||
"Downloading ": "Téléchargement ",
|
"Downloading ": "Téléchargement ",
|
||||||
|
"Downloaded ": "Téléchargé",
|
||||||
|
"Removed ": "Supprimé ",
|
||||||
"Checking MD5": "Vérification MD5",
|
"Checking MD5": "Vérification MD5",
|
||||||
"Loading...": "Chargement...",
|
"Loading...": "Chargement...",
|
||||||
"Loading": "Chargement",
|
"Loading": "Chargement",
|
||||||
@@ -220,11 +239,19 @@
|
|||||||
"Update avaliable: ": "Mise à jour disponible: ",
|
"Update avaliable: ": "Mise à jour disponible: ",
|
||||||
"Download update: ": "Télécharger la mise à jour: ",
|
"Download update: ": "Télécharger la mise à jour: ",
|
||||||
"Updated to ": "Mis à jour vers ",
|
"Updated to ": "Mis à jour vers ",
|
||||||
|
"Press OK to restart Sphaira": "Appuyez sur OK pour redémarrer Sphaira",
|
||||||
"Restart Sphaira?": "Redémarrer Sphaira?",
|
"Restart Sphaira?": "Redémarrer Sphaira?",
|
||||||
"Failed to download update": "Echec du téléchargement de la mise à jour",
|
"Failed to download update": "Echec du téléchargement de la mise à jour",
|
||||||
|
"Restore hbmenu?": "Restaurer hbmenu?",
|
||||||
|
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "/switch/hbmenu.nro n'a pas été trouvé\nUtiliser l'Appstore pour réinstaller le hbmenu",
|
||||||
|
"Failed to restore hbmenu, please re-download hbmenu": "Echec de la restauration de hbmenu, veuillez le réinstaller",
|
||||||
|
"Failed to restore hbmenu, using sphaira instead": "Echec de la restauration de hbmenu, sphaira sera utilisé à la place",
|
||||||
|
"Restored hbmenu, closing sphaira": "Hbmenu restauré, fermeture de sphaira",
|
||||||
|
"Restored hbmenu": "Hbmenu restauré",
|
||||||
"Delete Selected files?": "Supprimer les fichiers sélectionnés?",
|
"Delete Selected files?": "Supprimer les fichiers sélectionnés?",
|
||||||
"Completely remove ": "Supprimer totalement ",
|
"Completely remove ": "Supprimer totalement ",
|
||||||
"Are you sure you want to delete ": "Êtes-vous sûr de vouloir supprimer ",
|
"Are you sure you want to delete ": "Êtes-vous sûr de vouloir supprimer ",
|
||||||
"Are you sure you wish to cancel?": "Souhaitez-vous vraiment annuler?",
|
"Are you sure you wish to cancel?": "Souhaitez-vous vraiment annuler?",
|
||||||
|
"Audio disabled due to suspended game": "Audio désactivé à cause d'un jeu suspendu",
|
||||||
"If this message appears repeatedly, please open an issue.": "Si ce message apparait en boucle veuillez ouvrir une issue."
|
"If this message appears repeatedly, please open an issue.": "Si ce message apparait en boucle veuillez ouvrir une issue."
|
||||||
}
|
}
|
||||||
@@ -1,31 +1,38 @@
|
|||||||
{
|
{
|
||||||
"[Applet Mode]": "[Modalità applet]",
|
"[Applet Mode]": "[Modalità applet]",
|
||||||
"No Internet": "Niente Internet",
|
"No Internet": "Niente Internet",
|
||||||
"Files": "",
|
"Files": "File",
|
||||||
"Apps": "",
|
"Apps": "App",
|
||||||
"Store": "",
|
"Store": "Store",
|
||||||
"Menu": "Menu",
|
"Menu": "Menu",
|
||||||
"Options": "Opzioni",
|
"Options": "Opzioni",
|
||||||
"OK": "",
|
"OK": "OK",
|
||||||
"Back": "Indietro",
|
"Back": "Indietro",
|
||||||
"Select": "",
|
"Select": "Seleziona",
|
||||||
"Open": "Apri",
|
"Open": "Apri",
|
||||||
"Launch": "Lancia",
|
"Launch": "Lancia",
|
||||||
"Info": "Informazioni",
|
"Info": "Informazioni",
|
||||||
"Install": "Installa",
|
"Install": "Installa",
|
||||||
"Delete": "Elimina",
|
"Delete": "Elimina",
|
||||||
"Restart": "",
|
"Restart": "Riavvia",
|
||||||
"Changelog": "",
|
"Changelog": "Patch notes",
|
||||||
"Details": "",
|
"Details": "Dettagli",
|
||||||
"Update": "",
|
"Update": "Aggiorna",
|
||||||
"Remove": "",
|
"Remove": "Rimuovi",
|
||||||
|
"Restore": "Ripristina",
|
||||||
"Download": "Download",
|
"Download": "Download",
|
||||||
"Next Page": "Pagina successiva",
|
"Next Page": "Pagina successiva",
|
||||||
"Prev Page": "Pagina precedente",
|
"Prev Page": "Pagina precedente",
|
||||||
"Unstar": "",
|
"Unstar": "Rimuovi dai preferiti",
|
||||||
"Star": "",
|
"Star": "Aggiungi ai preferiti",
|
||||||
"System memory": "",
|
"System memory": "Memoria di sistema",
|
||||||
"microSD card": "",
|
"microSD card": "Scheda microSD",
|
||||||
|
"Sd": "SD",
|
||||||
|
"Image System memory": "Immagine memoria di sistema",
|
||||||
|
"Image microSD card": "Immagine scheda microSD",
|
||||||
|
"Slow": "",
|
||||||
|
"Normal": "",
|
||||||
|
"Fast": "",
|
||||||
"Yes": "Sì",
|
"Yes": "Sì",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
"Enabled": "Abilitato",
|
"Enabled": "Abilitato",
|
||||||
@@ -40,13 +47,13 @@
|
|||||||
"Updated": "Aggiornato",
|
"Updated": "Aggiornato",
|
||||||
"Updated (Star)": "",
|
"Updated (Star)": "",
|
||||||
"Downloads": "Download",
|
"Downloads": "Download",
|
||||||
"Size": "Misurare",
|
"Size": "Dimensione",
|
||||||
"Size (Star)": "",
|
"Size (Star)": "Dimensione (Preferiti)",
|
||||||
"Alphabetical": "Alfabetico",
|
"Alphabetical": "Alfabetico",
|
||||||
"Alphabetical (Star)": "",
|
"Alphabetical (Star)": "Alfabetico (Preferiti)",
|
||||||
"Likes": "",
|
"Likes": "Mi Piace",
|
||||||
"ID": "",
|
"ID": "ID",
|
||||||
"Decending": "Decrescente",
|
"Descending": "Decrescente",
|
||||||
"Descending (down)": "Decrescente",
|
"Descending (down)": "Decrescente",
|
||||||
"Desc": "Decrescente",
|
"Desc": "Decrescente",
|
||||||
"Ascending": "Crescente",
|
"Ascending": "Crescente",
|
||||||
@@ -54,22 +61,24 @@
|
|||||||
"Asc": "Crescente",
|
"Asc": "Crescente",
|
||||||
|
|
||||||
"Menu Options": "Opzioni menu",
|
"Menu Options": "Opzioni menu",
|
||||||
"Header": "Intestazione",
|
|
||||||
"Theme": "Tema",
|
"Theme": "Tema",
|
||||||
"Theme Options": "Opzioni tema",
|
"Theme Options": "Opzioni tema",
|
||||||
"Select Theme": "Seleziona tema",
|
"Select Theme": "Seleziona tema",
|
||||||
"Shuffle": "Mescola",
|
"Shuffle": "Mescola",
|
||||||
"Music": "Musica",
|
"Music": "Musica",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "Rete",
|
"Network": "Rete",
|
||||||
"Network Options": "Opzioni di rete",
|
"Network Options": "Opzioni di rete",
|
||||||
|
"Ftp": "FTP",
|
||||||
|
"Mtp": "MTP",
|
||||||
"Nxlink": "Nxlink",
|
"Nxlink": "Nxlink",
|
||||||
"Nxlink Connected": "",
|
"Nxlink Connected": "Nxlink connesso",
|
||||||
"Nxlink Upload": "",
|
"Nxlink Upload": "Nxlink upload",
|
||||||
"Nxlink Finished": "",
|
"Nxlink Finished": "Nxlink finito",
|
||||||
"Switch-Handheld!": "",
|
"Switch-Handheld!": "Switch Portatile",
|
||||||
"Switch-Docked!": "",
|
"Switch-Docked!": "Switch Dock",
|
||||||
"Language": "Lingua",
|
"Language": "Lingua",
|
||||||
"Auto": "",
|
"Auto": "Auto",
|
||||||
"English": "English",
|
"English": "English",
|
||||||
"Japanese": "日本語",
|
"Japanese": "日本語",
|
||||||
"French": "Français",
|
"French": "Français",
|
||||||
@@ -82,14 +91,16 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "Logging",
|
"Logging": "Logging",
|
||||||
"Replace hbmenu on exit": "Sostituisci hbmenu all'uscita",
|
"Replace hbmenu on exit": "Sostituisci hbmenu all'uscita",
|
||||||
"Misc": "Varie",
|
"Misc": "Varie",
|
||||||
"Misc Options": "Opzioni varie",
|
"Misc Options": "Opzioni varie",
|
||||||
"Web": "Rete",
|
"Web": "Rete",
|
||||||
"Install forwarders": "",
|
"Install forwarders": "Installa forwarder",
|
||||||
"Install location": "",
|
"Install location": "Installa posizione",
|
||||||
"Show install warning": "",
|
"Show install warning": "Mostra avvertimento installazione",
|
||||||
|
"Text scroll speed": "",
|
||||||
|
|
||||||
"FileBrowser": "FileBrowser",
|
"FileBrowser": "FileBrowser",
|
||||||
"%zd files": "%zd files",
|
"%zd files": "%zd files",
|
||||||
@@ -100,23 +111,25 @@
|
|||||||
"Hidden Last": "Ultimo nascosto",
|
"Hidden Last": "Ultimo nascosto",
|
||||||
"Cut": "Taglia",
|
"Cut": "Taglia",
|
||||||
"Copy": "Copia",
|
"Copy": "Copia",
|
||||||
"Paste": "",
|
"Paste": "Incolla",
|
||||||
"Paste ": "",
|
"Paste ": "Incolla ",
|
||||||
" file(s)?": "",
|
" file(s)?": "(i)file?",
|
||||||
"Rename": "Rinomina",
|
"Rename": "Rinomina",
|
||||||
"Set New File Name": "",
|
"Set New File Name": "Imposta nuovo nome",
|
||||||
"Advanced": "Avanzato",
|
"Advanced": "Avanzato",
|
||||||
"Advanced Options": "Opzioni avanzate",
|
"Advanced Options": "Opzioni avanzate",
|
||||||
"Create File": "Crea file",
|
"Create File": "Crea file",
|
||||||
"Set File Name": "",
|
"Set File Name": "Imposta nome",
|
||||||
"Create Folder": "Crea cartella",
|
"Create Folder": "Crea cartella",
|
||||||
"Set Folder Name": "",
|
"Set Folder Name": "Imposta nome",
|
||||||
"View as text (unfinished)": "Visualizza come testo (non finito)",
|
"View as text (unfinished)": "Visualizza come testo (non finito)",
|
||||||
"Empty...": "",
|
"Ignore read only": "Ignora read only",
|
||||||
"Open with DayBreak?": "",
|
"Mount": "Monta",
|
||||||
"Launch ": "",
|
"Empty...": "Vuoto...",
|
||||||
"Launch option for: ": "",
|
"Open with DayBreak?": "Vuoi aprire con Daybreak?",
|
||||||
"Select launcher for: ": "",
|
"Launch ": "Lancia",
|
||||||
|
"Launch option for: ": "Lancia opzione per",
|
||||||
|
"Select launcher for: ": "Scegli launcher per",
|
||||||
|
|
||||||
"Homebrew": "Homebrew",
|
"Homebrew": "Homebrew",
|
||||||
"Homebrew Options": "Opzioni Homebrew",
|
"Homebrew Options": "Opzioni Homebrew",
|
||||||
@@ -191,40 +204,54 @@
|
|||||||
"Load Default": "Carica predefinito",
|
"Load Default": "Carica predefinito",
|
||||||
|
|
||||||
"Themezer": "Themezer",
|
"Themezer": "Themezer",
|
||||||
"Themezer Options": "",
|
"Themezer Options": "Impostazioni Themezer",
|
||||||
"Nsfw": "",
|
"Nsfw": "NSFW",
|
||||||
"Page": "",
|
"Page": "Pagina",
|
||||||
"Page %zu / %zu": "Page %zu / %zu",
|
"Page %zu / %zu": "Pagina %zu / %zu",
|
||||||
"Enter Page Number": "",
|
"Enter Page Number": "Inserisci il numero della pagina",
|
||||||
"Bad Page": "",
|
"Bad Page": "Pagina invalida",
|
||||||
"Download theme?": "",
|
"Download theme?": "Vuoi scaricare il tema?",
|
||||||
|
|
||||||
"Installing ": "",
|
"GitHub": "GitHub",
|
||||||
"Uninstalling ": "",
|
"Downloading json": "Scaricamento json",
|
||||||
"Deleting ": "",
|
"Select asset to download for": "Scegli l'asset da scaricare per",
|
||||||
"Deleting": "",
|
|
||||||
"Pasting ": "",
|
"Installing ": "Installazione",
|
||||||
"Pasting": "",
|
"Uninstalling ": "Disinstallazione",
|
||||||
"Removing ": "",
|
"Deleting ": "Eliminazione",
|
||||||
"Scanning ": "",
|
"Deleting": "Eliminazione",
|
||||||
"Creating ": "",
|
"Pasting ": "Incollo",
|
||||||
"Copying ": "",
|
"Pasting": "Incollo",
|
||||||
"Trying to load ": "",
|
"Removing ": "Rimozione",
|
||||||
"Downloading ": "",
|
"Scanning ": "Scan",
|
||||||
"Checking MD5": "",
|
"Creating ": "Creazione",
|
||||||
"Loading...": "",
|
"Copying ": "Copio",
|
||||||
"Loading": "",
|
"Trying to load ": "Cercando di caricare",
|
||||||
"Empty!": "",
|
"Downloading ": "Scaricando",
|
||||||
"Not Ready...": "",
|
"Downloaded ": "Scaricato",
|
||||||
"Error loading page!": "",
|
"Removed ": ""Rimosso,
|
||||||
"Update avaliable: ": "",
|
"Checking MD5": "Controllo MD5",
|
||||||
"Download update: ": "",
|
"Loading...": "Caricamento...",
|
||||||
"Updated to ": "",
|
"Loading": "Caricamento",
|
||||||
"Restart Sphaira?": "",
|
"Empty!": "Vuoto!",
|
||||||
"Failed to download update": "",
|
"Not Ready...": "Non pronto...",
|
||||||
"Delete Selected files?": "",
|
"Error loading page!": "Errore nel caricare la pagina!",
|
||||||
"Completely remove ": "",
|
"Update avaliable: ": "Aggiornamento disponibile",
|
||||||
|
"Download update: ": "Scarica aggiornamento",
|
||||||
|
"Updated to ": "Aggiornato a",
|
||||||
|
"Press OK to restart Sphaira": "Premi OK per riavviare Sphaira",
|
||||||
|
"Restart Sphaira?": "Vuoi riavviare Sphaira?",
|
||||||
|
"Failed to download update": "Download aggiornamento fallito",
|
||||||
|
"Restore hbmenu?": "Vuoi ripristinare hbmenu?",
|
||||||
|
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Impossibile trovare /switch/hbmenu.nro\nUsa l'Appstore per reinstallare hbmenu",
|
||||||
|
"Failed to restore hbmenu, please re-download hbmenu": "Impossibile ripristinare hbmenu, per favore riscaricalo",
|
||||||
|
"Failed to restore hbmenu, using sphaira instead": "Impossibile ripristinare hbmenu, uso Sphaira invece",
|
||||||
|
"Restored hbmenu, closing sphaira": "hbmenu ripristinato, chiudo Sphaira",
|
||||||
|
"Restored hbmenu": "hbmenu ripristinato",
|
||||||
|
"Delete Selected files?": "Vuoi rimuovere i file selezionati?",
|
||||||
|
"Completely remove ": "Elimina definitivamente",
|
||||||
"Are you sure you want to delete ": "Sei sicuro di voler eliminare? ",
|
"Are you sure you want to delete ": "Sei sicuro di voler eliminare? ",
|
||||||
"Are you sure you wish to cancel?": "",
|
"Are you sure you wish to cancel?": "Sei sicuro di voler annullare?",
|
||||||
"If this message appears repeatedly, please open an issue.": ""
|
"Audio disabled due to suspended game": "Audio disabilitato poichè un app è in pausa",
|
||||||
|
"If this message appears repeatedly, please open an issue.": "Se questo messaggio appare frequentemente, segnala il bug."
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"Details": "詳細",
|
"Details": "詳細",
|
||||||
"Update": "アップデート",
|
"Update": "アップデート",
|
||||||
"Remove": "除去",
|
"Remove": "除去",
|
||||||
|
"Restore": "復元",
|
||||||
"Download": "ダウンロード",
|
"Download": "ダウンロード",
|
||||||
"Next Page": "次のページ",
|
"Next Page": "次のページ",
|
||||||
"Prev Page": "前のページ",
|
"Prev Page": "前のページ",
|
||||||
@@ -26,6 +27,12 @@
|
|||||||
"Star": "お気に入り",
|
"Star": "お気に入り",
|
||||||
"System memory": "システムメモリ",
|
"System memory": "システムメモリ",
|
||||||
"microSD card": "SDメモリーカード",
|
"microSD card": "SDメモリーカード",
|
||||||
|
"Sd": "SDメモリーカード",
|
||||||
|
"Image System memory": "システムメモリイメージ",
|
||||||
|
"Image microSD card": "SDイメージ",
|
||||||
|
"Slow": "遅い",
|
||||||
|
"Normal": "普通",
|
||||||
|
"Fast": "速い",
|
||||||
"Yes": "はい",
|
"Yes": "はい",
|
||||||
"No": "いいえ",
|
"No": "いいえ",
|
||||||
"Enabled": "",
|
"Enabled": "",
|
||||||
@@ -46,7 +53,7 @@
|
|||||||
"Alphabetical (Star)": "アルファベット順(お気に入り)",
|
"Alphabetical (Star)": "アルファベット順(お気に入り)",
|
||||||
"Likes": "いいね順",
|
"Likes": "いいね順",
|
||||||
"ID": "デベロッパー順",
|
"ID": "デベロッパー順",
|
||||||
"Decending": "降順",
|
"Descending": "降順",
|
||||||
"Descending (down)": "降順",
|
"Descending (down)": "降順",
|
||||||
"Desc": "降順",
|
"Desc": "降順",
|
||||||
"Ascending": "上昇",
|
"Ascending": "上昇",
|
||||||
@@ -54,14 +61,16 @@
|
|||||||
"Asc": "上昇",
|
"Asc": "上昇",
|
||||||
|
|
||||||
"Menu Options": "メニュー設定",
|
"Menu Options": "メニュー設定",
|
||||||
"Header": "ヘッダー",
|
|
||||||
"Theme": "テーマ",
|
"Theme": "テーマ",
|
||||||
"Theme Options": "テーマ設定",
|
"Theme Options": "テーマ設定",
|
||||||
"Select Theme": "テーマを選ぶ",
|
"Select Theme": "テーマを選ぶ",
|
||||||
"Shuffle": "シャッフル",
|
"Shuffle": "シャッフル",
|
||||||
"Music": "BGM",
|
"Music": "BGM",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "ネットワーク",
|
"Network": "ネットワーク",
|
||||||
"Network Options": "ネットワーク設定",
|
"Network Options": "ネットワーク設定",
|
||||||
|
"Ftp": "FTP",
|
||||||
|
"Mtp": "MTP",
|
||||||
"Nxlink": "Nxlink",
|
"Nxlink": "Nxlink",
|
||||||
"Nxlink Connected": "Nxlink 接続",
|
"Nxlink Connected": "Nxlink 接続",
|
||||||
"Nxlink Upload": "Nxlink アップロード",
|
"Nxlink Upload": "Nxlink アップロード",
|
||||||
@@ -82,6 +91,7 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "ログの取得",
|
"Logging": "ログの取得",
|
||||||
"Replace hbmenu on exit": "終了時に hbmenu を置き換える",
|
"Replace hbmenu on exit": "終了時に hbmenu を置き換える",
|
||||||
"Misc": "その他",
|
"Misc": "その他",
|
||||||
@@ -90,6 +100,7 @@
|
|||||||
"Install forwarders": "Forwarderのインストール機能",
|
"Install forwarders": "Forwarderのインストール機能",
|
||||||
"Install location": "インストール経路",
|
"Install location": "インストール経路",
|
||||||
"Show install warning": "警告文を示す",
|
"Show install warning": "警告文を示す",
|
||||||
|
"Text scroll speed": "流れる文字の速さ",
|
||||||
|
|
||||||
"FileBrowser": "ファイルブラウザ",
|
"FileBrowser": "ファイルブラウザ",
|
||||||
"%zd files": "%zd個のファイル",
|
"%zd files": "%zd個のファイル",
|
||||||
@@ -112,6 +123,8 @@
|
|||||||
"Create Folder": "フォルダーの作成",
|
"Create Folder": "フォルダーの作成",
|
||||||
"Set Folder Name": "名前を入力",
|
"Set Folder Name": "名前を入力",
|
||||||
"View as text (unfinished)": "テキストとして表示 (未完成)",
|
"View as text (unfinished)": "テキストとして表示 (未完成)",
|
||||||
|
"Ignore read only": "読み取り専用を無視する",
|
||||||
|
"Mount": "マウント",
|
||||||
"Empty...": "このフォルダーは空です",
|
"Empty...": "このフォルダーは空です",
|
||||||
"Open with DayBreak?": "DayBreakで開きますか?",
|
"Open with DayBreak?": "DayBreakで開きますか?",
|
||||||
"Launch ": "起動しますか",
|
"Launch ": "起動しますか",
|
||||||
@@ -199,6 +212,10 @@
|
|||||||
"Bad Page": "ページが見つかりません",
|
"Bad Page": "ページが見つかりません",
|
||||||
"Download theme?": "テーマをインストールしますか?",
|
"Download theme?": "テーマをインストールしますか?",
|
||||||
|
|
||||||
|
"GitHub": "GitHub",
|
||||||
|
"Downloading json": "JSONからダウンロード",
|
||||||
|
"Select asset to download for ": "ダウンロードアイテムを選択 ",
|
||||||
|
|
||||||
"Installing ": "インストール中 ",
|
"Installing ": "インストール中 ",
|
||||||
"Uninstalling ": "アンインストール中 ",
|
"Uninstalling ": "アンインストール中 ",
|
||||||
"Deleting ": "削除中 ",
|
"Deleting ": "削除中 ",
|
||||||
@@ -211,6 +228,8 @@
|
|||||||
"Copying ": "コピー中 ",
|
"Copying ": "コピー中 ",
|
||||||
"Trying to load ": "サムネイルを取得中 ",
|
"Trying to load ": "サムネイルを取得中 ",
|
||||||
"Downloading ": "ダウンロード中 ",
|
"Downloading ": "ダウンロード中 ",
|
||||||
|
"Downloaded ": "ダウンロード完了 ",
|
||||||
|
"Removed ": "除去完了 ",
|
||||||
"Checking MD5": "MD5を確認中 ",
|
"Checking MD5": "MD5を確認中 ",
|
||||||
"Loading...": "ロード中",
|
"Loading...": "ロード中",
|
||||||
"Loading": "ロード中",
|
"Loading": "ロード中",
|
||||||
@@ -220,11 +239,19 @@
|
|||||||
"Update avaliable: ": "アップデート可能: ",
|
"Update avaliable: ": "アップデート可能: ",
|
||||||
"Download update: ": "アップデートをダウンロード: ",
|
"Download update: ": "アップデートをダウンロード: ",
|
||||||
"Updated to ": "アップデート: ",
|
"Updated to ": "アップデート: ",
|
||||||
|
"Press OK to restart Sphaira": "確認ボタンを押してSphairaを再起動",
|
||||||
"Restart Sphaira?": "Sphairaを再起動しますか?",
|
"Restart Sphaira?": "Sphairaを再起動しますか?",
|
||||||
"Failed to download update": "アップデートのダウンロード失敗",
|
"Failed to download update": "アップデートのダウンロード失敗",
|
||||||
|
"Restore hbmenu?": "hbmenuに戻しますか?",
|
||||||
|
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "/switch/hbmemu.nroが見つかりません\nAppstoreから再インストールしてください",
|
||||||
|
"Failed to restore hbmenu, please re-download hbmenu": "hbmenuを復元できませんでした、再インストールしてください",
|
||||||
|
"Failed to restore hbmenu, using sphaira instead": "hbmenuを復元できませんでした、sphairaを引き続き使います",
|
||||||
|
"Restored hbmenu, closing sphaira": "hbmenuに復元されました、sphairaを終了します",
|
||||||
|
"Restored hbmenu": "hbmenuに復元されました",
|
||||||
"Delete Selected files?": "本当に削除しますか?",
|
"Delete Selected files?": "本当に削除しますか?",
|
||||||
"Completely remove ": "除去しますか ",
|
"Completely remove ": "除去しますか ",
|
||||||
"Are you sure you want to delete ": "消去してもよろしいですか ",
|
"Are you sure you want to delete ": "消去してもよろしいですか ",
|
||||||
"Are you sure you wish to cancel?": "本当に取り消しますか?",
|
"Are you sure you wish to cancel?": "本当に取り消しますか?",
|
||||||
|
"Audio disabled due to suspended game": "ゲームが一時停止状態の場合、オーディオは無効になります",
|
||||||
"If this message appears repeatedly, please open an issue.": "このメッセージが繰り返し表示される場合は、問題を開いてください。"
|
"If this message appears repeatedly, please open an issue.": "このメッセージが繰り返し表示される場合は、問題を開いてください。"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"[Applet Mode]": "[애플릿 모드]",
|
"[Applet Mode]": "[ 애플릿 모드 ]",
|
||||||
"No Internet": "네트워크 연결 없음",
|
"No Internet": "인터넷 연결 없음",
|
||||||
"Files": "파일 탐색기",
|
"Files": "파일 탐색기",
|
||||||
"Apps": "홈브류",
|
"Apps": "홈브류",
|
||||||
"Store": "앱스토어",
|
"Store": "앱스토어",
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"Details": "상세",
|
"Details": "상세",
|
||||||
"Update": "업데이트",
|
"Update": "업데이트",
|
||||||
"Remove": "제거",
|
"Remove": "제거",
|
||||||
|
"Restore": "복원",
|
||||||
"Download": "다운로드",
|
"Download": "다운로드",
|
||||||
"Next Page": "다음 페이지",
|
"Next Page": "다음 페이지",
|
||||||
"Prev Page": "이전 페이지",
|
"Prev Page": "이전 페이지",
|
||||||
@@ -26,13 +27,19 @@
|
|||||||
"Star": "즐겨찾기",
|
"Star": "즐겨찾기",
|
||||||
"System memory": "낸드 저장소",
|
"System memory": "낸드 저장소",
|
||||||
"microSD card": "SD 카드",
|
"microSD card": "SD 카드",
|
||||||
|
"Sd": "SD 카드",
|
||||||
|
"Image System memory": "낸드 이미지",
|
||||||
|
"Image microSD card": "SD 이미지",
|
||||||
|
"Slow": "느림",
|
||||||
|
"Normal": "보통",
|
||||||
|
"Fast": "빠름",
|
||||||
"Yes": "예",
|
"Yes": "예",
|
||||||
"No": "아니요",
|
"No": "아니요",
|
||||||
"Enabled": "",
|
"Enabled": "",
|
||||||
"Disabled": "",
|
"Disabled": "",
|
||||||
|
|
||||||
"Sort By": "정렬",
|
"Sort By": "정렬",
|
||||||
"Sort Options": "정렬 설정",
|
"Sort Options": "정렬 옵션",
|
||||||
"Filter": "필터",
|
"Filter": "필터",
|
||||||
"Sort": "분류",
|
"Sort": "분류",
|
||||||
"Order": "정렬",
|
"Order": "정렬",
|
||||||
@@ -45,8 +52,8 @@
|
|||||||
"Alphabetical": "알파벳순",
|
"Alphabetical": "알파벳순",
|
||||||
"Alphabetical (Star)": "알파벳순 (즐겨찾기)",
|
"Alphabetical (Star)": "알파벳순 (즐겨찾기)",
|
||||||
"Likes": "좋아요순",
|
"Likes": "좋아요순",
|
||||||
"ID": "작성자순",
|
"ID": "ID순",
|
||||||
"Decending": "내림차순",
|
"Descending": "내림차순",
|
||||||
"Descending (down)": "내림차순",
|
"Descending (down)": "내림차순",
|
||||||
"Desc": "내림차순",
|
"Desc": "내림차순",
|
||||||
"Ascending": "오름차순",
|
"Ascending": "오름차순",
|
||||||
@@ -54,20 +61,22 @@
|
|||||||
"Asc": "오름차순",
|
"Asc": "오름차순",
|
||||||
|
|
||||||
"Menu Options": "메뉴",
|
"Menu Options": "메뉴",
|
||||||
"Header": "헤더",
|
|
||||||
"Theme": "테마",
|
"Theme": "테마",
|
||||||
"Theme Options": "테마 설정",
|
"Theme Options": "테마 옵션",
|
||||||
"Select Theme": "테마 선택",
|
"Select Theme": "테마 선택",
|
||||||
"Shuffle": "셔플",
|
"Shuffle": "셔플",
|
||||||
"Music": "BGM",
|
"Music": "BGM",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "네트워크",
|
"Network": "네트워크",
|
||||||
"Network Options": "네트워크 설정",
|
"Network Options": "네트워크 옵션",
|
||||||
|
"Ftp": "FTP (무선)",
|
||||||
|
"Mtp": "MTP (유선)",
|
||||||
"Nxlink": "Nxlink",
|
"Nxlink": "Nxlink",
|
||||||
"Nxlink Connected": "Nxlink 연결됨",
|
"Nxlink Connected": "Nxlink 연결됨",
|
||||||
"Nxlink Upload": "Nxlink 업로드",
|
"Nxlink Upload": "Nxlink 업로드",
|
||||||
"Nxlink Finished": "Nxlink 종료됨",
|
"Nxlink Finished": "Nxlink 종료됨",
|
||||||
"Switch-Handheld!": "휴대모드로 전환되었습니다!",
|
"Switch-Handheld!": "휴대모드로 전환됨!",
|
||||||
"Switch-Docked!": "독 모드로 전환되었습니다!",
|
"Switch-Docked!": "독 모드로 전환됨!",
|
||||||
"Language": "언어",
|
"Language": "언어",
|
||||||
"Auto": "자동",
|
"Auto": "자동",
|
||||||
"English": "English",
|
"English": "English",
|
||||||
@@ -82,19 +91,21 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "로깅",
|
"Logging": "로깅",
|
||||||
"Replace hbmenu on exit": "hbmenu sphaira 교체",
|
"Replace hbmenu on exit": "종료 시 hbmenu 교체",
|
||||||
"Misc": "기타",
|
"Misc": "기타",
|
||||||
"Misc Options": "기타",
|
"Misc Options": "기타 옵션",
|
||||||
"Web": "웹 브라우저",
|
"Web": "웹 브라우저",
|
||||||
"Install forwarders": "바로가기 설치 기능",
|
"Install forwarders": "바로가기 설치",
|
||||||
"Install location": "설치 경로",
|
"Install location": "설치 위치",
|
||||||
"Show install warning": "바로가기 설치 경고문 표시",
|
"Show install warning": "경고 메시지",
|
||||||
|
"Text scroll speed": "흐르는 텍스트 속도",
|
||||||
|
|
||||||
"FileBrowser": "파일 탐색기",
|
"FileBrowser": "파일 탐색기",
|
||||||
"%zd files": "%zd개의 파일",
|
"%zd files": "%zd 개 파일",
|
||||||
"%zd dirs": "%zd개의 폴더",
|
"%zd dirs": "%zd 개 폴더",
|
||||||
"File Options": "파일 설정",
|
"File Options": "파일 옵션",
|
||||||
"Show Hidden": "숨겨진 항목 표시",
|
"Show Hidden": "숨겨진 항목 표시",
|
||||||
"Folders First": "폴더 우선 정렬",
|
"Folders First": "폴더 우선 정렬",
|
||||||
"Hidden Last": "숨겨진 항목 후순 정렬",
|
"Hidden Last": "숨겨진 항목 후순 정렬",
|
||||||
@@ -102,34 +113,36 @@
|
|||||||
"Copy": "복사",
|
"Copy": "복사",
|
||||||
"Paste": "붙여넣기",
|
"Paste": "붙여넣기",
|
||||||
"Paste ": " ",
|
"Paste ": " ",
|
||||||
" file(s)?": "개 항목을 붙여넣으시겠습니까?",
|
" file(s)?": "개 항목을 붙여넣을까요?",
|
||||||
"Rename": "이름 바꾸기",
|
"Rename": "이름 바꾸기",
|
||||||
"Set New File Name": "새 파일명 입력",
|
"Set New File Name": "새 파일명 입력",
|
||||||
"Advanced": "고급",
|
"Advanced": "고급",
|
||||||
"Advanced Options": "고급 설정",
|
"Advanced Options": "고급 옵션",
|
||||||
"Create File": "파일 생성",
|
"Create File": "새 파일",
|
||||||
"Set File Name": "파일명 입력",
|
"Set File Name": "파일명 입력",
|
||||||
"Create Folder": "새 폴더",
|
"Create Folder": "새 폴더",
|
||||||
"Set Folder Name": "폴더명 입력",
|
"Set Folder Name": "폴더명 입력",
|
||||||
"View as text (unfinished)": "텍스트로 보기 (미완성)",
|
"View as text (unfinished)": "텍스트로 보기 (미완성)",
|
||||||
"Empty...": "비어있습니다...",
|
"Ignore read only": "읽기 전용 설정 무시",
|
||||||
"Open with DayBreak?": "DayBreak로 여시겠습니까?",
|
"Mount": "마운트",
|
||||||
"Launch ": "실행하시겠습니까 ",
|
"Empty...": "비어있음...",
|
||||||
"Launch option for: ": "실행 설정: ",
|
"Open with DayBreak?": "DayBreak로 열까요?",
|
||||||
|
"Launch ": "실행할까요 ",
|
||||||
|
"Launch option for: ": "실행 옵션: ",
|
||||||
"Select launcher for: ": "실행 런처: ",
|
"Select launcher for: ": "실행 런처: ",
|
||||||
|
|
||||||
"Homebrew": "홈브류",
|
"Homebrew": "홈브류",
|
||||||
"Homebrew Options": "홈브류 설정",
|
"Homebrew Options": "홈브류 옵션",
|
||||||
"Hide Sphaira": "Sphaira 숨기기",
|
"Hide Sphaira": "Sphaira 숨기기",
|
||||||
"Install Forwarder": "바로가기 설치",
|
"Install Forwarder": "바로가기 설치",
|
||||||
"WARNING: Installing forwarders will lead to a ban!": "주의: 시스낸드에서 바로가기 설치시 BAN 위험이 있습니다!",
|
"WARNING: Installing forwarders will lead to a ban!": "경고: 시스낸드에 바로가기 설치 시, 밴 위험이 있습니다!",
|
||||||
"Installing Forwarder": "바로가기 설치중...",
|
"Installing Forwarder": "바로가기 설치",
|
||||||
"Creating Program": "프로그램 작성중...",
|
"Creating Program": "프로그램 생성",
|
||||||
"Creating Control": "컨트롤 작성중...",
|
"Creating Control": "컨트롤 생성",
|
||||||
"Creating Meta": "메타 작성중...",
|
"Creating Meta": "메타 생성",
|
||||||
"Writing Nca": "Nca 쓰는중...",
|
"Writing Nca": "Nca 쓰기",
|
||||||
"Updating ncm databse": "ncm 데이터베이스 업데이트중...",
|
"Updating ncm databse": "Ncm 데이터베이스 업데이트",
|
||||||
"Pushing application record": "응용 프로그램 기록 푸시중...",
|
"Pushing application record": "응용 프로그램 기록 푸싱",
|
||||||
"Installed!": "설치 완료!",
|
"Installed!": "설치 완료!",
|
||||||
"Failed to install forwarder": "바로가기 설치 실패",
|
"Failed to install forwarder": "바로가기 설치 실패",
|
||||||
"Unstarred ": "즐겨찾기 해제: ",
|
"Unstarred ": "즐겨찾기 해제: ",
|
||||||
@@ -137,40 +150,40 @@
|
|||||||
|
|
||||||
"AppStore": "앱스토어",
|
"AppStore": "앱스토어",
|
||||||
"Filter: %s | Sort: %s | Order: %s": "필터: %s | 분류: %s | 정렬: %s",
|
"Filter: %s | Sort: %s | Order: %s": "필터: %s | 분류: %s | 정렬: %s",
|
||||||
"AppStore Options": "앱스토어 설정",
|
"AppStore Options": "앱스토어 옵션",
|
||||||
"All": "전체",
|
"All": "모두",
|
||||||
"Games": "게임",
|
"Games": "게임",
|
||||||
"Emulators": "에뮬레이터",
|
"Emulators": "에뮬레이터",
|
||||||
"Tools": "도구",
|
"Tools": "도구",
|
||||||
"Themes": "테마",
|
"Themes": "테마",
|
||||||
"Legacy": "레거시",
|
"Legacy": "레거시",
|
||||||
"version: %s": "버전: %s",
|
"version: %s": "버전: %s",
|
||||||
"updated: %s": "갱신일: %s",
|
"updated: %s": "업데이트: %s",
|
||||||
"category: %s": "카테고리: %s",
|
"category: %s": "카테고리: %s",
|
||||||
"extracted: %.2f MiB": "용량: %.2f MiB",
|
"extracted: %.2f MiB": "용량: %.2f MiB",
|
||||||
"app_dls: %s": "다운로드 횟수: %s",
|
"app_dls: %s": "다운로드 횟수: %s",
|
||||||
"More by Author": "개발자의 다른 앱 더보기",
|
"More by Author": "개발자의 다른 앱 더 보기",
|
||||||
"Leave Feedback": "피드백 남기기",
|
"Leave Feedback": "피드백 남기기",
|
||||||
|
|
||||||
"Irs": "Joy-Con IR 카메라",
|
"Irs": "조이콘 적외선 카메라",
|
||||||
"Ambient Noise Level: ": "노이즈 레벨: ",
|
"Ambient Noise Level: ": "주변 노이즈 레벨: ",
|
||||||
"Controller": "컨트롤러",
|
"Controller": "컨트롤러",
|
||||||
"Pad ": "조이콘 ",
|
"Pad ": "조이콘 ",
|
||||||
" (Available)": " (사용 가능)",
|
" (Available)": " (사용 가능)",
|
||||||
" (Unsupported)": " (지원 안됨)",
|
" (Unsupported)": " (지원 안됨)",
|
||||||
" (Unconnected)": " (연결 없음)",
|
" (Unconnected)": " (연결 없음)",
|
||||||
"HandHeld": "- 본체 연결",
|
"HandHeld": "본체 연결",
|
||||||
"Rotation": "화면 회전",
|
"Rotation": "화면 회전",
|
||||||
"0 (Sideways)": "0 (좌회전)",
|
"0 (Sideways)": "반시계방향 90° 회전",
|
||||||
"90 (Flat)": "90 (정방향)",
|
"90 (Flat)": "정방향",
|
||||||
"180 (-Sideways)": "180 (우회전)",
|
"180 (-Sideways)": "시계방향 90° 회전",
|
||||||
"270 (Upside down)": "270 (역전)",
|
"270 (Upside down)": "상하반전",
|
||||||
"Colour": "컬러 팔레트",
|
"Colour": "색상",
|
||||||
"Grey": "그레이",
|
"Grey": "회색",
|
||||||
"Ironbow": "아이언보우",
|
"Ironbow": "아이언보우",
|
||||||
"Green": "그린",
|
"Green": "초록색",
|
||||||
"Red": "레드",
|
"Red": "빨간색",
|
||||||
"Blue": "블루",
|
"Blue": "파란색",
|
||||||
"Light Target": "반사 표적",
|
"Light Target": "반사 표적",
|
||||||
"All leds": "모든 LED 켜기",
|
"All leds": "모든 LED 켜기",
|
||||||
"Bright group": "Bright LED 켜기",
|
"Bright group": "Bright LED 켜기",
|
||||||
@@ -191,40 +204,54 @@
|
|||||||
"Load Default": "기본값으로 설정",
|
"Load Default": "기본값으로 설정",
|
||||||
|
|
||||||
"Themezer": "Themezer",
|
"Themezer": "Themezer",
|
||||||
"Themezer Options": "Themezer 설정",
|
"Themezer Options": "Themezer 옵션",
|
||||||
"Nsfw": "선정성 테마",
|
"Nsfw": "선정성 테마",
|
||||||
"Page": "페이지",
|
"Page": "페이지",
|
||||||
"Page %zu / %zu": "페이지 %zu / %zu",
|
"Page %zu / %zu": "페이지 %zu / %zu",
|
||||||
"Enter Page Number": "페이지 번호 입력",
|
"Enter Page Number": "페이지 번호 입력",
|
||||||
"Bad Page": "잘못된 페이지",
|
"Bad Page": "잘못된 페이지",
|
||||||
"Download theme?": "테마를 내려받으시겠습니까?",
|
"Download theme?": "테마를 다운로드할까요?",
|
||||||
|
|
||||||
"Installing ": "설치중... ",
|
"GitHub": "GitHub",
|
||||||
"Uninstalling ": "설치 제거중... ",
|
"Downloading json": "JSON에서 다운로드",
|
||||||
"Deleting ": "삭제중... ",
|
"Select asset to download for ": "다운로드 아이템: ",
|
||||||
"Deleting": "삭제중...",
|
|
||||||
"Pasting ": "붙여넣는중... ",
|
"Installing ": "설치 ",
|
||||||
"Pasting": "붙여넣는중...",
|
"Uninstalling ": "설치 제거 ",
|
||||||
"Removing ": "제거중... ",
|
"Deleting ": "삭제 ",
|
||||||
"Scanning ": "스캔중... ",
|
"Deleting": "삭제",
|
||||||
"Creating ": "작성중... ",
|
"Pasting ": "붙여넣기 ",
|
||||||
"Copying ": "복사중... ",
|
"Pasting": "붙여넣기",
|
||||||
|
"Removing ": "제거 ",
|
||||||
|
"Scanning ": "스캔 ",
|
||||||
|
"Creating ": "생성 ",
|
||||||
|
"Copying ": "복사 ",
|
||||||
"Trying to load ": "썸네일 받아오는 중... ",
|
"Trying to load ": "썸네일 받아오는 중... ",
|
||||||
"Downloading ": "다운로드중... ",
|
"Downloading ": "다운로드 ",
|
||||||
"Checking MD5": "MD5 확인중... ",
|
"Downloaded ": "다운로드 완료: ",
|
||||||
"Loading...": "로딩중...",
|
"Removed ": "제거 됨: ",
|
||||||
"Loading": "로딩중...",
|
"Checking MD5": "MD5 확인",
|
||||||
|
"Loading...": "로딩 중...",
|
||||||
|
"Loading": "로딩 중...",
|
||||||
"Empty!": "찾을 수 없습니다!",
|
"Empty!": "찾을 수 없습니다!",
|
||||||
"Not Ready...": "준비되지 않았습니다...",
|
"Not Ready...": "준비되지 않음...",
|
||||||
"Error loading page!": "페이지 로딩 오류!",
|
"Error loading page!": "페이지 로딩 오류!",
|
||||||
"Update avaliable: ": "업데이트 가능: ",
|
"Update avaliable: ": "업데이트 가능: ",
|
||||||
"Download update: ": "업데이트 다운로드: ",
|
"Download update: ": "업데이트 다운로드: ",
|
||||||
"Updated to ": "업데이트: ",
|
"Updated to ": "업데이트: ",
|
||||||
"Restart Sphaira?": "Sphaira를 재시작 하시겠습니까?",
|
"Press OK to restart Sphaira": "확인 버튼 입력하여 Sphaira 재시작",
|
||||||
"Failed to download update": "업데이트 다운로드 실패!",
|
"Restart Sphaira?": "Sphaira를 재시작할까요?",
|
||||||
"Delete Selected files?": "정말 삭제하시겠습니까?",
|
"Failed to download update": "업데이트 다운로드 실패함",
|
||||||
"Completely remove ": "제거하시겠습니까 ",
|
"Restore hbmenu?": "hbmenu로 교체할까요?",
|
||||||
"Are you sure you want to delete ": "정말 삭제하시겠습니까 ",
|
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "/switch/hbmemu.nro를 찾을 수 없습니다\n앱스토어에서 다시 설치하세요",
|
||||||
"Are you sure you wish to cancel?": "정말 취소하시겠습니까?",
|
"Failed to restore hbmenu, please re-download hbmenu": "hbmenu 교체 실패함, hbmenu를 다시 다운로드하세요",
|
||||||
"If this message appears repeatedly, please open an issue.": "해당 메시지가 반복해서 나타나는 경우, 이슈를 열어주세요."
|
"Failed to restore hbmenu, using sphaira instead": "hbmenu 교체 실패함, sphaira를 계속 사용합니다",
|
||||||
|
"Restored hbmenu, closing sphaira": "hbmenu로 교체됨, sphaira를 종료합니다",
|
||||||
|
"Restored hbmenu": "hbmenu로 교체됨",
|
||||||
|
"Delete Selected files?": "선택한 파일을 삭제할까요?",
|
||||||
|
"Completely remove ": "정말 삭제할까요 ",
|
||||||
|
"Are you sure you want to delete ": "정말 삭제할까요 ",
|
||||||
|
"Are you sure you wish to cancel?": "정말 취소할까요?",
|
||||||
|
"Audio disabled due to suspended game": "게임 실행 중에는 BGM이 비활성화 됩니다",
|
||||||
|
"If this message appears repeatedly, please open an issue.": "해당 메시지가 반복해서 나타나는 경우, 이슈를 등록하세요."
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"Details": "",
|
"Details": "",
|
||||||
"Update": "",
|
"Update": "",
|
||||||
"Remove": "",
|
"Remove": "",
|
||||||
|
"Restore": "",
|
||||||
"Download": "Downloaden",
|
"Download": "Downloaden",
|
||||||
"Next Page": "Volgende pagina",
|
"Next Page": "Volgende pagina",
|
||||||
"Prev Page": "Vorige pagina",
|
"Prev Page": "Vorige pagina",
|
||||||
@@ -26,6 +27,12 @@
|
|||||||
"Star": "",
|
"Star": "",
|
||||||
"System memory": "",
|
"System memory": "",
|
||||||
"microSD card": "",
|
"microSD card": "",
|
||||||
|
"Sd": "",
|
||||||
|
"Image System memory": "",
|
||||||
|
"Image microSD card": "",
|
||||||
|
"Slow": "",
|
||||||
|
"Normal": "",
|
||||||
|
"Fast": "",
|
||||||
"Yes": "Ja",
|
"Yes": "Ja",
|
||||||
"No": "Nee",
|
"No": "Nee",
|
||||||
"Enabled": "Ingeschakeld",
|
"Enabled": "Ingeschakeld",
|
||||||
@@ -46,7 +53,7 @@
|
|||||||
"Alphabetical (Star)": "",
|
"Alphabetical (Star)": "",
|
||||||
"Likes": "",
|
"Likes": "",
|
||||||
"ID": "",
|
"ID": "",
|
||||||
"Decending": "Aflopend",
|
"Descending": "Aflopend",
|
||||||
"Descending (down)": "Aflopend",
|
"Descending (down)": "Aflopend",
|
||||||
"Desc": "Aflopend",
|
"Desc": "Aflopend",
|
||||||
"Ascending": "Oplopend",
|
"Ascending": "Oplopend",
|
||||||
@@ -54,14 +61,16 @@
|
|||||||
"Asc": "Oplopend",
|
"Asc": "Oplopend",
|
||||||
|
|
||||||
"Menu Options": "Menu-opties",
|
"Menu Options": "Menu-opties",
|
||||||
"Header": "Koptekst",
|
|
||||||
"Theme": "Thema",
|
"Theme": "Thema",
|
||||||
"Theme Options": "Thema Opties",
|
"Theme Options": "Thema Opties",
|
||||||
"Select Theme": "Selecteer Thema",
|
"Select Theme": "Selecteer Thema",
|
||||||
"Shuffle": "Schudden",
|
"Shuffle": "Schudden",
|
||||||
"Music": "Muziek",
|
"Music": "Muziek",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "Netwerk",
|
"Network": "Netwerk",
|
||||||
"Network Options": "Netwerkopties",
|
"Network Options": "Netwerkopties",
|
||||||
|
"Ftp": "FTP",
|
||||||
|
"Mtp": "MTP",
|
||||||
"Nxlink": "Nxlink",
|
"Nxlink": "Nxlink",
|
||||||
"Nxlink Connected": "",
|
"Nxlink Connected": "",
|
||||||
"Nxlink Upload": "",
|
"Nxlink Upload": "",
|
||||||
@@ -82,6 +91,7 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "Loggen",
|
"Logging": "Loggen",
|
||||||
"Replace hbmenu on exit": "Vervang hbmenu bij afsluiten",
|
"Replace hbmenu on exit": "Vervang hbmenu bij afsluiten",
|
||||||
"Misc": "Diversen",
|
"Misc": "Diversen",
|
||||||
@@ -90,6 +100,7 @@
|
|||||||
"Install forwarders": "",
|
"Install forwarders": "",
|
||||||
"Install location": "",
|
"Install location": "",
|
||||||
"Show install warning": "",
|
"Show install warning": "",
|
||||||
|
"Text scroll speed": "",
|
||||||
|
|
||||||
"FileBrowser": "Bestandsbrowser",
|
"FileBrowser": "Bestandsbrowser",
|
||||||
"%zd files": "%zd files",
|
"%zd files": "%zd files",
|
||||||
@@ -112,6 +123,8 @@
|
|||||||
"Create Folder": "Map maken",
|
"Create Folder": "Map maken",
|
||||||
"Set Folder Name": "",
|
"Set Folder Name": "",
|
||||||
"View as text (unfinished)": "Bekijk als tekst (onvoltooid)",
|
"View as text (unfinished)": "Bekijk als tekst (onvoltooid)",
|
||||||
|
"Ignore read only": "",
|
||||||
|
"Mount": "",
|
||||||
"Empty...": "",
|
"Empty...": "",
|
||||||
"Open with DayBreak?": "",
|
"Open with DayBreak?": "",
|
||||||
"Launch ": "",
|
"Launch ": "",
|
||||||
@@ -199,6 +212,10 @@
|
|||||||
"Bad Page": "",
|
"Bad Page": "",
|
||||||
"Download theme?": "",
|
"Download theme?": "",
|
||||||
|
|
||||||
|
"GitHub": "",
|
||||||
|
"Downloading json": "",
|
||||||
|
"Select asset to download for ": "",
|
||||||
|
|
||||||
"Installing ": "",
|
"Installing ": "",
|
||||||
"Uninstalling ": "",
|
"Uninstalling ": "",
|
||||||
"Deleting ": "",
|
"Deleting ": "",
|
||||||
@@ -211,6 +228,8 @@
|
|||||||
"Copying ": "",
|
"Copying ": "",
|
||||||
"Trying to load ": "",
|
"Trying to load ": "",
|
||||||
"Downloading ": "",
|
"Downloading ": "",
|
||||||
|
"Downloaded ": "",
|
||||||
|
"Removed ": "",
|
||||||
"Checking MD5": "",
|
"Checking MD5": "",
|
||||||
"Loading...": "",
|
"Loading...": "",
|
||||||
"Loading": "",
|
"Loading": "",
|
||||||
@@ -220,11 +239,19 @@
|
|||||||
"Update avaliable: ": "",
|
"Update avaliable: ": "",
|
||||||
"Download update: ": "",
|
"Download update: ": "",
|
||||||
"Updated to ": "",
|
"Updated to ": "",
|
||||||
|
"Press OK to restart Sphaira": "",
|
||||||
"Restart Sphaira?": "",
|
"Restart Sphaira?": "",
|
||||||
"Failed to download update": "",
|
"Failed to download update": "",
|
||||||
|
"Restore hbmenu?": "",
|
||||||
|
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "",
|
||||||
|
"Failed to restore hbmenu, please re-download hbmenu": "",
|
||||||
|
"Failed to restore hbmenu, using sphaira instead": "",
|
||||||
|
"Restored hbmenu, closing sphaira": "",
|
||||||
|
"Restored hbmenu": "",
|
||||||
"Delete Selected files?": "",
|
"Delete Selected files?": "",
|
||||||
"Completely remove ": "",
|
"Completely remove ": "",
|
||||||
"Are you sure you want to delete ": "Weet u zeker dat u wilt verwijderen ",
|
"Are you sure you want to delete ": "Weet u zeker dat u wilt verwijderen ",
|
||||||
"Are you sure you wish to cancel?": "",
|
"Are you sure you wish to cancel?": "",
|
||||||
|
"Audio disabled due to suspended game": "",
|
||||||
"If this message appears repeatedly, please open an issue.": ""
|
"If this message appears repeatedly, please open an issue.": ""
|
||||||
}
|
}
|
||||||
@@ -1,75 +1,84 @@
|
|||||||
{
|
{
|
||||||
"[Applet Mode]": "[Modo Applet]",
|
"[Applet Mode]": "[Modo Applet]",
|
||||||
"No Internet": "Sem Internet",
|
"No Internet": "Sem Internet",
|
||||||
"Files": "",
|
"Files": "Arquivos",
|
||||||
"Apps": "",
|
"Apps": "Aplicativos",
|
||||||
"Store": "",
|
"Store": "Loja",
|
||||||
"Menu": "Menu",
|
"Menu": "Menu",
|
||||||
"Options": "Opções",
|
"Options": "Opções",
|
||||||
"OK": "",
|
"OK": "OK",
|
||||||
"Back": "Voltar",
|
"Back": "Voltar",
|
||||||
"Select": "",
|
"Select": "Selecionar",
|
||||||
"Open": "Abrir",
|
"Open": "Abrir",
|
||||||
"Launch": "Iniciar",
|
"Launch": "Iniciar",
|
||||||
"Info": "Informações",
|
"Info": "Informações",
|
||||||
"Install": "Instalar",
|
"Install": "Instalar",
|
||||||
"Delete": "Excluir",
|
"Delete": "Excluir",
|
||||||
"Restart": "",
|
"Restart": "Reiniciar",
|
||||||
"Changelog": "",
|
"Changelog": "Alterações",
|
||||||
"Details": "",
|
"Details": "Detalhes",
|
||||||
"Update": "",
|
"Update": "Atualizar",
|
||||||
"Remove": "",
|
"Remove": "Remover",
|
||||||
"Download": "Download",
|
"Restore": "Restaurar",
|
||||||
|
"Download": "Baixar",
|
||||||
"Next Page": "Próxima página",
|
"Next Page": "Próxima página",
|
||||||
"Prev Page": "Página anterior",
|
"Prev Page": "Página anterior",
|
||||||
"Unstar": "",
|
"Unstar": "Desfavoritar",
|
||||||
"Star": "",
|
"Star": "Favoritar",
|
||||||
"System memory": "",
|
"System memory": "Memória do console",
|
||||||
"microSD card": "",
|
"microSD card": "Cartão microSD",
|
||||||
|
"Sd": "SD",
|
||||||
|
"Image System memory": "Imagem (memória do console)",
|
||||||
|
"Image microSD card": "Imagem (cartão microSD)",
|
||||||
|
"Slow": "Lenta",
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Fast": "Rápida",
|
||||||
"Yes": "Sim",
|
"Yes": "Sim",
|
||||||
"No": "Não",
|
"No": "Não",
|
||||||
"Enabled": "Habilitado",
|
"Enabled": "Sim",
|
||||||
"Disabled": "Desabilitado",
|
"Disabled": "Não",
|
||||||
|
|
||||||
"Sort By": "Ordenar por",
|
"Sort By": "Ordernar/Organizar",
|
||||||
"Sort Options": "Opções de classificação",
|
"Sort Options": "Ordernar/Organizar",
|
||||||
"Filter": "Filtro",
|
"Filter": "Filtro",
|
||||||
"Sort": "Organizar",
|
"Sort": "Organizar por",
|
||||||
"Order": "Ordem",
|
"Order": "Ordem",
|
||||||
"Search": "Procurar",
|
"Search": "Buscar",
|
||||||
"Updated": "Atualizado",
|
"Updated": "Atualizado",
|
||||||
"Updated (Star)": "",
|
"Updated (Star)": "Atualizado (favoritos)",
|
||||||
"Downloads": "Downloads",
|
"Downloads": "Nº de downloads",
|
||||||
"Size": "Tamanho",
|
"Size": "Tamanho",
|
||||||
"Size (Star)": "",
|
"Size (Star)": "Tamanho (favoritos)",
|
||||||
"Alphabetical": "Alfabético",
|
"Alphabetical": "Ordem alfabética",
|
||||||
"Alphabetical (Star)": "",
|
"Alphabetical (Star)": "Ordem alfabética (favoritos)",
|
||||||
"Likes": "",
|
"Likes": "Nº de curtidas",
|
||||||
"ID": "",
|
"ID": "ID",
|
||||||
"Decending": "Decrescente",
|
"Descending": "Decrescente",
|
||||||
"Descending (down)": "Decrescente",
|
"Descending (down)": "Decrescente (baixo)",
|
||||||
"Desc": "Decrescente",
|
"Desc": "Decr.",
|
||||||
"Ascending": "Ascendente",
|
"Ascending": "Ascendente",
|
||||||
"Ascending (Up)": "Ascendente",
|
"Ascending (Up)": "Ascendente (cima)",
|
||||||
"Asc": "Ascendente",
|
"Asc": "Asc.",
|
||||||
|
|
||||||
"Menu Options": "Opções do menu",
|
"Menu Options": "Opções do menu",
|
||||||
"Header": "Cabeçalho",
|
|
||||||
"Theme": "Tema",
|
"Theme": "Tema",
|
||||||
"Theme Options": "Opções de tema",
|
"Theme Options": "Opções de tema",
|
||||||
"Select Theme": "Selecionar tema",
|
"Select Theme": "Tema atual",
|
||||||
"Shuffle": "Embaralhar",
|
"Shuffle": "Embaralhar temas",
|
||||||
"Music": "Música",
|
"Music": "Música",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "Rede",
|
"Network": "Rede",
|
||||||
"Network Options": "Opções de rede",
|
"Network Options": "Opções de rede",
|
||||||
|
"Ftp": "Servidor FTP",
|
||||||
|
"Mtp": "Escuta MTP",
|
||||||
"Nxlink": "Nxlink",
|
"Nxlink": "Nxlink",
|
||||||
"Nxlink Connected": "",
|
"Nxlink Connected": "Nxlink conectado",
|
||||||
"Nxlink Upload": "",
|
"Nxlink Upload": "Envio Nxlink",
|
||||||
"Nxlink Finished": "",
|
"Nxlink Finished": "Nxlink finalizado",
|
||||||
"Switch-Handheld!": "",
|
"Switch-Handheld!": "Switch-Portátil",
|
||||||
"Switch-Docked!": "",
|
"Switch-Docked!": "Switch-Docado",
|
||||||
"Language": "Idioma",
|
"Language": "Idioma",
|
||||||
"Auto": "",
|
"Auto": "Automático",
|
||||||
"English": "English",
|
"English": "English",
|
||||||
"Japanese": "日本語",
|
"Japanese": "日本語",
|
||||||
"French": "Français",
|
"French": "Français",
|
||||||
@@ -82,96 +91,100 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
"Logging": "Logging",
|
"Vietnamese": "Vietnamese",
|
||||||
"Replace hbmenu on exit": "Substitua hbmenu ao sair",
|
"Logging": "Registro de depuração",
|
||||||
|
"Replace hbmenu on exit": "Substituir hbmenu ao sair",
|
||||||
"Misc": "Diversos",
|
"Misc": "Diversos",
|
||||||
"Misc Options": "Opções diversas",
|
"Misc Options": "Opções diversas",
|
||||||
"Web": "Rede",
|
"Web": "Navegador de internet",
|
||||||
"Install forwarders": "",
|
"Install forwarders": "Instalar atalhos (forwarders)",
|
||||||
"Install location": "",
|
"Install location": "Local de instalação",
|
||||||
"Show install warning": "",
|
"Show install warning": "Mostrar aviso de instalação",
|
||||||
|
"Text scroll speed": "Rolagem do texto",
|
||||||
|
|
||||||
"FileBrowser": "Navegador de arquivos",
|
"FileBrowser": "Arquivos",
|
||||||
"%zd files": "%zd files",
|
"%zd files": "%zd arquivo(s)",
|
||||||
"%zd dirs": "%zd dirs",
|
"%zd dirs": "%zd diretório(s)",
|
||||||
"File Options": "Opções de arquivo",
|
"File Options": "Opções de arquivo",
|
||||||
"Show Hidden": "Mostrar oculto",
|
"Show Hidden": "Mostrar ocultos",
|
||||||
"Folders First": "Pastas primeiro",
|
"Folders First": "Pastas primeiro",
|
||||||
"Hidden Last": "Oculto por último",
|
"Hidden Last": "Ocultos por último",
|
||||||
"Cut": "Cortar",
|
"Cut": "Recortar",
|
||||||
"Copy": "Copiar",
|
"Copy": "Copiar",
|
||||||
"Paste": "",
|
"Paste": "Colar",
|
||||||
"Paste ": "",
|
"Paste ": "Colar ",
|
||||||
" file(s)?": "",
|
" file(s)?": " arquivo(s)?",
|
||||||
"Rename": "Renomear",
|
"Rename": "Renomear",
|
||||||
"Set New File Name": "",
|
"Set New File Name": "Defina o nome do novo arquivo",
|
||||||
"Advanced": "Avançado",
|
"Advanced": "Avançado",
|
||||||
"Advanced Options": "Criar arquivo",
|
"Advanced Options": "Opções avançadas",
|
||||||
"Create File": "Criar arquivo",
|
"Create File": "Criar arquivo",
|
||||||
"Set File Name": "",
|
"Set File Name": "Defina o nome do arquivo",
|
||||||
"Create Folder": "Criar pasta",
|
"Create Folder": "Criar pasta",
|
||||||
"Set Folder Name": "",
|
"Set Folder Name": "Defina o nome da pasta",
|
||||||
"View as text (unfinished)": "Ver como texto (inacabado)",
|
"View as text (unfinished)": "Ver como texto (inacabado)",
|
||||||
"Empty...": "",
|
"Ignore read only": "Ignorar somente leitura",
|
||||||
"Open with DayBreak?": "",
|
"Mount": "Montar",
|
||||||
"Launch ": "",
|
"Empty...": "Vazio...",
|
||||||
"Launch option for: ": "",
|
"Open with DayBreak?": "Abrir com DayBreak?",
|
||||||
"Select launcher for: ": "",
|
"Launch ": "Iniciar ",
|
||||||
|
"Launch option for: ": "Opções de inicialização para: ",
|
||||||
|
"Select launcher for: ": "Selecionar launcher para: ",
|
||||||
|
|
||||||
"Homebrew": "Homebrew",
|
"Homebrew": "Aplicativos",
|
||||||
"Homebrew Options": "Opções do Homebrew",
|
"Homebrew Options": "Opções do aplicativo",
|
||||||
"Hide Sphaira": "Esconder Sphaira",
|
"Hide Sphaira": "Esconder sphaira",
|
||||||
"Install Forwarder": "Instalar forwarder",
|
"Install Forwarder": "Instalar atalho (forwarder)",
|
||||||
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Isso pode resultar em um banimento!",
|
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Instalar atalhos pode\nresultar em um banimento!",
|
||||||
"Installing Forwarder": "",
|
"Installing Forwarder": "Instalando forwarder",
|
||||||
"Creating Program": "",
|
"Creating Program": "Criando Program",
|
||||||
"Creating Control": "",
|
"Creating Control": "Criando Control",
|
||||||
"Creating Meta": "",
|
"Creating Meta": "Criando Meta",
|
||||||
"Writing Nca": "",
|
"Writing Nca": "Escrevendo NCA",
|
||||||
"Updating ncm databse": "",
|
"Updating ncm databse": "Atualizando base de dados NCM",
|
||||||
"Pushing application record": "",
|
"Pushing application record": "Aplicando registro do aplicativo",
|
||||||
"Installed!": "",
|
"Installed!": "Instalado!",
|
||||||
"Failed to install forwarder": "",
|
"Failed to install forwarder": "Falha ao instalar forwarder",
|
||||||
"Unstarred ": "",
|
"Unstarred ": "Desfavoritado ",
|
||||||
"Starred ": "",
|
"Starred ": "Favoritado ",
|
||||||
|
|
||||||
"AppStore": "",
|
"AppStore": "Loja",
|
||||||
"Filter: %s | Sort: %s | Order: %s": "Filtro: %s | Organizar: %s | Ordem: %s",
|
"Filter: %s | Sort: %s | Order: %s": "Filtro: %s | Por: %s | Ordem: %s",
|
||||||
"AppStore Options": "Opções da AppStore",
|
"AppStore Options": "Opções da loja",
|
||||||
"All": "Todos",
|
"All": "Todos",
|
||||||
"Games": "Jogos",
|
"Games": "Jogos",
|
||||||
"Emulators": "Emuladores",
|
"Emulators": "Emuladores",
|
||||||
"Tools": "Ferramentas",
|
"Tools": "Ferramentas",
|
||||||
"Themes": "Temas",
|
"Themes": "Temas",
|
||||||
"Legacy": "Legado",
|
"Legacy": "Legado",
|
||||||
"version: %s": "version: %s",
|
"version: %s": "versão: %s",
|
||||||
"updated: %s": "updated: %s",
|
"updated: %s": "atualizado: %s",
|
||||||
"category: %s": "category: %s",
|
"category: %s": "categoria: %s",
|
||||||
"extracted: %.2f MiB": "extracted: %.2f MiB",
|
"extracted: %.2f MiB": "tam. extraído: %.2f MiB",
|
||||||
"app_dls: %s": "app_dls: %s",
|
"app_dls: %s": "downloads: %s",
|
||||||
"More by Author": "",
|
"More by Author": "Mais deste autor",
|
||||||
"Leave Feedback": "",
|
"Leave Feedback": "Deixar um feedback",
|
||||||
|
|
||||||
"Irs": "Irs",
|
"Irs": "Sensor infravermelho",
|
||||||
"Ambient Noise Level: ": "",
|
"Ambient Noise Level: ": "Nível de ruído ambiente: ",
|
||||||
"Controller": "Controle",
|
"Controller": "Controle",
|
||||||
"Pad ": "Pad ",
|
"Pad ": "Pad ",
|
||||||
" (Available)": " (Disponível)",
|
" (Available)": " (disponível)",
|
||||||
" (Unsupported)": "",
|
" (Unsupported)": "(não suportado)",
|
||||||
" (Unconnected)": " (Desconectado)",
|
" (Unconnected)": " (desconectado)",
|
||||||
"HandHeld": "Portátil",
|
"HandHeld": "Portátil",
|
||||||
"Rotation": "Rotação",
|
"Rotation": "Rotação",
|
||||||
"0 (Sideways)": "0 (Lateralmente)",
|
"0 (Sideways)": "0 (lateralmente)",
|
||||||
"90 (Flat)": "90 (plano)",
|
"90 (Flat)": "90 (plano)",
|
||||||
"180 (-Sideways)": "180 (-Lateralmente)",
|
"180 (-Sideways)": "180 (-lateralmente)",
|
||||||
"270 (Upside down)": "270 (De cabeça para baixo)",
|
"270 (Upside down)": "270 (de cabeça para baixo)",
|
||||||
"Colour": "Cor",
|
"Colour": "Cor",
|
||||||
"Grey": "Cinza",
|
"Grey": "Cinza",
|
||||||
"Ironbow": "Arco de ferro",
|
"Ironbow": "Ferro",
|
||||||
"Green": "Verde",
|
"Green": "Verde",
|
||||||
"Red": "Vermelho",
|
"Red": "Vermelho",
|
||||||
"Blue": "Azul",
|
"Blue": "Azul",
|
||||||
"Light Target": "Alvo leve",
|
"Light Target": "Alvo de luz",
|
||||||
"All leds": "Todos os LEDs",
|
"All leds": "Todos os LEDs",
|
||||||
"Bright group": "Grupo claro",
|
"Bright group": "Grupo claro",
|
||||||
"Dim group": "Grupo escuro",
|
"Dim group": "Grupo escuro",
|
||||||
@@ -180,51 +193,65 @@
|
|||||||
"Negative Image": "Imagem negativa",
|
"Negative Image": "Imagem negativa",
|
||||||
"Normal image": "Imagem normal",
|
"Normal image": "Imagem normal",
|
||||||
"Negative image": "Imagem negativa",
|
"Negative image": "Imagem negativa",
|
||||||
"Format": "Formatar",
|
"Format": "Formato",
|
||||||
"320x240": "320×240",
|
"320x240": "320×240",
|
||||||
"160x120": "160×120",
|
"160x120": "160×120",
|
||||||
"80x60": "80×60",
|
"80x60": "80×60",
|
||||||
"40x30": "40×30",
|
"40x30": "40×30",
|
||||||
"20x15": "20×15",
|
"20x15": "20×15",
|
||||||
"Trimming Format": "Formato de corte",
|
"Trimming Format": "Formato do recorte",
|
||||||
"External Light Filter": "Filtro de luz externo",
|
"External Light Filter": "Filtro de luz externa",
|
||||||
"Load Default": "Carregar padrão",
|
"Load Default": "Restaurar padrão",
|
||||||
|
|
||||||
"Themezer": "Themezer",
|
"Themezer": "Themezer",
|
||||||
"Themezer Options": "",
|
"Themezer Options": "Opções do Themezer",
|
||||||
"Nsfw": "",
|
"Nsfw": "Temas 18+ (NSFW)",
|
||||||
"Page": "",
|
"Page": "Ir para página",
|
||||||
"Page %zu / %zu": "Page %zu / %zu",
|
"Page %zu / %zu": "Página %zu / %zu",
|
||||||
"Enter Page Number": "",
|
"Enter Page Number": "Número da página",
|
||||||
"Bad Page": "",
|
"Bad Page": "Página inválida",
|
||||||
"Download theme?": "",
|
"Download theme?": "Baixar tema?",
|
||||||
|
|
||||||
"Installing ": "",
|
"GitHub": "GitHub",
|
||||||
"Uninstalling ": "",
|
"Downloading json": "Baixando JSON",
|
||||||
"Deleting ": "",
|
"Select asset to download for ": "Selecione o recurso para baixar de ",
|
||||||
"Deleting": "",
|
|
||||||
"Pasting ": "",
|
"Installing ": "Instalando ",
|
||||||
"Pasting": "",
|
"Uninstalling ": "Desinstalando ",
|
||||||
"Removing ": "",
|
"Deleting ": "Excluindo ",
|
||||||
"Scanning ": "",
|
"Deleting": "Excluindo",
|
||||||
"Creating ": "",
|
"Pasting ": "Colando ",
|
||||||
"Copying ": "",
|
"Pasting": "Colando ",
|
||||||
"Trying to load ": "",
|
"Removing ": "Removendo ",
|
||||||
"Downloading ": "",
|
"Scanning ": "Analisando ",
|
||||||
"Checking MD5": "",
|
"Creating ": "Criando ",
|
||||||
"Loading...": "",
|
"Copying ": "Copiando ",
|
||||||
"Loading": "",
|
"Trying to load ": "Tentando carregar ",
|
||||||
"Empty!": "",
|
"Downloading ": "Baixando ",
|
||||||
"Not Ready...": "",
|
"Downloaded ": "Baixado ",
|
||||||
"Error loading page!": "",
|
"Removed ": "Removido ",
|
||||||
"Update avaliable: ": "",
|
"Checking MD5": "Checando MD5",
|
||||||
"Download update: ": "",
|
"Loading...": "Carregando...",
|
||||||
"Updated to ": "",
|
"Loading": "Carregando",
|
||||||
"Restart Sphaira?": "",
|
"Empty!": "Vazio",
|
||||||
"Failed to download update": "",
|
"Not Ready...": "Não está pronto...",
|
||||||
"Delete Selected files?": "",
|
"Error loading page!": "Erro ao carregar página!",
|
||||||
"Completely remove ": "",
|
"Update avaliable: ": "Atualização disponível: ",
|
||||||
"Are you sure you want to delete ": "Excluir ",
|
"Download update: ": "Baixar autalização: ",
|
||||||
"Are you sure you wish to cancel?": "",
|
"Updated to ": "Atualizado para ",
|
||||||
"If this message appears repeatedly, please open an issue.": ""
|
"Press OK to restart Sphaira": "Selecione OK para reiniciar o sphaira",
|
||||||
|
"Restart Sphaira?": "Reiniciar sphaira?",
|
||||||
|
"Failed to download update": "Falha ao baixar a atualização",
|
||||||
|
"Restore hbmenu?": "Restaurar hbmenu?",
|
||||||
|
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Falha ao buscar /switch/hbmenu.nro\nUse a AppStore para reinstalar o hbmenu",
|
||||||
|
"Failed to restore hbmenu, please re-download hbmenu": "Falha ao restaurar o hbmenu, baixe o hbmenu novamente",
|
||||||
|
"Failed to restore hbmenu, using sphaira instead": "Falha ao restaurar hbmenu, usando sphaira",
|
||||||
|
"Restored hbmenu, closing sphaira": "hbmenu restaurado, fechando sphaira",
|
||||||
|
"Restored hbmenu": "hbmenu restaurado",
|
||||||
|
"Delete Selected files?": "Excluir os arquivos selecionados?",
|
||||||
|
"Completely remove ": "Remover completamente ",
|
||||||
|
"Are you sure you want to delete ": "Você tem certeza que quer excluir ",
|
||||||
|
"Are you sure you wish to cancel?": "Você tem certeza que quer cancelar?",
|
||||||
|
"Audio disabled due to suspended game": "Áudio desativado devido ao software suspenso",
|
||||||
|
"If this message appears repeatedly, please open an issue.": "Se esta mensagem aparecer repetidamente, abra um issue."
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"Details": "",
|
"Details": "",
|
||||||
"Update": "",
|
"Update": "",
|
||||||
"Remove": "",
|
"Remove": "",
|
||||||
|
"Restore": "",
|
||||||
"Download": "Скачать",
|
"Download": "Скачать",
|
||||||
"Next Page": "Следующая страница",
|
"Next Page": "Следующая страница",
|
||||||
"Prev Page": "Предыдущая страница",
|
"Prev Page": "Предыдущая страница",
|
||||||
@@ -26,6 +27,12 @@
|
|||||||
"Star": "",
|
"Star": "",
|
||||||
"System memory": "",
|
"System memory": "",
|
||||||
"microSD card": "",
|
"microSD card": "",
|
||||||
|
"Sd": "",
|
||||||
|
"Image System memory": "",
|
||||||
|
"Image microSD card": "",
|
||||||
|
"Slow": "",
|
||||||
|
"Normal": "",
|
||||||
|
"Fast": "",
|
||||||
"Yes": "Да",
|
"Yes": "Да",
|
||||||
"No": "Нет",
|
"No": "Нет",
|
||||||
"Enabled": "Включено",
|
"Enabled": "Включено",
|
||||||
@@ -46,7 +53,7 @@
|
|||||||
"Alphabetical (Star)": "",
|
"Alphabetical (Star)": "",
|
||||||
"Likes": "",
|
"Likes": "",
|
||||||
"ID": "",
|
"ID": "",
|
||||||
"Decending": "По убыванию",
|
"Descending": "По убыванию",
|
||||||
"Descending (down)": "По убыванию",
|
"Descending (down)": "По убыванию",
|
||||||
"Desc": "По убыванию",
|
"Desc": "По убыванию",
|
||||||
"Ascending": "По возрастанию",
|
"Ascending": "По возрастанию",
|
||||||
@@ -54,14 +61,16 @@
|
|||||||
"Asc": "По возрастанию",
|
"Asc": "По возрастанию",
|
||||||
|
|
||||||
"Menu Options": "Параметры меню",
|
"Menu Options": "Параметры меню",
|
||||||
"Header": "Заголовок",
|
|
||||||
"Theme": "Тема",
|
"Theme": "Тема",
|
||||||
"Theme Options": "Параметры темы",
|
"Theme Options": "Параметры темы",
|
||||||
"Select Theme": "Выберите тему",
|
"Select Theme": "Выберите тему",
|
||||||
"Shuffle": "Перетасовать",
|
"Shuffle": "Перетасовать",
|
||||||
"Music": "Музыка",
|
"Music": "Музыка",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "Сеть",
|
"Network": "Сеть",
|
||||||
"Network Options": "Параметры сети",
|
"Network Options": "Параметры сети",
|
||||||
|
"Ftp": "FTP",
|
||||||
|
"Mtp": "MTP",
|
||||||
"Nxlink": "Nxlink",
|
"Nxlink": "Nxlink",
|
||||||
"Nxlink Connected": "",
|
"Nxlink Connected": "",
|
||||||
"Nxlink Upload": "",
|
"Nxlink Upload": "",
|
||||||
@@ -82,6 +91,7 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "Журналирование",
|
"Logging": "Журналирование",
|
||||||
"Replace hbmenu on exit": "Заменить hbmenu при выходе",
|
"Replace hbmenu on exit": "Заменить hbmenu при выходе",
|
||||||
"Misc": "Прочее",
|
"Misc": "Прочее",
|
||||||
@@ -90,6 +100,7 @@
|
|||||||
"Install forwarders": "",
|
"Install forwarders": "",
|
||||||
"Install location": "",
|
"Install location": "",
|
||||||
"Show install warning": "",
|
"Show install warning": "",
|
||||||
|
"Text scroll speed": "",
|
||||||
|
|
||||||
"FileBrowser": "Файловый менеджер",
|
"FileBrowser": "Файловый менеджер",
|
||||||
"%zd files": "%zd files",
|
"%zd files": "%zd files",
|
||||||
@@ -112,6 +123,8 @@
|
|||||||
"Create Folder": "Создать папку",
|
"Create Folder": "Создать папку",
|
||||||
"Set Folder Name": "",
|
"Set Folder Name": "",
|
||||||
"View as text (unfinished)": "Посмотреть как текст (незакончено)",
|
"View as text (unfinished)": "Посмотреть как текст (незакончено)",
|
||||||
|
"Ignore read only": "",
|
||||||
|
"Mount": "",
|
||||||
"Empty...": "",
|
"Empty...": "",
|
||||||
"Open with DayBreak?": "",
|
"Open with DayBreak?": "",
|
||||||
"Launch ": "",
|
"Launch ": "",
|
||||||
@@ -199,6 +212,10 @@
|
|||||||
"Bad Page": "",
|
"Bad Page": "",
|
||||||
"Download theme?": "",
|
"Download theme?": "",
|
||||||
|
|
||||||
|
"GitHub": "",
|
||||||
|
"Downloading json": "",
|
||||||
|
"Select asset to download for ": "",
|
||||||
|
|
||||||
"Installing ": "",
|
"Installing ": "",
|
||||||
"Uninstalling ": "",
|
"Uninstalling ": "",
|
||||||
"Deleting ": "",
|
"Deleting ": "",
|
||||||
@@ -211,6 +228,8 @@
|
|||||||
"Copying ": "",
|
"Copying ": "",
|
||||||
"Trying to load ": "",
|
"Trying to load ": "",
|
||||||
"Downloading ": "",
|
"Downloading ": "",
|
||||||
|
"Downloaded ": "",
|
||||||
|
"Removed ": "",
|
||||||
"Checking MD5": "",
|
"Checking MD5": "",
|
||||||
"Loading...": "",
|
"Loading...": "",
|
||||||
"Loading": "",
|
"Loading": "",
|
||||||
@@ -220,11 +239,19 @@
|
|||||||
"Update avaliable: ": "",
|
"Update avaliable: ": "",
|
||||||
"Download update: ": "",
|
"Download update: ": "",
|
||||||
"Updated to ": "",
|
"Updated to ": "",
|
||||||
|
"Press OK to restart Sphaira": "",
|
||||||
"Restart Sphaira?": "",
|
"Restart Sphaira?": "",
|
||||||
"Failed to download update": "",
|
"Failed to download update": "",
|
||||||
|
"Restore hbmenu?": "",
|
||||||
|
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "",
|
||||||
|
"Failed to restore hbmenu, please re-download hbmenu": "",
|
||||||
|
"Failed to restore hbmenu, using sphaira instead": "",
|
||||||
|
"Restored hbmenu, closing sphaira": "",
|
||||||
|
"Restored hbmenu": "",
|
||||||
"Delete Selected files?": "",
|
"Delete Selected files?": "",
|
||||||
"Completely remove ": "",
|
"Completely remove ": "",
|
||||||
"Are you sure you want to delete ": "Вы уверены, что хотите удалить ",
|
"Are you sure you want to delete ": "Вы уверены, что хотите удалить ",
|
||||||
"Are you sure you wish to cancel?": "",
|
"Are you sure you wish to cancel?": "",
|
||||||
|
"Audio disabled due to suspended game": "",
|
||||||
"If this message appears repeatedly, please open an issue.": ""
|
"If this message appears repeatedly, please open an issue.": ""
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"[Applet Mode]": "[Applet-läge]",
|
"[Applet Mode]": "[Applet-läge]",
|
||||||
"No Internet": "Ingen internetanslutning",
|
"No Internet": "Ingen Internetanslutning",
|
||||||
"Files": "Filer",
|
"Files": "Filer",
|
||||||
"Apps": "Appar",
|
"Apps": "Appar",
|
||||||
"Store": "Butik",
|
"Store": "Butik",
|
||||||
@@ -14,60 +14,69 @@
|
|||||||
"Info": "Info",
|
"Info": "Info",
|
||||||
"Install": "Installera",
|
"Install": "Installera",
|
||||||
"Delete": "Radera",
|
"Delete": "Radera",
|
||||||
"Restart": "",
|
"Restart": "Starta om",
|
||||||
"Changelog": "Ändringslogg",
|
"Changelog": "Ändringslogg",
|
||||||
"Details": "Detaljer",
|
"Details": "Detaljer",
|
||||||
"Update": "Uppdatera",
|
"Update": "Uppdatera",
|
||||||
"Remove": "Ta bort",
|
"Remove": "Ta bort",
|
||||||
|
"Restore": "Återställ",
|
||||||
"Download": "Ladda ner",
|
"Download": "Ladda ner",
|
||||||
"Next Page": "Nästa sida",
|
"Next Page": "Nästa sida",
|
||||||
"Prev Page": "Föregående sida",
|
"Prev Page": "Föregående sida",
|
||||||
"Unstar": "",
|
"Unstar": "Avmarkera stjärna",
|
||||||
"Star": "",
|
"Star": "Markera stjärna",
|
||||||
"System memory": "",
|
"System memory": "Systemminne",
|
||||||
"microSD card": "",
|
"microSD card": "microSD-kort",
|
||||||
|
"Sd": "Sd",
|
||||||
|
"Image System memory": "Avbild Systemminne",
|
||||||
|
"Image microSD card": "Avbild microSD-kort",
|
||||||
|
"Slow": "",
|
||||||
|
"Normal": "",
|
||||||
|
"Fast": "",
|
||||||
"Yes": "Ja",
|
"Yes": "Ja",
|
||||||
"No": "Nej",
|
"No": "Nej",
|
||||||
"Enabled": "Aktiverad",
|
"Enabled": "Aktiverad",
|
||||||
"Disabled": "Avaktiverad",
|
"Disabled": "Inaktiverad",
|
||||||
|
|
||||||
"Sort By": "Sortera efter",
|
"Sort By": "Sortera efter",
|
||||||
"Sort Options": "Sorteringsalternativ",
|
"Sort Options": "Sorteringsalternativ",
|
||||||
"Filter": "Filter",
|
"Filter": "Filtrera",
|
||||||
"Sort": "Sortera",
|
"Sort": "Sortera",
|
||||||
"Order": "Ordning",
|
"Order": "Ordning",
|
||||||
"Search": "Sök",
|
"Search": "Sök",
|
||||||
"Updated": "Uppdaterad",
|
"Updated": "Uppdaterad",
|
||||||
"Updated (Star)": "",
|
"Updated (Star)": "Uppdaterad (Stjärna)",
|
||||||
"Downloads": "Nedladdningar",
|
"Downloads": "Nedladdningar",
|
||||||
"Size": "Storlek",
|
"Size": "Storlek",
|
||||||
"Size (Star)": "",
|
"Size (Star)": "Storlek (Stjärna)",
|
||||||
"Alphabetical": "Alfabetisk",
|
"Alphabetical": "Alfabetisk",
|
||||||
"Alphabetical (Star)": "",
|
"Alphabetical (Star)": "Alfabetisk (Stjärna)",
|
||||||
"Likes": "Gillar",
|
"Likes": "Gillar",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"Decending": "Fallande",
|
"Descending": "Fallande",
|
||||||
"Descending (down)": "Fallande (nedåt)",
|
"Descending (down)": "Fallande (nedåt)",
|
||||||
"Desc": "Fallande",
|
"Desc": "Fall",
|
||||||
"Ascending": "Stigande",
|
"Ascending": "Stigande",
|
||||||
"Ascending (Up)": "Stigande (uppåt)",
|
"Ascending (Up)": "Stigande (uppåt)",
|
||||||
"Asc": "Stigande",
|
"Asc": "Stig",
|
||||||
|
|
||||||
"Menu Options": "Menyalternativ",
|
"Menu Options": "Menyalternativ",
|
||||||
"Header": "Rubrik",
|
|
||||||
"Theme": "Tema",
|
"Theme": "Tema",
|
||||||
"Theme Options": "Temaalternativ",
|
"Theme Options": "Temaalternativ",
|
||||||
"Select Theme": "Välj tema",
|
"Select Theme": "Välj tema",
|
||||||
"Shuffle": "Blanda",
|
"Shuffle": "Blanda",
|
||||||
"Music": "Musik",
|
"Music": "Musik",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "Nätverk",
|
"Network": "Nätverk",
|
||||||
"Network Options": "Nätverksalternativ",
|
"Network Options": "Nätverksalternativ",
|
||||||
|
"Ftp": "FTP",
|
||||||
|
"Mtp": "MTP",
|
||||||
"Nxlink": "Nxlink",
|
"Nxlink": "Nxlink",
|
||||||
"Nxlink Connected": "Nxlink ansluten",
|
"Nxlink Connected": "Nxlink ansluten",
|
||||||
"Nxlink Upload": "Nxlink uppladdning",
|
"Nxlink Upload": "Nxlink överför",
|
||||||
"Nxlink Finished": "Nxlink klar",
|
"Nxlink Finished": "Nxlink klar",
|
||||||
"Switch-Handheld!": "",
|
"Switch-Handheld!": "Switch Handhållen!",
|
||||||
"Switch-Docked!": "",
|
"Switch-Docked!": "Switch Dockad!",
|
||||||
"Language": "Språk",
|
"Language": "Språk",
|
||||||
"Auto": "Auto",
|
"Auto": "Auto",
|
||||||
"English": "Engelska",
|
"English": "Engelska",
|
||||||
@@ -82,14 +91,16 @@
|
|||||||
"Portuguese": "Portugisiska",
|
"Portuguese": "Portugisiska",
|
||||||
"Russian": "Ryska",
|
"Russian": "Ryska",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "Loggning",
|
"Logging": "Loggning",
|
||||||
"Replace hbmenu on exit": "Ersätt hbmenu vid avslut",
|
"Replace hbmenu on exit": "Ersätt hbmenu vid avslut",
|
||||||
"Misc": "Övrigt",
|
"Misc": "Övrigt",
|
||||||
"Misc Options": "Övriga alternativ",
|
"Misc Options": "Övriga alternativ",
|
||||||
"Web": "Webb",
|
"Web": "Webb",
|
||||||
"Install forwarders": "",
|
"Install forwarders": "Installera genvägar",
|
||||||
"Install location": "",
|
"Install location": "Installationsplats",
|
||||||
"Show install warning": "",
|
"Show install warning": "Visa installationsvarning",
|
||||||
|
"Text scroll speed": "",
|
||||||
|
|
||||||
"FileBrowser": "Filbläddrare",
|
"FileBrowser": "Filbläddrare",
|
||||||
"%zd files": "%zd filer",
|
"%zd files": "%zd filer",
|
||||||
@@ -112,28 +123,30 @@
|
|||||||
"Create Folder": "Skapa mapp",
|
"Create Folder": "Skapa mapp",
|
||||||
"Set Folder Name": "Ange mappnamn",
|
"Set Folder Name": "Ange mappnamn",
|
||||||
"View as text (unfinished)": "Visa som text (ofärdig)",
|
"View as text (unfinished)": "Visa som text (ofärdig)",
|
||||||
|
"Ignore read only": "Ignorera skrivskydd",
|
||||||
|
"Mount": "Montera",
|
||||||
"Empty...": "Tom...",
|
"Empty...": "Tom...",
|
||||||
"Open with DayBreak?": "Öppna med DayBreak?",
|
"Open with DayBreak?": "Öppna med DayBreak?",
|
||||||
"Launch ": "",
|
"Launch ": "Starta ",
|
||||||
"Launch option for: ": "Startalternativ för: ",
|
"Launch option for: ": "Startalternativ för: ",
|
||||||
"Select launcher for: ": "",
|
"Select launcher for: ": "Välj startprogram för: ",
|
||||||
|
|
||||||
"Homebrew": "Homebrew",
|
"Homebrew": "Homebrew",
|
||||||
"Homebrew Options": "Homebrew-alternativ",
|
"Homebrew Options": "Homebrew-alternativ",
|
||||||
"Hide Sphaira": "Dölj Sphaira",
|
"Hide Sphaira": "Dölj Sphaira",
|
||||||
"Install Forwarder": "Installera forwarder",
|
"Install Forwarder": "Installera genväg",
|
||||||
"WARNING: Installing forwarders will lead to a ban!": "VARNING: Att installera forwarders leder till en avstängning!",
|
"WARNING: Installing forwarders will lead to a ban!": "VARNING: Att installera genvägar kan leda till avstängning!",
|
||||||
"Installing Forwarder": "Installerar forwarder",
|
"Installing Forwarder": "Installerar genväg",
|
||||||
"Creating Program": "Skapar program",
|
"Creating Program": "Skapar program",
|
||||||
"Creating Control": "Skapar kontroll",
|
"Creating Control": "Skapar kontroll",
|
||||||
"Creating Meta": "Skapar meta",
|
"Creating Meta": "Skapar metadata",
|
||||||
"Writing Nca": "Skriver Nca",
|
"Writing Nca": "Skriver Nca",
|
||||||
"Updating ncm databse": "Uppdaterar ncm-databas",
|
"Updating ncm databse": "Uppdaterar ncm-databas",
|
||||||
"Pushing application record": "Lägger till applikationspost",
|
"Pushing application record": "Skickar programpost",
|
||||||
"Installed!": "Installerad!",
|
"Installed!": "Installerad!",
|
||||||
"Failed to install forwarder": "Misslyckades med att installera forwarder",
|
"Failed to install forwarder": "Misslyckades att installera genväg",
|
||||||
"Unstarred ": "",
|
"Unstarred ": "Avmarkerad ",
|
||||||
"Starred ": "",
|
"Starred ": "Markerad ",
|
||||||
|
|
||||||
"AppStore": "AppStore",
|
"AppStore": "AppStore",
|
||||||
"Filter: %s | Sort: %s | Order: %s": "Filter: %s | Sortering: %s | Ordning: %s",
|
"Filter: %s | Sort: %s | Order: %s": "Filter: %s | Sortering: %s | Ordning: %s",
|
||||||
@@ -149,21 +162,21 @@
|
|||||||
"category: %s": "kategori: %s",
|
"category: %s": "kategori: %s",
|
||||||
"extracted: %.2f MiB": "extraherad: %.2f MiB",
|
"extracted: %.2f MiB": "extraherad: %.2f MiB",
|
||||||
"app_dls: %s": "app_nedladdningar: %s",
|
"app_dls: %s": "app_nedladdningar: %s",
|
||||||
"More by Author": "Mer från författaren",
|
"More by Author": "Mer av författaren",
|
||||||
"Leave Feedback": "Lämna feedback",
|
"Leave Feedback": "Lämna feedback",
|
||||||
|
|
||||||
"Irs": "Irs",
|
"Irs": "Irs",
|
||||||
"Ambient Noise Level: ": "Omgivningsljudnivå: ",
|
"Ambient Noise Level: ": "Omgivningsljudnivå: ",
|
||||||
"Controller": "Kontroll",
|
"Controller": "Kontroller",
|
||||||
"Pad ": "Handkontroll ",
|
"Pad ": "Handkontroll ",
|
||||||
" (Available)": " (Tillgänglig)",
|
" (Available)": " (Tillgänglig)",
|
||||||
" (Unsupported)": "",
|
" (Unsupported)": " (Ej stödd)",
|
||||||
" (Unconnected)": " (Ej ansluten)",
|
" (Unconnected)": " (Ej ansluten)",
|
||||||
"HandHeld": "Handhållen",
|
"HandHeld": "Handhållen",
|
||||||
"Rotation": "Rotation",
|
"Rotation": "Rotation",
|
||||||
"0 (Sideways)": "0 (Sido)",
|
"0 (Sideways)": "0 (Sidan)",
|
||||||
"90 (Flat)": "90 (Platt)",
|
"90 (Flat)": "90 (Platt)",
|
||||||
"180 (-Sideways)": "180 (-Sido)",
|
"180 (-Sideways)": "180 (-Sidan)",
|
||||||
"270 (Upside down)": "270 (Upp och ner)",
|
"270 (Upside down)": "270 (Upp och ner)",
|
||||||
"Colour": "Färg",
|
"Colour": "Färg",
|
||||||
"Grey": "Grå",
|
"Grey": "Grå",
|
||||||
@@ -173,8 +186,8 @@
|
|||||||
"Blue": "Blå",
|
"Blue": "Blå",
|
||||||
"Light Target": "Ljusmål",
|
"Light Target": "Ljusmål",
|
||||||
"All leds": "Alla lysdioder",
|
"All leds": "Alla lysdioder",
|
||||||
"Bright group": "Ljusstark grupp",
|
"Bright group": "Ljus grupp",
|
||||||
"Dim group": "Dämpad grupp",
|
"Dim group": "Dimma grupp",
|
||||||
"None": "Ingen",
|
"None": "Ingen",
|
||||||
"Gain": "Förstärkning",
|
"Gain": "Förstärkning",
|
||||||
"Negative Image": "Negativ bild",
|
"Negative Image": "Negativ bild",
|
||||||
@@ -186,9 +199,9 @@
|
|||||||
"80x60": "80×60",
|
"80x60": "80×60",
|
||||||
"40x30": "40×30",
|
"40x30": "40×30",
|
||||||
"20x15": "20×15",
|
"20x15": "20×15",
|
||||||
"Trimming Format": "Trimformat",
|
"Trimming Format": "Trimningsformat",
|
||||||
"External Light Filter": "Extern ljusfilter",
|
"External Light Filter": "Externt ljusfilter",
|
||||||
"Load Default": "Ladda standardinställningar",
|
"Load Default": "Ladda standard",
|
||||||
|
|
||||||
"Themezer": "Themezer",
|
"Themezer": "Themezer",
|
||||||
"Themezer Options": "Themezer-alternativ",
|
"Themezer Options": "Themezer-alternativ",
|
||||||
@@ -199,6 +212,10 @@
|
|||||||
"Bad Page": "Ogiltig sida",
|
"Bad Page": "Ogiltig sida",
|
||||||
"Download theme?": "Ladda ner tema?",
|
"Download theme?": "Ladda ner tema?",
|
||||||
|
|
||||||
|
"GitHub": "GitHub",
|
||||||
|
"Downloading json": "Laddar ner JSON",
|
||||||
|
"Select asset to download for ": "Välj tillgång att ladda ner för ",
|
||||||
|
|
||||||
"Installing ": "Installerar ",
|
"Installing ": "Installerar ",
|
||||||
"Uninstalling ": "Avinstallerar ",
|
"Uninstalling ": "Avinstallerar ",
|
||||||
"Deleting ": "Raderar ",
|
"Deleting ": "Raderar ",
|
||||||
@@ -209,22 +226,32 @@
|
|||||||
"Scanning ": "Skannar ",
|
"Scanning ": "Skannar ",
|
||||||
"Creating ": "Skapar ",
|
"Creating ": "Skapar ",
|
||||||
"Copying ": "Kopierar ",
|
"Copying ": "Kopierar ",
|
||||||
"Trying to load ": "",
|
"Trying to load ": "Försöker ladda ",
|
||||||
"Downloading ": "Laddar ner ",
|
"Downloading ": "Laddar ner ",
|
||||||
|
"Downloaded ": "Nedladdad ",
|
||||||
|
"Removed ": "Borttagen ",
|
||||||
"Checking MD5": "Kontrollerar MD5",
|
"Checking MD5": "Kontrollerar MD5",
|
||||||
"Loading...": "Laddar...",
|
"Loading...": "Laddar...",
|
||||||
"Loading": "Laddar",
|
"Loading": "Laddar",
|
||||||
"Empty!": "Tomt!",
|
"Empty!": "Tomt!",
|
||||||
"Not Ready...": "Ej redo...",
|
"Not Ready...": "Inte redo...",
|
||||||
"Error loading page!": "Fel vid laddning av sida!",
|
"Error loading page!": "Fel vid laddning av sida!",
|
||||||
"Update avaliable: ": "Uppdatering tillgänglig: ",
|
"Update avaliable: ": "Uppdatering tillgänglig: ",
|
||||||
"Download update: ": "Ladda ner uppdatering: ",
|
"Download update: ": "Ladda ner uppdatering: ",
|
||||||
"Updated to ": "",
|
"Updated to ": "Uppdaterad till ",
|
||||||
"Restart Sphaira?": "",
|
"Press OK to restart Sphaira": "",
|
||||||
"Failed to download update": "Misslyckades med att ladda ner uppdatering",
|
"Restart Sphaira?": "Starta om Sphaira?",
|
||||||
|
"Failed to download update": "Misslyckades att ladda ner uppdatering",
|
||||||
|
"Restore hbmenu?": "Återställ hbmenu?",
|
||||||
|
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Kunde inte hitta /switch/hbmenu.nro\nInstallera om hbmenu från Appstore ",
|
||||||
|
"Failed to restore hbmenu, please re-download hbmenu": "Misslyckades med att återställa hbmenu, vänligen ladda ner hbmenu igen.",
|
||||||
|
"Failed to restore hbmenu, using sphaira instead": "Misslyckades med att återställa hbmenu, använder istället sphaira.",
|
||||||
|
"Restored hbmenu, closing sphaira": "Återställde hbmenu, stänger sphaira.",
|
||||||
|
"Restored hbmenu": "Återställde hbmenu.",
|
||||||
"Delete Selected files?": "Radera valda filer?",
|
"Delete Selected files?": "Radera valda filer?",
|
||||||
"Completely remove ": "Ta bort helt ",
|
"Completely remove ": "Ta bort helt ",
|
||||||
"Are you sure you want to delete ": "Är du säker på att du vill radera ",
|
"Are you sure you want to delete ": "Är du säker på att du vill radera ",
|
||||||
"Are you sure you wish to cancel?": "Är du säker på att du vill avbryta?",
|
"Are you sure you wish to cancel?": "Är du säker på att du vill avbryta?",
|
||||||
"If this message appears repeatedly, please open an issue.": ""
|
"Audio disabled due to suspended game": "Ljud är avstängt på grund av bakgrundsprogram",
|
||||||
|
"If this message appears repeatedly, please open an issue.": "Om detta meddelande visas upprepade gånger, vänligen öppna en felanmälan.",
|
||||||
}
|
}
|
||||||
257
assets/romfs/i18n/vi.json
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
{
|
||||||
|
"[Applet Mode]": "[Applet Mode]",
|
||||||
|
"No Internet": "Không có Internet",
|
||||||
|
"Files": "Tập tin",
|
||||||
|
"Apps": "Ứng dụng",
|
||||||
|
"Store": "Cửa hàng",
|
||||||
|
"Menu": "Menu",
|
||||||
|
"Options": "Tuỳ chọn",
|
||||||
|
"OK": "OK",
|
||||||
|
"Back": "Trở về",
|
||||||
|
"Select": "Chọn",
|
||||||
|
"Open": "Mở",
|
||||||
|
"Launch": "Chạy",
|
||||||
|
"Info": "Thông tin",
|
||||||
|
"Install": "Cài đặt",
|
||||||
|
"Delete": "Xoá",
|
||||||
|
"Restart": "Khởi động lại",
|
||||||
|
"Changelog": "Thay đổi",
|
||||||
|
"Details": "Chi tiết",
|
||||||
|
"Update": "Cập nhật",
|
||||||
|
"Remove": "Gỡ",
|
||||||
|
"Restore": "Khôi phục",
|
||||||
|
"Download": "Tải về",
|
||||||
|
"Next Page": "Trang kế",
|
||||||
|
"Prev Page": "Trang trước",
|
||||||
|
"Unstar": "Xoá yêu thích",
|
||||||
|
"Star": "Yêu thích",
|
||||||
|
"System memory": "Bộ nhớ máy",
|
||||||
|
"microSD card": "Thẻ nhớ",
|
||||||
|
"Sd": "Sd",
|
||||||
|
"Image System memory": "Bộ nhớ hệ thống hình ảnh",
|
||||||
|
"Image microSD card": "Thẻ nhớ hệ thống hình ảnh",
|
||||||
|
"Slow": "",
|
||||||
|
"Normal": "",
|
||||||
|
"Fast": "",
|
||||||
|
"Yes": "Có",
|
||||||
|
"No": "Không",
|
||||||
|
"Enabled": "Bật",
|
||||||
|
"Disabled": "Tắt",
|
||||||
|
|
||||||
|
"Sort By": "Sắp xếp bởi",
|
||||||
|
"Sort Options": "Tuỳ chọn sắp xếp",
|
||||||
|
"Filter": "Lọc",
|
||||||
|
"Sort": "Sắp xếp",
|
||||||
|
"Order": "Thứ tự",
|
||||||
|
"Search": "Tìm kiếm",
|
||||||
|
"Updated": "Updated",
|
||||||
|
"Updated (Star)": "Đã cập nhật (Yêu thích)",
|
||||||
|
"Downloads": "Danh sách tải về",
|
||||||
|
"Size": "Kích thước",
|
||||||
|
"Size (Star)": "Kích thước (Yêu thích)",
|
||||||
|
"Alphabetical": "A-Z",
|
||||||
|
"Alphabetical (Star)": "A-Z (Yêu thích)",
|
||||||
|
"Likes": "Thích",
|
||||||
|
"ID": "ID",
|
||||||
|
"Descending": "Giảm dần",
|
||||||
|
"Descending (down)": "Giảm dần (xuống)",
|
||||||
|
"Desc": "Giảm",
|
||||||
|
"Ascending": "Tăng dần",
|
||||||
|
"Ascending (Up)": "Tăng dần (lên)",
|
||||||
|
"Asc": "Tăng",
|
||||||
|
|
||||||
|
"Menu Options": "Menu tuỳ chọn",
|
||||||
|
"Theme": "Theme",
|
||||||
|
"Theme Options": "Theme tuỳ chọn",
|
||||||
|
"Select Theme": "Chọn Theme",
|
||||||
|
"Shuffle": "Trộn",
|
||||||
|
"Music": "Âm nhạc",
|
||||||
|
"12 Hour Time": "",
|
||||||
|
"Network": "Mạng",
|
||||||
|
"Network Options": "Tuỳ chọn mạng",
|
||||||
|
"Ftp": "FTP",
|
||||||
|
"Mtp": "MTP",
|
||||||
|
"Nxlink": "Nxlink",
|
||||||
|
"Nxlink Connected": "Nxlink Kết Nối",
|
||||||
|
"Nxlink Upload": "Nxlink Đăng Tải",
|
||||||
|
"Nxlink Finished": "Nxlink Hoàn Thành",
|
||||||
|
"Switch-Handheld!": "Switch-Handheld!",
|
||||||
|
"Switch-Docked!": "Switch-Docked!",
|
||||||
|
"Language": "Ngôn ngữ",
|
||||||
|
"Auto": "Tự động",
|
||||||
|
"English": "English",
|
||||||
|
"Japanese": "日本語",
|
||||||
|
"French": "Français",
|
||||||
|
"German": "Deutsch",
|
||||||
|
"Italian": "Italiano",
|
||||||
|
"Spanish": "Español",
|
||||||
|
"Chinese": "中文",
|
||||||
|
"Korean": "한국어",
|
||||||
|
"Dutch": "Dutch",
|
||||||
|
"Portuguese": "Português",
|
||||||
|
"Russian": "Русский",
|
||||||
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Việt Nam",
|
||||||
|
"Logging": "Logging",
|
||||||
|
"Replace hbmenu on exit": "Thay thế hbmenu khi thoát",
|
||||||
|
"Misc": "Tiện ích",
|
||||||
|
"Misc Options": "Tiện ích mở rộng",
|
||||||
|
"Web": "Web",
|
||||||
|
"Install forwarders": "Cài ra màn hình",
|
||||||
|
"Install location": "Vị trí cài đặt",
|
||||||
|
"Show install warning": "Hiển thị cảnh báo cài đặt",
|
||||||
|
"Text scroll speed": "",
|
||||||
|
|
||||||
|
"FileBrowser": "Duyệt tập tin",
|
||||||
|
"%zd files": "%zd tập tin",
|
||||||
|
"%zd dirs": "%zd thư mục",
|
||||||
|
"File Options": "Tuỳ chọn tập tin",
|
||||||
|
"Show Hidden": "Hiển thị tập tin ẩn",
|
||||||
|
"Folders First": "Thư mục đầu tiên",
|
||||||
|
"Hidden Last": "Ẩn cuối",
|
||||||
|
"Cut": "Cắt",
|
||||||
|
"Copy": "Sao chép",
|
||||||
|
"Paste": "Dán",
|
||||||
|
"Paste ": "Paste ",
|
||||||
|
" file(s)?": " tập tin(nhiều)?",
|
||||||
|
"Rename": "Đổi tên",
|
||||||
|
"Set New File Name": "Đặt tên mới cho tập tin",
|
||||||
|
"Advanced": "Mở rộng",
|
||||||
|
"Advanced Options": "Tuỳ chọn mở rộng",
|
||||||
|
"Create File": "Tạo tập tin",
|
||||||
|
"Set File Name": "Đặt tên cho tập tin",
|
||||||
|
"Create Folder": "Tạo thư mục",
|
||||||
|
"Set Folder Name": "Đặt tên thư mục",
|
||||||
|
"View as text (unfinished)": "Xem dạng văn bản (chưa xong)",
|
||||||
|
"Ignore read only": "Bỏ qua chỉ đọc",
|
||||||
|
"Mount": "Gắn",
|
||||||
|
"Empty...": "Rỗng...",
|
||||||
|
"Open with DayBreak?": "Mở với DayBreak?",
|
||||||
|
"Launch ": "Chạy ",
|
||||||
|
"Launch option for: ": "Chạy với tuỳ chọn cho: ",
|
||||||
|
"Select launcher for: ": "Chọn trình chạy cho: ",
|
||||||
|
|
||||||
|
"Homebrew": "Homebrew",
|
||||||
|
"Homebrew Options": "Tuỳ chọn Homebrew",
|
||||||
|
"Hide Sphaira": "Ẩn Sphaira",
|
||||||
|
"Install Forwarder": "Cài ra ngoài màn hình",
|
||||||
|
"WARNING: Installing forwarders will lead to a ban!": "CẢNH BÁO: Bạn có chắn muốn cài ra ngoài màn hình!",
|
||||||
|
"Installing Forwarder": "Đang cài đặt ra ngoài màn hình",
|
||||||
|
"Creating Program": "Tạo chương trình",
|
||||||
|
"Creating Control": "Tạo điều khiển",
|
||||||
|
"Creating Meta": "Tạo Meta",
|
||||||
|
"Writing Nca": "Ghi Nca",
|
||||||
|
"Updating ncm databse": "Cập nhật ncm databse",
|
||||||
|
"Pushing application record": "Đẩy ứng dụng",
|
||||||
|
"Installed!": "Đã cài xong!",
|
||||||
|
"Failed to install forwarder": "Cài đặt ra ngoài màn hình thất bại",
|
||||||
|
"Unstarred ": "Bỏ yêu thích ",
|
||||||
|
"Starred ": "Đã yêu thích ",
|
||||||
|
|
||||||
|
"AppStore": "AppStore",
|
||||||
|
"Filter: %s | Sort: %s | Order: %s": "Lọc: %s | Sắp xếp: %s | Thứ tự: %s",
|
||||||
|
"AppStore Options": "Tuỳ chọn AppStore",
|
||||||
|
"All": "Tất cả",
|
||||||
|
"Games": "Games",
|
||||||
|
"Emulators": "Emulators",
|
||||||
|
"Tools": "Tools",
|
||||||
|
"Themes": "Themes",
|
||||||
|
"Legacy": "Legacy",
|
||||||
|
"version: %s": "version: %s",
|
||||||
|
"updated: %s": "updated: %s",
|
||||||
|
"category: %s": "category: %s",
|
||||||
|
"extracted: %.2f MiB": "extracted: %.2f MiB",
|
||||||
|
"app_dls: %s": "app_dls: %s",
|
||||||
|
"More by Author": "Xem thêm tác giả",
|
||||||
|
"Leave Feedback": "Để lại phản hồi",
|
||||||
|
|
||||||
|
"Irs": "Irs",
|
||||||
|
"Ambient Noise Level: ": "Mức ồn xung quanh: ",
|
||||||
|
"Controller": "Điều khiển",
|
||||||
|
"Pad ": "Pad ",
|
||||||
|
" (Available)": " (Có sẵn)",
|
||||||
|
" (Unsupported)": " (Không hỗ trợ)",
|
||||||
|
" (Unconnected)": " (Không kết nối)",
|
||||||
|
"HandHeld": "Cầm tay",
|
||||||
|
"Rotation": "Xoay",
|
||||||
|
"0 (Sideways)": "0 (Đi ngang)",
|
||||||
|
"90 (Flat)": "90 (Phẳng)",
|
||||||
|
"180 (-Sideways)": "180 (-Đi ngang)",
|
||||||
|
"270 (Upside down)": "270 (Lộn ngược)",
|
||||||
|
"Colour": "Màu sắc",
|
||||||
|
"Grey": "Xám",
|
||||||
|
"Ironbow": "Ironbow",
|
||||||
|
"Green": "Xanh",
|
||||||
|
"Red": "Đỏ",
|
||||||
|
"Blue": "Xanh dương",
|
||||||
|
"Light Target": "Điểm sáng",
|
||||||
|
"All leds": "Tất cả đèn led",
|
||||||
|
"Bright group": "Nhóm sáng",
|
||||||
|
"Dim group": "Nhóm tối",
|
||||||
|
"None": "Không có",
|
||||||
|
"Gain": "Tăng",
|
||||||
|
"Negative Image": "Ảnh âm bản",
|
||||||
|
"Normal image": "Ảnh bình thường",
|
||||||
|
"Negative image": "Ảnh âm bản",
|
||||||
|
"Format": "Định dạng",
|
||||||
|
"320x240": "320×240",
|
||||||
|
"160x120": "160×120",
|
||||||
|
"80x60": "80×60",
|
||||||
|
"40x30": "40×30",
|
||||||
|
"20x15": "20×15",
|
||||||
|
"Trimming Format": "Định dạng cắt tỉa",
|
||||||
|
"External Light Filter": "Bộ lộc ánh sáng bên ngoài",
|
||||||
|
"Load Default": "Tải mặc định",
|
||||||
|
|
||||||
|
"Themezer": "Themezer",
|
||||||
|
"Themezer Options": "Tuỳ chọn Themezer",
|
||||||
|
"Nsfw": "18+",
|
||||||
|
"Page": "Trang",
|
||||||
|
"Page %zu / %zu": "Trang %zu / %zu",
|
||||||
|
"Enter Page Number": "Nhập số trang",
|
||||||
|
"Bad Page": "Trang không tồn tại",
|
||||||
|
"Download theme?": "Tải theme?",
|
||||||
|
|
||||||
|
"GitHub": "GitHub",
|
||||||
|
"Downloading json": "Đang tải json",
|
||||||
|
"Select asset to download for ": "Chọn nội dung để tải xuống cho ",
|
||||||
|
|
||||||
|
"Installing ": "Đang cài đặt ",
|
||||||
|
"Uninstalling ": "Đang gỡ cài đặt ",
|
||||||
|
"Deleting ": "Đang xoá ",
|
||||||
|
"Deleting": "Đang xoá",
|
||||||
|
"Pasting ": "Đang dán ",
|
||||||
|
"Pasting": "Đang dán",
|
||||||
|
"Removing ": "Đang gỡ ",
|
||||||
|
"Scanning ": "Đang quét ",
|
||||||
|
"Creating ": "Đang tạo ",
|
||||||
|
"Copying ": "Đang sao chép ",
|
||||||
|
"Trying to load ": "Đang cố gắn mở ",
|
||||||
|
"Downloading ": "Đang tải xuống ",
|
||||||
|
"Downloaded ": "Đã tải xong ",
|
||||||
|
"Removed ": "Đã gỡ ",
|
||||||
|
"Checking MD5": "Kiểm tra MD5",
|
||||||
|
"Loading...": "Đang tải...",
|
||||||
|
"Loading": "Đang tải",
|
||||||
|
"Empty!": "Trống!",
|
||||||
|
"Not Ready...": "Chưa sẵn sàng...",
|
||||||
|
"Error loading page!": "Lỗi tải trang!",
|
||||||
|
"Update avaliable: ": "Cập nhậc có sẵn: ",
|
||||||
|
"Download update: ": "Tải cập nhật: ",
|
||||||
|
"Updated to ": "Đã cập nhật ",
|
||||||
|
"Press OK to restart Sphaira": "",
|
||||||
|
"Restart Sphaira?": "Khởi động lại Sphaira?",
|
||||||
|
"Failed to download update": "Cập nhật thất bại",
|
||||||
|
"Restore hbmenu?": "Khôi phục hbmenu?",
|
||||||
|
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Không tìm thấy /switch/hbmenu.nro\nSử dụng AppStore để cài lại hbmenu",
|
||||||
|
"Failed to restore hbmenu, please re-download hbmenu": "Không thể khôi phục hbmenu, Vui lòng tải lại hbmenu",
|
||||||
|
"Failed to restore hbmenu, using sphaira instead": "Không thể khôi phục hbmenu, thay vào đó sử dụng Sphira",
|
||||||
|
"Restored hbmenu, closing sphaira": "Khôi mục hbmenu thành công, đóng sphaira",
|
||||||
|
"Restored hbmenu": "Đã khôi phục hbmenu",
|
||||||
|
"Delete Selected files?": "Xoá những tập tin được chọn?",
|
||||||
|
"Completely remove ": "Đã gỡ thành công ",
|
||||||
|
"Are you sure you want to delete ": "Bạn có muốn xoá ",
|
||||||
|
"Are you sure you wish to cancel?": "Bạn có chắn muốn huỷ không?",
|
||||||
|
"Audio disabled due to suspended game": "",
|
||||||
|
"If this message appears repeatedly, please open an issue.": "Nếu thấy tin nhắn này, hãy báo lỗi."
|
||||||
|
}
|
||||||
@@ -14,11 +14,12 @@
|
|||||||
"Info": "信息",
|
"Info": "信息",
|
||||||
"Install": "安装",
|
"Install": "安装",
|
||||||
"Delete": "删除",
|
"Delete": "删除",
|
||||||
"Restart": "",
|
"Restart": "重启",
|
||||||
"Changelog": "更新日志",
|
"Changelog": "更新日志",
|
||||||
"Details": "详情",
|
"Details": "详情",
|
||||||
"Update": "更新",
|
"Update": "更新",
|
||||||
"Remove": "删除",
|
"Remove": "删除",
|
||||||
|
"Restore": "恢复",
|
||||||
"Download": "下载",
|
"Download": "下载",
|
||||||
"Next Page": "下一页",
|
"Next Page": "下一页",
|
||||||
"Prev Page": "上一页",
|
"Prev Page": "上一页",
|
||||||
@@ -26,6 +27,12 @@
|
|||||||
"Star": "星标",
|
"Star": "星标",
|
||||||
"System memory": "主机内存",
|
"System memory": "主机内存",
|
||||||
"microSD card": "SD卡",
|
"microSD card": "SD卡",
|
||||||
|
"Sd": "SD卡",
|
||||||
|
"Image System memory": "主机内存图像",
|
||||||
|
"Image microSD card": "SD卡图像",
|
||||||
|
"Slow": "慢",
|
||||||
|
"Normal": "正常",
|
||||||
|
"Fast": "快",
|
||||||
"Yes": "是",
|
"Yes": "是",
|
||||||
"No": "否",
|
"No": "否",
|
||||||
"Enabled": "启用",
|
"Enabled": "启用",
|
||||||
@@ -46,7 +53,7 @@
|
|||||||
"Alphabetical (Star)": "按字母顺序(星标优先)",
|
"Alphabetical (Star)": "按字母顺序(星标优先)",
|
||||||
"Likes": "点赞量",
|
"Likes": "点赞量",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"Decending": "降序",
|
"Descending": "降序",
|
||||||
"Descending (down)": "降序",
|
"Descending (down)": "降序",
|
||||||
"Desc": "降序",
|
"Desc": "降序",
|
||||||
"Ascending": "升序",
|
"Ascending": "升序",
|
||||||
@@ -54,20 +61,22 @@
|
|||||||
"Asc": "升序",
|
"Asc": "升序",
|
||||||
|
|
||||||
"Menu Options": "菜单选项",
|
"Menu Options": "菜单选项",
|
||||||
"Header": "标题",
|
|
||||||
"Theme": "主题",
|
"Theme": "主题",
|
||||||
"Theme Options": "主题选项",
|
"Theme Options": "主题选项",
|
||||||
"Select Theme": "选择主题",
|
"Select Theme": "选择主题",
|
||||||
"Shuffle": "随机播放",
|
"Shuffle": "随机播放",
|
||||||
"Music": "音乐",
|
"Music": "音乐",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "网络",
|
"Network": "网络",
|
||||||
"Network Options": "网络选项",
|
"Network Options": "网络选项",
|
||||||
"Nxlink": "Nxlink",
|
"Ftp": "FTP",
|
||||||
|
"Mtp": "MTP",
|
||||||
|
"Nxlink": "Nxlink插件提交",
|
||||||
"Nxlink Connected": "Nxlink 已连接",
|
"Nxlink Connected": "Nxlink 已连接",
|
||||||
"Nxlink Upload": "Nxlink 上传中",
|
"Nxlink Upload": "Nxlink 上传中",
|
||||||
"Nxlink Finished": "Nxlink 已结束",
|
"Nxlink Finished": "Nxlink 已结束",
|
||||||
"Switch-Handheld!": "",
|
"Switch-Handheld!": "切换至掌机模式!",
|
||||||
"Switch-Docked!": "",
|
"Switch-Docked!": "切换至底座模式!",
|
||||||
"Language": "语言",
|
"Language": "语言",
|
||||||
"Auto": "自动",
|
"Auto": "自动",
|
||||||
"English": "English",
|
"English": "English",
|
||||||
@@ -82,14 +91,16 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "日志",
|
"Logging": "日志",
|
||||||
"Replace hbmenu on exit": "退出后用Sphaira替换hbmenu",
|
"Replace hbmenu on exit": "退出后用Sphaira替换hbmenu",
|
||||||
"Misc": "杂项",
|
"Misc": "拓展",
|
||||||
"Misc Options": "杂项设置",
|
"Misc Options": "拓展设置",
|
||||||
"Web": "网页浏览器",
|
"Web": "网页浏览器",
|
||||||
"Install forwarders": "允许安装前端应用",
|
"Install forwarders": "允许安装前端应用",
|
||||||
"Install location": "安装位置",
|
"Install location": "安装位置",
|
||||||
"Show install warning": "显示安装警告",
|
"Show install warning": "显示安装警告",
|
||||||
|
"Text scroll speed": "文本滚动速度",
|
||||||
|
|
||||||
"FileBrowser": "文件浏览",
|
"FileBrowser": "文件浏览",
|
||||||
"%zd files": "%zd 个文件",
|
"%zd files": "%zd 个文件",
|
||||||
@@ -112,11 +123,13 @@
|
|||||||
"Create Folder": "新建文件夹",
|
"Create Folder": "新建文件夹",
|
||||||
"Set Folder Name": "输入文件夹名",
|
"Set Folder Name": "输入文件夹名",
|
||||||
"View as text (unfinished)": "以文本形式查看(未完善)",
|
"View as text (unfinished)": "以文本形式查看(未完善)",
|
||||||
|
"Ignore read only": "忽略只读",
|
||||||
|
"Mount": "挂载",
|
||||||
"Empty...": "空...",
|
"Empty...": "空...",
|
||||||
"Open with DayBreak?": "使用DayBreak打开?",
|
"Open with DayBreak?": "使用DayBreak打开?",
|
||||||
"Launch ": "",
|
"Launch ": "启动 ",
|
||||||
"Launch option for: ": "启动选项:",
|
"Launch option for: ": "启动选项:",
|
||||||
"Select launcher for: ": "",
|
"Select launcher for: ": "选择启动器用于:",
|
||||||
|
|
||||||
"Homebrew": "应用列表",
|
"Homebrew": "应用列表",
|
||||||
"Homebrew Options": "应用选项",
|
"Homebrew Options": "应用选项",
|
||||||
@@ -132,8 +145,8 @@
|
|||||||
"Pushing application record": "正在推送应用记录",
|
"Pushing application record": "正在推送应用记录",
|
||||||
"Installed!": "安装完成!",
|
"Installed!": "安装完成!",
|
||||||
"Failed to install forwarder": "前端应用安装失败",
|
"Failed to install forwarder": "前端应用安装失败",
|
||||||
"Unstarred ": "",
|
"Unstarred ": "取消星标 ",
|
||||||
"Starred ": "",
|
"Starred ": "已星标 ",
|
||||||
|
|
||||||
"AppStore": "应用商店",
|
"AppStore": "应用商店",
|
||||||
"Filter: %s | Sort: %s | Order: %s": "筛选: %s | 排序: %s | 顺序: %s",
|
"Filter: %s | Sort: %s | Order: %s": "筛选: %s | 排序: %s | 顺序: %s",
|
||||||
@@ -176,7 +189,7 @@
|
|||||||
"Bright group": "亮色组",
|
"Bright group": "亮色组",
|
||||||
"Dim group": "暗色组",
|
"Dim group": "暗色组",
|
||||||
"None": "无",
|
"None": "无",
|
||||||
"Gain": "增益",
|
"Gain": "曝光",
|
||||||
"Negative Image": "负片图像",
|
"Negative Image": "负片图像",
|
||||||
"Normal image": "正常图像",
|
"Normal image": "正常图像",
|
||||||
"Negative image": "负片图像",
|
"Negative image": "负片图像",
|
||||||
@@ -199,6 +212,10 @@
|
|||||||
"Bad Page": "错误的页面",
|
"Bad Page": "错误的页面",
|
||||||
"Download theme?": "下载该主题?",
|
"Download theme?": "下载该主题?",
|
||||||
|
|
||||||
|
"GitHub": "GitHub",
|
||||||
|
"Downloading json": "正在下载 json",
|
||||||
|
"Select asset to download for ": "选择要下载的资源用于 ",
|
||||||
|
|
||||||
"Installing ": "正在安装 ",
|
"Installing ": "正在安装 ",
|
||||||
"Uninstalling ": "正在卸载 ",
|
"Uninstalling ": "正在卸载 ",
|
||||||
"Deleting ": "正在删除 ",
|
"Deleting ": "正在删除 ",
|
||||||
@@ -209,22 +226,32 @@
|
|||||||
"Scanning ": "正在扫描 ",
|
"Scanning ": "正在扫描 ",
|
||||||
"Creating ": "正在创建 ",
|
"Creating ": "正在创建 ",
|
||||||
"Copying ": "正在复制 ",
|
"Copying ": "正在复制 ",
|
||||||
"Trying to load ": "",
|
"Trying to load ": "尝试加载 ",
|
||||||
"Downloading ": "正在下载 ",
|
"Downloading ": "正在下载 ",
|
||||||
|
"Downloaded ": "已下载 ",
|
||||||
|
"Removed ": "已移除 ",
|
||||||
"Checking MD5": "正在校验 MD5",
|
"Checking MD5": "正在校验 MD5",
|
||||||
"Loading...": "加载中...",
|
"Loading...": "加载中...",
|
||||||
"Loading": "加载中",
|
"Loading": "加载中",
|
||||||
"Empty!": "空!",
|
"Empty!": "空空如野!",
|
||||||
"Not Ready...": "尚未准备好...",
|
"Not Ready...": "尚未准备好...",
|
||||||
"Error loading page!": "页面加载失败!",
|
"Error loading page!": "页面加载失败!",
|
||||||
"Update avaliable: ": "有可用更新!",
|
"Update avaliable: ": "有可用更新!",
|
||||||
"Download update: ": "下载更新:",
|
"Download update: ": "下载更新:",
|
||||||
"Updated to ": "",
|
"Updated to ": "更新至 ",
|
||||||
"Restart Sphaira?": "",
|
"Press OK to restart Sphaira": "",
|
||||||
|
"Restart Sphaira?": "重启 Sphaira?",
|
||||||
"Failed to download update": "更新下载失败",
|
"Failed to download update": "更新下载失败",
|
||||||
|
"Restore hbmenu?": "恢复 hbmenu?",
|
||||||
|
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "未能找到 /switch/hbmenu.nro\n请使用应用商店重新安装 hbmenu",
|
||||||
|
"Failed to restore hbmenu, please re-download hbmenu": "恢复 hbmenu 失败,请重新下载 hbmenu",
|
||||||
|
"Failed to restore hbmenu, using sphaira instead": "恢复 hbmenu 失败,改用 Sphaira",
|
||||||
|
"Restored hbmenu, closing sphaira": "已恢复 hbmenu,正在关闭 Sphaira",
|
||||||
|
"Restored hbmenu": "已恢复 hbmenu",
|
||||||
"Delete Selected files?": "删除选中的文件?",
|
"Delete Selected files?": "删除选中的文件?",
|
||||||
"Completely remove ": "彻底删除 ",
|
"Completely remove ": "彻底删除 ",
|
||||||
"Are you sure you want to delete ": "您确定要删除吗 ",
|
"Are you sure you want to delete ": "您确定要删除吗 ",
|
||||||
"Are you sure you wish to cancel?": "您确定要取消吗?",
|
"Are you sure you wish to cancel?": "您确定要取消吗?",
|
||||||
"If this message appears repeatedly, please open an issue.": ""
|
"Audio disabled due to suspended game": "由于游戏暂停,音频已禁用",
|
||||||
|
"If this message appears repeatedly, please open an issue.": "若此消息反复出现,请提交问题报告。"
|
||||||
}
|
}
|
||||||
BIN
assets/romfs/theme/bubble1.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
assets/romfs/theme/bubble2.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
assets/romfs/theme/bubble3.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_audio.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_file.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_folder.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_image.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_nro.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_video.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_zip.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
@@ -1,22 +1,23 @@
|
|||||||
[meta]
|
[meta]
|
||||||
name=Abyss
|
name=Abyss
|
||||||
author=TotalJustice
|
author=TotalJustice
|
||||||
version=1.0.0
|
version=1.1.0
|
||||||
; unused currently
|
inherit=romfs:/themes/base_black_theme.ini
|
||||||
preview=romfs:/theme/preview.jpg
|
|
||||||
|
|
||||||
[theme]
|
[theme]
|
||||||
background=0x0f111aff
|
background = 0x0f111a
|
||||||
grid=0x0f115c30
|
grid = 0x0f115c30
|
||||||
selected=0x0f115cff
|
popup = 0x0f115c
|
||||||
selected_overlay=0x529cffff
|
|
||||||
text=0xffbc41ff
|
|
||||||
text_selected=0x529cffff
|
|
||||||
|
|
||||||
icon_audio=romfs:/theme/icon_audio.png
|
line = 0xffbc41
|
||||||
icon_video=romfs:/theme/icon_video.png
|
line_seperator = 0xffbc41
|
||||||
icon_image=romfs:/theme/icon_image.png
|
|
||||||
icon_file=romfs:/theme/icon_file.png
|
text = 0xffbc41
|
||||||
icon_folder=romfs:/theme/icon_folder.png
|
text_info = 0xd79f36
|
||||||
icon_zip=romfs:/theme/icon_zip.png
|
text_selected = 0x529cff
|
||||||
icon_nro=romfs:/theme/icon_nro.png
|
selected_background = 0x0f115c
|
||||||
|
|
||||||
|
scrollbar = 0x529cff
|
||||||
|
scrollbar_background = ; hide the background
|
||||||
|
|
||||||
|
progressbar = 0x3250f0
|
||||||
|
|||||||
33
assets/romfs/themes/base_black_theme.ini
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
[theme]
|
||||||
|
background = 0x2d2d2d
|
||||||
|
grid = 0x46464630
|
||||||
|
popup = 0x2d2d2d
|
||||||
|
error = 0xfa5a3a
|
||||||
|
|
||||||
|
line = 0xfbfbfb
|
||||||
|
line_separator = 0x707070
|
||||||
|
|
||||||
|
text = 0xfbfbfb
|
||||||
|
text_info = 0xd1d1d1
|
||||||
|
text_selected = 0x00ffc8
|
||||||
|
selected_background = 0x212227
|
||||||
|
|
||||||
|
sidebar = 0x000000dc
|
||||||
|
|
||||||
|
scrollbar = 0x00ffc8
|
||||||
|
scrollbar_background = ; hide the background
|
||||||
|
; scrollbar_background = 0x464646
|
||||||
|
|
||||||
|
progressbar = 0x00ffc8
|
||||||
|
progressbar_background = 0x464646
|
||||||
|
|
||||||
|
highlight_1 = 0x1989c6
|
||||||
|
highlight_2 = 0x89f0f2
|
||||||
|
|
||||||
|
icon_audio = romfs:/theme/icon_audio.png
|
||||||
|
icon_video = romfs:/theme/icon_video.png
|
||||||
|
icon_image = romfs:/theme/icon_image.png
|
||||||
|
icon_file = romfs:/theme/icon_file.png
|
||||||
|
icon_folder = romfs:/theme/icon_folder.png
|
||||||
|
icon_zip = romfs:/theme/icon_zip.png
|
||||||
|
icon_nro = romfs:/theme/icon_nro.png
|
||||||
34
assets/romfs/themes/base_white_theme.ini
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[theme]
|
||||||
|
background = 0xebebeb
|
||||||
|
grid = 0xf0f0f0
|
||||||
|
popup = 0xebebeb
|
||||||
|
error = 0xfa5a3a
|
||||||
|
|
||||||
|
line = 0x373737
|
||||||
|
line_separator = 0x6d787a
|
||||||
|
|
||||||
|
text = 0x373737
|
||||||
|
text_info = 0x808080
|
||||||
|
text_selected = 0x3250f0
|
||||||
|
selected_background = 0xfdfdfd
|
||||||
|
|
||||||
|
sidebar = 0xe2e2e2f5
|
||||||
|
|
||||||
|
scrollbar = 0xB0B0B0
|
||||||
|
scrollbar_background = ; hide the background
|
||||||
|
; scrollbar_background = 0xababab
|
||||||
|
|
||||||
|
progressbar = 0x3250f0
|
||||||
|
progressbar_background = 0x808080
|
||||||
|
|
||||||
|
highlight_1 = 0x1989c6
|
||||||
|
highlight_2 = 0x89f0f2
|
||||||
|
|
||||||
|
icon_colour = 0x6d787a
|
||||||
|
icon_audio = romfs:/theme/icon_audio.png
|
||||||
|
icon_video = romfs:/theme/icon_video.png
|
||||||
|
icon_image = romfs:/theme/icon_image.png
|
||||||
|
icon_file = romfs:/theme/icon_file.png
|
||||||
|
icon_folder = romfs:/theme/icon_folder.png
|
||||||
|
icon_zip = romfs:/theme/icon_zip.png
|
||||||
|
icon_nro = romfs:/theme/icon_nro.png
|
||||||
@@ -1,23 +1,5 @@
|
|||||||
[meta]
|
[meta]
|
||||||
name=Black
|
name=Black
|
||||||
author=TotalJustice
|
author=TotalJustice
|
||||||
version=1.0.0
|
version=1.1.0
|
||||||
preview=romfs:/theme/preview.jpg
|
inherit=romfs:/themes/base_black_theme.ini
|
||||||
|
|
||||||
[theme]
|
|
||||||
background=0x2d2d2dff
|
|
||||||
cursor=romfs:/theme/cursor.png
|
|
||||||
cursor_drag=romfs:/theme/cursor_drag.png
|
|
||||||
grid=0x46464630
|
|
||||||
selected=0x464646ff
|
|
||||||
selected_overlay=0x00ffc8ff
|
|
||||||
text=0xfbfbfbff
|
|
||||||
text_selected=0x00ffc8ff
|
|
||||||
|
|
||||||
icon_audio=romfs:/theme/icon_audio.png
|
|
||||||
icon_video=romfs:/theme/icon_video.png
|
|
||||||
icon_image=romfs:/theme/icon_image.png
|
|
||||||
icon_file=romfs:/theme/icon_file.png
|
|
||||||
icon_folder=romfs:/theme/icon_folder.png
|
|
||||||
icon_zip=romfs:/theme/icon_zip.png
|
|
||||||
icon_nro=romfs:/theme/icon_nro.png
|
|
||||||
|
|||||||
15
assets/romfs/themes/black_theme_sp_icons.ini
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[meta]
|
||||||
|
name=Black alt-icons-SP
|
||||||
|
author=spkatsi
|
||||||
|
version=1.0.0
|
||||||
|
inherit=romfs:/themes/base_black_theme.ini
|
||||||
|
|
||||||
|
[theme]
|
||||||
|
icon_audio = romfs:/theme/icons-sp/icon_SP_audio.png
|
||||||
|
icon_video = romfs:/theme/icons-sp/icon_SP_video.png
|
||||||
|
icon_image = romfs:/theme/icons-sp/icon_SP_image.png
|
||||||
|
icon_file = romfs:/theme/icons-sp/icon_SP_file.png
|
||||||
|
icon_folder = romfs:/theme/icons-sp/icon_SP_folder.png
|
||||||
|
icon_zip = romfs:/theme/icons-sp/icon_SP_zip.png
|
||||||
|
icon_nro = romfs:/theme/icons-sp/icon_SP_nro.png
|
||||||
|
|
||||||
@@ -1,23 +1,13 @@
|
|||||||
[meta]
|
[meta]
|
||||||
name=OLED Black
|
name=OLED Black
|
||||||
author=iTotalJustice/Sanras
|
author=TotalJustice/Sanras
|
||||||
version=1.0.0
|
version=1.1.0
|
||||||
preview=romfs:/theme/preview.jpg
|
inherit=romfs:/themes/base_black_theme.ini
|
||||||
|
|
||||||
[theme]
|
[theme]
|
||||||
background=0x000000ff
|
background = 0x000000
|
||||||
cursor=romfs:/theme/cursor.png
|
grid = 0x46464640
|
||||||
cursor_drag=romfs:/theme/cursor_drag.png
|
popup = 0x323232
|
||||||
grid=0x46464640
|
text = 0xfbfbfb
|
||||||
selected=0x323232ff
|
text_selected = 0x00ffc8
|
||||||
selected_overlay=0x00ffc8ff
|
selected_background = 0x323232
|
||||||
text=0xfbfbfbff
|
|
||||||
text_selected=0x00ffc8ff
|
|
||||||
|
|
||||||
icon_audio=romfs:/theme/icon_audio.png
|
|
||||||
icon_video=romfs:/theme/icon_video.png
|
|
||||||
icon_image=romfs:/theme/icon_image.png
|
|
||||||
icon_file=romfs:/theme/icon_file.png
|
|
||||||
icon_folder=romfs:/theme/icon_folder.png
|
|
||||||
icon_zip=romfs:/theme/icon_zip.png
|
|
||||||
icon_nro=romfs:/theme/icon_nro.png
|
|
||||||
|
|||||||
5
assets/romfs/themes/white_theme.ini
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[meta]
|
||||||
|
name=White
|
||||||
|
author=TotalJustice/Yorunokyujitsu
|
||||||
|
version=1.0.0
|
||||||
|
inherit=romfs:/themes/base_white_theme.ini
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
cmake_minimum_required(VERSION 3.13)
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
|
||||||
set(sphaira_VERSION 0.5.0)
|
set(sphaira_VERSION 0.6.3)
|
||||||
|
|
||||||
project(sphaira
|
project(sphaira
|
||||||
VERSION ${sphaira_VERSION}
|
VERSION ${sphaira_VERSION}
|
||||||
@@ -45,18 +45,19 @@ add_executable(sphaira
|
|||||||
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
|
||||||
|
source/ui/menus/ghdl.cpp
|
||||||
|
|
||||||
source/ui/error_box.cpp
|
source/ui/error_box.cpp
|
||||||
source/ui/notification.cpp
|
source/ui/notification.cpp
|
||||||
source/ui/nvg_util.cpp
|
source/ui/nvg_util.cpp
|
||||||
source/ui/option_box.cpp
|
source/ui/option_box.cpp
|
||||||
source/ui/option_list.cpp
|
|
||||||
source/ui/popup_list.cpp
|
source/ui/popup_list.cpp
|
||||||
source/ui/progress_box.cpp
|
source/ui/progress_box.cpp
|
||||||
source/ui/scrollable_text.cpp
|
source/ui/scrollable_text.cpp
|
||||||
source/ui/scrollbar.cpp
|
|
||||||
source/ui/sidebar.cpp
|
source/ui/sidebar.cpp
|
||||||
source/ui/widget.cpp
|
source/ui/widget.cpp
|
||||||
|
source/ui/list.cpp
|
||||||
|
source/ui/bubbles.cpp
|
||||||
|
|
||||||
source/app.cpp
|
source/app.cpp
|
||||||
source/download.cpp
|
source/download.cpp
|
||||||
@@ -80,12 +81,54 @@ target_compile_definitions(sphaira PRIVATE
|
|||||||
-DAPP_VERSION_HASH="${sphaira_VERSION_HASH}"
|
-DAPP_VERSION_HASH="${sphaira_VERSION_HASH}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_compile_options(sphaira PRIVATE
|
||||||
|
-Wall
|
||||||
|
-Wextra
|
||||||
|
|
||||||
|
# unsure if it's a good idea to enable these by default as
|
||||||
|
# it may cause breakage upon compiler updates.
|
||||||
|
# -Werror
|
||||||
|
# -Wfatal-errors
|
||||||
|
|
||||||
|
# disabled as nx uses s64 for size and offset, however stl uses size_t instead, thus
|
||||||
|
# there being a lot of warnings.
|
||||||
|
-Wno-sign-compare
|
||||||
|
# disabled as many overriden methods don't use the params.
|
||||||
|
-Wno-unused-parameter
|
||||||
|
# pedantic warning, missing fields are set to 0.
|
||||||
|
-Wno-missing-field-initializers
|
||||||
|
# disabled as it warns for strcat 2 paths together, but it will never
|
||||||
|
# overflow due to fs enforcing a max path len anyway.
|
||||||
|
-Wno-format-truncation
|
||||||
|
|
||||||
|
# the below are taken from my gba emulator, they've served me well ;)
|
||||||
|
-Wformat-overflow=2
|
||||||
|
-Wundef
|
||||||
|
-Wmissing-include-dirs
|
||||||
|
-fstrict-aliasing
|
||||||
|
-Wstrict-overflow=2
|
||||||
|
-Walloca
|
||||||
|
-Wduplicated-cond
|
||||||
|
-Wwrite-strings
|
||||||
|
-Wdate-time
|
||||||
|
-Wlogical-op
|
||||||
|
-Wpacked
|
||||||
|
-Wcast-qual
|
||||||
|
-Wcast-align
|
||||||
|
-Wimplicit-fallthrough=5
|
||||||
|
-Wsuggest-final-types
|
||||||
|
-Wuninitialized
|
||||||
|
-fimplicit-constexpr
|
||||||
|
-Wmissing-requires
|
||||||
|
)
|
||||||
|
|
||||||
include(FetchContent)
|
include(FetchContent)
|
||||||
set(FETCHCONTENT_QUIET FALSE)
|
set(FETCHCONTENT_QUIET FALSE)
|
||||||
|
|
||||||
FetchContent_Declare(ftpsrv
|
FetchContent_Declare(ftpsrv
|
||||||
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
|
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
|
||||||
GIT_TAG 8d5a14e
|
GIT_TAG 1.2.2
|
||||||
|
SOURCE_SUBDIR NONE
|
||||||
)
|
)
|
||||||
|
|
||||||
FetchContent_Declare(libhaze
|
FetchContent_Declare(libhaze
|
||||||
@@ -95,12 +138,12 @@ FetchContent_Declare(libhaze
|
|||||||
|
|
||||||
FetchContent_Declare(libpulsar
|
FetchContent_Declare(libpulsar
|
||||||
GIT_REPOSITORY https://github.com/ITotalJustice/switch-libpulsar.git
|
GIT_REPOSITORY https://github.com/ITotalJustice/switch-libpulsar.git
|
||||||
GIT_TAG d729be3
|
GIT_TAG de656e4
|
||||||
)
|
)
|
||||||
|
|
||||||
FetchContent_Declare(nanovg
|
FetchContent_Declare(nanovg
|
||||||
GIT_REPOSITORY https://github.com/ITotalJustice/nanovg-deko3d.git
|
GIT_REPOSITORY https://github.com/ITotalJustice/nanovg-deko3d.git
|
||||||
GIT_TAG 1902b38
|
GIT_TAG 845c9fc
|
||||||
)
|
)
|
||||||
|
|
||||||
FetchContent_Declare(stb
|
FetchContent_Declare(stb
|
||||||
@@ -113,12 +156,12 @@ FetchContent_Declare(yyjson
|
|||||||
GIT_TAG 0.10.0
|
GIT_TAG 0.10.0
|
||||||
)
|
)
|
||||||
|
|
||||||
FetchContent_Declare(minIni-sphaira
|
FetchContent_Declare(minIni
|
||||||
GIT_REPOSITORY https://github.com/ITotalJustice/minIni-nx.git
|
GIT_REPOSITORY https://github.com/ITotalJustice/minIni-nx.git
|
||||||
GIT_TAG 63ec295
|
GIT_TAG 11cac8b
|
||||||
)
|
)
|
||||||
|
|
||||||
set(MININI_LIB_NAME minIni-sphaira)
|
set(MININI_LIB_NAME minIni)
|
||||||
set(MININI_USE_STDIO ON)
|
set(MININI_USE_STDIO ON)
|
||||||
set(MININI_USE_NX ON)
|
set(MININI_USE_NX ON)
|
||||||
set(MININI_USE_FLOAT OFF)
|
set(MININI_USE_FLOAT OFF)
|
||||||
@@ -135,34 +178,66 @@ set(NANOVG_NO_GIF ON)
|
|||||||
set(NANOVG_NO_HDR ON)
|
set(NANOVG_NO_HDR ON)
|
||||||
set(NANOVG_NO_PIC ON)
|
set(NANOVG_NO_PIC ON)
|
||||||
set(NANOVG_NO_PNM ON)
|
set(NANOVG_NO_PNM ON)
|
||||||
set(NANOVG_STBI_STATIC OFF)
|
|
||||||
set(NANOVG_STBTT_STATIC ON)
|
|
||||||
|
|
||||||
set(YYJSON_DISABLE_READER OFF)
|
set(YYJSON_DISABLE_READER OFF)
|
||||||
set(YYJSON_DISABLE_WRITER ON)
|
set(YYJSON_DISABLE_WRITER OFF)
|
||||||
set(YYJSON_DISABLE_UTILS ON)
|
set(YYJSON_DISABLE_UTILS ON)
|
||||||
set(YYJSON_DISABLE_FAST_FP_CONV ON)
|
set(YYJSON_DISABLE_FAST_FP_CONV ON)
|
||||||
set(YYJSON_DISABLE_NON_STANDARD ON)
|
set(YYJSON_DISABLE_NON_STANDARD ON)
|
||||||
set(YYJSON_DISABLE_UTF8_VALIDATION ON)
|
set(YYJSON_DISABLE_UTF8_VALIDATION ON)
|
||||||
set(YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS OFF)
|
set(YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS OFF)
|
||||||
|
|
||||||
set(FTPSRV_LIB_BUILD TRUE)
|
|
||||||
set(FTPSRV_LIB_SOCK_UNISTD TRUE)
|
|
||||||
set(FTPSRV_LIB_VFS_CUSTOM ${CMAKE_CURRENT_SOURCE_DIR}/include/ftpsrv_helper.hpp)
|
|
||||||
set(FTPSRV_LIB_PATH_SIZE 0x301)
|
|
||||||
set(FTPSRV_LIB_SESSIONS 32)
|
|
||||||
set(FTPSRV_LIB_BUF_SIZE 1024*64)
|
|
||||||
|
|
||||||
FetchContent_MakeAvailable(
|
FetchContent_MakeAvailable(
|
||||||
ftpsrv
|
ftpsrv
|
||||||
libhaze
|
libhaze
|
||||||
libpulsar
|
libpulsar
|
||||||
nanovg
|
nanovg
|
||||||
stb
|
stb
|
||||||
minIni-sphaira
|
minIni
|
||||||
yyjson
|
yyjson
|
||||||
)
|
)
|
||||||
|
|
||||||
|
set(FTPSRV_LIB_BUILD TRUE)
|
||||||
|
set(FTPSRV_LIB_SOCK_UNISTD TRUE)
|
||||||
|
set(FTPSRV_LIB_VFS_CUSTOM ${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs_nx.h)
|
||||||
|
set(FTPSRV_LIB_PATH_SIZE 0x301)
|
||||||
|
set(FTPSRV_LIB_SESSIONS 32)
|
||||||
|
set(FTPSRV_LIB_BUF_SIZE 1024*64)
|
||||||
|
|
||||||
|
# workaround until a64 container has latest libnx release.
|
||||||
|
if (NOT DEFINED USE_VFS_GC)
|
||||||
|
set(USE_VFS_GC TRUE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(FTPSRV_LIB_CUSTOM_DEFINES
|
||||||
|
USE_VFS_SAVE=$<BOOL:TRUE>
|
||||||
|
USE_VFS_STORAGE=$<BOOL:TRUE>
|
||||||
|
USE_VFS_GC=$<BOOL:${USE_VFS_GC}>
|
||||||
|
USE_VFS_USBHSFS=$<BOOL:FALSE>
|
||||||
|
VFS_NX_BUFFER_IO=$<BOOL:TRUE>
|
||||||
|
)
|
||||||
|
|
||||||
|
add_subdirectory(${ftpsrv_SOURCE_DIR} binary_dir)
|
||||||
|
|
||||||
|
add_library(ftpsrv_helper
|
||||||
|
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs_nx.c
|
||||||
|
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_none.c
|
||||||
|
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_root.c
|
||||||
|
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_fs.c
|
||||||
|
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_save.c
|
||||||
|
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_storage.c
|
||||||
|
${ftpsrv_SOURCE_DIR}/src/platform/nx/utils.c
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(ftpsrv_helper PUBLIC ftpsrv)
|
||||||
|
target_include_directories(ftpsrv_helper PUBLIC ${ftpsrv_SOURCE_DIR}/src/platform)
|
||||||
|
|
||||||
|
if (USE_VFS_GC)
|
||||||
|
target_sources(ftpsrv_helper PRIVATE
|
||||||
|
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_gc.c
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
# todo: upstream cmake
|
# todo: upstream cmake
|
||||||
add_library(libhaze
|
add_library(libhaze
|
||||||
${libhaze_SOURCE_DIR}/source/async_usb_server.cpp
|
${libhaze_SOURCE_DIR}/source/async_usb_server.cpp
|
||||||
@@ -188,39 +263,6 @@ set_target_properties(libhaze PROPERTIES
|
|||||||
COMPILE_OPTIONS "$<$<CONFIG:Debug>:-Os>"
|
COMPILE_OPTIONS "$<$<CONFIG:Debug>:-Os>"
|
||||||
)
|
)
|
||||||
|
|
||||||
# todo: upstream cmake
|
|
||||||
add_library(libpulsar
|
|
||||||
${libpulsar_SOURCE_DIR}/src/archive/archive_file.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/archive/archive.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/bfgrp/bfgrp_location.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/bfgrp/bfgrp.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/bfsar/bfsar_file.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/bfsar/bfsar_group.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/bfsar/bfsar_sound.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/bfsar/bfsar_string.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/bfsar/bfsar_wave_archive.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/bfsar/bfsar.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/bfstm/bfstm_channel.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/bfstm/bfstm_info.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/bfstm/bfstm.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/bfwar/bfwar_file.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/bfwar/bfwar.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/bfwav/bfwav_info.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/bfwav/bfwav.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/bfwsd/bfwsd_sound_data.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/bfwsd/bfwsd_wave_id.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/bfwsd/bfwsd.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/player/player_load_formats.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/player/player_load_lookup.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/player/player_load.c
|
|
||||||
${libpulsar_SOURCE_DIR}/src/player/player.c
|
|
||||||
)
|
|
||||||
target_include_directories(libpulsar PUBLIC ${libpulsar_SOURCE_DIR}/include)
|
|
||||||
set_target_properties(libpulsar PROPERTIES
|
|
||||||
C_STANDARD 11
|
|
||||||
C_EXTENSIONS ON
|
|
||||||
)
|
|
||||||
|
|
||||||
add_library(stb INTERFACE)
|
add_library(stb INTERFACE)
|
||||||
target_include_directories(stb INTERFACE ${stb_SOURCE_DIR})
|
target_include_directories(stb INTERFACE ${stb_SOURCE_DIR})
|
||||||
|
|
||||||
@@ -240,10 +282,10 @@ set_target_properties(sphaira PROPERTIES
|
|||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(sphaira PRIVATE
|
target_link_libraries(sphaira PRIVATE
|
||||||
ftpsrv
|
ftpsrv_helper
|
||||||
libhaze
|
libhaze
|
||||||
libpulsar
|
libpulsar
|
||||||
minIni-sphaira
|
minIni
|
||||||
nanovg
|
nanovg
|
||||||
stb
|
stb
|
||||||
yyjson
|
yyjson
|
||||||
@@ -288,7 +330,7 @@ nx_generate_nacp(
|
|||||||
OUTPUT sphaira.nacp
|
OUTPUT sphaira.nacp
|
||||||
NAME ${CMAKE_PROJECT_NAME}
|
NAME ${CMAKE_PROJECT_NAME}
|
||||||
AUTHOR TotalJustice
|
AUTHOR TotalJustice
|
||||||
VERSION ${CMAKE_PROJECT_VERSION}
|
VERSION ${sphaira_VERSION}
|
||||||
)
|
)
|
||||||
|
|
||||||
# create nro
|
# create nro
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ enum SoundEffect {
|
|||||||
SoundEffect_Startup,
|
SoundEffect_Startup,
|
||||||
SoundEffect_Install,
|
SoundEffect_Install,
|
||||||
SoundEffect_Error,
|
SoundEffect_Error,
|
||||||
|
SoundEffect_MAX,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class LaunchType {
|
enum class LaunchType {
|
||||||
@@ -33,7 +34,9 @@ enum class LaunchType {
|
|||||||
Forwader_Sphaira,
|
Forwader_Sphaira,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// todo: why is this global???
|
||||||
void DrawElement(float x, float y, float w, float h, ThemeEntryID id);
|
void DrawElement(float x, float y, float w, float h, ThemeEntryID id);
|
||||||
|
void DrawElement(const Vec4&, ThemeEntryID id);
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
public:
|
public:
|
||||||
@@ -45,6 +48,8 @@ public:
|
|||||||
static void ExitRestart();
|
static void ExitRestart();
|
||||||
static auto GetVg() -> NVGcontext*;
|
static auto GetVg() -> NVGcontext*;
|
||||||
static void Push(std::shared_ptr<ui::Widget>);
|
static void Push(std::shared_ptr<ui::Widget>);
|
||||||
|
// pops all widgets above a menu
|
||||||
|
static void PopToMenu();
|
||||||
|
|
||||||
// this is thread safe
|
// this is thread safe
|
||||||
static void Notify(std::string text, ui::NotifEntry::Side side = ui::NotifEntry::Side::RIGHT);
|
static void Notify(std::string text, ui::NotifEntry::Side side = ui::NotifEntry::Side::RIGHT);
|
||||||
@@ -54,8 +59,10 @@ public:
|
|||||||
static void NotifyFlashLed();
|
static void NotifyFlashLed();
|
||||||
|
|
||||||
static auto GetThemeMetaList() -> std::span<ThemeMeta>;
|
static auto GetThemeMetaList() -> std::span<ThemeMeta>;
|
||||||
static void SetTheme(u64 theme_index);
|
static void SetTheme(s64 theme_index);
|
||||||
static auto GetThemeIndex() -> u64;
|
static auto GetThemeIndex() -> s64;
|
||||||
|
|
||||||
|
static auto GetDefaultImage(int* w = nullptr, int* h = nullptr) -> int;
|
||||||
|
|
||||||
// returns argv[0]
|
// returns argv[0]
|
||||||
static auto GetExePath() -> fs::FsPath;
|
static auto GetExePath() -> fs::FsPath;
|
||||||
@@ -72,7 +79,9 @@ public:
|
|||||||
static auto GetInstallPrompt() -> bool;
|
static auto GetInstallPrompt() -> bool;
|
||||||
static auto GetThemeShuffleEnable() -> bool;
|
static auto GetThemeShuffleEnable() -> bool;
|
||||||
static auto GetThemeMusicEnable() -> bool;
|
static auto GetThemeMusicEnable() -> bool;
|
||||||
|
static auto Get12HourTimeEnable() -> bool;
|
||||||
static auto GetLanguage() -> long;
|
static auto GetLanguage() -> long;
|
||||||
|
static auto GetTextScrollSpeed() -> long;
|
||||||
|
|
||||||
static void SetMtpEnable(bool enable);
|
static void SetMtpEnable(bool enable);
|
||||||
static void SetFtpEnable(bool enable);
|
static void SetFtpEnable(bool enable);
|
||||||
@@ -84,7 +93,9 @@ public:
|
|||||||
static void SetInstallPrompt(bool enable);
|
static void SetInstallPrompt(bool enable);
|
||||||
static void SetThemeShuffleEnable(bool enable);
|
static void SetThemeShuffleEnable(bool enable);
|
||||||
static void SetThemeMusicEnable(bool enable);
|
static void SetThemeMusicEnable(bool enable);
|
||||||
|
static void Set12HourTimeEnable(bool enable);
|
||||||
static void SetLanguage(long index);
|
static void SetLanguage(long index);
|
||||||
|
static void SetTextScrollSpeed(long index);
|
||||||
|
|
||||||
static auto Install(OwoConfig& config) -> Result;
|
static auto Install(OwoConfig& config) -> Result;
|
||||||
static auto Install(ui::ProgressBox* pbox, OwoConfig& config) -> Result;
|
static auto Install(ui::ProgressBox* pbox, OwoConfig& config) -> Result;
|
||||||
@@ -98,9 +109,9 @@ public:
|
|||||||
// void DrawElement(float x, float y, float w, float h, ui::ThemeEntryID id);
|
// void DrawElement(float x, float y, float w, float h, ui::ThemeEntryID id);
|
||||||
auto LoadElementImage(std::string_view value) -> ElementEntry;
|
auto LoadElementImage(std::string_view value) -> ElementEntry;
|
||||||
auto LoadElementColour(std::string_view value) -> ElementEntry;
|
auto LoadElementColour(std::string_view value) -> ElementEntry;
|
||||||
auto LoadElement(std::string_view data) -> ElementEntry;
|
auto LoadElement(std::string_view data, ElementType type) -> ElementEntry;
|
||||||
|
|
||||||
void LoadTheme(const fs::FsPath& path);
|
void LoadTheme(const ThemeMeta& meta);
|
||||||
void CloseTheme();
|
void CloseTheme();
|
||||||
void ScanThemes(const std::string& path);
|
void ScanThemes(const std::string& path);
|
||||||
void ScanThemeEntries();
|
void ScanThemeEntries();
|
||||||
@@ -110,6 +121,21 @@ public:
|
|||||||
return type == AppletType_Application || type == AppletType_SystemApplication;
|
return type == AppletType_Application || type == AppletType_SystemApplication;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static auto IsApplet() -> bool {
|
||||||
|
return !IsApplication();
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if launched in applet mode with a title suspended in the background.
|
||||||
|
static auto IsAppletWithSuspendedApp() -> bool {
|
||||||
|
R_UNLESS(IsApplet(), false);
|
||||||
|
R_TRY_RESULT(pmdmntInitialize(), false);
|
||||||
|
ON_SCOPE_EXIT(pmdmntExit());
|
||||||
|
|
||||||
|
u64 pid;
|
||||||
|
return R_SUCCEEDED(pmdmntGetApplicationProcessId(&pid));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// private:
|
// private:
|
||||||
static constexpr inline auto CONFIG_PATH = "/config/sphaira/config.ini";
|
static constexpr inline auto CONFIG_PATH = "/config/sphaira/config.ini";
|
||||||
static constexpr inline auto PLAYLOG_PATH = "/config/sphaira/playlog.ini";
|
static constexpr inline auto PLAYLOG_PATH = "/config/sphaira/playlog.ini";
|
||||||
@@ -119,6 +145,7 @@ public:
|
|||||||
u64 m_start_timestamp{};
|
u64 m_start_timestamp{};
|
||||||
u64 m_prev_timestamp{};
|
u64 m_prev_timestamp{};
|
||||||
fs::FsPath m_prev_last_launch{};
|
fs::FsPath m_prev_last_launch{};
|
||||||
|
int m_default_image{};
|
||||||
|
|
||||||
bool m_is_launched_via_sphaira_forwader{};
|
bool m_is_launched_via_sphaira_forwader{};
|
||||||
|
|
||||||
@@ -138,7 +165,7 @@ public:
|
|||||||
|
|
||||||
Theme m_theme{};
|
Theme m_theme{};
|
||||||
fs::FsPath theme_path{};
|
fs::FsPath theme_path{};
|
||||||
std::size_t m_theme_index{};
|
s64 m_theme_index{};
|
||||||
|
|
||||||
bool m_quit{};
|
bool m_quit{};
|
||||||
|
|
||||||
@@ -152,10 +179,12 @@ public:
|
|||||||
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
|
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
|
||||||
option::OptionBool m_theme_shuffle{INI_SECTION, "theme_shuffle", false};
|
option::OptionBool m_theme_shuffle{INI_SECTION, "theme_shuffle", false};
|
||||||
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::OptionLong m_language{INI_SECTION, "language", 0}; // auto
|
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto
|
||||||
|
// todo: move this into it's own menu
|
||||||
|
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal
|
||||||
|
|
||||||
PLSR_BFSAR m_qlaunch_bfsar{};
|
PLSR_PlayerSoundId m_sound_ids[SoundEffect_MAX]{};
|
||||||
PLSR_PlayerSoundId m_sound_ids[24]{};
|
|
||||||
|
|
||||||
private: // from nanovg decko3d example by adubbz
|
private: // from nanovg decko3d example by adubbz
|
||||||
static constexpr unsigned NumFramebuffers = 2;
|
static constexpr unsigned NumFramebuffers = 2;
|
||||||
|
|||||||
@@ -1,41 +1,238 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "fs.hpp"
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <stop_token>
|
||||||
#include <switch.h>
|
#include <switch.h>
|
||||||
|
|
||||||
namespace sphaira {
|
namespace sphaira::curl {
|
||||||
|
|
||||||
using DownloadCallback = std::function<void(std::vector<u8>& data, bool success)>;
|
enum {
|
||||||
using ProgressCallback = std::function<bool(u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow)>;
|
Flag_None = 0,
|
||||||
|
// requests to download send etag in the header.
|
||||||
|
// the received etag is then saved on success.
|
||||||
|
// this api is only available on downloading to file.
|
||||||
|
Flag_Cache = 1 << 0,
|
||||||
|
};
|
||||||
|
|
||||||
enum class DownloadPriority {
|
enum class Priority {
|
||||||
Normal, // gets pushed to the back of the queue
|
Normal, // gets pushed to the back of the queue
|
||||||
High, // gets pushed to the front of the queue
|
High, // gets pushed to the front of the queue
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DownloadEventData {
|
struct Api;
|
||||||
DownloadCallback callback;
|
struct ApiResult;
|
||||||
std::vector<u8> data;
|
|
||||||
bool result;
|
using Path = fs::FsPath;
|
||||||
|
using OnComplete = std::function<void(ApiResult& result)>;
|
||||||
|
using OnProgress = std::function<bool(u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow)>;
|
||||||
|
using StopToken = std::stop_token;
|
||||||
|
|
||||||
|
struct Url {
|
||||||
|
Url() = default;
|
||||||
|
Url(const std::string& str) : m_str{str} {}
|
||||||
|
std::string m_str;
|
||||||
};
|
};
|
||||||
|
|
||||||
auto DownloadInit() -> bool;
|
struct Fields {
|
||||||
void DownloadExit();
|
Fields() = default;
|
||||||
|
Fields(const std::string& str) : m_str{str} {}
|
||||||
|
std::string m_str;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Header {
|
||||||
|
Header() = default;
|
||||||
|
Header(std::initializer_list<std::pair<const std::string, std::string>> p) : m_map{p} {}
|
||||||
|
std::unordered_map<std::string, std::string> m_map;
|
||||||
|
|
||||||
|
auto Find(const std::string& key) const {
|
||||||
|
return std::find_if(m_map.cbegin(), m_map.cend(), [&key](auto& e) {
|
||||||
|
return !strcasecmp(key.c_str(), e.first.c_str());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Flags {
|
||||||
|
Flags() = default;
|
||||||
|
Flags(u32 flags) : m_flags{flags} {}
|
||||||
|
u32 m_flags{Flag_None};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ApiResult {
|
||||||
|
bool success;
|
||||||
|
long code;
|
||||||
|
Header header; // returned headers in request
|
||||||
|
std::vector<u8> data; // empty if downloaded a file
|
||||||
|
fs::FsPath path; // empty if downloaded memory
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DownloadEventData {
|
||||||
|
OnComplete callback;
|
||||||
|
ApiResult result;
|
||||||
|
StopToken stoken;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto Init() -> bool;
|
||||||
|
void Exit();
|
||||||
|
|
||||||
// sync functions
|
// sync functions
|
||||||
auto DownloadMemory(const std::string& url, const std::string& post, ProgressCallback pcallback = nullptr) -> std::vector<u8>;
|
auto ToMemory(const Api& e) -> ApiResult;
|
||||||
auto DownloadFile(const std::string& url, const std::string& out, const std::string& post, ProgressCallback pcallback = nullptr) -> bool;
|
auto ToFile(const Api& e) -> ApiResult;
|
||||||
|
|
||||||
// async functions
|
// async functions
|
||||||
// starts the downloads in a new thread, pushes an event when complete
|
auto ToMemoryAsync(const Api& e) -> bool;
|
||||||
// then, the callback will be called on the main thread.
|
auto ToFileAsync(const Api& e) -> bool;
|
||||||
// auto DownloadMemoryAsync(const std::string& url, DownloadCallback callback, DownloadPriority prio = DownloadPriority::Normal) -> bool;
|
|
||||||
// auto DownloadFileAsync(const std::string& url, const std::string& out, DownloadCallback callback, DownloadPriority prio = DownloadPriority::Normal) -> bool;
|
|
||||||
|
|
||||||
auto DownloadMemoryAsync(const std::string& url, const std::string& post, DownloadCallback callback, ProgressCallback pcallback = nullptr, DownloadPriority prio = DownloadPriority::Normal) -> bool;
|
// uses curl to convert string to their %XX
|
||||||
auto DownloadFileAsync(const std::string& url, const std::string& out, const std::string& post, DownloadCallback callback, ProgressCallback pcallback = nullptr, DownloadPriority prio = DownloadPriority::Normal) -> bool;
|
auto EscapeString(const std::string& str) -> std::string;
|
||||||
|
|
||||||
void DownloadClearCache(const std::string& url);
|
struct Api {
|
||||||
|
Api() = default;
|
||||||
|
|
||||||
} // namespace sphaira
|
template <typename... Ts>
|
||||||
|
Api(Ts&&... ts) {
|
||||||
|
Api::set_option(std::forward<Ts>(ts)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Ts>
|
||||||
|
auto To(Ts&&... ts) {
|
||||||
|
if constexpr(std::disjunction_v<std::is_same<Path, Ts>...>) {
|
||||||
|
return ToFile(std::forward<Ts>(ts)...);
|
||||||
|
} else {
|
||||||
|
return ToMemory(std::forward<Ts>(ts)...);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Ts>
|
||||||
|
auto ToAsync(Ts&&... ts) {
|
||||||
|
if constexpr(std::disjunction_v<std::is_same<Path, Ts>...>) {
|
||||||
|
return ToFileAsync(std::forward<Ts>(ts)...);
|
||||||
|
} else {
|
||||||
|
return ToMemoryAsync(std::forward<Ts>(ts)...);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Ts>
|
||||||
|
auto ToMemory(Ts&&... ts) {
|
||||||
|
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
||||||
|
static_assert(!std::disjunction_v<std::is_same<Path, Ts>...>, "Path must not valid for memory");
|
||||||
|
static_assert(!std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must not be specified");
|
||||||
|
Api::set_option(std::forward<Ts>(ts)...);
|
||||||
|
return curl::ToMemory(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Ts>
|
||||||
|
auto ToFile(Ts&&... ts) {
|
||||||
|
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
||||||
|
static_assert(std::disjunction_v<std::is_same<Path, Ts>...>, "Path must be specified");
|
||||||
|
static_assert(!std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must not be specified");
|
||||||
|
Api::set_option(std::forward<Ts>(ts)...);
|
||||||
|
return curl::ToFile(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Ts>
|
||||||
|
auto ToMemoryAsync(Ts&&... ts) {
|
||||||
|
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
||||||
|
static_assert(std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must be specified");
|
||||||
|
static_assert(!std::disjunction_v<std::is_same<Path, Ts>...>, "Path must not valid for memory");
|
||||||
|
static_assert(std::disjunction_v<std::is_same<StopToken, Ts>...>, "StopToken must be specified");
|
||||||
|
Api::set_option(std::forward<Ts>(ts)...);
|
||||||
|
return curl::ToMemoryAsync(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Ts>
|
||||||
|
auto ToFileAsync(Ts&&... ts) {
|
||||||
|
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
||||||
|
static_assert(std::disjunction_v<std::is_same<Path, Ts>...>, "Path must be specified");
|
||||||
|
static_assert(std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must be specified");
|
||||||
|
static_assert(std::disjunction_v<std::is_same<StopToken, Ts>...>, "StopToken must be specified");
|
||||||
|
Api::set_option(std::forward<Ts>(ts)...);
|
||||||
|
return curl::ToFileAsync(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& GetUrl() const {
|
||||||
|
return m_url.m_str;
|
||||||
|
}
|
||||||
|
auto& GetFields() const {
|
||||||
|
return m_fields.m_str;
|
||||||
|
}
|
||||||
|
auto& GetHeader() const {
|
||||||
|
return m_header;
|
||||||
|
}
|
||||||
|
auto& GetFlags() const {
|
||||||
|
return m_flags.m_flags;
|
||||||
|
}
|
||||||
|
auto& GetPath() const {
|
||||||
|
return m_path;
|
||||||
|
}
|
||||||
|
auto& GetOnComplete() const {
|
||||||
|
return m_on_complete;
|
||||||
|
}
|
||||||
|
auto& GetOnProgress() const {
|
||||||
|
return m_on_progress;
|
||||||
|
}
|
||||||
|
auto& GetPriority() const {
|
||||||
|
return m_prio;
|
||||||
|
}
|
||||||
|
auto& GetToken() const {
|
||||||
|
return m_stoken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void SetOption(Url&& v) {
|
||||||
|
m_url = v;
|
||||||
|
}
|
||||||
|
void SetOption(Fields&& v) {
|
||||||
|
m_fields = v;
|
||||||
|
}
|
||||||
|
void SetOption(Header&& v) {
|
||||||
|
m_header = v;
|
||||||
|
}
|
||||||
|
void SetOption(Flags&& v) {
|
||||||
|
m_flags = v;
|
||||||
|
}
|
||||||
|
void SetOption(Path&& v) {
|
||||||
|
m_path = v;
|
||||||
|
}
|
||||||
|
void SetOption(OnComplete&& v) {
|
||||||
|
m_on_complete = v;
|
||||||
|
}
|
||||||
|
void SetOption(OnProgress&& v) {
|
||||||
|
m_on_progress = v;
|
||||||
|
}
|
||||||
|
void SetOption(Priority&& v) {
|
||||||
|
m_prio = v;
|
||||||
|
}
|
||||||
|
void SetOption(StopToken&& v) {
|
||||||
|
m_stoken = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void set_option(T&& t) {
|
||||||
|
SetOption(std::forward<T>(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T, typename... Ts>
|
||||||
|
void set_option(T&& t, Ts&&... ts) {
|
||||||
|
set_option(std::forward<T>(t));
|
||||||
|
set_option(std::forward<Ts>(ts)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Url m_url;
|
||||||
|
Fields m_fields{};
|
||||||
|
Header m_header{};
|
||||||
|
Flags m_flags{};
|
||||||
|
Path m_path{};
|
||||||
|
OnComplete m_on_complete{nullptr};
|
||||||
|
OnProgress m_on_progress{nullptr};
|
||||||
|
Priority m_prio{Priority::High};
|
||||||
|
std::stop_source m_stop_source{};
|
||||||
|
StopToken m_stoken{m_stop_source.get_token()};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::curl
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ using EventData = std::variant<
|
|||||||
ExitEventData,
|
ExitEventData,
|
||||||
HazeCallbackData,
|
HazeCallbackData,
|
||||||
NxlinkCallbackData,
|
NxlinkCallbackData,
|
||||||
DownloadEventData
|
curl::DownloadEventData
|
||||||
>;
|
>;
|
||||||
|
|
||||||
// returns number of events
|
// returns number of events
|
||||||
|
|||||||
@@ -171,39 +171,39 @@ static_assert(FsPath::TestFrom(FsPath{"abc"}));
|
|||||||
|
|
||||||
FsPath AppendPath(const fs::FsPath& root_path, const fs::FsPath& file_path);
|
FsPath AppendPath(const fs::FsPath& root_path, const fs::FsPath& file_path);
|
||||||
|
|
||||||
Result CreateFile(FsFileSystem* fs, const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false);
|
Result CreateFile(FsFileSystem* fs, const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = true);
|
||||||
Result CreateDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false);
|
Result CreateDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
|
||||||
Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false);
|
Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
|
||||||
Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false);
|
Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
|
||||||
Result DeleteFile(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false);
|
Result DeleteFile(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
|
||||||
Result DeleteDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false);
|
Result DeleteDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
|
||||||
Result DeleteDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false);
|
Result DeleteDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
|
||||||
Result RenameFile(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only = false);
|
Result RenameFile(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only = true);
|
||||||
Result RenameDirectory(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only = false);
|
Result RenameDirectory(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only = true);
|
||||||
Result GetEntryType(FsFileSystem* fs, const FsPath& path, FsDirEntryType* out);
|
Result GetEntryType(FsFileSystem* fs, const FsPath& path, FsDirEntryType* out);
|
||||||
Result GetFileTimeStampRaw(FsFileSystem* fs, const FsPath& path, FsTimeStampRaw *out);
|
Result GetFileTimeStampRaw(FsFileSystem* fs, const FsPath& path, FsTimeStampRaw *out);
|
||||||
bool FileExists(FsFileSystem* fs, const FsPath& path);
|
bool FileExists(FsFileSystem* fs, const FsPath& path);
|
||||||
bool DirExists(FsFileSystem* fs, const FsPath& path);
|
bool DirExists(FsFileSystem* fs, const FsPath& path);
|
||||||
Result read_entire_file(FsFileSystem* fs, const FsPath& path, std::vector<u8>& out);
|
Result read_entire_file(FsFileSystem* fs, const FsPath& path, std::vector<u8>& out);
|
||||||
Result write_entire_file(FsFileSystem* fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = false);
|
Result write_entire_file(FsFileSystem* fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = true);
|
||||||
Result copy_entire_file(FsFileSystem* fs, const FsPath& dst, const FsPath& src, bool ignore_read_only = false);
|
Result copy_entire_file(FsFileSystem* fs, const FsPath& dst, const FsPath& src, bool ignore_read_only = true);
|
||||||
|
|
||||||
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false);
|
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = true);
|
||||||
Result CreateDirectory(const FsPath& path, bool ignore_read_only = false);
|
Result CreateDirectory(const FsPath& path, bool ignore_read_only = true);
|
||||||
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = false);
|
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = true);
|
||||||
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = false);
|
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = true);
|
||||||
Result DeleteFile(const FsPath& path, bool ignore_read_only = false);
|
Result DeleteFile(const FsPath& path, bool ignore_read_only = true);
|
||||||
Result DeleteDirectory(const FsPath& path, bool ignore_read_only = false);
|
Result DeleteDirectory(const FsPath& path, bool ignore_read_only = true);
|
||||||
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = false);
|
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = true);
|
||||||
Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = false);
|
Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = true);
|
||||||
Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = false);
|
Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = true);
|
||||||
Result GetEntryType(const FsPath& path, FsDirEntryType* out);
|
Result GetEntryType(const FsPath& path, FsDirEntryType* out);
|
||||||
Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out);
|
Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out);
|
||||||
bool FileExists(const FsPath& path);
|
bool FileExists(const FsPath& path);
|
||||||
bool DirExists(const FsPath& path);
|
bool DirExists(const FsPath& path);
|
||||||
Result read_entire_file(const FsPath& path, std::vector<u8>& out);
|
Result read_entire_file(const FsPath& path, std::vector<u8>& out);
|
||||||
Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = false);
|
Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = true);
|
||||||
Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only = false);
|
Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only = true);
|
||||||
|
|
||||||
struct Fs {
|
struct Fs {
|
||||||
static constexpr inline u32 FsModule = 505;
|
static constexpr inline u32 FsModule = 505;
|
||||||
@@ -222,51 +222,64 @@ struct Fs {
|
|||||||
static constexpr inline Result ResultUnknownStdioError = MAKERESULT(FsModule, 13);
|
static constexpr inline Result ResultUnknownStdioError = MAKERESULT(FsModule, 13);
|
||||||
static constexpr inline Result ResultReadOnly = MAKERESULT(FsModule, 14);
|
static constexpr inline Result ResultReadOnly = MAKERESULT(FsModule, 14);
|
||||||
|
|
||||||
virtual Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false) = 0;
|
Fs(bool ignore_read_only = true) : m_ignore_read_only{ignore_read_only} {}
|
||||||
virtual Result CreateDirectory(const FsPath& path, bool ignore_read_only = false) = 0;
|
virtual ~Fs() = default;
|
||||||
virtual Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) = 0;
|
|
||||||
virtual Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = false) = 0;
|
virtual Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0) = 0;
|
||||||
virtual Result DeleteFile(const FsPath& path, bool ignore_read_only = false) = 0;
|
virtual Result CreateDirectory(const FsPath& path) = 0;
|
||||||
virtual Result DeleteDirectory(const FsPath& path, bool ignore_read_only = false) = 0;
|
virtual Result CreateDirectoryRecursively(const FsPath& path) = 0;
|
||||||
virtual Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) = 0;
|
virtual Result CreateDirectoryRecursivelyWithPath(const FsPath& path) = 0;
|
||||||
virtual Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) = 0;
|
virtual Result DeleteFile(const FsPath& path) = 0;
|
||||||
virtual Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) = 0;
|
virtual Result DeleteDirectory(const FsPath& path) = 0;
|
||||||
|
virtual Result DeleteDirectoryRecursively(const FsPath& path) = 0;
|
||||||
|
virtual Result RenameFile(const FsPath& src, const FsPath& dst) = 0;
|
||||||
|
virtual Result RenameDirectory(const FsPath& src, const FsPath& dst) = 0;
|
||||||
virtual Result GetEntryType(const FsPath& path, FsDirEntryType* out) = 0;
|
virtual Result GetEntryType(const FsPath& path, FsDirEntryType* out) = 0;
|
||||||
virtual Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out) = 0;
|
virtual Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out) = 0;
|
||||||
virtual bool FileExists(const FsPath& path) = 0;
|
virtual bool FileExists(const FsPath& path) = 0;
|
||||||
virtual bool DirExists(const FsPath& path) = 0;
|
virtual bool DirExists(const FsPath& path) = 0;
|
||||||
virtual Result read_entire_file(const FsPath& path, std::vector<u8>& out) = 0;
|
virtual Result read_entire_file(const FsPath& path, std::vector<u8>& out) = 0;
|
||||||
virtual Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = false) = 0;
|
virtual Result write_entire_file(const FsPath& path, const std::vector<u8>& in) = 0;
|
||||||
virtual Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only = false) = 0;
|
virtual Result copy_entire_file(const FsPath& dst, const FsPath& src) = 0;
|
||||||
|
|
||||||
|
void SetIgnoreReadOnly(bool enable) {
|
||||||
|
m_ignore_read_only = enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool m_ignore_read_only;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FsStdio : Fs {
|
struct FsStdio : Fs {
|
||||||
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false) override {
|
FsStdio(bool ignore_read_only = true) : Fs{ignore_read_only} {}
|
||||||
return fs::CreateFile(path, size, option, ignore_read_only);
|
virtual ~FsStdio() = default;
|
||||||
|
|
||||||
|
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0) override {
|
||||||
|
return fs::CreateFile(path, size, option, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result CreateDirectory(const FsPath& path, bool ignore_read_only = false) override {
|
Result CreateDirectory(const FsPath& path) override {
|
||||||
return fs::CreateDirectory(path, ignore_read_only);
|
return fs::CreateDirectory(path, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) override {
|
Result CreateDirectoryRecursively(const FsPath& path) override {
|
||||||
return fs::CreateDirectoryRecursively(path, ignore_read_only);
|
return fs::CreateDirectoryRecursively(path, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = false) override {
|
Result CreateDirectoryRecursivelyWithPath(const FsPath& path) override {
|
||||||
return fs::CreateDirectoryRecursivelyWithPath(path, ignore_read_only);
|
return fs::CreateDirectoryRecursivelyWithPath(path, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result DeleteFile(const FsPath& path, bool ignore_read_only = false) override {
|
Result DeleteFile(const FsPath& path) override {
|
||||||
return fs::DeleteFile(path, ignore_read_only);
|
return fs::DeleteFile(path, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result DeleteDirectory(const FsPath& path, bool ignore_read_only = false) override {
|
Result DeleteDirectory(const FsPath& path) override {
|
||||||
return fs::DeleteDirectory(path, ignore_read_only);
|
return fs::DeleteDirectory(path, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) override {
|
Result DeleteDirectoryRecursively(const FsPath& path) override {
|
||||||
return fs::DeleteDirectoryRecursively(path, ignore_read_only);
|
return fs::DeleteDirectoryRecursively(path, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) override {
|
Result RenameFile(const FsPath& src, const FsPath& dst) override {
|
||||||
return fs::RenameFile(src, dst, ignore_read_only);
|
return fs::RenameFile(src, dst, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) override {
|
Result RenameDirectory(const FsPath& src, const FsPath& dst) override {
|
||||||
return fs::RenameDirectory(src, dst, ignore_read_only);
|
return fs::RenameDirectory(src, dst, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result GetEntryType(const FsPath& path, FsDirEntryType* out) override {
|
Result GetEntryType(const FsPath& path, FsDirEntryType* out) override {
|
||||||
return fs::GetEntryType(path, out);
|
return fs::GetEntryType(path, out);
|
||||||
@@ -283,17 +296,17 @@ struct FsStdio : Fs {
|
|||||||
Result read_entire_file(const FsPath& path, std::vector<u8>& out) override {
|
Result read_entire_file(const FsPath& path, std::vector<u8>& out) override {
|
||||||
return fs::read_entire_file(path, out);
|
return fs::read_entire_file(path, out);
|
||||||
}
|
}
|
||||||
Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = false) override {
|
Result write_entire_file(const FsPath& path, const std::vector<u8>& in) override {
|
||||||
return fs::write_entire_file(path, in, ignore_read_only);
|
return fs::write_entire_file(path, in, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only = false) override {
|
Result copy_entire_file(const FsPath& dst, const FsPath& src) override {
|
||||||
return fs::copy_entire_file(dst, src, ignore_read_only);
|
return fs::copy_entire_file(dst, src, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FsNative : Fs {
|
struct FsNative : Fs {
|
||||||
FsNative() = default;
|
explicit FsNative(bool ignore_read_only = true) : Fs{ignore_read_only} {}
|
||||||
FsNative(FsFileSystem* fs, bool own) : m_fs{*fs}, m_own{own} {}
|
explicit FsNative(FsFileSystem* fs, bool own, bool ignore_read_only = true) : Fs{ignore_read_only}, m_fs{*fs}, m_own{own} {}
|
||||||
|
|
||||||
virtual ~FsNative() {
|
virtual ~FsNative() {
|
||||||
if (m_own) {
|
if (m_own) {
|
||||||
@@ -355,32 +368,32 @@ struct FsNative : Fs {
|
|||||||
return m_open_result;
|
return m_open_result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false) override {
|
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0) override {
|
||||||
return fs::CreateFile(&m_fs, path, size, option, ignore_read_only);
|
return fs::CreateFile(&m_fs, path, size, option, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result CreateDirectory(const FsPath& path, bool ignore_read_only = false) override {
|
Result CreateDirectory(const FsPath& path) override {
|
||||||
return fs::CreateDirectory(&m_fs, path, ignore_read_only);
|
return fs::CreateDirectory(&m_fs, path, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) override {
|
Result CreateDirectoryRecursively(const FsPath& path) override {
|
||||||
return fs::CreateDirectoryRecursively(&m_fs, path, ignore_read_only);
|
return fs::CreateDirectoryRecursively(&m_fs, path, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = false) override {
|
Result CreateDirectoryRecursivelyWithPath(const FsPath& path) override {
|
||||||
return fs::CreateDirectoryRecursivelyWithPath(&m_fs, path, ignore_read_only);
|
return fs::CreateDirectoryRecursivelyWithPath(&m_fs, path, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result DeleteFile(const FsPath& path, bool ignore_read_only = false) override {
|
Result DeleteFile(const FsPath& path) override {
|
||||||
return fs::DeleteFile(&m_fs, path, ignore_read_only);
|
return fs::DeleteFile(&m_fs, path, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result DeleteDirectory(const FsPath& path, bool ignore_read_only = false) override {
|
Result DeleteDirectory(const FsPath& path) override {
|
||||||
return fs::DeleteDirectory(&m_fs, path, ignore_read_only);
|
return fs::DeleteDirectory(&m_fs, path, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) override {
|
Result DeleteDirectoryRecursively(const FsPath& path) override {
|
||||||
return fs::DeleteDirectoryRecursively(&m_fs, path, ignore_read_only);
|
return fs::DeleteDirectoryRecursively(&m_fs, path, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) override {
|
Result RenameFile(const FsPath& src, const FsPath& dst) override {
|
||||||
return fs::RenameFile(&m_fs, src, dst, ignore_read_only);
|
return fs::RenameFile(&m_fs, src, dst, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) override {
|
Result RenameDirectory(const FsPath& src, const FsPath& dst) override {
|
||||||
return fs::RenameDirectory(&m_fs, src, dst, ignore_read_only);
|
return fs::RenameDirectory(&m_fs, src, dst, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result GetEntryType(const FsPath& path, FsDirEntryType* out) override {
|
Result GetEntryType(const FsPath& path, FsDirEntryType* out) override {
|
||||||
return fs::GetEntryType(&m_fs, path, out);
|
return fs::GetEntryType(&m_fs, path, out);
|
||||||
@@ -397,11 +410,11 @@ struct FsNative : Fs {
|
|||||||
Result read_entire_file(const FsPath& path, std::vector<u8>& out) override {
|
Result read_entire_file(const FsPath& path, std::vector<u8>& out) override {
|
||||||
return fs::read_entire_file(&m_fs, path, out);
|
return fs::read_entire_file(&m_fs, path, out);
|
||||||
}
|
}
|
||||||
Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = false) override {
|
Result write_entire_file(const FsPath& path, const std::vector<u8>& in) override {
|
||||||
return fs::write_entire_file(&m_fs, path, in, ignore_read_only);
|
return fs::write_entire_file(&m_fs, path, in, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only = false) override {
|
Result copy_entire_file(const FsPath& dst, const FsPath& src) override {
|
||||||
return fs::copy_entire_file(&m_fs, dst, src, ignore_read_only);
|
return fs::copy_entire_file(&m_fs, dst, src, m_ignore_read_only);
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFileSystem m_fs{};
|
FsFileSystem m_fs{};
|
||||||
@@ -417,43 +430,28 @@ struct FsNativeSd final : FsNative {
|
|||||||
};
|
};
|
||||||
#else
|
#else
|
||||||
struct FsNativeSd final : FsNative {
|
struct FsNativeSd final : FsNative {
|
||||||
FsNativeSd() : FsNative{fsdevGetDeviceFileSystem("sdmc:"), false} {
|
FsNativeSd(bool ignore_read_only = true) : FsNative{fsdevGetDeviceFileSystem("sdmc:"), false, ignore_read_only} {
|
||||||
m_open_result = 0;
|
m_open_result = 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
struct FsNativeBis final : FsNative {
|
struct FsNativeBis final : FsNative {
|
||||||
FsNativeBis(FsBisPartitionId id, const FsPath& string) {
|
FsNativeBis(FsBisPartitionId id, const FsPath& string, bool ignore_read_only = true) : FsNative{ignore_read_only} {
|
||||||
m_open_result = fsOpenBisFileSystem(&m_fs, id, string);
|
m_open_result = fsOpenBisFileSystem(&m_fs, id, string);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FsNativeImage final : FsNative {
|
struct FsNativeImage final : FsNative {
|
||||||
FsNativeImage(FsImageDirectoryId id) {
|
FsNativeImage(FsImageDirectoryId id, bool ignore_read_only = true) : FsNative{ignore_read_only} {
|
||||||
m_open_result = fsOpenImageDirectoryFileSystem(&m_fs, id);
|
m_open_result = fsOpenImageDirectoryFileSystem(&m_fs, id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FsNativeContentStorage final : FsNative {
|
struct FsNativeContentStorage final : FsNative {
|
||||||
FsNativeContentStorage(FsContentStorageId id) {
|
FsNativeContentStorage(FsContentStorageId id, bool ignore_read_only = true) : FsNative{ignore_read_only} {
|
||||||
m_open_result = fsOpenContentStorageFileSystem(&m_fs, id);
|
m_open_result = fsOpenContentStorageFileSystem(&m_fs, id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// auto file_exists(const FsPath& path) -> bool;
|
|
||||||
// auto create_file(const FsPath& path, u64 size = 0) -> Result;
|
|
||||||
// auto delete_file(const FsPath& path) -> Result;
|
|
||||||
// auto create_directory(const FsPath& path) -> Result;
|
|
||||||
// auto create_directory_recursively(const FsPath& path) -> Result;
|
|
||||||
// auto delete_directory(const FsPath& path) -> Result;
|
|
||||||
// auto delete_directory_recursively(const FsPath& path) -> Result;
|
|
||||||
// auto rename_file(const FsPath& src, const FsPath& dst) -> Result;
|
|
||||||
// auto rename_directory(const FsPath& src, const FsPath& dst) -> Result;
|
|
||||||
|
|
||||||
// auto read_entire_file(const FsPath& path, std::vector<u8>& out) -> Result;
|
|
||||||
// auto write_entire_file(const FsPath& path, const std::vector<u8>& in) -> Result;
|
|
||||||
// // single threaded one shot copy, only use for very small files!
|
|
||||||
// auto copy_entire_file(const FsPath& dst, const FsPath& src) -> Result;
|
|
||||||
|
|
||||||
} // namespace fs
|
} // namespace fs
|
||||||
|
|||||||
@@ -1,33 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <switch.h>
|
|
||||||
|
|
||||||
struct FtpVfsFile {
|
|
||||||
FsFile fd;
|
|
||||||
s64 off;
|
|
||||||
s64 buf_off;
|
|
||||||
s64 buf_size;
|
|
||||||
bool is_write;
|
|
||||||
bool is_valid;
|
|
||||||
u8 buf[1024 * 1024 * 1];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct FtpVfsDir {
|
|
||||||
FsDir dir;
|
|
||||||
bool is_valid;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct FtpVfsDirEntry {
|
|
||||||
FsDirectoryEntry buf;
|
|
||||||
};
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
|
|
||||||
namespace sphaira::ftpsrv {
|
namespace sphaira::ftpsrv {
|
||||||
|
|
||||||
bool Init();
|
bool Init();
|
||||||
void Exit();
|
void Exit();
|
||||||
|
|
||||||
} // namespace sphaira::ftpsrv
|
} // namespace sphaira::ftpsrv
|
||||||
|
|
||||||
#endif // __cplusplus
|
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
|
|
||||||
#define sphaira_USE_LOG 1
|
#define sphaira_USE_LOG 1
|
||||||
|
|
||||||
|
#include <cstdarg>
|
||||||
|
|
||||||
#if sphaira_USE_LOG
|
#if sphaira_USE_LOG
|
||||||
auto log_file_init() -> bool;
|
auto log_file_init() -> bool;
|
||||||
auto log_nxlink_init() -> bool;
|
auto log_nxlink_init() -> bool;
|
||||||
void log_file_exit();
|
void log_file_exit();
|
||||||
void log_nxlink_exit();
|
void log_nxlink_exit();
|
||||||
void log_write(const char* s, ...) __attribute__ ((format (printf, 1, 2)));
|
void log_write(const char* s, ...) __attribute__ ((format (printf, 1, 2)));
|
||||||
|
void log_write_arg(const char* s, std::va_list& v);
|
||||||
#else
|
#else
|
||||||
inline auto log_file_init() -> bool {
|
inline auto log_file_init() -> bool {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ struct NroEntry {
|
|||||||
s64 size{};
|
s64 size{};
|
||||||
NacpStruct nacp{};
|
NacpStruct nacp{};
|
||||||
|
|
||||||
std::vector<u8> icon{};
|
|
||||||
u64 icon_size{};
|
u64 icon_size{};
|
||||||
u64 icon_offset{};
|
u64 icon_offset{};
|
||||||
|
|
||||||
@@ -76,4 +75,10 @@ auto nro_add_arg_file(std::string arg) -> std::string;
|
|||||||
// strips sdmc:
|
// strips sdmc:
|
||||||
auto nro_normalise_path(const std::string& p) -> std::string;
|
auto nro_normalise_path(const std::string& p) -> std::string;
|
||||||
|
|
||||||
|
// helpers to find nro entry, will be made methods soon once i convert vector into a struct.
|
||||||
|
auto nro_find(std::span<const NroEntry> array, std::string_view name, std::string_view author, const fs::FsPath& path) -> std::optional<NroEntry>;
|
||||||
|
auto nro_find_name(std::span<const NroEntry> array, std::string_view name) -> std::optional<NroEntry>;
|
||||||
|
auto nro_find_author(std::span<const NroEntry> array, std::string_view author) -> std::optional<NroEntry>;
|
||||||
|
auto nro_find_path(std::span<const NroEntry> array, const fs::FsPath& path) -> std::optional<NroEntry>;
|
||||||
|
|
||||||
} // namespace sphaira
|
} // namespace sphaira
|
||||||
|
|||||||
10
sphaira/include/ui/bubbles.hpp
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#include "ui/types.hpp"
|
||||||
|
#include "ui/object.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::ui::bubble {
|
||||||
|
|
||||||
|
void Init();
|
||||||
|
void Draw(NVGcontext* vg, Theme* theme);
|
||||||
|
void Exit();
|
||||||
|
|
||||||
|
} // namespace sphaira::ui::bubble
|
||||||
@@ -10,14 +10,13 @@ public:
|
|||||||
ErrorBox(Result code, const std::string& message);
|
ErrorBox(Result code, const std::string& message);
|
||||||
|
|
||||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||||
auto OnLayoutChange() -> void override;
|
|
||||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Result m_code;
|
Result m_code{};
|
||||||
std::string m_message;
|
std::string m_message{};
|
||||||
std::string m_module_str;
|
std::string m_module_str{};
|
||||||
std::string m_description_str;
|
std::string m_description_str{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::ui
|
} // namespace sphaira::ui
|
||||||
|
|||||||
57
sphaira/include/ui/list.hpp
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ui/object.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::ui {
|
||||||
|
|
||||||
|
struct List final : Object {
|
||||||
|
using Callback = std::function<void(NVGcontext* vg, Theme* theme, Vec4 v, s64 index)>;
|
||||||
|
using TouchCallback = std::function<void(s64 index)>;
|
||||||
|
|
||||||
|
List(s64 row, s64 page, const Vec4& pos, const Vec4& v, const Vec2& pad = {});
|
||||||
|
|
||||||
|
void OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCallback callback);
|
||||||
|
|
||||||
|
void Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const;
|
||||||
|
|
||||||
|
auto SetScrollBarPos(float x, float y, float h) {
|
||||||
|
m_scrollbar.x = x;
|
||||||
|
m_scrollbar.y = y;
|
||||||
|
m_scrollbar.h = h;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ScrollDown(s64& index, s64 step, s64 count) -> bool;
|
||||||
|
auto ScrollUp(s64& index, s64 step, s64 count) -> bool;
|
||||||
|
|
||||||
|
auto GetYoff() const {
|
||||||
|
return m_yoff;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetYoff(float y = 0) {
|
||||||
|
m_yoff = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetMaxY() const {
|
||||||
|
return m_v.h + m_pad.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
auto Draw(NVGcontext* vg, Theme* theme) -> void override {}
|
||||||
|
auto ClampY(float y, s64 count) const -> float;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const s64 m_row;
|
||||||
|
const s64 m_page;
|
||||||
|
|
||||||
|
Vec4 m_v{};
|
||||||
|
Vec2 m_pad{};
|
||||||
|
|
||||||
|
Vec4 m_scrollbar{};
|
||||||
|
|
||||||
|
// current y offset.
|
||||||
|
float m_yoff{};
|
||||||
|
// in progress y offset, used when scrolling.
|
||||||
|
float m_y_prog{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::ui
|
||||||
@@ -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/list.hpp"
|
||||||
#include "nro.hpp"
|
#include "nro.hpp"
|
||||||
#include "fs.hpp"
|
#include "fs.hpp"
|
||||||
#include <span>
|
#include <span>
|
||||||
@@ -27,6 +28,8 @@ struct LazyImage {
|
|||||||
~LazyImage();
|
~LazyImage();
|
||||||
int image{};
|
int image{};
|
||||||
int w{}, h{};
|
int w{}, h{};
|
||||||
|
bool tried_cache{};
|
||||||
|
bool cached{};
|
||||||
ImageDownloadState state{ImageDownloadState::None};
|
ImageDownloadState state{ImageDownloadState::None};
|
||||||
u8 first_pixel[4]{};
|
u8 first_pixel[4]{};
|
||||||
};
|
};
|
||||||
@@ -39,26 +42,26 @@ enum class EntryStatus {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct Entry {
|
struct Entry {
|
||||||
std::string category; // todo: lable
|
std::string category{}; // todo: lable
|
||||||
std::string binary; // optional, only valid for .nro
|
std::string binary{}; // optional, only valid for .nro
|
||||||
std::string updated; // date of update
|
std::string updated{}; // date of update
|
||||||
std::string name;
|
std::string name{};
|
||||||
std::string license; // optional
|
std::string license{}; // optional
|
||||||
std::string title; // same as name but with spaces
|
std::string title{}; // same as name but with spaces
|
||||||
std::string url; // url of repo (optional?)
|
std::string url{}; // url of repo (optional?)
|
||||||
std::string description;
|
std::string description{};
|
||||||
std::string author;
|
std::string author{};
|
||||||
std::string changelog; // optional
|
std::string changelog{}; // optional
|
||||||
u64 screens; // number of screenshots
|
u64 screens{}; // number of screenshots
|
||||||
u64 extracted; // extracted size in KiB
|
u64 extracted{}; // extracted size in KiB
|
||||||
std::string version;
|
std::string version{};
|
||||||
u64 filesize; // compressed size in KiB
|
u64 filesize{}; // compressed size in KiB
|
||||||
std::string details;
|
std::string details{};
|
||||||
u64 app_dls;
|
u64 app_dls{};
|
||||||
std::string md5; // md5 of the zip
|
std::string md5{}; // md5 of the zip
|
||||||
|
|
||||||
LazyImage image;
|
LazyImage image{};
|
||||||
u32 updated_num;
|
u32 updated_num{};
|
||||||
EntryStatus status{EntryStatus::Get};
|
EntryStatus status{EntryStatus::Get};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,7 +78,7 @@ struct EntryMenu final : MenuBase {
|
|||||||
// void OnFocusGained() override;
|
// void OnFocusGained() override;
|
||||||
|
|
||||||
void ShowChangelogAction();
|
void ShowChangelogAction();
|
||||||
void SetIndex(std::size_t index);
|
void SetIndex(s64 index);
|
||||||
|
|
||||||
void UpdateOptions();
|
void UpdateOptions();
|
||||||
|
|
||||||
@@ -95,14 +98,14 @@ private:
|
|||||||
const LazyImage& m_default_icon;
|
const LazyImage& m_default_icon;
|
||||||
Menu& m_menu;
|
Menu& m_menu;
|
||||||
|
|
||||||
std::size_t m_index{}; // where i am in the array
|
s64 m_index{}; // where i am in the array
|
||||||
std::vector<Option> m_options;
|
std::vector<Option> m_options{};
|
||||||
LazyImage m_banner;
|
LazyImage m_banner{};
|
||||||
std::vector<LazyImage> m_screens;
|
std::unique_ptr<List> m_list{};
|
||||||
|
|
||||||
std::shared_ptr<ScrollableText> m_details;
|
std::shared_ptr<ScrollableText> m_details{};
|
||||||
std::shared_ptr<ScrollableText> m_changelog;
|
std::shared_ptr<ScrollableText> m_changelog{};
|
||||||
std::shared_ptr<ScrollableText> m_detail_changelog;
|
std::shared_ptr<ScrollableText> m_detail_changelog{};
|
||||||
|
|
||||||
bool m_show_changlog{};
|
bool m_show_changlog{};
|
||||||
};
|
};
|
||||||
@@ -127,39 +130,10 @@ enum SortType {
|
|||||||
};
|
};
|
||||||
|
|
||||||
enum OrderType {
|
enum OrderType {
|
||||||
OrderType_Decending,
|
OrderType_Descending,
|
||||||
OrderType_Ascending,
|
OrderType_Ascending,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FeedbackEntry {
|
|
||||||
u32 id;
|
|
||||||
u64 time;
|
|
||||||
std::string package; // name of package
|
|
||||||
std::string content; // the feedback message that was sent
|
|
||||||
std::string reply; // the reply, "" if no reply yet :)
|
|
||||||
};
|
|
||||||
|
|
||||||
struct FeedbackMenu final : MenuBase {
|
|
||||||
FeedbackMenu(const std::vector<Entry>& package_entries, LazyImage& default_image);
|
|
||||||
~FeedbackMenu();
|
|
||||||
|
|
||||||
void Update(Controller* controller, TouchInfo* touch) override;
|
|
||||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
|
||||||
void OnFocusGained() override;
|
|
||||||
|
|
||||||
void SetIndex(std::size_t index);
|
|
||||||
void ScanHomebrew();
|
|
||||||
void Sort();
|
|
||||||
|
|
||||||
private:
|
|
||||||
const std::vector<Entry>& m_package_entries;
|
|
||||||
LazyImage& m_default_image;
|
|
||||||
std::vector<FeedbackEntry> m_entries;
|
|
||||||
std::size_t m_start{};
|
|
||||||
std::size_t m_index{}; // where i am in the array
|
|
||||||
ImageDownloadState m_repo_download_state{ImageDownloadState::None};
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Menu final : MenuBase {
|
struct Menu final : MenuBase {
|
||||||
Menu(const std::vector<NroEntry>& nro_entries);
|
Menu(const std::vector<NroEntry>& nro_entries);
|
||||||
~Menu();
|
~Menu();
|
||||||
@@ -168,7 +142,7 @@ struct Menu final : MenuBase {
|
|||||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
void OnFocusGained() override;
|
void OnFocusGained() override;
|
||||||
|
|
||||||
void SetIndex(std::size_t index);
|
void SetIndex(s64 index);
|
||||||
void ScanHomebrew();
|
void ScanHomebrew();
|
||||||
void Sort();
|
void Sort();
|
||||||
|
|
||||||
@@ -189,29 +163,29 @@ struct Menu final : MenuBase {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
const std::vector<NroEntry>& m_nro_entries;
|
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{};
|
||||||
|
|
||||||
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_Decending};
|
OrderType m_order{OrderType::OrderType_Descending};
|
||||||
|
|
||||||
std::size_t m_start{};
|
s64 m_index{}; // where i am in the array
|
||||||
std::size_t m_index{}; // where i am in the array
|
LazyImage m_default_image{};
|
||||||
LazyImage m_default_image;
|
LazyImage m_update{};
|
||||||
LazyImage m_update;
|
LazyImage m_get{};
|
||||||
LazyImage m_get;
|
LazyImage m_local{};
|
||||||
LazyImage m_local;
|
LazyImage m_installed{};
|
||||||
LazyImage m_installed;
|
|
||||||
ImageDownloadState m_repo_download_state{ImageDownloadState::None};
|
ImageDownloadState m_repo_download_state{ImageDownloadState::None};
|
||||||
|
std::unique_ptr<List> m_list{};
|
||||||
|
|
||||||
std::string m_search_term;
|
std::string m_search_term{};
|
||||||
std::string m_author_term;
|
std::string m_author_term{};
|
||||||
u64 m_entry_search_jump_back{};
|
s64 m_entry_search_jump_back{};
|
||||||
u64 m_entry_author_jump_back{};
|
s64 m_entry_author_jump_back{};
|
||||||
bool m_is_search{};
|
bool m_is_search{};
|
||||||
bool m_is_author{};
|
bool m_is_author{};
|
||||||
bool m_dirty{}; // if set, does a sort
|
bool m_dirty{}; // if set, does a sort
|
||||||
|
|||||||
@@ -16,15 +16,15 @@ struct Menu final : MenuBase {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
const fs::FsPath m_path;
|
const fs::FsPath m_path;
|
||||||
fs::FsNativeSd m_fs;
|
fs::FsNativeSd m_fs{};
|
||||||
FsFile m_file;
|
FsFile m_file{};
|
||||||
s64 m_file_size{};
|
s64 m_file_size{};
|
||||||
s64 m_file_offset{};
|
s64 m_file_offset{};
|
||||||
|
|
||||||
std::unique_ptr<ScrollableText> m_scroll_text;
|
std::unique_ptr<ScrollableText> m_scroll_text{};
|
||||||
|
|
||||||
std::size_t m_start{};
|
s64 m_start{};
|
||||||
std::size_t m_index{}; // where i am in the array
|
s64 m_index{}; // where i am in the array
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::ui::menu::fileview
|
} // namespace sphaira::ui::menu::fileview
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ui/menus/menu_base.hpp"
|
#include "ui/menus/menu_base.hpp"
|
||||||
|
#include "ui/list.hpp"
|
||||||
#include "nro.hpp"
|
#include "nro.hpp"
|
||||||
#include "fs.hpp"
|
#include "fs.hpp"
|
||||||
#include "option.hpp"
|
#include "option.hpp"
|
||||||
@@ -9,6 +10,12 @@
|
|||||||
|
|
||||||
namespace sphaira::ui::menu::filebrowser {
|
namespace sphaira::ui::menu::filebrowser {
|
||||||
|
|
||||||
|
enum class FsType {
|
||||||
|
Sd,
|
||||||
|
ImageNand,
|
||||||
|
ImageSd,
|
||||||
|
};
|
||||||
|
|
||||||
enum class SelectedType {
|
enum class SelectedType {
|
||||||
None,
|
None,
|
||||||
Copy,
|
Copy,
|
||||||
@@ -22,7 +29,7 @@ enum SortType {
|
|||||||
};
|
};
|
||||||
|
|
||||||
enum OrderType {
|
enum OrderType {
|
||||||
OrderType_Decending,
|
OrderType_Descending,
|
||||||
OrderType_Ascending,
|
OrderType_Ascending,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,17 +86,27 @@ struct FileEntry : FsDirectoryEntry {
|
|||||||
|
|
||||||
struct FileAssocEntry {
|
struct FileAssocEntry {
|
||||||
fs::FsPath path{}; // ini name
|
fs::FsPath path{}; // ini name
|
||||||
std::string name; // ini name
|
std::string name{}; // ini name
|
||||||
std::vector<std::string> ext; // list of ext
|
std::vector<std::string> ext{}; // list of ext
|
||||||
std::vector<std::string> database; // list of systems
|
std::vector<std::string> database{}; // list of systems
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LastFile {
|
struct LastFile {
|
||||||
fs::FsPath name;
|
fs::FsPath name{};
|
||||||
u64 index;
|
s64 index{};
|
||||||
u64 offset;
|
float offset{};
|
||||||
u64 entries_count;
|
s64 entries_count{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct FsDirCollection {
|
||||||
|
fs::FsPath path{};
|
||||||
|
fs::FsPath parent_name{};
|
||||||
|
std::vector<FsDirectoryEntry> files{};
|
||||||
|
std::vector<FsDirectoryEntry> dirs{};
|
||||||
|
};
|
||||||
|
|
||||||
|
using FsDirCollections = std::vector<FsDirCollection>;
|
||||||
|
|
||||||
struct Menu final : MenuBase {
|
struct Menu final : MenuBase {
|
||||||
Menu(const std::vector<NroEntry>& nro_entries);
|
Menu(const std::vector<NroEntry>& nro_entries);
|
||||||
~Menu();
|
~Menu();
|
||||||
@@ -103,7 +120,7 @@ struct Menu final : MenuBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void SetIndex(std::size_t index);
|
void SetIndex(s64 index);
|
||||||
void InstallForwarder();
|
void InstallForwarder();
|
||||||
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;
|
||||||
|
|
||||||
@@ -115,7 +132,7 @@ private:
|
|||||||
return GetNewPath(m_path, entry.name);
|
return GetNewPath(m_path, entry.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto GetNewPath(u64 index) const -> fs::FsPath {
|
auto GetNewPath(s64 index) const -> fs::FsPath {
|
||||||
return GetNewPath(m_path, GetEntry(index).name);
|
return GetNewPath(m_path, GetEntry(index).name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,46 +220,54 @@ private:
|
|||||||
void OnDeleteCallback();
|
void OnDeleteCallback();
|
||||||
void OnPasteCallback();
|
void OnPasteCallback();
|
||||||
void OnRenameCallback();
|
void OnRenameCallback();
|
||||||
|
auto CheckIfUpdateFolder() -> Result;
|
||||||
|
|
||||||
|
auto get_collection(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollection& out, bool inc_file, bool inc_dir, bool inc_size) -> Result;
|
||||||
|
auto get_collections(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out) -> Result;
|
||||||
|
|
||||||
|
void SetFs(const fs::FsPath& new_path, u32 new_type);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr inline const char* INI_SECTION = "filebrowser";
|
static constexpr inline const char* INI_SECTION = "filebrowser";
|
||||||
|
|
||||||
const std::vector<NroEntry>& m_nro_entries;
|
const std::vector<NroEntry>& m_nro_entries;
|
||||||
fs::FsPath m_path;
|
std::unique_ptr<fs::FsNative> m_fs{};
|
||||||
std::vector<FileEntry> m_entries;
|
FsType m_fs_type{};
|
||||||
std::vector<u32> m_entries_index; // files not including hidden
|
fs::FsPath m_path{};
|
||||||
std::vector<u32> m_entries_index_hidden; // includes hidden files
|
std::vector<FileEntry> m_entries{};
|
||||||
std::vector<u32> m_entries_index_search; // files found via search
|
std::vector<u32> m_entries_index{}; // files not including hidden
|
||||||
std::span<u32> m_entries_current;
|
std::vector<u32> m_entries_index_hidden{}; // includes hidden files
|
||||||
|
std::vector<u32> m_entries_index_search{}; // files found via search
|
||||||
|
std::span<u32> m_entries_current{};
|
||||||
|
|
||||||
|
std::unique_ptr<List> m_list{};
|
||||||
|
std::optional<fs::FsPath> m_daybreak_path{};
|
||||||
|
|
||||||
// search options
|
// search options
|
||||||
// show files [X]
|
// show files [X]
|
||||||
// show folders [X]
|
// show folders [X]
|
||||||
// recursive (slow) [ ]
|
// recursive (slow) [ ]
|
||||||
|
|
||||||
std::vector<FileAssocEntry> m_assoc_entries;
|
std::vector<FileAssocEntry> m_assoc_entries{};
|
||||||
std::vector<FileEntry> m_selected_files;
|
std::vector<FileEntry> m_selected_files{};
|
||||||
|
|
||||||
// this keeps track of the highlighted file before opening a folder
|
// this keeps track of the highlighted file before opening a folder
|
||||||
// if the user presses B to go back to the previous dir
|
// if the user presses B to go back to the previous dir
|
||||||
// this vector is popped, then, that entry is checked if it still exists
|
// this vector is popped, then, that entry is checked if it still exists
|
||||||
// if it does, the index becomes that file.
|
// if it does, the index becomes that file.
|
||||||
std::vector<LastFile> m_previous_highlighted_file;
|
std::vector<LastFile> m_previous_highlighted_file{};
|
||||||
fs::FsPath m_selected_path;
|
fs::FsPath m_selected_path{};
|
||||||
std::size_t m_index{};
|
s64 m_index{};
|
||||||
std::size_t m_index_offset{};
|
s64 m_selected_count{};
|
||||||
std::size_t m_selected_count{};
|
|
||||||
SelectedType m_selected_type{SelectedType::None};
|
SelectedType m_selected_type{SelectedType::None};
|
||||||
|
|
||||||
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Alphabetical};
|
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Alphabetical};
|
||||||
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Decending};
|
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
|
||||||
option::OptionBool m_show_hidden{INI_SECTION, "show_hidden", false};
|
option::OptionBool m_show_hidden{INI_SECTION, "show_hidden", false};
|
||||||
option::OptionBool m_folders_first{INI_SECTION, "folders_first", true};
|
option::OptionBool m_folders_first{INI_SECTION, "folders_first", true};
|
||||||
option::OptionBool m_hidden_last{INI_SECTION, "hidden_last", false};
|
option::OptionBool m_hidden_last{INI_SECTION, "hidden_last", false};
|
||||||
|
option::OptionBool m_ignore_read_only{INI_SECTION, "ignore_read_only", false};
|
||||||
option::OptionBool m_search_show_files{INI_SECTION, "search_show_files", true};
|
option::OptionLong m_mount{INI_SECTION, "mount", 0};
|
||||||
option::OptionBool m_search_show_folders{INI_SECTION, "search_show_folders", true};
|
|
||||||
option::OptionBool m_search_recursive{INI_SECTION, "search_recursive", false};
|
|
||||||
|
|
||||||
bool m_loaded_assoc_entries{};
|
bool m_loaded_assoc_entries{};
|
||||||
bool m_is_update_folder{};
|
bool m_is_update_folder{};
|
||||||
|
|||||||
74
sphaira/include/ui/menus/ghdl.hpp
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ui/menus/menu_base.hpp"
|
||||||
|
#include "ui/list.hpp"
|
||||||
|
#include "fs.hpp"
|
||||||
|
#include "option.hpp"
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace sphaira::ui::menu::gh {
|
||||||
|
|
||||||
|
struct AssetEntry {
|
||||||
|
std::string name{};
|
||||||
|
std::string path{};
|
||||||
|
std::string pre_install_message{};
|
||||||
|
std::string post_install_message{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
fs::FsPath json_path{};
|
||||||
|
std::string url{};
|
||||||
|
std::string owner{};
|
||||||
|
std::string repo{};
|
||||||
|
std::string tag{};
|
||||||
|
std::string pre_install_message{};
|
||||||
|
std::string post_install_message{};
|
||||||
|
std::vector<AssetEntry> assets{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GhApiAsset {
|
||||||
|
std::string name{};
|
||||||
|
std::string content_type{};
|
||||||
|
u64 size{};
|
||||||
|
u64 download_count{};
|
||||||
|
std::string browser_download_url{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GhApiEntry {
|
||||||
|
std::string tag_name{};
|
||||||
|
std::string name{};
|
||||||
|
std::vector<GhApiAsset> assets{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Menu final : MenuBase {
|
||||||
|
Menu();
|
||||||
|
~Menu();
|
||||||
|
|
||||||
|
void Update(Controller* controller, TouchInfo* touch) override;
|
||||||
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
|
void OnFocusGained() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void SetIndex(s64 index);
|
||||||
|
void Scan();
|
||||||
|
void LoadEntriesFromPath(const fs::FsPath& path);
|
||||||
|
|
||||||
|
auto GetEntry() -> Entry& {
|
||||||
|
return m_entries[m_index];
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetEntry() const -> const Entry& {
|
||||||
|
return m_entries[m_index];
|
||||||
|
}
|
||||||
|
|
||||||
|
void Sort();
|
||||||
|
void UpdateSubheading();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<Entry> m_entries{};
|
||||||
|
s64 m_index{};
|
||||||
|
std::unique_ptr<List> m_list{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::ui::menu::gh
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ui/menus/menu_base.hpp"
|
#include "ui/menus/menu_base.hpp"
|
||||||
|
#include "ui/list.hpp"
|
||||||
#include "nro.hpp"
|
#include "nro.hpp"
|
||||||
#include "fs.hpp"
|
#include "fs.hpp"
|
||||||
#include "option.hpp"
|
#include "option.hpp"
|
||||||
@@ -17,7 +18,7 @@ enum SortType {
|
|||||||
};
|
};
|
||||||
|
|
||||||
enum OrderType {
|
enum OrderType {
|
||||||
OrderType_Decending,
|
OrderType_Descending,
|
||||||
OrderType_Ascending,
|
OrderType_Ascending,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ struct Menu final : MenuBase {
|
|||||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
void OnFocusGained() override;
|
void OnFocusGained() override;
|
||||||
|
|
||||||
void SetIndex(std::size_t index);
|
void SetIndex(s64 index);
|
||||||
void InstallHomebrew();
|
void InstallHomebrew();
|
||||||
void ScanHomebrew();
|
void ScanHomebrew();
|
||||||
void Sort();
|
void Sort();
|
||||||
@@ -49,12 +50,12 @@ struct Menu final : MenuBase {
|
|||||||
private:
|
private:
|
||||||
static constexpr inline const char* INI_SECTION = "homebrew";
|
static constexpr inline const char* INI_SECTION = "homebrew";
|
||||||
|
|
||||||
std::vector<NroEntry> m_entries;
|
std::vector<NroEntry> m_entries{};
|
||||||
std::size_t m_start{};
|
s64 m_index{}; // where i am in the array
|
||||||
std::size_t m_index{}; // where i am in the array
|
std::unique_ptr<List> m_list{};
|
||||||
|
|
||||||
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_Decending};
|
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};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ private:
|
|||||||
Rotation m_rotation{Rotation_90};
|
Rotation m_rotation{Rotation_90};
|
||||||
Colour m_colour{Colour_Grey};
|
Colour m_colour{Colour_Grey};
|
||||||
int m_image{};
|
int m_image{};
|
||||||
std::size_t m_index{};
|
s64 m_index{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::ui::menu::irs
|
} // namespace sphaira::ui::menu::irs
|
||||||
|
|||||||
@@ -28,10 +28,13 @@ struct MainMenu final : Widget {
|
|||||||
void OnFocusGained() override;
|
void OnFocusGained() override;
|
||||||
void OnFocusLost() override;
|
void OnFocusLost() override;
|
||||||
|
|
||||||
|
auto IsMenu() const -> bool override {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void OnLRPress(std::shared_ptr<MenuBase> menu, Button b);
|
void OnLRPress(std::shared_ptr<MenuBase> menu, Button b);
|
||||||
void AddOnLPress();
|
void AddOnLRPress();
|
||||||
void AddOnRPress();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::shared_ptr<homebrew::Menu> m_homebrew_menu{};
|
std::shared_ptr<homebrew::Menu> m_homebrew_menu{};
|
||||||
|
|||||||
@@ -12,15 +12,31 @@ struct MenuBase : Widget {
|
|||||||
|
|
||||||
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);
|
||||||
|
|
||||||
|
auto IsMenu() const -> bool override {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void SetTitle(std::string title);
|
void SetTitle(std::string title);
|
||||||
void SetTitleSubHeading(std::string sub_heading);
|
void SetTitleSubHeading(std::string sub_heading);
|
||||||
void SetSubHeading(std::string sub_heading);
|
void SetSubHeading(std::string sub_heading);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string m_title;
|
void UpdateVars();
|
||||||
std::string m_title_sub_heading;
|
|
||||||
std::string m_sub_heading;
|
private:
|
||||||
AppletType m_applet_type;
|
std::string m_title{};
|
||||||
|
std::string m_title_sub_heading{};
|
||||||
|
std::string m_sub_heading{};
|
||||||
|
|
||||||
|
struct tm m_tm{};
|
||||||
|
TimeStamp m_poll_timestamp{};
|
||||||
|
u32 m_battery_percetange{};
|
||||||
|
PsmChargerType m_charger_type{};
|
||||||
|
NifmInternetConnectionType m_type{};
|
||||||
|
NifmInternetConnectionStatus m_status{};
|
||||||
|
u32 m_strength{};
|
||||||
|
u32 m_ip{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::ui::menu
|
} // namespace sphaira::ui::menu
|
||||||
|
|||||||
@@ -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/list.hpp"
|
||||||
#include "option.hpp"
|
#include "option.hpp"
|
||||||
#include <span>
|
#include <span>
|
||||||
|
|
||||||
@@ -15,28 +16,14 @@ enum class ImageDownloadState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct LazyImage {
|
struct LazyImage {
|
||||||
LazyImage() = default;
|
|
||||||
~LazyImage();
|
~LazyImage();
|
||||||
int image{};
|
int image{};
|
||||||
int w{}, h{};
|
int w{}, h{};
|
||||||
|
bool tried_cache{};
|
||||||
|
bool cached{};
|
||||||
ImageDownloadState state{ImageDownloadState::None};
|
ImageDownloadState state{ImageDownloadState::None};
|
||||||
u8 first_pixel[4]{};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// "mutation setLike($type: String!, $id: String!, $value: Boolean!) {\n setLike(type: $type, id: $id, value: $value)\n}\n"
|
|
||||||
|
|
||||||
// https://api.themezer.net/?query=query($nsfw:Boolean,$target:String,$page:Int,$limit:Int,$sort:String,$order:String,$query:String){themeList(nsfw:$nsfw,target:$target,page:$page,limit:$limit,sort:$sort,order:$order,query:$query){id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}}&variables={"nsfw":false,"target":null,"page":1,"limit":10,"sort":"updated","order":"desc","query":null}
|
|
||||||
// https://api.themezer.net/?query=query($nsfw:Boolean,$page:Int,$limit:Int,$sort:String,$order:String,$query:String){packList(nsfw:$nsfw,page:$page,limit:$limit,sort:$sort,order:$order,query:$query){id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count,themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}}}&variables={"nsfw":false,"page":1,"limit":10,"sort":"updated","order":"desc","query":null}
|
|
||||||
// https://api.themezer.net/?query=query($id:String!){pack(id:$id){id,creator{display_name},details{name,description},last_updated,categories,dl_count,like_count,themes{id,details{name},layout{id,details{name}},categories,target,preview{original,thumb},last_updated,dl_count,like_count}}}&variables={"id":"16d"}
|
|
||||||
|
|
||||||
// https://api.themezer.net/?query=query{nxinstaller(id:"t9a6"){themes{filename,url,mimetype}}}
|
|
||||||
// https://api.themezer.net/?query=query{downloadTheme(id:"t9a6"){filename,url,mimetype}}
|
|
||||||
// https://api.themezer.net/?query=query{downloadPack(id:"t9a6"){filename,url,mimetype}}
|
|
||||||
|
|
||||||
// {"data":{"setLike":true}}
|
|
||||||
// https://api.themezer.net/?query=mutation{setLike(type:"packs",id:"5",value:true){data{setLike}}}
|
|
||||||
// https://api.themezer.net/?query=mutation($type:String!,$id:String!,$value:Boolean!){setLike(type:$type,id:$id,value:$value){data{setLike}}}&variables={"type":"packs","id":"5","value":true}
|
|
||||||
|
|
||||||
enum MenuState {
|
enum MenuState {
|
||||||
MenuState_Normal,
|
MenuState_Normal,
|
||||||
MenuState_Search,
|
MenuState_Search,
|
||||||
@@ -56,73 +43,49 @@ enum class PageLoadState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct Creator {
|
struct Creator {
|
||||||
std::string id;
|
std::string id{};
|
||||||
std::string display_name;
|
std::string display_name{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Details {
|
struct Details {
|
||||||
std::string name;
|
std::string name{};
|
||||||
std::string description;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Preview {
|
struct Preview {
|
||||||
std::string original;
|
std::string thumb{};
|
||||||
std::string thumb;
|
LazyImage lazy_image{};
|
||||||
LazyImage lazy_image;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DownloadPack {
|
struct DownloadPack {
|
||||||
std::string filename;
|
std::string filename{};
|
||||||
std::string url;
|
std::string url{};
|
||||||
std::string mimetype;
|
std::string mimetype{};
|
||||||
};
|
};
|
||||||
|
|
||||||
using DownloadTheme = DownloadPack;
|
using DownloadTheme = DownloadPack;
|
||||||
|
|
||||||
struct ThemeEntry {
|
struct ThemeEntry {
|
||||||
std::string id;
|
std::string id{};
|
||||||
Creator creator;
|
Preview preview{};
|
||||||
Details details;
|
|
||||||
std::string last_updated;
|
|
||||||
u64 dl_count;
|
|
||||||
u64 like_count;
|
|
||||||
std::vector<std::string> categories;
|
|
||||||
std::string target;
|
|
||||||
Preview preview;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// struct Pack {
|
|
||||||
// std::string id;
|
|
||||||
// Creator creator;
|
|
||||||
// Details details;
|
|
||||||
// std::string last_updated;
|
|
||||||
// std::vector<std::string> categories;
|
|
||||||
// u64 dl_count;
|
|
||||||
// u64 like_count;
|
|
||||||
// std::vector<ThemeEntry> themes;
|
|
||||||
// };
|
|
||||||
|
|
||||||
struct PackListEntry {
|
struct PackListEntry {
|
||||||
std::string id;
|
std::string id{};
|
||||||
Creator creator;
|
Creator creator{};
|
||||||
Details details;
|
Details details{};
|
||||||
std::string last_updated;
|
std::vector<ThemeEntry> themes{};
|
||||||
std::vector<std::string> categories;
|
|
||||||
u64 dl_count;
|
|
||||||
u64 like_count;
|
|
||||||
std::vector<ThemeEntry> themes;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Pagination {
|
struct Pagination {
|
||||||
u64 page;
|
u64 page{};
|
||||||
u64 limit;
|
u64 limit{};
|
||||||
u64 page_count;
|
u64 page_count{};
|
||||||
u64 item_count;
|
u64 item_count{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PackList {
|
struct PackList {
|
||||||
std::vector<PackListEntry> packList;
|
std::vector<PackListEntry> packList{};
|
||||||
Pagination pagination;
|
Pagination pagination{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Config {
|
struct Config {
|
||||||
@@ -131,10 +94,10 @@ struct Config {
|
|||||||
u32 sort_index{};
|
u32 sort_index{};
|
||||||
u32 order_index{};
|
u32 order_index{};
|
||||||
// search query, if empty, its not used
|
// search query, if empty, its not used
|
||||||
std::string query;
|
std::string query{};
|
||||||
// this is actually an array of creator ids, but we don't support that feature
|
// this is actually an array of creator ids, but we don't support that feature
|
||||||
// if empty, its not used
|
// if empty, its not used
|
||||||
std::string creator;
|
std::string creator{};
|
||||||
// defaults
|
// defaults
|
||||||
u32 page{1};
|
u32 page{1};
|
||||||
u32 limit{18};
|
u32 limit{18};
|
||||||
@@ -160,7 +123,7 @@ struct Config {
|
|||||||
struct Menu; // fwd
|
struct Menu; // fwd
|
||||||
|
|
||||||
struct PageEntry {
|
struct PageEntry {
|
||||||
std::vector<PackListEntry> m_packList;
|
std::vector<PackListEntry> m_packList{};
|
||||||
Pagination m_pagination{};
|
Pagination m_pagination{};
|
||||||
PageLoadState m_ready{PageLoadState::None};
|
PageLoadState m_ready{PageLoadState::None};
|
||||||
};
|
};
|
||||||
@@ -173,13 +136,13 @@ struct Menu final : MenuBase {
|
|||||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
void OnFocusGained() override;
|
void OnFocusGained() override;
|
||||||
|
|
||||||
void SetIndex(std::size_t index) {
|
void SetIndex(s64 index) {
|
||||||
m_index = index;
|
m_index = index;
|
||||||
|
if (!m_index) {
|
||||||
|
m_list->SetYoff(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// void SetSearch(const std::string& term);
|
|
||||||
// void SetAuthor();
|
|
||||||
|
|
||||||
void InvalidateAllPages();
|
void InvalidateAllPages();
|
||||||
void PackListDownload();
|
void PackListDownload();
|
||||||
void OnPackListDownload();
|
void OnPackListDownload();
|
||||||
@@ -188,14 +151,14 @@ private:
|
|||||||
static constexpr inline const char* INI_SECTION = "themezer";
|
static constexpr inline const char* INI_SECTION = "themezer";
|
||||||
static constexpr inline u32 MAX_ON_PAGE = 16; // same as website
|
static constexpr inline u32 MAX_ON_PAGE = 16; // same as website
|
||||||
|
|
||||||
std::vector<PageEntry> m_pages;
|
std::vector<PageEntry> m_pages{};
|
||||||
std::size_t m_page_index{};
|
s64 m_page_index{};
|
||||||
std::size_t m_page_index_max{1};
|
s64 m_page_index_max{1};
|
||||||
|
|
||||||
std::string m_search{};
|
std::string m_search{};
|
||||||
|
|
||||||
std::size_t m_start{};
|
s64 m_index{}; // where i am in the array
|
||||||
std::size_t m_index{}; // where i am in the array
|
std::unique_ptr<List> m_list{};
|
||||||
|
|
||||||
// options
|
// options
|
||||||
option::OptionLong m_sort{INI_SECTION, "sort", 0};
|
option::OptionLong m_sort{INI_SECTION, "sort", 0};
|
||||||
|
|||||||
@@ -19,13 +19,12 @@ public:
|
|||||||
auto IsDone() const noexcept { return m_count == 0; }
|
auto IsDone() const noexcept { return m_count == 0; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void OnLayoutChange() override;
|
|
||||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string m_text;
|
std::string m_text{};
|
||||||
std::size_t m_count{180}; // count down to zero
|
std::size_t m_count{180}; // count down to zero
|
||||||
Side m_side;
|
Side m_side{};
|
||||||
bool m_bounds_measured{};
|
bool m_bounds_measured{};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,7 +33,6 @@ public:
|
|||||||
NotifMananger() = default;
|
NotifMananger() = default;
|
||||||
~NotifMananger() = default;
|
~NotifMananger() = default;
|
||||||
|
|
||||||
void OnLayoutChange() override;
|
|
||||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
|
|
||||||
void Push(const NotifEntry& entry);
|
void Push(const NotifEntry& entry);
|
||||||
@@ -49,8 +47,8 @@ private:
|
|||||||
void Draw(NVGcontext* vg, Theme* theme, Entries& entries);
|
void Draw(NVGcontext* vg, Theme* theme, Entries& entries);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Entries m_entries_left;
|
Entries m_entries_left{};
|
||||||
Entries m_entries_right;
|
Entries m_entries_right{};
|
||||||
Mutex m_mutex{};
|
Mutex m_mutex{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,90 +1,41 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "nanovg.h"
|
#include "nanovg.h"
|
||||||
#include "ui/widget.hpp"
|
#include "ui/types.hpp"
|
||||||
|
|
||||||
namespace sphaira::ui::gfx {
|
namespace sphaira::ui::gfx {
|
||||||
|
|
||||||
enum class Colour {
|
|
||||||
BLACK,
|
|
||||||
LIGHT_BLACK,
|
|
||||||
SILVER,
|
|
||||||
DARK_GREY,
|
|
||||||
GREY,
|
|
||||||
WHITE,
|
|
||||||
CYAN,
|
|
||||||
TEAL,
|
|
||||||
BLUE,
|
|
||||||
LIGHT_BLUE,
|
|
||||||
YELLOW,
|
|
||||||
RED,
|
|
||||||
};
|
|
||||||
|
|
||||||
void drawImage(NVGcontext*, float x, float y, float w, float h, int texture);
|
void drawImage(NVGcontext*, float x, float y, float w, float h, int texture);
|
||||||
void drawImage(NVGcontext*, Vec4 v, int texture);
|
void drawImage(NVGcontext*, const Vec4& v, int texture);
|
||||||
void drawImageRounded(NVGcontext*, float x, float y, float w, float h, int texture);
|
void drawImageRounded(NVGcontext*, float x, float y, float w, float h, int texture);
|
||||||
void drawImageRounded(NVGcontext*, Vec4 v, int texture);
|
void drawImageRounded(NVGcontext*, const Vec4& v, int texture);
|
||||||
|
|
||||||
auto getColour(Colour c) -> NVGcolor;
|
|
||||||
|
|
||||||
void dimBackground(NVGcontext*);
|
void dimBackground(NVGcontext*);
|
||||||
|
|
||||||
void drawRect(NVGcontext*, float x, float y, float w, float h, Colour c, bool rounded = false);
|
|
||||||
void drawRect(NVGcontext*, Vec4 vec, Colour c, bool rounded = false);
|
|
||||||
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGcolor& c, bool rounded = false);
|
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGcolor& c, bool rounded = false);
|
||||||
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGcolor&& c, bool rounded = false);
|
void drawRect(NVGcontext*, const Vec4& v, const NVGcolor& c, bool rounded = false);
|
||||||
void drawRect(NVGcontext*, Vec4 vec, const NVGcolor& c, bool rounded = false);
|
|
||||||
void drawRect(NVGcontext*, Vec4 vec, const NVGcolor&& c, bool rounded = false);
|
|
||||||
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGpaint& p, bool rounded = false);
|
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGpaint& p, bool rounded = false);
|
||||||
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGpaint&& p, bool rounded = false);
|
void drawRect(NVGcontext*, const Vec4& v, const NVGpaint& p, bool rounded = false);
|
||||||
void drawRect(NVGcontext*, Vec4 vec, const NVGpaint& p, bool rounded = false);
|
|
||||||
void drawRect(NVGcontext*, Vec4 vec, const NVGpaint&& p, bool rounded = false);
|
|
||||||
|
|
||||||
void drawRectOutline(NVGcontext*, float size, const NVGcolor& out_col, float x, float y, float w, float h, Colour c);
|
void drawRectOutline(NVGcontext*, const Theme*, float size, float x, float y, float w, float h);
|
||||||
void drawRectOutline(NVGcontext*, float size, const NVGcolor& out_col, Vec4 vec, Colour c);
|
void drawRectOutline(NVGcontext*, const Theme*, float size, const Vec4& v);
|
||||||
void drawRectOutline(NVGcontext*, float size, const NVGcolor& out_col, float x, float y, float w, float h, const NVGcolor& c);
|
|
||||||
void drawRectOutline(NVGcontext*, float size, const NVGcolor& out_col, float x, float y, float w, float h, const NVGcolor&& c);
|
|
||||||
void drawRectOutline(NVGcontext*, float size, const NVGcolor& out_col, Vec4 vec, const NVGcolor& c);
|
|
||||||
void drawRectOutline(NVGcontext*, float size, const NVGcolor& out_col, Vec4 vec, const NVGcolor&& c);
|
|
||||||
void drawRectOutline(NVGcontext*, float size, const NVGcolor& out_col, float x, float y, float w, float h, const NVGpaint& p);
|
|
||||||
void drawRectOutline(NVGcontext*, float size, const NVGcolor& out_col, float x, float y, float w, float h, const NVGpaint&& p);
|
|
||||||
void drawRectOutline(NVGcontext*, float size, const NVGcolor& out_col, Vec4 vec, const NVGpaint& p);
|
|
||||||
void drawRectOutline(NVGcontext*, float size, const NVGcolor& out_col, Vec4 vec, const NVGpaint&& p);
|
|
||||||
|
|
||||||
void drawTriangle(NVGcontext*, float aX, float aY, float bX, float bY, float cX, float cY, Colour c);
|
|
||||||
void drawTriangle(NVGcontext*, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor& c);
|
|
||||||
void drawTriangle(NVGcontext*, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor&& c);
|
|
||||||
void drawTriangle(NVGcontext*, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p);
|
|
||||||
void drawTriangle(NVGcontext*, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint&& p);
|
|
||||||
|
|
||||||
void drawText(NVGcontext*, float x, float y, float size, const char* str, const char* end, int align, Colour c);
|
|
||||||
void drawText(NVGcontext*, float x, float y, float size, Colour c, const char* str, int align = NVG_ALIGN_LEFT | NVG_ALIGN_TOP, const char* end = nullptr);
|
|
||||||
void drawText(NVGcontext*, Vec2 vec, float size, const char* str, const char* end, int align, Colour c);
|
|
||||||
void drawText(NVGcontext*, Vec2 vec, float size, Colour c, const char* str, int align = NVG_ALIGN_LEFT | NVG_ALIGN_TOP, const char* end = nullptr);
|
|
||||||
void drawText(NVGcontext*, float x, float y, float size, const char* str, const char* end, int align, const NVGcolor& c);
|
void drawText(NVGcontext*, float x, float y, float size, const char* str, const char* end, int align, const NVGcolor& c);
|
||||||
void drawText(NVGcontext*, float x, float y, float size, const char* str, const char* end, int align, const NVGcolor&& c);
|
|
||||||
void drawText(NVGcontext*, float x, float y, float size, const NVGcolor& c, const char* str, int align = NVG_ALIGN_LEFT | NVG_ALIGN_TOP, const char* end = nullptr);
|
void drawText(NVGcontext*, float x, float y, float size, const NVGcolor& c, const char* str, int align = NVG_ALIGN_LEFT | NVG_ALIGN_TOP, const char* end = nullptr);
|
||||||
void drawText(NVGcontext*, float x, float y, float size, const NVGcolor&& c, const char* str, int align = NVG_ALIGN_LEFT | NVG_ALIGN_TOP, const char* end = nullptr);
|
void drawText(NVGcontext*, const Vec2& v, float size, const char* str, const char* end, int align, const NVGcolor& c);
|
||||||
void drawText(NVGcontext*, Vec2 vec, float size, const char* str, const char* end, int align, const NVGcolor& c);
|
void drawText(NVGcontext*, const Vec2& v, float size, const NVGcolor& c, const char* str, int align = NVG_ALIGN_LEFT | NVG_ALIGN_TOP, const char* end = nullptr);
|
||||||
void drawText(NVGcontext*, Vec2 vec, float size, const char* str, const char* end, int align, const NVGcolor&& c);
|
|
||||||
void drawText(NVGcontext*, Vec2 vec, float size, const NVGcolor& c, const char* str, int align = NVG_ALIGN_LEFT | NVG_ALIGN_TOP, const char* end = nullptr);
|
|
||||||
void drawText(NVGcontext*, Vec2 vec, float size, const NVGcolor&& c, const char* str, int align = NVG_ALIGN_LEFT | NVG_ALIGN_TOP, const char* end = nullptr);
|
|
||||||
void drawTextArgs(NVGcontext*, float x, float y, float size, int align, Colour c, const char* str, ...) __attribute__ ((format (printf, 7, 8)));
|
|
||||||
void drawTextArgs(NVGcontext*, float x, float y, float size, int align, const NVGcolor& c, const char* str, ...) __attribute__ ((format (printf, 7, 8)));
|
void drawTextArgs(NVGcontext*, float x, float y, float size, int align, const NVGcolor& c, const char* str, ...) __attribute__ ((format (printf, 7, 8)));
|
||||||
|
|
||||||
void drawTextBox(NVGcontext*, float x, float y, float size, float bound, NVGcolor& c, const char* str, int align = NVG_ALIGN_LEFT | NVG_ALIGN_TOP, const char* end = nullptr);
|
void drawTextBox(NVGcontext*, float x, float y, float size, float bound, const NVGcolor& c, const char* str, int align = NVG_ALIGN_LEFT | NVG_ALIGN_TOP, const char* end = nullptr);
|
||||||
void drawTextBox(NVGcontext*, float x, float y, float size, float bound, NVGcolor&& c, const char* str, int align = NVG_ALIGN_LEFT | NVG_ALIGN_TOP, const char* end = nullptr);
|
|
||||||
void drawTextBox(NVGcontext*, float x, float y, float size, float bound, Colour c, const char* str, int align = NVG_ALIGN_LEFT | NVG_ALIGN_TOP, const char* end = nullptr);
|
|
||||||
|
|
||||||
void textBounds(NVGcontext*, float x, float y, float *bounds, const char* str, ...) __attribute__ ((format (printf, 5, 6)));
|
void textBounds(NVGcontext*, float x, float y, float *bounds, const char* str, ...) __attribute__ ((format (printf, 5, 6)));
|
||||||
// void textBounds(NVGcontext*, float *bounds, const char* str, ...) __attribute__ ((format (printf, 5, 6)));
|
|
||||||
// void textBounds(NVGcontext*, float *bounds, const char* str);
|
|
||||||
|
|
||||||
auto getButton(Button button) -> const char*;
|
auto getButton(Button button) -> const char*;
|
||||||
void drawButton(NVGcontext* vg, float x, float y, float size, Button button);
|
void drawScrollbar(NVGcontext*, const Theme*, u32 index_off, u32 count, u32 max_per_page);
|
||||||
void drawButtons(NVGcontext* vg, const Widget::Actions& actions, const NVGcolor& c, float start_x = 1220.f);
|
void drawScrollbar(NVGcontext*, const Theme*, float x, float y, float h, u32 index_off, u32 count, u32 max_per_page);
|
||||||
|
|
||||||
void drawDimBackground(NVGcontext* vg);
|
void drawScrollbar2(NVGcontext*, const Theme*, float x, float y, float h, s64 index_off, s64 count, s64 row, s64 page);
|
||||||
|
void drawScrollbar2(NVGcontext*, const Theme*, s64 index_off, s64 count, s64 row, s64 page);
|
||||||
|
|
||||||
void updateHighlightAnimation();
|
void updateHighlightAnimation();
|
||||||
void getHighlightAnimation(float* gradientX, float* gradientY, float* color);
|
void getHighlightAnimation(float* gradientX, float* gradientY, float* color);
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "types.hpp"
|
#include "types.hpp"
|
||||||
|
#include <stop_token>
|
||||||
|
|
||||||
namespace sphaira::ui {
|
namespace sphaira::ui {
|
||||||
|
|
||||||
class Object {
|
class Object {
|
||||||
public:
|
public:
|
||||||
Object() = default;
|
Object() = default;
|
||||||
virtual ~Object() = default;
|
virtual ~Object() {
|
||||||
|
m_stop_source.request_stop();
|
||||||
|
}
|
||||||
|
|
||||||
// virtual auto OnLayoutChange() -> void = 0;
|
|
||||||
virtual auto OnLayoutChange() -> void {};
|
|
||||||
virtual auto Draw(NVGcontext* vg, Theme* theme) -> void = 0;
|
virtual auto Draw(NVGcontext* vg, Theme* theme) -> void = 0;
|
||||||
|
|
||||||
auto GetPos() const noexcept {
|
auto GetPos() const noexcept {
|
||||||
@@ -73,8 +74,14 @@ public:
|
|||||||
m_hidden = value;
|
m_hidden = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto GetToken() const {
|
||||||
|
return m_stop_source.get_token();
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
Vec4 m_pos{};
|
Vec4 m_pos{};
|
||||||
|
// used for lifetime management across threads.
|
||||||
|
std::stop_source m_stop_source{};
|
||||||
bool m_hidden{false};
|
bool m_hidden{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,13 @@ public:
|
|||||||
OptionBoxEntry(const std::string& text, Vec4 pos);
|
OptionBoxEntry(const std::string& text, Vec4 pos);
|
||||||
|
|
||||||
auto Update(Controller* controller, TouchInfo* touch) -> void override {}
|
auto Update(Controller* controller, TouchInfo* touch) -> void override {}
|
||||||
auto OnLayoutChange() -> void override {}
|
|
||||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||||
|
|
||||||
auto Selected(bool enable) -> void;
|
auto Selected(bool enable) -> void;
|
||||||
private:
|
private:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string m_text;
|
std::string m_text{};
|
||||||
Vec2 m_text_pos{};
|
Vec2 m_text_pos{};
|
||||||
bool m_selected{false};
|
bool m_selected{false};
|
||||||
};
|
};
|
||||||
@@ -28,32 +27,34 @@ private:
|
|||||||
// todo: support upto 4 options.
|
// todo: support upto 4 options.
|
||||||
class OptionBox final : public Widget {
|
class OptionBox final : public Widget {
|
||||||
public:
|
public:
|
||||||
using Callback = std::function<void(std::optional<std::size_t> index)>;
|
using Callback = std::function<void(std::optional<s64> index)>;
|
||||||
using Option = std::string;
|
using Option = std::string;
|
||||||
using Options = std::vector<Option>;
|
using Options = std::vector<Option>;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
OptionBox(const std::string& message, const Option& a, Callback cb); // confirm
|
OptionBox(const std::string& message, const Option& a, Callback cb = [](auto){}); // confirm
|
||||||
OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb); // yesno
|
OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb); // yesno
|
||||||
OptionBox(const std::string& message, const Option& a, const Option& b, std::size_t index, Callback cb); // yesno
|
OptionBox(const std::string& message, const Option& a, const Option& b, s64 index, Callback cb); // yesno
|
||||||
OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, Callback cb); // tri
|
OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, Callback cb); // tri
|
||||||
OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, std::size_t index, 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 OnLayoutChange() -> void override;
|
|
||||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||||
|
auto OnFocusGained() noexcept -> void override;
|
||||||
|
auto OnFocusLost() noexcept -> void override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
auto Setup(std::size_t index) -> void; // common setup values
|
auto Setup(s64 index) -> void; // common setup values
|
||||||
|
void SetIndex(s64 index);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string m_message;
|
std::string m_message{};
|
||||||
Callback m_callback;
|
Callback m_callback{};
|
||||||
|
|
||||||
Vec4 m_spacer_line{};
|
Vec4 m_spacer_line{};
|
||||||
|
|
||||||
std::size_t m_index{};
|
s64 m_index{};
|
||||||
std::vector<OptionBoxEntry> m_entries;
|
std::vector<OptionBoxEntry> m_entries{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::ui
|
} // namespace sphaira::ui
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "ui/widget.hpp"
|
|
||||||
#include <optional>
|
|
||||||
|
|
||||||
namespace sphaira::ui {
|
|
||||||
|
|
||||||
class OptionList final : public Widget {
|
|
||||||
public:
|
|
||||||
using Options = std::vector<std::pair<std::string, std::function<void()>>>;
|
|
||||||
|
|
||||||
public:
|
|
||||||
OptionList(Options _options);
|
|
||||||
|
|
||||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
|
||||||
auto OnLayoutChange() -> void override;
|
|
||||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
Options m_options;
|
|
||||||
std::size_t m_index{};
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace sphaira::ui
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ui/widget.hpp"
|
#include "ui/widget.hpp"
|
||||||
#include "ui/scrollbar.hpp"
|
#include "ui/list.hpp"
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
namespace sphaira::ui {
|
namespace sphaira::ui {
|
||||||
@@ -9,18 +9,22 @@ namespace sphaira::ui {
|
|||||||
class PopupList final : public Widget {
|
class PopupList final : public Widget {
|
||||||
public:
|
public:
|
||||||
using Items = std::vector<std::string>;
|
using Items = std::vector<std::string>;
|
||||||
using Callback = std::function<void(std::optional<std::size_t>)>;
|
using Callback = std::function<void(std::optional<s64>)>;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit PopupList(std::string title, Items items, Callback cb, std::size_t index = 0);
|
explicit PopupList(std::string title, Items items, Callback cb, s64 index = 0);
|
||||||
PopupList(std::string title, Items items, Callback cb, std::string index);
|
PopupList(std::string title, Items items, Callback cb, std::string index);
|
||||||
PopupList(std::string title, Items items, std::string& index_str_ref, std::size_t& index);
|
PopupList(std::string title, Items items, std::string& index_str_ref, s64& index);
|
||||||
PopupList(std::string title, Items items, std::string& index_ref);
|
PopupList(std::string title, Items items, std::string& index_ref);
|
||||||
PopupList(std::string title, Items items, std::size_t& index_ref);
|
PopupList(std::string title, Items items, s64& index_ref);
|
||||||
|
|
||||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||||
auto OnLayoutChange() -> void override;
|
|
||||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||||
|
auto OnFocusGained() noexcept -> void override;
|
||||||
|
auto OnFocusLost() noexcept -> void override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void SetIndex(s64 index);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr Vec2 m_title_pos{70.f, 28.f};
|
static constexpr Vec2 m_title_pos{70.f, 28.f};
|
||||||
@@ -28,20 +32,16 @@ private:
|
|||||||
static constexpr float m_text_xoffset{15.f};
|
static constexpr float m_text_xoffset{15.f};
|
||||||
static constexpr float m_line_width{1220.f};
|
static constexpr float m_line_width{1220.f};
|
||||||
|
|
||||||
std::string m_title;
|
std::string m_title{};
|
||||||
Items m_items;
|
Items m_items{};
|
||||||
Callback m_callback;
|
Callback m_callback{};
|
||||||
std::size_t m_index; // index in list array
|
s64 m_index{}; // index in list array
|
||||||
std::size_t m_index_offset{}; // drawing from array start
|
|
||||||
|
|
||||||
// std::size_t& index_ref;
|
std::unique_ptr<List> m_list{};
|
||||||
// std::string& index_str_ref;
|
|
||||||
|
|
||||||
float m_selected_y{};
|
|
||||||
float m_yoff{};
|
float m_yoff{};
|
||||||
float m_line_top{};
|
float m_line_top{};
|
||||||
float m_line_bottom{};
|
float m_line_bottom{};
|
||||||
ScrollBar m_scrollbar;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::ui
|
} // namespace sphaira::ui
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ struct ProgressBox final : Widget {
|
|||||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||||
|
|
||||||
auto NewTransfer(const std::string& transfer) -> ProgressBox&;
|
auto NewTransfer(const std::string& transfer) -> ProgressBox&;
|
||||||
auto UpdateTransfer(u64 offset, u64 size) -> ProgressBox&;
|
auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
|
||||||
void RequestExit();
|
void RequestExit();
|
||||||
auto ShouldExit() -> bool;
|
auto ShouldExit() -> bool;
|
||||||
|
|
||||||
@@ -30,11 +30,21 @@ struct ProgressBox final : Widget {
|
|||||||
auto CopyFile(const fs::FsPath& src, const fs::FsPath& dst) -> Result;
|
auto CopyFile(const fs::FsPath& src, const fs::FsPath& dst) -> Result;
|
||||||
void Yield();
|
void Yield();
|
||||||
|
|
||||||
|
auto OnDownloadProgressCallback() {
|
||||||
|
return [this](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
|
||||||
|
if (this->ShouldExit()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this->UpdateTransfer(dlnow, dltotal);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
struct ThreadData {
|
struct ThreadData {
|
||||||
ProgressBox* pbox;
|
ProgressBox* pbox{};
|
||||||
ProgressBoxCallback callback;
|
ProgressBoxCallback callback{};
|
||||||
bool result;
|
bool result{};
|
||||||
};
|
};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -45,9 +55,8 @@ private:
|
|||||||
ProgressBoxDoneCallback m_done{};
|
ProgressBoxDoneCallback m_done{};
|
||||||
std::string m_title{};
|
std::string m_title{};
|
||||||
std::string m_transfer{};
|
std::string m_transfer{};
|
||||||
u64 m_size{};
|
s64 m_size{};
|
||||||
u64 m_offset{};
|
s64 m_offset{};
|
||||||
bool m_exit_requested{};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// this is a helper function that does many things.
|
// this is a helper function that does many things.
|
||||||
|
|||||||
@@ -14,15 +14,15 @@ struct ScrollableText final : Widget {
|
|||||||
// float m_y_off = m_y_off_base;
|
// float m_y_off = m_y_off_base;
|
||||||
// static constexpr float m_clip_y = 250.0F;
|
// static constexpr float m_clip_y = 250.0F;
|
||||||
|
|
||||||
|
static constexpr inline float m_step = 30;
|
||||||
|
const float m_font_size;
|
||||||
const float m_y_off_base;
|
const float m_y_off_base;
|
||||||
float m_y_off;
|
|
||||||
const float m_clip_y;
|
const float m_clip_y;
|
||||||
const float m_end_w;
|
const float m_end_w;
|
||||||
static constexpr float m_step = 30;
|
|
||||||
|
|
||||||
int m_index = 0;
|
float m_y_off{};
|
||||||
const float m_font_size;
|
int m_index{};
|
||||||
float m_bounds[4];
|
float m_bounds[4]{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::ui
|
} // namespace sphaira::ui
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "ui/widget.hpp"
|
|
||||||
|
|
||||||
namespace sphaira::ui {
|
|
||||||
|
|
||||||
class ScrollBar final : public Widget {
|
|
||||||
public:
|
|
||||||
enum class Direction { DOWN, UP };
|
|
||||||
|
|
||||||
public:
|
|
||||||
ScrollBar() = default;
|
|
||||||
ScrollBar(Vec4 bounds, float entry_height, std::size_t entries);
|
|
||||||
|
|
||||||
auto Update(Controller* controller, TouchInfo* touch) -> void override {}
|
|
||||||
auto OnLayoutChange() -> void override;
|
|
||||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
|
||||||
|
|
||||||
auto Setup(Vec4 bounds, float entry_height, std::size_t entries) -> void;
|
|
||||||
auto Move(Direction direction) -> void;
|
|
||||||
|
|
||||||
private:
|
|
||||||
auto Setup() -> void;
|
|
||||||
|
|
||||||
private:
|
|
||||||
Vec4 m_bounds{};
|
|
||||||
std::size_t m_entries{};
|
|
||||||
std::size_t m_index{};
|
|
||||||
float m_entry_height{};
|
|
||||||
float m_step_size{};
|
|
||||||
bool m_should_draw{false};
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace sphaira::ui
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ui/widget.hpp"
|
#include "ui/widget.hpp"
|
||||||
|
#include "ui/list.hpp"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace sphaira::ui {
|
namespace sphaira::ui {
|
||||||
@@ -9,11 +10,9 @@ class SidebarEntryBase : public Widget {
|
|||||||
public:
|
public:
|
||||||
SidebarEntryBase(std::string&& title);
|
SidebarEntryBase(std::string&& title);
|
||||||
virtual auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
virtual auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||||
virtual auto OnLayoutChange() -> void override {}
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::string m_title;
|
std::string m_title;
|
||||||
Vec2 m_offset{};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class SidebarEntryBool final : public SidebarEntryBase {
|
class SidebarEntryBool final : public SidebarEntryBase {
|
||||||
@@ -24,9 +23,9 @@ public:
|
|||||||
SidebarEntryBool(std::string title, bool option, Callback cb, std::string true_str = "On", std::string false_str = "Off");
|
SidebarEntryBool(std::string title, bool option, Callback cb, std::string true_str = "On", std::string false_str = "Off");
|
||||||
SidebarEntryBool(std::string title, bool& option, std::string true_str = "On", std::string false_str = "Off");
|
SidebarEntryBool(std::string title, bool& option, std::string true_str = "On", std::string false_str = "Off");
|
||||||
|
|
||||||
|
private:
|
||||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||||
|
|
||||||
private:
|
|
||||||
bool m_option;
|
bool m_option;
|
||||||
Callback m_callback;
|
Callback m_callback;
|
||||||
std::string m_true_str;
|
std::string m_true_str;
|
||||||
@@ -50,20 +49,24 @@ class SidebarEntryArray final : public SidebarEntryBase {
|
|||||||
public:
|
public:
|
||||||
using Items = std::vector<std::string>;
|
using Items = std::vector<std::string>;
|
||||||
using ListCallback = std::function<void()>;
|
using ListCallback = std::function<void()>;
|
||||||
using Callback = std::function<void(std::size_t& index)>;
|
using Callback = std::function<void(s64& index)>;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit SidebarEntryArray(std::string title, Items items, Callback cb, std::size_t index = 0);
|
explicit SidebarEntryArray(std::string title, Items items, Callback cb, s64 index = 0);
|
||||||
SidebarEntryArray(std::string title, Items items, Callback cb, std::string index);
|
SidebarEntryArray(std::string title, Items items, Callback cb, std::string index);
|
||||||
SidebarEntryArray(std::string title, Items items, std::string& index);
|
SidebarEntryArray(std::string title, Items items, std::string& index);
|
||||||
|
|
||||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||||
|
auto OnFocusGained() noexcept -> void override;
|
||||||
|
auto OnFocusLost() noexcept -> void override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Items m_items;
|
Items m_items;
|
||||||
ListCallback m_list_callback;
|
ListCallback m_list_callback;
|
||||||
Callback m_callback;
|
Callback m_callback;
|
||||||
std::size_t m_index;
|
s64 m_index;
|
||||||
|
s64 m_tick{};
|
||||||
|
float m_text_yoff{};
|
||||||
};
|
};
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
@@ -101,33 +104,30 @@ public:
|
|||||||
Sidebar(std::string title, std::string sub, Side side);
|
Sidebar(std::string title, std::string sub, Side side);
|
||||||
|
|
||||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||||
auto OnLayoutChange() -> void override {}
|
|
||||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||||
auto OnFocusGained() noexcept -> void override;
|
auto OnFocusGained() noexcept -> void override;
|
||||||
auto OnFocusLost() noexcept -> void override;
|
auto OnFocusLost() noexcept -> void override;
|
||||||
|
|
||||||
void Add(std::shared_ptr<SidebarEntryBase> entry);
|
void Add(std::shared_ptr<SidebarEntryBase> entry);
|
||||||
void AddSpacer();
|
|
||||||
void AddHeader(std::string name);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void SetIndex(std::size_t index);
|
void SetIndex(s64 index);
|
||||||
|
void SetupButtons();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string m_title;
|
std::string m_title;
|
||||||
std::string m_sub;
|
std::string m_sub;
|
||||||
Side m_side;
|
Side m_side;
|
||||||
Items m_items;
|
Items m_items;
|
||||||
std::size_t m_index{};
|
s64 m_index{};
|
||||||
std::size_t m_index_offset{};
|
|
||||||
|
std::unique_ptr<List> m_list;
|
||||||
|
|
||||||
Vec4 m_top_bar{};
|
Vec4 m_top_bar{};
|
||||||
Vec4 m_bottom_bar{};
|
Vec4 m_bottom_bar{};
|
||||||
Vec2 m_title_pos{};
|
Vec2 m_title_pos{};
|
||||||
Vec4 m_base_pos{};
|
Vec4 m_base_pos{};
|
||||||
|
|
||||||
float m_selected_y{};
|
|
||||||
|
|
||||||
static constexpr float m_title_size{28.f};
|
static constexpr float m_title_size{28.f};
|
||||||
// static constexpr Vec2 box_size{380.f, 70.f};
|
// static constexpr Vec2 box_size{380.f, 70.f};
|
||||||
static constexpr Vec2 m_box_size{400.f, 70.f};
|
static constexpr Vec2 m_box_size{400.f, 70.f};
|
||||||
|
|||||||
@@ -114,15 +114,34 @@ struct [[nodiscard]] Vec4 {
|
|||||||
|
|
||||||
struct TimeStamp {
|
struct TimeStamp {
|
||||||
TimeStamp() {
|
TimeStamp() {
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Update() {
|
||||||
start = armGetSystemTick();
|
start = armGetSystemTick();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto GetNs() -> u64 {
|
auto GetNs() const -> u64 {
|
||||||
const auto end_ticks = armGetSystemTick();
|
const auto end_ticks = armGetSystemTick();
|
||||||
return armTicksToNs(end_ticks) - armTicksToNs(start);
|
return armTicksToNs(end_ticks) - armTicksToNs(start);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto GetSeconds() -> double {
|
auto GetMs() const -> u64 {
|
||||||
|
const auto ns = GetNs();
|
||||||
|
return ns/1000/1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetSeconds() const -> u64 {
|
||||||
|
const auto ns = GetNs();
|
||||||
|
return ns/1000/1000/1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetMsD() const -> double {
|
||||||
|
const double ns = GetNs();
|
||||||
|
return ns/1000.0/1000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetSecondsD() const -> double {
|
||||||
const double ns = GetNs();
|
const double ns = GetNs();
|
||||||
return ns/1000.0/1000.0/1000.0;
|
return ns/1000.0/1000.0/1000.0;
|
||||||
}
|
}
|
||||||
@@ -143,21 +162,55 @@ struct ElementEntry {
|
|||||||
};
|
};
|
||||||
|
|
||||||
enum ThemeEntryID {
|
enum ThemeEntryID {
|
||||||
|
// colour of the background, can be an image.
|
||||||
ThemeEntryID_BACKGROUND,
|
ThemeEntryID_BACKGROUND,
|
||||||
|
// colour of the grid background (homebrew, appstore), can be an image.
|
||||||
ThemeEntryID_GRID,
|
ThemeEntryID_GRID,
|
||||||
ThemeEntryID_SELECTED,
|
// background colour of a popup.
|
||||||
ThemeEntryID_SELECTED_OVERLAY,
|
ThemeEntryID_POPUP,
|
||||||
ThemeEntryID_TEXT,
|
// colour of the error text / button.
|
||||||
ThemeEntryID_TEXT_SELECTED,
|
ThemeEntryID_ERROR,
|
||||||
|
|
||||||
|
// colour of all text.
|
||||||
|
ThemeEntryID_TEXT,
|
||||||
|
// colour of text info and subheaders.
|
||||||
|
ThemeEntryID_TEXT_INFO,
|
||||||
|
// colour of selected item text.
|
||||||
|
ThemeEntryID_TEXT_SELECTED,
|
||||||
|
// background colour of a selected item, can be an image (not recommended).
|
||||||
|
ThemeEntryID_SELECTED_BACKGROUND,
|
||||||
|
|
||||||
|
// colour of line separators in a list.
|
||||||
|
ThemeEntryID_LINE,
|
||||||
|
ThemeEntryID_LINE_SEPARATOR,
|
||||||
|
|
||||||
|
// colour of the sidebar backrgound.
|
||||||
|
ThemeEntryID_SIDEBAR,
|
||||||
|
|
||||||
|
// colour of the scrollbar (full portion).
|
||||||
|
ThemeEntryID_SCROLLBAR,
|
||||||
|
// colour of the scrollbar background (empty portion).
|
||||||
|
ThemeEntryID_SCROLLBAR_BACKGROUND,
|
||||||
|
|
||||||
|
// colour of the progressbar (full portion).
|
||||||
|
ThemeEntryID_PROGRESSBAR,
|
||||||
|
// colour of the progressbar background (empty portion).
|
||||||
|
ThemeEntryID_PROGRESSBAR_BACKGROUND,
|
||||||
|
|
||||||
|
// the colours of the pulsing effect, from 1 -> 2.
|
||||||
|
ThemeEntryID_HIGHLIGHT_1,
|
||||||
|
ThemeEntryID_HIGHLIGHT_2,
|
||||||
|
|
||||||
|
// changes the colours of the internal icons used below.
|
||||||
|
ThemeEntryID_ICON_COLOUR,
|
||||||
|
|
||||||
|
// images used in the filebrowser.
|
||||||
ThemeEntryID_ICON_AUDIO,
|
ThemeEntryID_ICON_AUDIO,
|
||||||
ThemeEntryID_ICON_VIDEO,
|
ThemeEntryID_ICON_VIDEO,
|
||||||
ThemeEntryID_ICON_IMAGE,
|
ThemeEntryID_ICON_IMAGE,
|
||||||
ThemeEntryID_ICON_FILE,
|
ThemeEntryID_ICON_FILE,
|
||||||
ThemeEntryID_ICON_FOLDER,
|
ThemeEntryID_ICON_FOLDER,
|
||||||
ThemeEntryID_ICON_ZIP,
|
ThemeEntryID_ICON_ZIP,
|
||||||
ThemeEntryID_ICON_GAME,
|
|
||||||
ThemeEntryID_ICON_NRO,
|
ThemeEntryID_ICON_NRO,
|
||||||
|
|
||||||
ThemeEntryID_MAX,
|
ThemeEntryID_MAX,
|
||||||
@@ -167,49 +220,43 @@ struct ThemeMeta {
|
|||||||
std::string name;
|
std::string name;
|
||||||
std::string author;
|
std::string author;
|
||||||
std::string version;
|
std::string version;
|
||||||
std::string ini_path;
|
fs::FsPath inherit;
|
||||||
|
fs::FsPath ini_path;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Theme {
|
struct Theme {
|
||||||
std::string name;
|
ThemeMeta meta;
|
||||||
std::string author;
|
|
||||||
std::string version;
|
|
||||||
fs::FsPath path;
|
|
||||||
PLSR_BFSTM music;
|
PLSR_BFSTM music;
|
||||||
ElementEntry elements[ThemeEntryID_MAX];
|
ElementEntry elements[ThemeEntryID_MAX];
|
||||||
|
|
||||||
// NVGcolor background; // bg
|
auto GetColour(ThemeEntryID id) const {
|
||||||
// NVGcolor lines; // grid lines
|
return elements[id].colour;
|
||||||
// NVGcolor spacer; // lines in popup box
|
}
|
||||||
// NVGcolor text; // text colour
|
|
||||||
// NVGcolor text_info; // description text
|
|
||||||
NVGcolor selected; // selected colours
|
|
||||||
// NVGcolor overlay; // popup overlay colour
|
|
||||||
|
|
||||||
// void DrawElement(float x, float y, float w, float h, ThemeEntryID id);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class TouchState {
|
// enum class TouchGesture {
|
||||||
Start, // set when touch has started
|
// None,
|
||||||
Touching, // set when touch is held longer than 1 frame
|
// Tap,
|
||||||
Stop, // set after touch is released
|
// Scroll,
|
||||||
None, // set when there is no touch
|
// };
|
||||||
};
|
|
||||||
|
|
||||||
struct TouchInfo {
|
struct TouchInfo {
|
||||||
s32 initial_x;
|
HidTouchState initial;
|
||||||
s32 initial_y;
|
HidTouchState cur;
|
||||||
|
|
||||||
s32 cur_x;
|
auto in_range(const Vec4& v) const -> bool {
|
||||||
s32 cur_y;
|
return cur.x >= v.x && cur.x <= v.x + v.w && cur.y >= v.y && cur.y <= v.y + v.h;
|
||||||
|
}
|
||||||
|
|
||||||
s32 prev_x;
|
auto in_range(s32 x, s32 y, s32 w, s32 h) const -> bool {
|
||||||
s32 prev_y;
|
return in_range(Vec4(x, y, w, h));
|
||||||
|
}
|
||||||
u32 finger_id;
|
|
||||||
|
|
||||||
bool is_touching;
|
bool is_touching;
|
||||||
bool is_tap;
|
bool is_tap;
|
||||||
|
bool is_scroll;
|
||||||
|
bool is_clicked;
|
||||||
|
bool is_end;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class Button : u64 {
|
enum class Button : u64 {
|
||||||
@@ -276,37 +323,36 @@ inline ActionType operator|(ActionType a, ActionType b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct Action final {
|
struct Action final {
|
||||||
|
using CallbackEmpty = std::function<void()>;
|
||||||
|
using CallbackWithBool = std::function<void(bool)>;
|
||||||
using Callback = std::variant<
|
using Callback = std::variant<
|
||||||
std::function<void()>,
|
CallbackEmpty,
|
||||||
std::function<void(bool)>
|
CallbackWithBool
|
||||||
>;
|
>;
|
||||||
|
|
||||||
Action(Callback cb) : m_type{ActionType::DOWN}, m_hint{""}, m_callback{cb}, m_hidden{true} {}
|
Action(Callback cb) : Action{ActionType::DOWN, "", cb} {}
|
||||||
Action(std::string hint, Callback cb) : m_type{ActionType::DOWN}, m_hint{hint}, m_callback{cb} {}
|
Action(std::string hint, Callback cb) : Action{ActionType::DOWN, hint, cb} {}
|
||||||
Action(u8 type, Callback cb) : m_type{type}, m_hint{""}, m_callback{cb}, m_hidden{true} {}
|
Action(u8 type, Callback cb) : Action{type, "", cb} {}
|
||||||
Action(u8 type, std::string hint, Callback cb) : m_type{type}, m_hint{hint}, m_callback{cb} {}
|
Action(u8 type, std::string hint, Callback cb) : m_type{type}, m_callback{cb}, m_hint{hint} {}
|
||||||
|
|
||||||
auto IsHidden() const noexcept { return m_hidden; }
|
auto IsHidden() const noexcept { return m_hint.empty(); }
|
||||||
|
|
||||||
auto Invoke(bool down) const {
|
auto Invoke(bool down) const {
|
||||||
// todo: make this a visit
|
std::visit([down](auto&& arg){
|
||||||
switch (m_callback.index()) {
|
using T = std::decay_t<decltype(arg)>;
|
||||||
case 0:
|
if constexpr(std::is_same_v<T, CallbackEmpty>) {
|
||||||
std::get<0>(m_callback)();
|
arg();
|
||||||
break;
|
} else if constexpr(std::is_same_v<T, CallbackWithBool>) {
|
||||||
case 1:
|
arg(down);
|
||||||
std::get<1>(m_callback)(down);
|
} else {
|
||||||
break;
|
static_assert(false, "non-exhaustive visitor!");
|
||||||
}
|
}
|
||||||
// std::visit([down, this](auto& cb){
|
}, m_callback);
|
||||||
// cb(down);
|
|
||||||
// }), m_callback;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u8 m_type;
|
u8 m_type{};
|
||||||
std::string m_hint; // todo: make optional
|
Callback m_callback{};
|
||||||
Callback m_callback;
|
std::string m_hint{};
|
||||||
bool m_hidden{false}; // replace this optional text
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Controller {
|
struct Controller {
|
||||||
@@ -340,7 +386,7 @@ struct Controller {
|
|||||||
m_kup = 0;
|
m_kup = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UpdateButtonHeld(HidNpadButton buttons) {
|
void UpdateButtonHeld(u64 buttons) {
|
||||||
if (m_kdown & buttons) {
|
if (m_kdown & buttons) {
|
||||||
m_step = 50;
|
m_step = 50;
|
||||||
m_counter = 0;
|
m_counter = 0;
|
||||||
@@ -348,7 +394,7 @@ struct Controller {
|
|||||||
m_counter += m_step;
|
m_counter += m_step;
|
||||||
|
|
||||||
if (m_counter >= m_MAX) {
|
if (m_counter >= m_MAX) {
|
||||||
m_kdown |= buttons;
|
m_kdown |= m_kheld & buttons;
|
||||||
m_counter = 0;
|
m_counter = 0;
|
||||||
m_step = std::min(m_step + 50, m_MAX_STEP);
|
m_step = std::min(m_step + 50, m_MAX_STEP);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,20 @@
|
|||||||
|
|
||||||
namespace sphaira::ui {
|
namespace sphaira::ui {
|
||||||
|
|
||||||
|
struct uiButton final : Object {
|
||||||
|
uiButton(Button button, Action action) : m_button{button}, m_action{action} {}
|
||||||
|
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||||
|
|
||||||
|
Button m_button;
|
||||||
|
Action m_action;
|
||||||
|
Vec4 m_button_pos{};
|
||||||
|
Vec4 m_hint_pos{};
|
||||||
|
};
|
||||||
|
|
||||||
struct Widget : public Object {
|
struct Widget : public Object {
|
||||||
|
using Actions = std::map<Button, Action>;
|
||||||
|
using uiButtons = std::vector<uiButton>;
|
||||||
|
|
||||||
virtual ~Widget() = default;
|
virtual ~Widget() = default;
|
||||||
|
|
||||||
virtual void Update(Controller* controller, TouchInfo* touch);
|
virtual void Update(Controller* controller, TouchInfo* touch);
|
||||||
@@ -26,6 +39,10 @@ struct Widget : public Object {
|
|||||||
return m_focus;
|
return m_focus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
virtual auto IsMenu() const -> bool {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
auto HasAction(Button button) const -> bool;
|
auto HasAction(Button button) const -> bool;
|
||||||
void SetAction(Button button, Action action);
|
void SetAction(Button button, Action action);
|
||||||
void SetActions(std::same_as<std::pair<Button, Action>> auto ...args) {
|
void SetActions(std::same_as<std::pair<Button, Action>> auto ...args) {
|
||||||
@@ -45,6 +62,8 @@ struct Widget : public Object {
|
|||||||
m_actions.clear();
|
m_actions.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto FireAction(Button button, u8 type = ActionType::DOWN) -> bool;
|
||||||
|
|
||||||
void SetPop(bool pop = true) {
|
void SetPop(bool pop = true) {
|
||||||
m_pop = pop;
|
m_pop = pop;
|
||||||
}
|
}
|
||||||
@@ -53,9 +72,14 @@ struct Widget : public Object {
|
|||||||
return m_pop;
|
return m_pop;
|
||||||
}
|
}
|
||||||
|
|
||||||
using Actions = std::map<Button, Action>;
|
auto SetUiButtonPos(Vec2 pos) {
|
||||||
// using Actions = std::unordered_map<Button, Action>;
|
m_button_pos = pos;
|
||||||
Actions m_actions;
|
}
|
||||||
|
|
||||||
|
auto GetUiButtons() const -> uiButtons;
|
||||||
|
|
||||||
|
Actions m_actions{};
|
||||||
|
Vec2 m_button_pos{1220, 675};
|
||||||
bool m_focus{false};
|
bool m_focus{false};
|
||||||
bool m_pop{false};
|
bool m_pop{false};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,13 +39,14 @@ constexpr auto cexprHash(const char *str, std::size_t v = 0) noexcept -> std::si
|
|||||||
JSON_SKIP_IF_NULL_PTR(str); \
|
JSON_SKIP_IF_NULL_PTR(str); \
|
||||||
e.name = str; \
|
e.name = str; \
|
||||||
} \
|
} \
|
||||||
}
|
} break
|
||||||
|
|
||||||
#define JSON_SET_OBJ(name) case cexprHash(#name): { \
|
#define JSON_SET_OBJ(name) case cexprHash(#name): { \
|
||||||
if (yyjson_is_obj(val)) { \
|
if (yyjson_is_obj(val)) { \
|
||||||
from_json(val, e.name); \
|
from_json(val, e.name); \
|
||||||
} \
|
} \
|
||||||
}
|
} break
|
||||||
|
|
||||||
#define JSON_SET_UINT(name) JSON_SET_TYPE(name, uint)
|
#define JSON_SET_UINT(name) JSON_SET_TYPE(name, uint)
|
||||||
#define JSON_SET_STR(name) JSON_SET_TYPE(name, str)
|
#define JSON_SET_STR(name) JSON_SET_TYPE(name, str)
|
||||||
#define JSON_SET_BOOL(name) JSON_SET_TYPE(name, bool)
|
#define JSON_SET_BOOL(name) JSON_SET_TYPE(name, bool)
|
||||||
@@ -72,7 +73,7 @@ constexpr auto cexprHash(const char *str, std::size_t v = 0) noexcept -> std::si
|
|||||||
JSON_SET_ARR_TYPE(name, type); \
|
JSON_SET_ARR_TYPE(name, type); \
|
||||||
} \
|
} \
|
||||||
} \
|
} \
|
||||||
}
|
} break
|
||||||
|
|
||||||
#define JSON_SET_ARR_OBJ2(name, member) case cexprHash(#name): { \
|
#define JSON_SET_ARR_OBJ2(name, member) case cexprHash(#name): { \
|
||||||
if (yyjson_is_arr(val)) { \
|
if (yyjson_is_arr(val)) { \
|
||||||
@@ -87,7 +88,7 @@ constexpr auto cexprHash(const char *str, std::size_t v = 0) noexcept -> std::si
|
|||||||
from_json(hit, member[idx]); \
|
from_json(hit, member[idx]); \
|
||||||
} \
|
} \
|
||||||
} \
|
} \
|
||||||
}
|
} break
|
||||||
|
|
||||||
#define JSON_SET_ARR_OBJ(name) JSON_SET_ARR_OBJ2(name, e.name)
|
#define JSON_SET_ARR_OBJ(name) JSON_SET_ARR_OBJ2(name, e.name)
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,9 @@
|
|||||||
#include <deque>
|
#include <deque>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
|
#include <yyjson.h>
|
||||||
|
|
||||||
namespace sphaira {
|
namespace sphaira::curl {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
#define CURL_EASY_SETOPT_LOG(handle, opt, v) \
|
#define CURL_EASY_SETOPT_LOG(handle, opt, v) \
|
||||||
@@ -24,10 +25,6 @@ namespace {
|
|||||||
log_write("curl_share_setopt(%s, %s) msg: %s\n", #opt, #v, curl_share_strerror(r)); \
|
log_write("curl_share_setopt(%s, %s) msg: %s\n", #opt, #v, curl_share_strerror(r)); \
|
||||||
} \
|
} \
|
||||||
|
|
||||||
void DownloadThread(void* p);
|
|
||||||
void DownloadThreadQueue(void* p);
|
|
||||||
|
|
||||||
#define USE_THREAD_QUEUE 1
|
|
||||||
constexpr auto API_AGENT = "ITotalJustice";
|
constexpr auto API_AGENT = "ITotalJustice";
|
||||||
constexpr u64 CHUNK_SIZE = 1024*1024;
|
constexpr u64 CHUNK_SIZE = 1024*1024;
|
||||||
constexpr auto MAX_THREADS = 4;
|
constexpr auto MAX_THREADS = 4;
|
||||||
@@ -38,42 +35,195 @@ std::atomic_bool g_running{};
|
|||||||
CURLSH* g_curl_share{};
|
CURLSH* g_curl_share{};
|
||||||
Mutex g_mutex_share[CURL_LOCK_DATA_LAST]{};
|
Mutex g_mutex_share[CURL_LOCK_DATA_LAST]{};
|
||||||
|
|
||||||
struct UrlCache {
|
struct DataStruct {
|
||||||
auto AddToCache(const std::string& url, bool force = false) {
|
std::vector<u8> data;
|
||||||
mutexLock(&mutex);
|
s64 offset{};
|
||||||
ON_SCOPE_EXIT(mutexUnlock(&mutex));
|
FsFile f{};
|
||||||
auto it = std::find(cache.begin(), cache.end(), url);
|
s64 file_offset{};
|
||||||
if (it == cache.end()) {
|
};
|
||||||
cache.emplace_back(url);
|
|
||||||
|
auto generate_key_from_path(const fs::FsPath& path) -> std::string {
|
||||||
|
const auto key = crc32Calculate(path.s, path.size());
|
||||||
|
return std::to_string(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Cache {
|
||||||
|
using Value = std::pair<std::string, std::string>;
|
||||||
|
|
||||||
|
bool init() {
|
||||||
|
mutexLock(&m_mutex);
|
||||||
|
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||||
|
|
||||||
|
if (m_json) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto json_in = yyjson_read_file(JSON_PATH, YYJSON_READ_NOFLAG, nullptr, nullptr);
|
||||||
|
if (json_in) {
|
||||||
|
log_write("loading old json doc\n");
|
||||||
|
m_json = yyjson_doc_mut_copy(json_in, nullptr);
|
||||||
|
yyjson_doc_free(json_in);
|
||||||
|
m_root = yyjson_mut_doc_get_root(m_json);
|
||||||
} else {
|
} else {
|
||||||
if (force) {
|
log_write("creating new json doc\n");
|
||||||
return true;
|
m_json = yyjson_mut_doc_new(nullptr);
|
||||||
} else {
|
m_root = yyjson_mut_obj(m_json);
|
||||||
return false;
|
yyjson_mut_doc_set_root(m_json, m_root);
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_json && m_root;
|
||||||
|
}
|
||||||
|
|
||||||
|
void exit() {
|
||||||
|
mutexLock(&m_mutex);
|
||||||
|
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||||
|
|
||||||
|
if (!m_json) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!yyjson_mut_write_file(JSON_PATH, m_json, YYJSON_WRITE_NOFLAG, nullptr, nullptr)) {
|
||||||
|
log_write("failed to write etag json: %s\n", JSON_PATH.s);
|
||||||
|
}
|
||||||
|
|
||||||
|
yyjson_mut_doc_free(m_json);
|
||||||
|
m_json = nullptr;
|
||||||
|
m_root = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void get(const fs::FsPath& path, curl::Header& header) {
|
||||||
|
const auto [etag, last_modified] = get_internal(path);
|
||||||
|
if (!etag.empty()) {
|
||||||
|
header.m_map.emplace("if-none-match", etag);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!last_modified.empty()) {
|
||||||
|
header.m_map.emplace("if-modified-since", last_modified);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void set(const fs::FsPath& path, const curl::Header& value) {
|
||||||
|
mutexLock(&m_mutex);
|
||||||
|
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||||
|
|
||||||
|
std::string etag_str;
|
||||||
|
std::string last_modified_str;
|
||||||
|
|
||||||
|
if (auto it = value.Find(ETAG_STR); it != value.m_map.end()) {
|
||||||
|
etag_str = it->second;
|
||||||
|
}
|
||||||
|
if (auto it = value.Find(LAST_MODIFIED_STR); it != value.m_map.end()) {
|
||||||
|
last_modified_str = it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!etag_str.empty() || !last_modified_str.empty()) {
|
||||||
|
set_internal(path, Value{etag_str, last_modified_str});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
auto get_internal(const fs::FsPath& path) -> Value {
|
||||||
|
if (!fs::FsNativeSd().FileExists(path)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto kkey = generate_key_from_path(path);
|
||||||
|
const auto it = m_cache.find(kkey);
|
||||||
|
if (it != m_cache.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto hash_key = yyjson_mut_obj_getn(m_root, kkey.c_str(), kkey.length());
|
||||||
|
if (!hash_key) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto etag_key = yyjson_mut_obj_get(hash_key, ETAG_STR);
|
||||||
|
auto last_modified_key = yyjson_mut_obj_get(hash_key, LAST_MODIFIED_STR);
|
||||||
|
|
||||||
|
const auto etag_value = yyjson_mut_get_str(etag_key);
|
||||||
|
const auto etag_value_len = yyjson_mut_get_len(etag_key);
|
||||||
|
const auto last_modified_value = yyjson_mut_get_str(last_modified_key);
|
||||||
|
const auto last_modified_value_len = yyjson_mut_get_len(last_modified_key);
|
||||||
|
|
||||||
|
if ((!etag_value || !etag_value_len) && (!last_modified_value || !last_modified_value_len)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string etag;
|
||||||
|
std::string last_modified;
|
||||||
|
if (etag_value && etag_value_len) {
|
||||||
|
etag.assign(etag_value, etag_value_len);
|
||||||
|
}
|
||||||
|
if (last_modified_value && last_modified_value_len) {
|
||||||
|
last_modified.assign(last_modified_value, last_modified_value_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Value ret{etag, last_modified};
|
||||||
|
m_cache.insert_or_assign(it, kkey, ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_internal(const fs::FsPath& path, const Value& value) {
|
||||||
|
const auto kkey = generate_key_from_path(path);
|
||||||
|
|
||||||
|
// check if we already have this entry
|
||||||
|
const auto it = m_cache.find(kkey);
|
||||||
|
if (it != m_cache.end() && it->second == value) {
|
||||||
|
log_write("already has etag, not updating, path: %s key: %s\n", path.s, kkey.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it != m_cache.end()) {
|
||||||
|
log_write("updating etag, path: %s key: %s\n", path.s, kkey.c_str());
|
||||||
|
} else {
|
||||||
|
log_write("setting new etag, path: %s key: %s\n", path.s, kkey.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert new entry into cache, this will never fail.
|
||||||
|
const auto& [jkey, jvalue] = *m_cache.insert_or_assign(it, kkey, value);
|
||||||
|
const auto& [etag, last_modified] = jvalue;
|
||||||
|
|
||||||
|
// check if we need to add a new entry to root or simply update the value.
|
||||||
|
auto hash_key = yyjson_mut_obj_getn(m_root, kkey.c_str(), kkey.length());
|
||||||
|
if (!hash_key) {
|
||||||
|
hash_key = yyjson_mut_obj_add_obj(m_json, m_root, jkey.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hash_key) {
|
||||||
|
log_write("failed to set new cache key obj, path: %s key: %s\n", path.s, jkey.c_str());
|
||||||
|
} else {
|
||||||
|
const auto update_entry = [this, &hash_key](const char* tag, const std::string& value) {
|
||||||
|
if (value.empty()) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
auto key = yyjson_mut_obj_get(hash_key, tag);
|
||||||
|
if (!key) {
|
||||||
|
return yyjson_mut_obj_add_str(m_json, hash_key, tag, value.c_str());
|
||||||
|
} else {
|
||||||
|
return yyjson_mut_set_str(key, value.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!update_entry("etag", etag)) {
|
||||||
|
log_write("failed to set new etag, path: %s key: %s\n", path.s, jkey.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!update_entry("last-modified", last_modified)) {
|
||||||
|
log_write("failed to set new last-modified, path: %s key: %s\n", path.s, jkey.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void RemoveFromCache(const std::string& url) {
|
static constexpr inline fs::FsPath JSON_PATH{"/switch/sphaira/cache/cache.json"};
|
||||||
mutexLock(&mutex);
|
static constexpr inline const char* ETAG_STR{"etag"};
|
||||||
ON_SCOPE_EXIT(mutexUnlock(&mutex));
|
static constexpr inline const char* LAST_MODIFIED_STR{"last-modified"};
|
||||||
auto it = std::find(cache.begin(), cache.end(), url);
|
|
||||||
if (it != cache.end()) {
|
|
||||||
cache.erase(it);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> cache;
|
Mutex m_mutex{};
|
||||||
Mutex mutex{};
|
yyjson_mut_doc* m_json{};
|
||||||
};
|
yyjson_mut_val* m_root{};
|
||||||
|
std::unordered_map<std::string, Value> m_cache{};
|
||||||
struct DataStruct {
|
|
||||||
std::vector<u8> data;
|
|
||||||
u64 offset{};
|
|
||||||
FsFileSystem fs{};
|
|
||||||
FsFile f{};
|
|
||||||
s64 file_offset{};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ThreadEntry {
|
struct ThreadEntry {
|
||||||
@@ -82,7 +232,7 @@ struct ThreadEntry {
|
|||||||
R_UNLESS(m_curl != nullptr, 0x1);
|
R_UNLESS(m_curl != nullptr, 0x1);
|
||||||
|
|
||||||
ueventCreate(&m_uevent, true);
|
ueventCreate(&m_uevent, true);
|
||||||
R_TRY(threadCreate(&m_thread, DownloadThread, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
|
R_TRY(threadCreate(&m_thread, ThreadFunc, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
|
||||||
R_TRY(threadStart(&m_thread));
|
R_TRY(threadStart(&m_thread));
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
@@ -101,7 +251,7 @@ struct ThreadEntry {
|
|||||||
return m_in_progress == true;
|
return m_in_progress == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Setup(DownloadCallback callback, ProgressCallback pcallback, std::string url, std::string file, std::string post) -> bool {
|
auto Setup(const Api& api) -> bool {
|
||||||
assert(m_in_progress == false && "Setting up thread while active");
|
assert(m_in_progress == false && "Setting up thread while active");
|
||||||
mutexLock(&m_mutex);
|
mutexLock(&m_mutex);
|
||||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||||
@@ -109,35 +259,25 @@ struct ThreadEntry {
|
|||||||
if (m_in_progress) {
|
if (m_in_progress) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
m_url = url;
|
m_api = api;
|
||||||
m_file = file;
|
|
||||||
m_post = post;
|
|
||||||
m_callback = callback;
|
|
||||||
m_pcallback = pcallback;
|
|
||||||
m_in_progress = true;
|
m_in_progress = true;
|
||||||
// log_write("started download :)\n");
|
// log_write("started download :)\n");
|
||||||
ueventSignal(&m_uevent);
|
ueventSignal(&m_uevent);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void ThreadFunc(void* p);
|
||||||
|
|
||||||
CURL* m_curl{};
|
CURL* m_curl{};
|
||||||
Thread m_thread{};
|
Thread m_thread{};
|
||||||
std::string m_url{};
|
Api m_api{};
|
||||||
std::string m_file{}; // if empty, downloads to buffer
|
|
||||||
std::string m_post{}; // if empty, downloads to buffer
|
|
||||||
DownloadCallback m_callback{};
|
|
||||||
ProgressCallback m_pcallback{};
|
|
||||||
std::atomic_bool m_in_progress{};
|
std::atomic_bool m_in_progress{};
|
||||||
Mutex m_mutex{};
|
Mutex m_mutex{};
|
||||||
UEvent m_uevent{};
|
UEvent m_uevent{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ThreadQueueEntry {
|
struct ThreadQueueEntry {
|
||||||
std::string url;
|
Api api;
|
||||||
std::string file;
|
|
||||||
std::string post;
|
|
||||||
DownloadCallback callback;
|
|
||||||
ProgressCallback pcallback;
|
|
||||||
bool m_delete{};
|
bool m_delete{};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -149,7 +289,7 @@ struct ThreadQueue {
|
|||||||
|
|
||||||
auto Create() -> Result {
|
auto Create() -> Result {
|
||||||
ueventCreate(&m_uevent, true);
|
ueventCreate(&m_uevent, true);
|
||||||
R_TRY(threadCreate(&m_thread, DownloadThreadQueue, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
|
R_TRY(threadCreate(&m_thread, ThreadFunc, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
|
||||||
R_TRY(threadStart(&m_thread));
|
R_TRY(threadStart(&m_thread));
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
@@ -160,34 +300,33 @@ struct ThreadQueue {
|
|||||||
threadClose(&m_thread);
|
threadClose(&m_thread);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Add(DownloadPriority prio, DownloadCallback callback, ProgressCallback pcallback, std::string url, std::string file, std::string post) -> bool {
|
auto Add(const Api& api) -> bool {
|
||||||
|
if (api.GetUrl().empty() || api.GetPath().empty() || !api.GetOnComplete()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
mutexLock(&m_mutex);
|
mutexLock(&m_mutex);
|
||||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||||
|
|
||||||
ThreadQueueEntry entry{};
|
switch (api.GetPriority()) {
|
||||||
entry.url = url;
|
case Priority::Normal:
|
||||||
entry.file = file;
|
m_entries.emplace_back(api);
|
||||||
entry.post = post;
|
|
||||||
entry.callback = callback;
|
|
||||||
entry.pcallback = pcallback;
|
|
||||||
|
|
||||||
switch (prio) {
|
|
||||||
case DownloadPriority::Normal:
|
|
||||||
m_entries.emplace_back(entry);
|
|
||||||
break;
|
break;
|
||||||
case DownloadPriority::High:
|
case Priority::High:
|
||||||
m_entries.emplace_front(entry);
|
m_entries.emplace_front(api);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
ueventSignal(&m_uevent);
|
ueventSignal(&m_uevent);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void ThreadFunc(void* p);
|
||||||
};
|
};
|
||||||
|
|
||||||
ThreadEntry g_threads[MAX_THREADS]{};
|
ThreadEntry g_threads[MAX_THREADS]{};
|
||||||
ThreadQueue g_thread_queue;
|
ThreadQueue g_thread_queue;
|
||||||
UrlCache g_url_cache;
|
Cache g_cache;
|
||||||
|
|
||||||
void GetDownloadTempPath(fs::FsPath& buf) {
|
void GetDownloadTempPath(fs::FsPath& buf) {
|
||||||
static Mutex mutex{};
|
static Mutex mutex{};
|
||||||
@@ -211,13 +350,13 @@ auto ProgressCallbackFunc1(void *clientp, curl_off_t dltotal, curl_off_t dlnow,
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto ProgressCallbackFunc2(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) -> size_t {
|
auto ProgressCallbackFunc2(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) -> size_t {
|
||||||
if (!g_running) {
|
auto api = static_cast<Api*>(clientp);
|
||||||
|
if (!g_running || api->GetToken().stop_requested()) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// log_write("pcall called %u %u %u %u\n", dltotal, dlnow, ultotal, ulnow);
|
// log_write("pcall called %u %u %u %u\n", dltotal, dlnow, ultotal, ulnow);
|
||||||
auto callback = *static_cast<ProgressCallback*>(clientp);
|
if (!api->GetOnProgress()(dltotal, dlnow, ultotal, ulnow)) {
|
||||||
if (!callback(dltotal, dlnow, ultotal, ulnow)) {
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,36 +422,65 @@ auto WriteFileCallback(void *contents, size_t size, size_t num_files, void *user
|
|||||||
return realsize;
|
return realsize;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto DownloadInternal(CURL* curl, DataStruct& chunk, ProgressCallback pcallback, const std::string& url, const std::string& file, const std::string& post) -> bool {
|
auto header_callback(char* b, size_t size, size_t nitems, void* userdata) -> size_t {
|
||||||
fs::FsPath safe_buf;
|
auto header = static_cast<Header*>(userdata);
|
||||||
fs::FsPath tmp_buf;
|
const auto numbytes = size * nitems;
|
||||||
const bool has_file = !file.empty() && file != "";
|
|
||||||
const bool has_post = !post.empty() && post != "";
|
|
||||||
|
|
||||||
ON_SCOPE_EXIT(if (has_file) { fsFsClose(&chunk.fs); } );
|
if (b && numbytes) {
|
||||||
|
const auto dilem = (const char*)memchr(b, ':', numbytes);
|
||||||
|
if (dilem) {
|
||||||
|
const int key_len = dilem - b;
|
||||||
|
const int value_len = numbytes - key_len - 4; // "\r\n"
|
||||||
|
if (key_len > 0 && value_len > 0) {
|
||||||
|
const std::string key(b, key_len);
|
||||||
|
const std::string value(dilem + 2, value_len);
|
||||||
|
header->m_map.insert_or_assign(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return numbytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
|
||||||
|
// check if stop has been requested before starting download
|
||||||
|
if (e.GetToken().stop_requested()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::FsPath tmp_buf;
|
||||||
|
const bool has_file = !e.GetPath().empty() && e.GetPath() != "";
|
||||||
|
const bool has_post = !e.GetFields().empty() && e.GetFields() != "";
|
||||||
|
|
||||||
|
DataStruct chunk;
|
||||||
|
Header header_in = e.GetHeader();
|
||||||
|
Header header_out;
|
||||||
|
fs::FsNativeSd fs;
|
||||||
|
|
||||||
if (has_file) {
|
if (has_file) {
|
||||||
std::strcpy(safe_buf, file.c_str());
|
|
||||||
GetDownloadTempPath(tmp_buf);
|
GetDownloadTempPath(tmp_buf);
|
||||||
R_TRY_RESULT(fsOpenSdCardFileSystem(&chunk.fs), false);
|
fs.CreateDirectoryRecursivelyWithPath(tmp_buf);
|
||||||
|
|
||||||
fs::CreateDirectoryRecursivelyWithPath(&chunk.fs, tmp_buf);
|
if (auto rc = fs.CreateFile(tmp_buf, 0, 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
||||||
|
log_write("failed to create file: %s\n", tmp_buf.s);
|
||||||
if (auto rc = fsFsCreateFile(&chunk.fs, tmp_buf, 0, 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
return {};
|
||||||
log_write("failed to create file: %s\n", tmp_buf);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (R_FAILED(fsFsOpenFile(&chunk.fs, tmp_buf, FsOpenMode_Write|FsOpenMode_Append, &chunk.f))) {
|
if (R_FAILED(fs.OpenFile(tmp_buf, FsOpenMode_Write|FsOpenMode_Append, &chunk.f))) {
|
||||||
log_write("failed to open file: %s\n", tmp_buf);
|
log_write("failed to open file: %s\n", tmp_buf.s);
|
||||||
return false;
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.GetFlags() & Flag_Cache) {
|
||||||
|
g_cache.get(e.GetPath(), header_in);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// reserve the first chunk
|
// reserve the first chunk
|
||||||
chunk.data.reserve(CHUNK_SIZE);
|
chunk.data.reserve(CHUNK_SIZE);
|
||||||
|
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, url.c_str());
|
curl_easy_reset(curl);
|
||||||
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, e.GetUrl().c_str());
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_USERAGENT, "TotalJustice");
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_USERAGENT, "TotalJustice");
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||||
@@ -320,15 +488,42 @@ auto DownloadInternal(CURL* curl, DataStruct& chunk, ProgressCallback pcallback,
|
|||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FAILONERROR, 1L);
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FAILONERROR, 1L);
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SHARE, g_curl_share);
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SHARE, g_curl_share);
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_BUFFERSIZE, 1024*512);
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_BUFFERSIZE, 1024*512);
|
||||||
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERFUNCTION, header_callback);
|
||||||
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERDATA, &header_out);
|
||||||
|
|
||||||
if (has_post) {
|
if (has_post) {
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_POSTFIELDS, post.c_str());
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_POSTFIELDS, e.GetFields().c_str());
|
||||||
log_write("setting post field: %s\n", post.c_str());
|
log_write("setting post field: %s\n", e.GetFields().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
struct curl_slist* list = NULL;
|
||||||
|
ON_SCOPE_EXIT(if (list) { curl_slist_free_all(list); } );
|
||||||
|
|
||||||
|
for (const auto& [key, value] : header_in.m_map) {
|
||||||
|
if (value.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create header key value pair.
|
||||||
|
const auto header_str = key + ": " + value;
|
||||||
|
|
||||||
|
// try to append header chunk.
|
||||||
|
auto temp = curl_slist_append(list, header_str.c_str());
|
||||||
|
if (temp) {
|
||||||
|
log_write("adding header: %s\n", header_str.c_str());
|
||||||
|
list = temp;
|
||||||
|
} else {
|
||||||
|
log_write("failed to append header\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list) {
|
||||||
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HTTPHEADER, list);
|
||||||
}
|
}
|
||||||
|
|
||||||
// progress calls.
|
// progress calls.
|
||||||
if (pcallback) {
|
if (e.GetOnProgress()) {
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &pcallback);
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &e);
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc2);
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc2);
|
||||||
} else {
|
} else {
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc1);
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc1);
|
||||||
@@ -343,21 +538,34 @@ auto DownloadInternal(CURL* curl, DataStruct& chunk, ProgressCallback pcallback,
|
|||||||
const auto res = curl_easy_perform(curl);
|
const auto res = curl_easy_perform(curl);
|
||||||
bool success = res == CURLE_OK;
|
bool success = res == CURLE_OK;
|
||||||
|
|
||||||
|
long http_code = 0;
|
||||||
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||||
|
|
||||||
if (has_file) {
|
if (has_file) {
|
||||||
|
ON_SCOPE_EXIT( fs.DeleteFile(tmp_buf) );
|
||||||
if (res == CURLE_OK && chunk.offset) {
|
if (res == CURLE_OK && chunk.offset) {
|
||||||
fsFileWrite(&chunk.f, chunk.file_offset, chunk.data.data(), chunk.offset, FsWriteOption_None);
|
fsFileWrite(&chunk.f, chunk.file_offset, chunk.data.data(), chunk.offset, FsWriteOption_None);
|
||||||
}
|
}
|
||||||
|
|
||||||
fsFileClose(&chunk.f);
|
fsFileClose(&chunk.f);
|
||||||
if (res != CURLE_OK) {
|
|
||||||
fsFsDeleteFile(&chunk.fs, tmp_buf);
|
if (res == CURLE_OK) {
|
||||||
} else {
|
if (http_code == 304) {
|
||||||
fsFsDeleteFile(&chunk.fs, safe_buf);
|
log_write("cached download: %s\n", e.GetUrl().c_str());
|
||||||
fs::CreateDirectoryRecursivelyWithPath(&chunk.fs, safe_buf);
|
} else {
|
||||||
if (R_FAILED(fsFsRenameFile(&chunk.fs, tmp_buf, safe_buf))) {
|
log_write("un-cached download: %s code: %lu\n", e.GetUrl().c_str(), http_code);
|
||||||
fsFsDeleteFile(&chunk.fs, tmp_buf);
|
if (e.GetFlags() & Flag_Cache) {
|
||||||
success = false;
|
g_cache.set(e.GetPath(), header_out);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.DeleteFile(e.GetPath());
|
||||||
|
fs.CreateDirectoryRecursivelyWithPath(e.GetPath());
|
||||||
|
if (R_FAILED(fs.RenameFile(tmp_buf, e.GetPath()))) {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
chunk.data.clear();
|
||||||
} else {
|
} else {
|
||||||
// empty data if we failed
|
// empty data if we failed
|
||||||
if (res != CURLE_OK) {
|
if (res != CURLE_OK) {
|
||||||
@@ -365,44 +573,46 @@ auto DownloadInternal(CURL* curl, DataStruct& chunk, ProgressCallback pcallback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log_write("Downloaded %s %s\n", url.c_str(), curl_easy_strerror(res));
|
log_write("Downloaded %s %s\n", e.GetUrl().c_str(), curl_easy_strerror(res));
|
||||||
return success;
|
return {success, http_code, header_out, chunk.data, e.GetPath()};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto DownloadInternal(DataStruct& chunk, ProgressCallback pcallback, const std::string& url, const std::string& file, const std::string& post) -> bool {
|
auto DownloadInternal(const Api& e) -> ApiResult {
|
||||||
auto curl = curl_easy_init();
|
auto curl = curl_easy_init();
|
||||||
if (!curl) {
|
if (!curl) {
|
||||||
log_write("curl init failed\n");
|
log_write("curl init failed\n");
|
||||||
return false;
|
return {};
|
||||||
}
|
}
|
||||||
ON_SCOPE_EXIT(curl_easy_cleanup(curl));
|
ON_SCOPE_EXIT(curl_easy_cleanup(curl));
|
||||||
return DownloadInternal(curl, chunk, pcallback, url, file, post);
|
return DownloadInternal(curl, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DownloadThread(void* p) {
|
void my_lock(CURL *handle, curl_lock_data data, curl_lock_access laccess, void *useptr) {
|
||||||
|
mutexLock(&g_mutex_share[data]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void my_unlock(CURL *handle, curl_lock_data data, void *useptr) {
|
||||||
|
mutexUnlock(&g_mutex_share[data]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThreadEntry::ThreadFunc(void* p) {
|
||||||
auto data = static_cast<ThreadEntry*>(p);
|
auto data = static_cast<ThreadEntry*>(p);
|
||||||
while (g_running) {
|
while (g_running) {
|
||||||
auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX);
|
auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX);
|
||||||
// log_write("woke up\n");
|
// log_write("woke up\n");
|
||||||
if (!g_running) {
|
if (!g_running) {
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (R_FAILED(rc)) {
|
if (R_FAILED(rc)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
DataStruct chunk;
|
const auto result = DownloadInternal(data->m_curl, data->m_api);
|
||||||
#if 1
|
if (g_running && data->m_api.GetOnComplete() && !data->m_api.GetToken().stop_requested()) {
|
||||||
const auto result = DownloadInternal(data->m_curl, chunk, data->m_pcallback, data->m_url, data->m_file, data->m_post);
|
const DownloadEventData event_data{data->m_api.GetOnComplete(), result, data->m_api.GetToken()};
|
||||||
if (g_running) {
|
|
||||||
DownloadEventData event_data{data->m_callback, std::move(chunk.data), result};
|
|
||||||
evman::push(std::move(event_data), false);
|
evman::push(std::move(event_data), false);
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
// mutexLock(&data->m_mutex);
|
|
||||||
// ON_SCOPE_EXIT(mutexUnlock(&data->m_mutex));
|
|
||||||
|
|
||||||
data->m_in_progress = false;
|
data->m_in_progress = false;
|
||||||
// notify the queue that there's a space free
|
// notify the queue that there's a space free
|
||||||
@@ -411,7 +621,7 @@ void DownloadThread(void* p) {
|
|||||||
log_write("exited download thread\n");
|
log_write("exited download thread\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
void DownloadThreadQueue(void* p) {
|
void ThreadQueue::ThreadFunc(void* p) {
|
||||||
auto data = static_cast<ThreadQueue*>(p);
|
auto data = static_cast<ThreadQueue*>(p);
|
||||||
while (g_running) {
|
while (g_running) {
|
||||||
auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX);
|
auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX);
|
||||||
@@ -444,7 +654,7 @@ void DownloadThreadQueue(void* p) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!thread.InProgress()) {
|
if (!thread.InProgress()) {
|
||||||
thread.Setup(entry.callback, entry.pcallback, entry.url, entry.file, entry.post);
|
thread.Setup(entry.api);
|
||||||
// log_write("[dl queue] starting download\n");
|
// log_write("[dl queue] starting download\n");
|
||||||
// mark entry for deletion
|
// mark entry for deletion
|
||||||
entry.m_delete = true;
|
entry.m_delete = true;
|
||||||
@@ -454,10 +664,6 @@ void DownloadThreadQueue(void* p) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!g_running) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!keep_going) {
|
if (!keep_going) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -467,29 +673,14 @@ void DownloadThreadQueue(void* p) {
|
|||||||
for (u32 i = 0; i < pop_count; i++) {
|
for (u32 i = 0; i < pop_count; i++) {
|
||||||
data->m_entries.pop_front();
|
data->m_entries.pop_front();
|
||||||
}
|
}
|
||||||
// if (delete_any) {
|
|
||||||
// data->m_entries.clear();
|
|
||||||
// data->m_entries.
|
|
||||||
// data->m_entries.erase(std::remove_if(data->m_entries.begin(), data->m_entries.end(), [](auto& a) {
|
|
||||||
// return a.m_delete;
|
|
||||||
// }));
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log_write("exited download thread queue\n");
|
log_write("exited download thread queue\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
void my_lock(CURL *handle, curl_lock_data data, curl_lock_access laccess, void *useptr) {
|
|
||||||
mutexLock(&g_mutex_share[data]);
|
|
||||||
}
|
|
||||||
|
|
||||||
void my_unlock(CURL *handle, curl_lock_data data, void *useptr) {
|
|
||||||
mutexUnlock(&g_mutex_share[data]);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
auto DownloadInit() -> bool {
|
auto Init() -> bool {
|
||||||
if (CURLE_OK != curl_global_init(CURL_GLOBAL_DEFAULT)) {
|
if (CURLE_OK != curl_global_init(CURL_GLOBAL_DEFAULT)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -518,10 +709,15 @@ auto DownloadInit() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log_write("finished creating threads\n");
|
log_write("finished creating threads\n");
|
||||||
|
|
||||||
|
if (!g_cache.init()) {
|
||||||
|
log_write("failed to init json cache\n");
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DownloadExit() {
|
void Exit() {
|
||||||
g_running = false;
|
g_running = false;
|
||||||
|
|
||||||
g_thread_queue.Close();
|
g_thread_queue.Close();
|
||||||
@@ -536,75 +732,39 @@ void DownloadExit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
curl_global_cleanup();
|
curl_global_cleanup();
|
||||||
|
g_cache.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto DownloadMemory(const std::string& url, const std::string& post, ProgressCallback pcallback) -> std::vector<u8> {
|
auto ToMemory(const Api& e) -> ApiResult {
|
||||||
if (g_url_cache.AddToCache(url)) {
|
if (!e.GetPath().empty()) {
|
||||||
DataStruct chunk{};
|
return {};
|
||||||
if (DownloadInternal(chunk, pcallback, url, "", post)) {
|
|
||||||
return chunk.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {};
|
return DownloadInternal(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto DownloadFile(const std::string& url, const std::string& out, const std::string& post, ProgressCallback pcallback) -> bool {
|
auto ToFile(const Api& e) -> ApiResult {
|
||||||
if (g_url_cache.AddToCache(url)) {
|
if (e.GetPath().empty()) {
|
||||||
DataStruct chunk{};
|
return {};
|
||||||
if (DownloadInternal(chunk, pcallback, url, out, post)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
return DownloadInternal(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto DownloadMemoryAsync(const std::string& url, const std::string& post, DownloadCallback callback, ProgressCallback pcallback, DownloadPriority prio) -> bool {
|
auto ToMemoryAsync(const Api& api) -> bool {
|
||||||
#if USE_THREAD_QUEUE
|
return g_thread_queue.Add(api);
|
||||||
if (g_url_cache.AddToCache(url)) {
|
|
||||||
return g_thread_queue.Add(prio, callback, pcallback, url, "", post);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
// mutexLock(&g_thread_queue.m_mutex);
|
|
||||||
// ON_SCOPE_EXIT(mutexUnlock(&g_thread_queue.m_mutex));
|
|
||||||
|
|
||||||
for (auto& entry : g_threads) {
|
|
||||||
if (!entry.InProgress()) {
|
|
||||||
return entry.Setup(callback, url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log_write("failed to start download, no avaliable threads\n");
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto DownloadFileAsync(const std::string& url, const std::string& out, const std::string& post, DownloadCallback callback, ProgressCallback pcallback, DownloadPriority prio) -> bool {
|
auto ToFileAsync(const Api& e) -> bool {
|
||||||
#if USE_THREAD_QUEUE
|
return g_thread_queue.Add(e);
|
||||||
if (g_url_cache.AddToCache(url)) {
|
|
||||||
return g_thread_queue.Add(prio, callback, pcallback, url, out, post);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
// mutexLock(&g_thread_queue.m_mutex);
|
|
||||||
// ON_SCOPE_EXIT(mutexUnlock(&g_thread_queue.m_mutex));
|
|
||||||
|
|
||||||
for (auto& entry : g_threads) {
|
|
||||||
if (!entry.InProgress()) {
|
|
||||||
return entry.Setup(callback, url, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log_write("failed to start download, no avaliable threads\n");
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DownloadClearCache(const std::string& url) {
|
auto EscapeString(const std::string& str) -> std::string {
|
||||||
g_url_cache.AddToCache(url);
|
std::string result;
|
||||||
g_url_cache.RemoveFromCache(url);
|
const auto s = curl_escape(str.data(), str.length());
|
||||||
|
if (s) {
|
||||||
|
result = s;
|
||||||
|
curl_free(s);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace sphaira
|
} // namespace sphaira::curl
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& _path, bool ig
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
if (R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
||||||
log_write("failed to create folder: %s\n", path);
|
log_write("failed to create folder: %s\n", path.s);
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& _path,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
if (R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
||||||
log_write("failed to create folder recursively: %s\n", path);
|
log_write("failed to create folder recursively: %s\n", path.s);
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,10 +248,10 @@ Result read_entire_file(FsFileSystem* _fs, const FsPath& path, std::vector<u8>&
|
|||||||
Result write_entire_file(FsFileSystem* _fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only) {
|
Result write_entire_file(FsFileSystem* _fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only) {
|
||||||
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
|
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
|
||||||
|
|
||||||
FsNative fs{_fs, false};
|
FsNative fs{_fs, false, ignore_read_only};
|
||||||
R_TRY(fs.GetFsOpenResult());
|
R_TRY(fs.GetFsOpenResult());
|
||||||
|
|
||||||
if (auto rc = fs.CreateFile(path, in.size(), 0, ignore_read_only); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
if (auto rc = fs.CreateFile(path, in.size(), 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
#include "ftpsrv_helper.hpp"
|
#include "ftpsrv_helper.hpp"
|
||||||
#include <ftpsrv.h>
|
|
||||||
#include <ftpsrv_vfs.h>
|
|
||||||
#include "app.hpp"
|
#include "app.hpp"
|
||||||
#include "fs.hpp"
|
#include "fs.hpp"
|
||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <errno.h>
|
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <minIni.h>
|
||||||
|
#include <ftpsrv.h>
|
||||||
|
#include <ftpsrv_vfs.h>
|
||||||
|
#include <nx/vfs_nx.h>
|
||||||
|
#include <nx/utils.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
const char* INI_PATH = "/config/ftpsrv/config.ini";
|
||||||
FtpSrvConfig g_ftpsrv_config = {0};
|
FtpSrvConfig g_ftpsrv_config = {0};
|
||||||
volatile bool g_should_exit = false;
|
volatile bool g_should_exit = false;
|
||||||
bool g_is_running{false};
|
bool g_is_running{false};
|
||||||
Thread g_thread;
|
Thread g_thread;
|
||||||
std::mutex g_mutex{};
|
std::mutex g_mutex{};
|
||||||
FsFileSystem* g_fs;
|
|
||||||
|
|
||||||
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
|
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
|
||||||
sphaira::App::NotifyFlashLed();
|
sphaira::App::NotifyFlashLed();
|
||||||
@@ -28,32 +29,6 @@ void ftp_progress_callback(void) {
|
|||||||
sphaira::App::NotifyFlashLed();
|
sphaira::App::NotifyFlashLed();
|
||||||
}
|
}
|
||||||
|
|
||||||
int vfs_fs_set_errno(Result rc) {
|
|
||||||
switch (rc) {
|
|
||||||
case FsError_TargetLocked: errno = EBUSY; break;
|
|
||||||
case FsError_PathNotFound: errno = ENOENT; break;
|
|
||||||
case FsError_PathAlreadyExists: errno = EEXIST; break;
|
|
||||||
case FsError_UsableSpaceNotEnoughMmcCalibration: errno = ENOSPC; break;
|
|
||||||
case FsError_UsableSpaceNotEnoughMmcSafe: errno = ENOSPC; break;
|
|
||||||
case FsError_UsableSpaceNotEnoughMmcUser: errno = ENOSPC; break;
|
|
||||||
case FsError_UsableSpaceNotEnoughMmcSystem: errno = ENOSPC; break;
|
|
||||||
case FsError_UsableSpaceNotEnoughSdCard: errno = ENOSPC; break;
|
|
||||||
case FsError_OutOfRange: errno = ESPIPE; break;
|
|
||||||
case FsError_TooLongPath: errno = ENAMETOOLONG; break;
|
|
||||||
case FsError_UnsupportedWriteForReadOnlyFileSystem: errno = EROFS; break;
|
|
||||||
default: errno = EIO; break;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Result flush_buffered_write(struct FtpVfsFile* f) {
|
|
||||||
Result rc;
|
|
||||||
if (R_SUCCEEDED(rc = fsFileSetSize(&f->fd, f->off + f->buf_off))) {
|
|
||||||
rc = fsFileWrite(&f->fd, f->off, f->buf, f->buf_off, FsWriteOption_None);
|
|
||||||
}
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop(void* arg) {
|
void loop(void* arg) {
|
||||||
while (!g_should_exit) {
|
while (!g_should_exit) {
|
||||||
ftpsrv_init(&g_ftpsrv_config);
|
ftpsrv_init(&g_ftpsrv_config);
|
||||||
@@ -77,16 +52,51 @@ bool Init() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
g_fs = fsdevGetDeviceFileSystem("sdmc");
|
if (R_FAILED(fsdev_wrapMountSdmc())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
g_ftpsrv_config.log_callback = ftp_log_callback;
|
g_ftpsrv_config.log_callback = ftp_log_callback;
|
||||||
g_ftpsrv_config.progress_callback = ftp_progress_callback;
|
g_ftpsrv_config.progress_callback = ftp_progress_callback;
|
||||||
g_ftpsrv_config.anon = true;
|
g_ftpsrv_config.anon = ini_getbool("Login", "anon", 0, INI_PATH);
|
||||||
g_ftpsrv_config.timeout = 15;
|
int user_len = ini_gets("Login", "user", "", g_ftpsrv_config.user, sizeof(g_ftpsrv_config.user), INI_PATH);
|
||||||
g_ftpsrv_config.port = 5000;
|
int pass_len = ini_gets("Login", "pass", "", g_ftpsrv_config.pass, sizeof(g_ftpsrv_config.pass), INI_PATH);
|
||||||
|
g_ftpsrv_config.port = ini_getl("Network", "port", 5000, INI_PATH); // 5000 to keep compat with older sphaira
|
||||||
|
g_ftpsrv_config.timeout = ini_getl("Network", "timeout", 0, INI_PATH);
|
||||||
|
g_ftpsrv_config.use_localtime = ini_getbool("Misc", "use_localtime", 0, INI_PATH);
|
||||||
|
bool log_enabled = ini_getbool("Log", "log", 0, INI_PATH);
|
||||||
|
|
||||||
|
// get nx config
|
||||||
|
bool mount_devices = ini_getbool("Nx", "mount_devices", 1, INI_PATH);
|
||||||
|
bool mount_bis = ini_getbool("Nx", "mount_bis", 0, INI_PATH);
|
||||||
|
bool save_writable = ini_getbool("Nx", "save_writable", 0, INI_PATH);
|
||||||
|
g_ftpsrv_config.port = ini_getl("Nx", "app_port", g_ftpsrv_config.port, INI_PATH); // compat
|
||||||
|
|
||||||
|
// get Nx-App overrides
|
||||||
|
g_ftpsrv_config.anon = ini_getbool("Nx-App", "anon", g_ftpsrv_config.anon, INI_PATH);
|
||||||
|
user_len = ini_gets("Nx-App", "user", g_ftpsrv_config.user, g_ftpsrv_config.user, sizeof(g_ftpsrv_config.user), INI_PATH);
|
||||||
|
pass_len = ini_gets("Nx-App", "pass", g_ftpsrv_config.pass, g_ftpsrv_config.pass, sizeof(g_ftpsrv_config.pass), INI_PATH);
|
||||||
|
g_ftpsrv_config.port = ini_getl("Nx-App", "port", g_ftpsrv_config.port, INI_PATH);
|
||||||
|
g_ftpsrv_config.timeout = ini_getl("Nx-App", "timeout", g_ftpsrv_config.timeout, INI_PATH);
|
||||||
|
g_ftpsrv_config.use_localtime = ini_getbool("Nx-App", "use_localtime", g_ftpsrv_config.use_localtime, INI_PATH);
|
||||||
|
log_enabled = ini_getbool("Nx-App", "log", log_enabled, INI_PATH);
|
||||||
|
mount_devices = ini_getbool("Nx-App", "mount_devices", mount_devices, INI_PATH);
|
||||||
|
mount_bis = ini_getbool("Nx-App", "mount_bis", mount_bis, INI_PATH);
|
||||||
|
save_writable = ini_getbool("Nx-App", "save_writable", save_writable, INI_PATH);
|
||||||
|
|
||||||
|
if (!g_ftpsrv_config.port) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep compat with older sphaira
|
||||||
|
if (!user_len && !pass_len) {
|
||||||
|
g_ftpsrv_config.anon = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
vfs_nx_init(mount_devices, save_writable, mount_bis);
|
||||||
|
|
||||||
Result rc;
|
Result rc;
|
||||||
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*64, 0x2C, 2))) {
|
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*16, 0x2C, 2))) {
|
||||||
log_write("failed to create nxlink thread: 0x%X\n", rc);
|
log_write("failed to create nxlink thread: 0x%X\n", rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -108,232 +118,24 @@ void Exit() {
|
|||||||
g_should_exit = true;
|
g_should_exit = true;
|
||||||
threadWaitForExit(&g_thread);
|
threadWaitForExit(&g_thread);
|
||||||
threadClose(&g_thread);
|
threadClose(&g_thread);
|
||||||
|
|
||||||
|
vfs_nx_exit();
|
||||||
|
fsdev_wrapUnmountAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace sphaira::ftpsrv
|
} // namespace sphaira::ftpsrv
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
||||||
#define VFS_NX_BUFFER_IO 1
|
void log_file_write(const char* msg) {
|
||||||
|
log_write("%s", msg);
|
||||||
int ftp_vfs_open(struct FtpVfsFile* f, const char* path, enum FtpVfsOpenMode mode) {
|
|
||||||
u32 open_mode;
|
|
||||||
if (mode == FtpVfsOpenMode_READ) {
|
|
||||||
open_mode = FsOpenMode_Read;
|
|
||||||
f->is_write = false;
|
|
||||||
} else {
|
|
||||||
fsFsCreateFile(g_fs, path, 0, 0);
|
|
||||||
open_mode = FsOpenMode_Write | FsOpenMode_Append;
|
|
||||||
#if !VFS_NX_BUFFER_IO
|
|
||||||
open_mode |= FsOpenMode_Append;
|
|
||||||
#endif
|
|
||||||
f->is_write = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Result rc;
|
|
||||||
if (R_FAILED(rc = fsFsOpenFile(g_fs, path, open_mode, &f->fd))) {
|
|
||||||
return vfs_fs_set_errno(rc);
|
|
||||||
}
|
|
||||||
|
|
||||||
f->off = f->buf_off = f->buf_size = 0;
|
|
||||||
|
|
||||||
if (mode == FtpVfsOpenMode_WRITE) {
|
|
||||||
if (R_FAILED(rc = fsFileSetSize(&f->fd, 0))) {
|
|
||||||
goto fail_close;
|
|
||||||
}
|
|
||||||
} else if (mode == FtpVfsOpenMode_APPEND) {
|
|
||||||
if (R_FAILED(rc = fsFileGetSize(&f->fd, &f->off))) {
|
|
||||||
goto fail_close;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
f->is_valid = true;
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
fail_close:
|
|
||||||
fsFileClose(&f->fd);
|
|
||||||
return vfs_fs_set_errno(rc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int ftp_vfs_read(struct FtpVfsFile* f, void* buf, size_t size) {
|
void log_file_fwrite(const char* fmt, ...) {
|
||||||
Result rc;
|
std::va_list v{};
|
||||||
|
va_start(v, fmt);
|
||||||
#if VFS_NX_BUFFER_IO
|
log_write_arg(fmt, v);
|
||||||
if (f->buf_off == f->buf_size) {
|
va_end(v);
|
||||||
u64 bytes_read;
|
|
||||||
if (R_FAILED(rc = fsFileRead(&f->fd, f->off, f->buf, sizeof(f->buf), FsReadOption_None, &bytes_read))) {
|
|
||||||
return vfs_fs_set_errno(rc);
|
|
||||||
}
|
|
||||||
|
|
||||||
f->buf_off = 0;
|
|
||||||
f->buf_size = bytes_read;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!f->buf_size) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
size = size < f->buf_size - f->buf_off ? size : f->buf_size - f->buf_off;
|
|
||||||
memcpy(buf, f->buf + f->buf_off, size);
|
|
||||||
f->off += size;
|
|
||||||
f->buf_off += size;
|
|
||||||
return size;
|
|
||||||
#else
|
|
||||||
u64 bytes_read;
|
|
||||||
if (R_FAILED(rc = fsFileRead(&f->fd, f->off, buf, size, FsReadOption_None, &bytes_read))) {
|
|
||||||
return vfs_fs_set_errno(rc);
|
|
||||||
}
|
|
||||||
f->off += bytes_read;
|
|
||||||
return bytes_read;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
int ftp_vfs_write(struct FtpVfsFile* f, const void* buf, size_t size) {
|
|
||||||
Result rc;
|
|
||||||
|
|
||||||
#if VFS_NX_BUFFER_IO
|
|
||||||
const size_t ret = size;
|
|
||||||
while (size) {
|
|
||||||
if (f->buf_off + size > sizeof(f->buf)) {
|
|
||||||
const u64 sz = sizeof(f->buf) - f->buf_off;
|
|
||||||
memcpy(f->buf + f->buf_off, buf, sz);
|
|
||||||
f->buf_off += sz;
|
|
||||||
|
|
||||||
if (R_FAILED(rc = flush_buffered_write(f))) {
|
|
||||||
return vfs_fs_set_errno(rc);
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma GCC diagnostic push
|
|
||||||
#pragma GCC diagnostic ignored "-Wpointer-arith"
|
|
||||||
buf += sz;
|
|
||||||
#pragma GCC diagnostic pop
|
|
||||||
|
|
||||||
size -= sz;
|
|
||||||
f->off += f->buf_off;
|
|
||||||
f->buf_off = 0;
|
|
||||||
} else {
|
|
||||||
memcpy(f->buf + f->buf_off, buf, size);
|
|
||||||
f->buf_off += size;
|
|
||||||
size = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
#else
|
|
||||||
if (R_FAILED(rc = fsFileWrite(&f->fd, f->off, buf, size, FsWriteOption_None))) {
|
|
||||||
return vfs_fs_set_errno(rc);
|
|
||||||
}
|
|
||||||
f->off += size;
|
|
||||||
return size;
|
|
||||||
const size_t ret = size;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
// buf and size is the amount of data sent.
|
|
||||||
int ftp_vfs_seek(struct FtpVfsFile* f, const void* buf, size_t size, size_t off) {
|
|
||||||
#if VFS_NX_BUFFER_IO
|
|
||||||
if (!f->is_write) {
|
|
||||||
f->buf_off -= f->off - off;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
f->off = off;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ftp_vfs_close(struct FtpVfsFile* f) {
|
|
||||||
if (!ftp_vfs_isfile_open(f)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (f->is_write && f->buf_off) {
|
|
||||||
flush_buffered_write(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
fsFileClose(&f->fd);
|
|
||||||
f->is_valid = false;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ftp_vfs_isfile_open(struct FtpVfsFile* f) {
|
|
||||||
return f->is_valid;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ftp_vfs_opendir(struct FtpVfsDir* f, const char* path) {
|
|
||||||
Result rc;
|
|
||||||
if (R_FAILED(rc = fsFsOpenDirectory(g_fs, path, FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize, &f->dir))) {
|
|
||||||
return vfs_fs_set_errno(rc);
|
|
||||||
}
|
|
||||||
f->is_valid = true;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* ftp_vfs_readdir(struct FtpVfsDir* f, struct FtpVfsDirEntry* entry) {
|
|
||||||
Result rc;
|
|
||||||
s64 total_entries;
|
|
||||||
if (R_FAILED(rc = fsDirRead(&f->dir, &total_entries, 1, &entry->buf))) {
|
|
||||||
vfs_fs_set_errno(rc);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (total_entries <= 0) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry->buf.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ftp_vfs_dirlstat(struct FtpVfsDir* f, const struct FtpVfsDirEntry* entry, const char* path, struct stat* st) {
|
|
||||||
return lstat(path, st);
|
|
||||||
}
|
|
||||||
|
|
||||||
int ftp_vfs_closedir(struct FtpVfsDir* f) {
|
|
||||||
if (!ftp_vfs_isdir_open(f)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fsDirClose(&f->dir);
|
|
||||||
f->is_valid = false;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ftp_vfs_isdir_open(struct FtpVfsDir* f) {
|
|
||||||
return f->is_valid;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ftp_vfs_stat(const char* path, struct stat* st) {
|
|
||||||
return stat(path, st);
|
|
||||||
}
|
|
||||||
|
|
||||||
int ftp_vfs_lstat(const char* path, struct stat* st) {
|
|
||||||
return lstat(path, st);
|
|
||||||
}
|
|
||||||
|
|
||||||
int ftp_vfs_mkdir(const char* path) {
|
|
||||||
return mkdir(path, 0777);
|
|
||||||
}
|
|
||||||
|
|
||||||
int ftp_vfs_unlink(const char* path) {
|
|
||||||
return unlink(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
int ftp_vfs_rmdir(const char* path) {
|
|
||||||
return rmdir(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
int ftp_vfs_rename(const char* src, const char* dst) {
|
|
||||||
return rename(src, dst);
|
|
||||||
}
|
|
||||||
|
|
||||||
int ftp_vfs_readlink(const char* path, char* buf, size_t buflen) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* ftp_vfs_getpwuid(const struct stat* st) {
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* ftp_vfs_getgrgid(const struct stat* st) {
|
|
||||||
return "unknown";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ std::string get_internal(const char* str, size_t len) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// add default entry
|
// add default entry
|
||||||
g_tr_cache.emplace(kkey, kkey);
|
const auto it = g_tr_cache.emplace(kkey, kkey).first;
|
||||||
|
|
||||||
if (!json || !root) {
|
if (!json || !root) {
|
||||||
log_write("no json or root\n");
|
log_write("no json or root\n");
|
||||||
@@ -43,7 +43,7 @@ std::string get_internal(const char* str, size_t len) {
|
|||||||
|
|
||||||
// update entry in cache
|
// update entry in cache
|
||||||
const std::string ret = {val, val_len};
|
const std::string ret = {val, val_len};
|
||||||
g_tr_cache.insert_or_assign(kkey, ret);
|
g_tr_cache.insert_or_assign(it, kkey, ret);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +77,7 @@ bool init(long index) {
|
|||||||
case 10: setLanguage = SetLanguage_PT; break; // "Portuguese"
|
case 10: setLanguage = SetLanguage_PT; break; // "Portuguese"
|
||||||
case 11: setLanguage = SetLanguage_RU; break; // "Russian"
|
case 11: setLanguage = SetLanguage_RU; break; // "Russian"
|
||||||
case 12: lang_name = "se"; break; // "Swedish"
|
case 12: lang_name = "se"; break; // "Swedish"
|
||||||
|
case 13: lang_name = "vi"; break; // "Vietnamese"
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (setLanguage) {
|
switch (setLanguage) {
|
||||||
@@ -91,6 +92,7 @@ bool init(long index) {
|
|||||||
case SetLanguage_PT: lang_name = "pt"; break;
|
case SetLanguage_PT: lang_name = "pt"; break;
|
||||||
case SetLanguage_RU: lang_name = "ru"; break;
|
case SetLanguage_RU: lang_name = "ru"; break;
|
||||||
case SetLanguage_ZHTW: lang_name = "zh"; break;
|
case SetLanguage_ZHTW: lang_name = "zh"; break;
|
||||||
|
default: break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fs::FsPath sdmc_path = "/config/sphaira/i18n/" + lang_name + ".json";
|
const fs::FsPath sdmc_path = "/config/sphaira/i18n/" + lang_name + ".json";
|
||||||
|
|||||||
@@ -4,14 +4,19 @@
|
|||||||
#pragma GCC diagnostic push
|
#pragma GCC diagnostic push
|
||||||
#pragma GCC diagnostic ignored "-Wunused-function"
|
#pragma GCC diagnostic ignored "-Wunused-function"
|
||||||
#pragma GCC diagnostic ignored "-Warray-bounds="
|
#pragma GCC diagnostic ignored "-Warray-bounds="
|
||||||
#include "nanovg/stb_image.h"
|
#pragma GCC diagnostic ignored "-Wcast-qual"
|
||||||
|
#pragma GCC diagnostic ignored "-Wimplicit-fallthrough"
|
||||||
|
#define STB_IMAGE_IMPLEMENTATION
|
||||||
|
#include <stb_image.h>
|
||||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||||
#define STB_IMAGE_WRITE_STATIC
|
#define STB_IMAGE_WRITE_STATIC
|
||||||
#define STBI_WRITE_NO_STDIO
|
#define STBI_WRITE_NO_STDIO
|
||||||
#include "stb_image_write.h"
|
#include <stb_image_write.h>
|
||||||
#define STB_IMAGE_RESIZE_IMPLEMENTATION
|
#define STB_IMAGE_RESIZE_IMPLEMENTATION
|
||||||
#define STB_IMAGE_RESIZE_STATIC
|
#define STB_IMAGE_RESIZE_STATIC
|
||||||
#include "stb_image_resize2.h"
|
#include <stb_image_resize2.h>
|
||||||
|
#pragma GCC diagnostic pop
|
||||||
|
#pragma GCC diagnostic pop
|
||||||
#pragma GCC diagnostic pop
|
#pragma GCC diagnostic pop
|
||||||
#pragma GCC diagnostic pop
|
#pragma GCC diagnostic pop
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ std::FILE* file{};
|
|||||||
int nxlink_socket{};
|
int nxlink_socket{};
|
||||||
std::mutex mutex{};
|
std::mutex mutex{};
|
||||||
|
|
||||||
|
void log_write_arg_internal(const char* s, std::va_list& v) {
|
||||||
|
if (file) {
|
||||||
|
std::vfprintf(file, s, v);
|
||||||
|
std::fflush(file);
|
||||||
|
}
|
||||||
|
if (nxlink_socket) {
|
||||||
|
std::vprintf(s, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
auto log_file_init() -> bool {
|
auto log_file_init() -> bool {
|
||||||
@@ -60,13 +70,17 @@ void log_write(const char* s, ...) {
|
|||||||
|
|
||||||
std::va_list v{};
|
std::va_list v{};
|
||||||
va_start(v, s);
|
va_start(v, s);
|
||||||
if (file) {
|
log_write_arg_internal(s, v);
|
||||||
std::vfprintf(file, s, v);
|
|
||||||
std::fflush(file);
|
|
||||||
}
|
|
||||||
if (nxlink_socket) {
|
|
||||||
std::vprintf(s, v);
|
|
||||||
}
|
|
||||||
va_end(v);
|
va_end(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void log_write_arg(const char* s, std::va_list& v) {
|
||||||
|
std::scoped_lock lock{mutex};
|
||||||
|
if (!file && !nxlink_socket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_write_arg_internal(s, v);
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ void userAppInit(void) {
|
|||||||
diagAbortWithResult(rc);
|
diagAbortWithResult(rc);
|
||||||
if (R_FAILED(rc = hidsysInitialize()))
|
if (R_FAILED(rc = hidsysInitialize()))
|
||||||
diagAbortWithResult(rc);
|
diagAbortWithResult(rc);
|
||||||
|
if (R_FAILED(rc = ncmInitialize()))
|
||||||
|
diagAbortWithResult(rc);
|
||||||
|
|
||||||
log_nxlink_init();
|
log_nxlink_init();
|
||||||
}
|
}
|
||||||
@@ -70,6 +72,7 @@ void userAppInit(void) {
|
|||||||
void userAppExit(void) {
|
void userAppExit(void) {
|
||||||
log_nxlink_exit();
|
log_nxlink_exit();
|
||||||
|
|
||||||
|
ncmExit();
|
||||||
hidsysExit();
|
hidsysExit();
|
||||||
setExit();
|
setExit();
|
||||||
accountExit();
|
accountExit();
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ auto nro_scan_internal(const fs::FsPath& path, std::vector<NroEntry>& nros, bool
|
|||||||
if (e.type == FsDirEntryType_Dir) {
|
if (e.type == FsDirEntryType_Dir) {
|
||||||
// assert(!root && "dir should only be scanned on non-root!");
|
// assert(!root && "dir should only be scanned on non-root!");
|
||||||
fs::FsPath fullpath;
|
fs::FsPath fullpath;
|
||||||
std::snprintf(fullpath, sizeof(fullpath), "%s/%s/%s.nro", path, e.name, e.name);
|
std::snprintf(fullpath, sizeof(fullpath), "%s/%s/%s.nro", path.s, e.name, e.name);
|
||||||
|
|
||||||
// fast path for detecting an nro in a folder
|
// fast path for detecting an nro in a folder
|
||||||
NroEntry entry;
|
NroEntry entry;
|
||||||
@@ -133,12 +133,12 @@ auto nro_scan_internal(const fs::FsPath& path, std::vector<NroEntry>& nros, bool
|
|||||||
nros.emplace_back(entry);
|
nros.emplace_back(entry);
|
||||||
} else {
|
} else {
|
||||||
// slow path...
|
// slow path...
|
||||||
std::snprintf(fullpath, sizeof(fullpath), "%s/%s", path, e.name);
|
std::snprintf(fullpath, sizeof(fullpath), "%s/%s", path.s, e.name);
|
||||||
nro_scan_internal(fullpath, nros, hide_sphaira, nested, scan_all_dir, false);
|
nro_scan_internal(fullpath, nros, hide_sphaira, nested, scan_all_dir, false);
|
||||||
}
|
}
|
||||||
} else if (e.type == FsDirEntryType_File && std::string_view{e.name}.ends_with(".nro")) {
|
} else if (e.type == FsDirEntryType_File && std::string_view{e.name}.ends_with(".nro")) {
|
||||||
fs::FsPath fullpath;
|
fs::FsPath fullpath;
|
||||||
std::snprintf(fullpath, sizeof(fullpath), "%s/%s", path, e.name);
|
std::snprintf(fullpath, sizeof(fullpath), "%s/%s", path.s, e.name);
|
||||||
|
|
||||||
NroEntry entry;
|
NroEntry entry;
|
||||||
if (R_SUCCEEDED(nro_parse_internal(fs, fullpath, entry))) {
|
if (R_SUCCEEDED(nro_parse_internal(fs, fullpath, entry))) {
|
||||||
@@ -148,7 +148,7 @@ auto nro_scan_internal(const fs::FsPath& path, std::vector<NroEntry>& nros, bool
|
|||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log_write("error when trying to parse %s\n", fullpath);
|
log_write("error when trying to parse %s\n", fullpath.s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,4 +309,37 @@ auto nro_normalise_path(const std::string& p) -> std::string {
|
|||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto nro_find(std::span<const NroEntry> array, std::string_view name, std::string_view author, const fs::FsPath& path) -> std::optional<NroEntry> {
|
||||||
|
const auto it = std::find_if(array.cbegin(), array.cend(), [name, author, path](auto& e){
|
||||||
|
if (!name.empty() && !author.empty() && !path.empty()) {
|
||||||
|
return e.GetName() == name && e.GetAuthor() == author && e.path == path;
|
||||||
|
} else if (!name.empty()) {
|
||||||
|
return e.GetName() == name;
|
||||||
|
} else if (!author.empty()) {
|
||||||
|
return e.GetAuthor() == author;
|
||||||
|
} else if (!path.empty()) {
|
||||||
|
return e.path == path;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (it == array.cend()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return *it;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto nro_find_name(std::span<const NroEntry> array, std::string_view name) -> std::optional<NroEntry> {
|
||||||
|
return nro_find(array, name, {}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto nro_find_author(std::span<const NroEntry> array, std::string_view author) -> std::optional<NroEntry> {
|
||||||
|
return nro_find(array, {}, author, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto nro_find_path(std::span<const NroEntry> array, const fs::FsPath& path) -> std::optional<NroEntry> {
|
||||||
|
return nro_find(array, {}, {}, path);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace sphaira
|
} // namespace sphaira
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ void loop(void* args) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
log_write("got name: %s\n", name);
|
log_write("got name: %s\n", name.s);
|
||||||
|
|
||||||
u32 filesize{};
|
u32 filesize{};
|
||||||
if (!recvall(connfd, &filesize, sizeof(filesize))) {
|
if (!recvall(connfd, &filesize, sizeof(filesize))) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "app.hpp"
|
#include "app.hpp"
|
||||||
#include "ui/progress_box.hpp"
|
#include "ui/progress_box.hpp"
|
||||||
#include "i18n.hpp"
|
#include "i18n.hpp"
|
||||||
|
#include "log.hpp"
|
||||||
|
|
||||||
namespace sphaira {
|
namespace sphaira {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -191,6 +192,40 @@ struct NpdmMeta {
|
|||||||
u32 acid_size;
|
u32 acid_size;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct NpdmAcid {
|
||||||
|
u8 rsa_sig[0x100];
|
||||||
|
u8 rsa_pub[0x100];
|
||||||
|
u32 magic; // "ACID"
|
||||||
|
u32 size;
|
||||||
|
u8 version;
|
||||||
|
u8 _0x209[0x1];
|
||||||
|
u8 _0x20A[0x2];
|
||||||
|
u32 flags;
|
||||||
|
u64 program_id_min;
|
||||||
|
u64 program_id_max;
|
||||||
|
u32 fac_offset;
|
||||||
|
u32 fac_size;
|
||||||
|
u32 sac_offset;
|
||||||
|
u32 sac_size;
|
||||||
|
u32 kac_offset;
|
||||||
|
u32 kac_size;
|
||||||
|
u8 _0x238[0x8];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NpdmAci0 {
|
||||||
|
u32 magic; // "ACI0"
|
||||||
|
u8 _0x4[0xC];
|
||||||
|
u64 program_id;
|
||||||
|
u8 _0x18[0x8];
|
||||||
|
u32 fac_offset;
|
||||||
|
u32 fac_size;
|
||||||
|
u32 sac_offset;
|
||||||
|
u32 sac_size;
|
||||||
|
u32 kac_offset;
|
||||||
|
u32 kac_size;
|
||||||
|
u8 _0x38[0x8];
|
||||||
|
};
|
||||||
|
|
||||||
struct NpdmPatch {
|
struct NpdmPatch {
|
||||||
char title_name[0x10]{"Application"};
|
char title_name[0x10]{"Application"};
|
||||||
char product_code[0x10]{};
|
char product_code[0x10]{};
|
||||||
@@ -578,17 +613,56 @@ auto romfs_build(const FileEntries& entries, u64 *out_size) -> std::vector<u8> {
|
|||||||
return buf.buf;
|
return buf.buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto npdm_patch_kc(std::vector<u8>& npdm, u32 off, u32 size, u32 bitmask, u32 value) -> bool {
|
||||||
|
const u32 pattern = BIT(bitmask) - 1;
|
||||||
|
const u32 mask = BIT(bitmask) | pattern;
|
||||||
|
|
||||||
|
for (u32 i = 0; i < size; i += 4) {
|
||||||
|
u32 cup;
|
||||||
|
std::memcpy(&cup, npdm.data() + off + i, sizeof(cup));
|
||||||
|
if ((cup & mask) == pattern) {
|
||||||
|
cup = value | pattern;
|
||||||
|
std::memcpy(npdm.data() + off + i, &cup, sizeof(cup));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// todo: manually build npdm
|
// todo: manually build npdm
|
||||||
void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
|
void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
|
||||||
NpdmMeta meta{};
|
NpdmMeta meta{};
|
||||||
|
NpdmAci0 aci0{};
|
||||||
|
NpdmAcid acid{};
|
||||||
std::memcpy(&meta, npdm.data(), sizeof(meta));
|
std::memcpy(&meta, npdm.data(), sizeof(meta));
|
||||||
|
std::memcpy(&aci0, npdm.data() + meta.aci0_offset, sizeof(aci0));
|
||||||
|
std::memcpy(&acid, npdm.data() + meta.acid_offset, sizeof(acid));
|
||||||
|
|
||||||
// apply patch
|
// apply patch
|
||||||
std::memcpy(npdm.data() + 0x20, &patch.title_name, sizeof(patch.title_name));
|
std::memcpy(meta.title_name, &patch.title_name, sizeof(meta.title_name));
|
||||||
std::memcpy(npdm.data() + 0x30, &patch.product_code, sizeof(patch.product_code));
|
std::memcpy(meta.product_code, &patch.product_code, sizeof(patch.product_code));
|
||||||
std::memcpy(npdm.data() + meta.aci0_offset + 0x10, &patch.tid, sizeof(patch.tid));
|
aci0.program_id = patch.tid;
|
||||||
std::memcpy(npdm.data() + meta.acid_offset + 0x210, &patch.tid, sizeof(patch.tid));
|
acid.program_id_min = patch.tid;
|
||||||
std::memcpy(npdm.data() + meta.acid_offset + 0x218, &patch.tid, sizeof(patch.tid));
|
acid.program_id_max = patch.tid;
|
||||||
|
|
||||||
|
// patch debug flags based on ams version
|
||||||
|
// SEE: https://github.com/ITotalJustice/sphaira/issues/67
|
||||||
|
u64 ver{};
|
||||||
|
splInitialize();
|
||||||
|
ON_SCOPE_EXIT(splExit());
|
||||||
|
const auto SplConfigItem_ExosphereVersion = (SplConfigItem)65000;
|
||||||
|
splGetConfig(SplConfigItem_ExosphereVersion, &ver);
|
||||||
|
ver >>= 40;
|
||||||
|
|
||||||
|
if (ver >= MAKEHOSVERSION(1,8,0)) {
|
||||||
|
npdm_patch_kc(npdm, meta.aci0_offset + aci0.kac_offset, aci0.kac_size, 16, BIT(19));
|
||||||
|
npdm_patch_kc(npdm, meta.acid_offset + acid.kac_offset, acid.kac_size, 16, BIT(19));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::memcpy(npdm.data(), &meta, sizeof(meta));
|
||||||
|
std::memcpy(npdm.data() + meta.aci0_offset, &aci0, sizeof(aci0));
|
||||||
|
std::memcpy(npdm.data() + meta.acid_offset, &acid, sizeof(acid));
|
||||||
}
|
}
|
||||||
|
|
||||||
void patch_nacp(NacpStruct& nacp, const NcapPatch& patch) {
|
void patch_nacp(NacpStruct& nacp, const NcapPatch& patch) {
|
||||||
|
|||||||
115
sphaira/source/ui/bubbles.cpp
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
#include "ui/types.hpp"
|
||||||
|
#include "ui/object.hpp"
|
||||||
|
#include "ui/nvg_util.hpp"
|
||||||
|
#include "app.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::ui::bubble {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto MAX_BUBBLES = 20;
|
||||||
|
|
||||||
|
struct Bubble {
|
||||||
|
int start_x;
|
||||||
|
int texture;
|
||||||
|
int x,y,w,h;
|
||||||
|
int y_inc;
|
||||||
|
float sway_inc;
|
||||||
|
float sway;
|
||||||
|
bool sway_right_flag;
|
||||||
|
};
|
||||||
|
|
||||||
|
Bubble bubbles[MAX_BUBBLES]{};
|
||||||
|
int g_textures[3];
|
||||||
|
bool g_is_init = false;
|
||||||
|
|
||||||
|
void setup_bubble(Bubble *bubble) {
|
||||||
|
// setup normal vars.
|
||||||
|
bubble->texture = (randomGet64() % std::size(g_textures));
|
||||||
|
bubble->start_x = randomGet64() % (int)SCREEN_WIDTH;
|
||||||
|
bubble->x = bubble->start_x;
|
||||||
|
bubble->y = (int)SCREEN_HEIGHT - ( randomGet64() % 60 );
|
||||||
|
const int size = (randomGet64() % 50) + 40;
|
||||||
|
bubble->w = size;
|
||||||
|
bubble->h = size;
|
||||||
|
bubble->y_inc = (randomGet64() % 5) + 1;
|
||||||
|
bubble->sway_inc = ((randomGet64() % 6) + 3) / 10;
|
||||||
|
bubble->sway = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup_bubbles(void) {
|
||||||
|
for (auto& bubble : bubbles) {
|
||||||
|
setup_bubble(&bubble);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_bubbles(void) {
|
||||||
|
for (auto& bubble : bubbles) {
|
||||||
|
if (bubble.y + bubble.h < 0) {
|
||||||
|
setup_bubble(&bubble);
|
||||||
|
} else {
|
||||||
|
bubble.y -= bubble.y_inc;
|
||||||
|
|
||||||
|
if (bubble.sway_right_flag) {
|
||||||
|
bubble.x = bubble.start_x + (bubble.sway -= bubble.sway_inc);
|
||||||
|
if (bubble.sway <= 0) {
|
||||||
|
bubble.sway_right_flag = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bubble.x = bubble.start_x + (bubble.sway += bubble.sway_inc);
|
||||||
|
if (bubble.sway > 30) {
|
||||||
|
bubble.sway_right_flag = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void Init() {
|
||||||
|
if (g_is_init) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (R_SUCCEEDED(romfsInit())) {
|
||||||
|
ON_SCOPE_EXIT(romfsExit());
|
||||||
|
|
||||||
|
auto vg = App::GetVg();
|
||||||
|
g_textures[0] = nvgCreateImage(vg, "romfs:/theme/bubble1.png", 0);
|
||||||
|
g_textures[1] = nvgCreateImage(vg, "romfs:/theme/bubble2.png", 0);
|
||||||
|
g_textures[2] = nvgCreateImage(vg, "romfs:/theme/bubble3.png", 0);
|
||||||
|
|
||||||
|
setup_bubbles();
|
||||||
|
g_is_init = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Draw(NVGcontext* vg, Theme* theme) {
|
||||||
|
if (!g_is_init) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
update_bubbles();
|
||||||
|
|
||||||
|
for (auto& bubble : bubbles) {
|
||||||
|
gfx::drawImage(vg, bubble.x, bubble.y, bubble.w, bubble.h, g_textures[bubble.texture]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Exit() {
|
||||||
|
if (!g_is_init) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto vg = App::GetVg();
|
||||||
|
for (auto& texture : g_textures) {
|
||||||
|
if (texture) {
|
||||||
|
nvgDeleteImage(vg, texture);
|
||||||
|
texture = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_is_init = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::ui::bubble
|
||||||
@@ -1134,24 +1134,20 @@ auto ErrorBox::Update(Controller* controller, TouchInfo* touch) -> void {
|
|||||||
Widget::Update(controller, touch);
|
Widget::Update(controller, touch);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ErrorBox::OnLayoutChange() -> void {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
auto ErrorBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
auto ErrorBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||||
gfx::dimBackground(vg);
|
gfx::dimBackground(vg);
|
||||||
gfx::drawRect(vg, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
|
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP));
|
||||||
|
|
||||||
const Vec4 box = { 455, 470, 365, 65 };
|
const Vec4 box = { 455, 470, 365, 65 };
|
||||||
const auto center_x = m_pos.x + m_pos.w/2;
|
const auto center_x = m_pos.x + m_pos.w/2;
|
||||||
|
|
||||||
gfx::drawTextArgs(vg, center_x, 180, 63, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::RED, "\uE140");
|
gfx::drawTextArgs(vg, center_x, 180, 63, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_ERROR), "\uE140");
|
||||||
gfx::drawTextArgs(vg, center_x, 270, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "Error code: 0x%X Module: %s Description: %s", m_code, m_module_str.c_str(), m_description_str.c_str());
|
gfx::drawTextArgs(vg, center_x, 270, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Error code: 0x%X Module: %s Description: %s", m_code, m_module_str.c_str(), m_description_str.c_str());
|
||||||
gfx::drawTextArgs(vg, center_x, 325, 23, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "%s", m_message.c_str());
|
gfx::drawTextArgs(vg, center_x, 325, 23, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", m_message.c_str());
|
||||||
gfx::drawTextArgs(vg, center_x, 380, 20, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::SILVER, "If this message appears repeatedly, please open an issue."_i18n.c_str());
|
gfx::drawTextArgs(vg, center_x, 380, 20, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_INFO), "If this message appears repeatedly, please open an issue."_i18n.c_str());
|
||||||
gfx::drawTextArgs(vg, center_x, 415, 20, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::SILVER, "https://github.com/ITotalJustice/sphaira/issues");
|
gfx::drawTextArgs(vg, center_x, 415, 20, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_INFO), "https://github.com/ITotalJustice/sphaira/issues");
|
||||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, box, theme->elements[ThemeEntryID_SELECTED].colour);
|
gfx::drawRectOutline(vg, theme, 4.f, box);
|
||||||
gfx::drawTextArgs(vg, center_x, box.y + box.h/2, 23, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::WHITE, "OK"_i18n.c_str());
|
gfx::drawTextArgs(vg, center_x, box.y + box.h/2, 23, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED), "OK"_i18n.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace sphaira::ui
|
} // namespace sphaira::ui
|
||||||
|
|||||||
189
sphaira/source/ui/list.cpp
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
#include "ui/list.hpp"
|
||||||
|
#include "ui/nvg_util.hpp"
|
||||||
|
#include "app.hpp"
|
||||||
|
#include "log.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::ui {
|
||||||
|
|
||||||
|
List::List(s64 row, s64 page, const Vec4& pos, const Vec4& v, const Vec2& pad)
|
||||||
|
: m_row{row}
|
||||||
|
, m_page{page}
|
||||||
|
, m_v{v}
|
||||||
|
, m_pad{pad} {
|
||||||
|
m_pos = pos;
|
||||||
|
SetScrollBarPos(SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto List::ClampY(float y, s64 count) const -> float {
|
||||||
|
float y_max = 0;
|
||||||
|
|
||||||
|
if (count >= m_page) {
|
||||||
|
// round up
|
||||||
|
if (count % m_row) {
|
||||||
|
count = count + (m_row - count % m_row);
|
||||||
|
}
|
||||||
|
y_max = (count - m_page) / m_row * GetMaxY();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y < 0) {
|
||||||
|
y = 0;
|
||||||
|
} else if (y > y_max) {
|
||||||
|
y = y_max;
|
||||||
|
}
|
||||||
|
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCallback callback) {
|
||||||
|
if (touch->is_clicked && touch->in_range(GetPos())) {
|
||||||
|
auto v = m_v;
|
||||||
|
v.y -= ClampY(m_yoff + m_y_prog, count);
|
||||||
|
|
||||||
|
for (s64 i = 0; i < count; v.y += v.h + m_pad.y) {
|
||||||
|
if (v.y > GetY() + GetH()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto x = v.x;
|
||||||
|
|
||||||
|
for (; i < count; i++, v.x += v.w + m_pad.x) {
|
||||||
|
// only draw if full x is in bounds
|
||||||
|
if (v.x + v.w > GetX() + GetW()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip anything not visible
|
||||||
|
if (v.y + v.h < GetY()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec4 vv = v;
|
||||||
|
// if not drawing, only return clipped v as its used for touch
|
||||||
|
vv.w = std::min(v.x + v.w, m_pos.x + m_pos.w) - v.x;
|
||||||
|
vv.h = std::min(v.y + v.h, m_pos.y + m_pos.h) - v.y;
|
||||||
|
|
||||||
|
if (touch->in_range(vv)) {
|
||||||
|
callback(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v.x = x;
|
||||||
|
}
|
||||||
|
} else if (touch->is_scroll && touch->in_range(GetPos())) {
|
||||||
|
m_y_prog = (float)touch->initial.y - (float)touch->cur.y;
|
||||||
|
} else if (touch->is_end) {
|
||||||
|
m_yoff = ClampY(m_yoff + m_y_prog, count);
|
||||||
|
m_y_prog = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void List::Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const {
|
||||||
|
const auto yoff = ClampY(m_yoff + m_y_prog, count);
|
||||||
|
const s64 start = yoff / GetMaxY() * m_row;
|
||||||
|
gfx::drawScrollbar2(vg, theme, m_scrollbar.x, m_scrollbar.y, m_scrollbar.h, start, count, m_row, m_page);
|
||||||
|
|
||||||
|
auto v = m_v;
|
||||||
|
v.y -= yoff;
|
||||||
|
|
||||||
|
nvgSave(vg);
|
||||||
|
nvgScissor(vg, GetX(), GetY(), GetW(), GetH());
|
||||||
|
|
||||||
|
for (s64 i = 0; i < count; v.y += v.h + m_pad.y) {
|
||||||
|
if (v.y > GetY() + GetH()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto x = v.x;
|
||||||
|
|
||||||
|
for (; i < count; i++, v.x += v.w + m_pad.x) {
|
||||||
|
// only draw if full x is in bounds
|
||||||
|
if (v.x + v.w > GetX() + GetW()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip anything not visible
|
||||||
|
if (v.y + v.h < GetY()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(vg, theme, v, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
v.x = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
nvgRestore(vg);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto List::ScrollDown(s64& index, s64 step, s64 count) -> bool {
|
||||||
|
const auto old_index = index;
|
||||||
|
|
||||||
|
if (!count) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index + step < count) {
|
||||||
|
index += step;
|
||||||
|
} else {
|
||||||
|
index = count - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index != old_index) {
|
||||||
|
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||||
|
s64 delta = index - old_index;
|
||||||
|
s64 start = m_yoff / GetMaxY() * m_row;
|
||||||
|
|
||||||
|
while (index < start) {
|
||||||
|
start -= m_row;
|
||||||
|
m_yoff -= GetMaxY();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index - start >= m_page) {
|
||||||
|
do {
|
||||||
|
start += m_row;
|
||||||
|
delta -= m_row;
|
||||||
|
m_yoff += GetMaxY();
|
||||||
|
} while (delta > 0 && start + m_page < count);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto List::ScrollUp(s64& index, s64 step, s64 count) -> bool {
|
||||||
|
const auto old_index = index;
|
||||||
|
|
||||||
|
if (!count) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index >= step) {
|
||||||
|
index -= step;
|
||||||
|
} else {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index != old_index) {
|
||||||
|
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||||
|
s64 start = m_yoff / GetMaxY() * m_row;
|
||||||
|
|
||||||
|
while (index < start) {
|
||||||
|
start -= m_row;
|
||||||
|
m_yoff -= GetMaxY();
|
||||||
|
}
|
||||||
|
|
||||||
|
while (index - start >= m_page && start + m_page < count) {
|
||||||
|
start += m_row;
|
||||||
|
m_yoff += GetMaxY();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::ui
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <yyjson.h>
|
#include <yyjson.h>
|
||||||
#include <nanovg/stb_image.h>
|
#include <stb_image.h>
|
||||||
#include <minizip/unzip.h>
|
#include <minizip/unzip.h>
|
||||||
#include <mbedtls/md5.h>
|
#include <mbedtls/md5.h>
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
@@ -65,60 +65,27 @@ auto BuildIconUrl(const Entry& e) -> std::string {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if 0
|
|
||||||
auto BuildInfoUrl(const Entry& e) -> std::string {
|
|
||||||
char out[0x100];
|
|
||||||
std::snprintf(out, sizeof(out), "%s/packages/%s/info.json", URL_BASE, e.name.c_str());
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
auto BuildBannerUrl(const Entry& e) -> std::string {
|
auto BuildBannerUrl(const Entry& e) -> std::string {
|
||||||
char out[0x100];
|
char out[0x100];
|
||||||
std::snprintf(out, sizeof(out), "%s/packages/%s/screen.png", URL_BASE, e.name.c_str());
|
std::snprintf(out, sizeof(out), "%s/packages/%s/screen.png", URL_BASE, e.name.c_str());
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if 0
|
|
||||||
auto BuildScreensUrl(const Entry& e, u8 num) -> std::string {
|
|
||||||
char out[0x100];
|
|
||||||
std::snprintf(out, sizeof(out), "%s/packages/%s/screen%u.png", URL_BASE, e.name.c_str(), num+1);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
auto BuildMainifestUrl(const Entry& e) -> std::string {
|
|
||||||
char out[0x100];
|
|
||||||
std::snprintf(out, sizeof(out), "%s/packages/%s/manifest.install", URL_BASE, e.name.c_str());
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto BuildZipUrl(const Entry& e) -> std::string {
|
auto BuildZipUrl(const Entry& e) -> std::string {
|
||||||
char out[0x100];
|
char out[0x100];
|
||||||
std::snprintf(out, sizeof(out), "%s/zips/%s.zip", URL_BASE, e.name.c_str());
|
std::snprintf(out, sizeof(out), "%s/zips/%s.zip", URL_BASE, e.name.c_str());
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto BuildFeedbackUrl(std::span<u32> ids) -> std::string {
|
|
||||||
std::string out{"https://wiiubru.com/feedback/messages?ids="};
|
|
||||||
for (u32 i = 0; i < ids.size(); i++) {
|
|
||||||
if (i != 0) {
|
|
||||||
out.push_back(',');
|
|
||||||
}
|
|
||||||
out += std::to_string(ids[i]);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto BuildIconCachePath(const Entry& e) -> fs::FsPath {
|
auto BuildIconCachePath(const Entry& e) -> fs::FsPath {
|
||||||
fs::FsPath out;
|
fs::FsPath out;
|
||||||
std::snprintf(out, sizeof(out), "%s/icons/%s.png", CACHE_PATH, e.name.c_str());
|
std::snprintf(out, sizeof(out), "%s/icons/%s.png", CACHE_PATH.s, e.name.c_str());
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto BuildBannerCachePath(const Entry& e) -> fs::FsPath {
|
auto BuildBannerCachePath(const Entry& e) -> fs::FsPath {
|
||||||
fs::FsPath out;
|
fs::FsPath out;
|
||||||
std::snprintf(out, sizeof(out), "%s/banners/%s.png", CACHE_PATH, e.name.c_str());
|
std::snprintf(out, sizeof(out), "%s/banners/%s.png", CACHE_PATH.s, e.name.c_str());
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,17 +173,17 @@ auto LoadAndParseManifest(const Entry& e) -> ManifestEntries {
|
|||||||
return ParseManifest(std::span{(const char*)data.data(), data.size()});
|
return ParseManifest(std::span{(const char*)data.data(), data.size()});
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntryLoadImageFile(fs::Fs& fs, const fs::FsPath& path, LazyImage& image) {
|
auto EntryLoadImageFile(fs::Fs& fs, const fs::FsPath& path, LazyImage& image) -> bool {
|
||||||
// already have the image
|
// already have the image
|
||||||
if (image.image) {
|
if (image.image) {
|
||||||
log_write("warning, tried to load image: %s when already loaded\n", path);
|
// log_write("warning, tried to load image: %s when already loaded\n", path);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
auto vg = App::GetVg();
|
auto vg = App::GetVg();
|
||||||
|
|
||||||
std::vector<u8> image_buf;
|
std::vector<u8> image_buf;
|
||||||
if (R_FAILED(fs.read_entire_file(path, image_buf))) {
|
if (R_FAILED(fs.read_entire_file(path, image_buf))) {
|
||||||
image.state = ImageDownloadState::Failed;
|
log_write("failed to load image from file: %s\n", path.s);
|
||||||
} else {
|
} else {
|
||||||
int channels_in_file;
|
int channels_in_file;
|
||||||
auto buf = stbi_load_from_memory(image_buf.data(), image_buf.size(), &image.w, &image.h, &channels_in_file, 4);
|
auto buf = stbi_load_from_memory(image_buf.data(), image_buf.size(), &image.w, &image.h, &channels_in_file, 4);
|
||||||
@@ -228,20 +195,21 @@ void EntryLoadImageFile(fs::Fs& fs, const fs::FsPath& path, LazyImage& image) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!image.image) {
|
if (!image.image) {
|
||||||
image.state = ImageDownloadState::Failed;
|
log_write("failed to load image from file: %s\n", path.s);
|
||||||
log_write("failed to load image from file: %s\n", path);
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// log_write("loaded image from file: %s\n", path);
|
// log_write("loaded image from file: %s\n", path);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntryLoadImageFile(const fs::FsPath& path, LazyImage& image) {
|
auto EntryLoadImageFile(const fs::FsPath& path, LazyImage& image) -> bool {
|
||||||
if (!strncasecmp("romfs:/", path, 7)) {
|
if (!strncasecmp("romfs:/", path, 7)) {
|
||||||
fs::FsStdio fs;
|
fs::FsStdio fs;
|
||||||
EntryLoadImageFile(fs, path, image);
|
return EntryLoadImageFile(fs, path, image);
|
||||||
} else {
|
} else {
|
||||||
fs::FsNativeSd fs;
|
fs::FsNativeSd fs;
|
||||||
EntryLoadImageFile(fs, path, image);
|
return EntryLoadImageFile(fs, path, image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,7 +241,7 @@ void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, float x, f
|
|||||||
if (iw > w || ih > h) {
|
if (iw > w || ih > h) {
|
||||||
crop = true;
|
crop = true;
|
||||||
nvgSave(vg);
|
nvgSave(vg);
|
||||||
nvgScissor(vg, x, y, w, h);
|
nvgIntersectScissor(vg, x, y, w, h);
|
||||||
}
|
}
|
||||||
if (rounded_image) {
|
if (rounded_image) {
|
||||||
gfx::drawImageRounded(vg, ix, iy, iw, ih, i.image);
|
gfx::drawImageRounded(vg, ix, iy, iw, ih, i.image);
|
||||||
@@ -289,26 +257,6 @@ void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, Vec4 vec,
|
|||||||
DrawIcon(vg, l, d, vec.x, vec.y, vec.w, vec.h, rounded, scale);
|
DrawIcon(vg, l, d, vec.x, vec.y, vec.w, vec.h, rounded, scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ScrollHelperDown(u64& index, u64& start, u64 step, u64 max, u64 size) -> bool {
|
|
||||||
if (size && index < (size - 1)) {
|
|
||||||
if (index < (size - step)) {
|
|
||||||
index = index + step;
|
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
|
||||||
} else {
|
|
||||||
index = size - 1;
|
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
|
||||||
}
|
|
||||||
if (index - start >= max) {
|
|
||||||
log_write("moved down\n");
|
|
||||||
start += step;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto AppDlToStr(u32 value) -> std::string {
|
auto AppDlToStr(u32 value) -> std::string {
|
||||||
auto str = std::to_string(value);
|
auto str = std::to_string(value);
|
||||||
u32 inc = 3;
|
u32 inc = 3;
|
||||||
@@ -321,7 +269,6 @@ auto AppDlToStr(u32 value) -> std::string {
|
|||||||
|
|
||||||
void ReadFromInfoJson(Entry& e) {
|
void ReadFromInfoJson(Entry& e) {
|
||||||
const auto info_path = BuildInfoCachePath(e);
|
const auto info_path = BuildInfoCachePath(e);
|
||||||
const auto manifest_path = BuildManifestCachePath(e);
|
|
||||||
|
|
||||||
yyjson_read_err err;
|
yyjson_read_err err;
|
||||||
auto doc = yyjson_read_file(info_path, YYJSON_READ_NOFLAG, nullptr, &err);
|
auto doc = yyjson_read_file(info_path, YYJSON_READ_NOFLAG, nullptr, &err);
|
||||||
@@ -359,9 +306,9 @@ auto UninstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
|||||||
const auto safe_buf = fs::AppendPath("/", e.path);
|
const auto safe_buf = fs::AppendPath("/", e.path);
|
||||||
// this will handle read only files, ie, hbmenu.nro
|
// this will handle read only files, ie, hbmenu.nro
|
||||||
if (R_FAILED(fs.DeleteFile(safe_buf))) {
|
if (R_FAILED(fs.DeleteFile(safe_buf))) {
|
||||||
log_write("failed to delete file: %s\n", safe_buf);
|
log_write("failed to delete file: %s\n", safe_buf.s);
|
||||||
} else {
|
} else {
|
||||||
log_write("deleted file: %s\n", safe_buf);
|
log_write("deleted file: %s\n", safe_buf.s);
|
||||||
// todo: delete empty directories!
|
// todo: delete empty directories!
|
||||||
// fs::delete_directory(safe_buf);
|
// fs::delete_directory(safe_buf);
|
||||||
}
|
}
|
||||||
@@ -372,9 +319,9 @@ auto UninstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
|||||||
const auto dir = BuildPackageCachePath(entry);
|
const auto dir = BuildPackageCachePath(entry);
|
||||||
pbox->NewTransfer("Removing "_i18n + dir);
|
pbox->NewTransfer("Removing "_i18n + dir);
|
||||||
if (R_FAILED(fs.DeleteDirectoryRecursively(dir))) {
|
if (R_FAILED(fs.DeleteDirectoryRecursively(dir))) {
|
||||||
log_write("failed to delete folder: %s\n", dir);
|
log_write("failed to delete folder: %s\n", dir.s);
|
||||||
} else {
|
} else {
|
||||||
log_write("deleted: %s\n", dir);
|
log_write("deleted: %s\n", dir.s);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -398,17 +345,13 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
|||||||
log_write("starting download\n");
|
log_write("starting download\n");
|
||||||
|
|
||||||
const auto url = BuildZipUrl(entry);
|
const auto url = BuildZipUrl(entry);
|
||||||
if (!DownloadFile(url, zip_out, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
|
if (!curl::Api().ToFile(
|
||||||
if (pbox->ShouldExit()) {
|
curl::Url{url},
|
||||||
return false;
|
curl::Path{zip_out},
|
||||||
}
|
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||||
pbox->UpdateTransfer(dlnow, dltotal);
|
).success) {
|
||||||
return true;
|
|
||||||
})) {
|
|
||||||
log_write("error with download\n");
|
log_write("error with download\n");
|
||||||
// push popup error box
|
|
||||||
return false;
|
return false;
|
||||||
// return appletEnterFatalSection();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,7 +424,7 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
|||||||
if (!pbox->ShouldExit()) {
|
if (!pbox->ShouldExit()) {
|
||||||
auto zfile = unzOpen64(zip_out);
|
auto zfile = unzOpen64(zip_out);
|
||||||
if (!zfile) {
|
if (!zfile) {
|
||||||
log_write("failed to open zip: %s\n", zip_out);
|
log_write("failed to open zip: %s\n", zip_out.s);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ON_SCOPE_EXIT(unzClose(zfile));
|
ON_SCOPE_EXIT(unzClose(zfile));
|
||||||
@@ -524,7 +467,7 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
|||||||
pbox->NewTransfer(inzip);
|
pbox->NewTransfer(inzip);
|
||||||
|
|
||||||
if (UNZ_END_OF_LIST_OF_FILE == unzLocateFile(zfile, inzip, 0)) {
|
if (UNZ_END_OF_LIST_OF_FILE == unzLocateFile(zfile, inzip, 0)) {
|
||||||
log_write("failed to find %s\n", inzip);
|
log_write("failed to find %s\n", inzip.s);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,23 +488,23 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create directories
|
// create directories
|
||||||
fs.CreateDirectoryRecursivelyWithPath(output, true);
|
fs.CreateDirectoryRecursivelyWithPath(output);
|
||||||
|
|
||||||
Result rc;
|
Result rc;
|
||||||
if (R_FAILED(rc = fs.CreateFile(output, info.uncompressed_size, 0, true)) && rc != FsError_PathAlreadyExists) {
|
if (R_FAILED(rc = fs.CreateFile(output, info.uncompressed_size, 0)) && rc != FsError_PathAlreadyExists) {
|
||||||
log_write("failed to create file: %s 0x%04X\n", output, rc);
|
log_write("failed to create file: %s 0x%04X\n", output.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (R_FAILED(rc = fs.OpenFile(output, FsOpenMode_Write, &f))) {
|
if (R_FAILED(rc = fs.OpenFile(output, FsOpenMode_Write, &f))) {
|
||||||
log_write("failed to open file: %s 0x%04X\n", output, rc);
|
log_write("failed to open file: %s 0x%04X\n", output.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ON_SCOPE_EXIT(fsFileClose(&f));
|
ON_SCOPE_EXIT(fsFileClose(&f));
|
||||||
|
|
||||||
if (R_FAILED(rc = fsFileSetSize(&f, info.uncompressed_size))) {
|
if (R_FAILED(rc = fsFileSetSize(&f, info.uncompressed_size))) {
|
||||||
log_write("failed to set file size: %s 0x%04X\n", output, rc);
|
log_write("failed to set file size: %s 0x%04X\n", output.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,12 +517,12 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
|||||||
|
|
||||||
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
|
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
|
||||||
if (bytes_read <= 0) {
|
if (bytes_read <= 0) {
|
||||||
log_write("failed to read zip file: %s\n", inzip);
|
log_write("failed to read zip file: %s\n", inzip.s);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (R_FAILED(rc = fsFileWrite(&f, offset, buf.data(), bytes_read, FsWriteOption_None))) {
|
if (R_FAILED(rc = fsFileWrite(&f, offset, buf.data(), bytes_read, FsWriteOption_None))) {
|
||||||
log_write("failed to write file: %s 0x%04X\n", output, rc);
|
log_write("failed to write file: %s 0x%04X\n", output.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,10 +580,10 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
|||||||
if (!found) {
|
if (!found) {
|
||||||
const auto safe_buf = fs::AppendPath("/", old_entry.path);
|
const auto safe_buf = fs::AppendPath("/", old_entry.path);
|
||||||
// std::strcat(safe_buf, old_entry.path);
|
// std::strcat(safe_buf, old_entry.path);
|
||||||
if (R_FAILED(fs.DeleteFile(safe_buf, true))) {
|
if (R_FAILED(fs.DeleteFile(safe_buf))) {
|
||||||
log_write("failed to delete: %s\n", safe_buf);
|
log_write("failed to delete: %s\n", safe_buf.s);
|
||||||
} else {
|
} else {
|
||||||
log_write("deleted file: %s\n", safe_buf);
|
log_write("deleted file: %s\n", safe_buf.s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -682,36 +625,30 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
|
|||||||
const auto post = "name=" "switch_user" "&package=" + m_entry.name + "&message=" + out;
|
const auto post = "name=" "switch_user" "&package=" + m_entry.name + "&message=" + out;
|
||||||
const auto file = BuildFeedbackCachePath(m_entry);
|
const auto file = BuildFeedbackCachePath(m_entry);
|
||||||
|
|
||||||
DownloadFileAsync(URL_POST_FEEDBACK, file, post, [](std::vector<u8>& data, bool success){
|
curl::Api().ToAsync(
|
||||||
if (success) {
|
curl::Url{URL_POST_FEEDBACK},
|
||||||
log_write("got feedback!\n");
|
curl::Path{file},
|
||||||
} else {
|
curl::Fields{post},
|
||||||
log_write("failed to send feedback :(");
|
curl::StopToken{this->GetToken()},
|
||||||
|
curl::OnComplete{[](auto& result){
|
||||||
|
if (result.success) {
|
||||||
|
log_write("got feedback!\n");
|
||||||
|
} else {
|
||||||
|
log_write("failed to send feedback :(");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, true));
|
}, true));
|
||||||
App::Push(sidebar);
|
App::Push(sidebar);
|
||||||
}}),
|
}}),
|
||||||
// std::make_pair(Button::A, Action{m_entry.status == EntryStatus::Update ? "Update" : "Install", [this](){
|
|
||||||
// App::Push(std::make_shared<ProgressBox>("App Install", [this](auto pbox){
|
|
||||||
// InstallApp(pbox, m_entry);
|
|
||||||
// }, 2));
|
|
||||||
// }}),
|
|
||||||
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||||
SetPop();
|
SetPop();
|
||||||
}})
|
}})
|
||||||
);
|
);
|
||||||
|
|
||||||
// SidebarEntryCallback
|
|
||||||
// if (!m_entries_current.empty() && !GetEntry().url.empty()) {
|
|
||||||
// options->Add(std::make_shared<SidebarEntryCallback>("Show Release Page"))
|
|
||||||
// }
|
|
||||||
|
|
||||||
SetTitleSubHeading("by " + m_entry.author);
|
SetTitleSubHeading("by " + m_entry.author);
|
||||||
|
|
||||||
// const char* very_long = "total@fedora:~/dev/switch/sphaira$ nxlink build/MinSizeRel/*.nro total@fedora:~/dev/switch/sphaira$ nxlink build/MinSizeRel/*.nro total@fedora:~/dev/switch/sphaira$ nxlink build/MinSizeRel/*.nro total@fedora:~/dev/switch/sphaira$ nxlink build/MinSizeRel/*.nro";
|
|
||||||
|
|
||||||
m_details = std::make_shared<ScrollableText>(m_entry.details, 0, 374, 250, 768, 18);
|
m_details = std::make_shared<ScrollableText>(m_entry.details, 0, 374, 250, 768, 18);
|
||||||
m_changelog = std::make_shared<ScrollableText>(m_entry.changelog, 0, 374, 250, 768, 18);
|
m_changelog = std::make_shared<ScrollableText>(m_entry.changelog, 0, 374, 250, 768, 18);
|
||||||
|
|
||||||
@@ -720,41 +657,33 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
|
|||||||
|
|
||||||
const auto path = BuildBannerCachePath(m_entry);
|
const auto path = BuildBannerCachePath(m_entry);
|
||||||
const auto url = BuildBannerUrl(m_entry);
|
const auto url = BuildBannerUrl(m_entry);
|
||||||
|
m_banner.cached = EntryLoadImageFile(path, m_banner);
|
||||||
if (fs::FsNativeSd().FileExists(path)) {
|
|
||||||
EntryLoadImageFile(path, m_banner);
|
|
||||||
}
|
|
||||||
|
|
||||||
// race condition if we pop the widget before the download completes
|
// race condition if we pop the widget before the download completes
|
||||||
if (!m_banner.image) {
|
curl::Api().ToFileAsync(
|
||||||
DownloadFileAsync(url, path, "", [this, path](std::vector<u8>& data, bool success){
|
curl::Url{url},
|
||||||
if (success) {
|
curl::Path{path},
|
||||||
EntryLoadImageFile(path, m_banner);
|
curl::Flags{curl::Flag_Cache},
|
||||||
|
curl::StopToken{this->GetToken()},
|
||||||
|
curl::OnComplete{[this, path](auto& result){
|
||||||
|
if (result.success) {
|
||||||
|
if (result.code == 304) {
|
||||||
|
m_banner.cached = false;
|
||||||
|
} else {
|
||||||
|
EntryLoadImageFile(path, m_banner);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, nullptr, DownloadPriority::High);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore screen shots, most apps don't have any sadly.
|
|
||||||
#if 0
|
|
||||||
m_screens.resize(m_entry.screens);
|
|
||||||
|
|
||||||
for (u32 i = 0; i < m_screens.size(); i++) {
|
|
||||||
path = BuildScreensCachePath(m_entry.name, i);
|
|
||||||
url = BuildScreensUrl(m_entry.name, i);
|
|
||||||
|
|
||||||
if (fs::file_exists(path.c_str())) {
|
|
||||||
EntryLoadImageFile(path, m_screens[i]);
|
|
||||||
} else {
|
|
||||||
DownloadFileAsync(url.c_str(), path.c_str(), [this, i, path](std::vector<u8>& data, bool success){
|
|
||||||
EntryLoadImageFile(path, m_screens[i]);
|
|
||||||
}, nullptr, DownloadPriority::High);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
#endif
|
|
||||||
|
|
||||||
SetSubHeading(m_entry.binary);
|
SetSubHeading(m_entry.binary);
|
||||||
SetSubHeading(m_entry.description);
|
SetSubHeading(m_entry.description);
|
||||||
UpdateOptions();
|
UpdateOptions();
|
||||||
|
|
||||||
|
// todo: see Draw()
|
||||||
|
// const Vec4 v{75, 110, 370, 155};
|
||||||
|
// const Vec2 pad{10, 10};
|
||||||
|
// m_list = std::make_unique<List>(3, 3, v, pad);
|
||||||
}
|
}
|
||||||
|
|
||||||
EntryMenu::~EntryMenu() {
|
EntryMenu::~EntryMenu() {
|
||||||
@@ -772,49 +701,35 @@ void EntryMenu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
constexpr Vec4 line_vec(30, 86, 1220, 646);
|
constexpr Vec4 line_vec(30, 86, 1220, 646);
|
||||||
constexpr Vec4 banner_vec(70, line_vec.y + 20, 848.f, 208.f);
|
constexpr Vec4 banner_vec(70, line_vec.y + 20, 848.f, 208.f);
|
||||||
constexpr Vec4 icon_vec(968, line_vec.y + 30, 256, 150);
|
constexpr Vec4 icon_vec(968, line_vec.y + 30, 256, 150);
|
||||||
|
constexpr Vec4 grid_vec(icon_vec.x - 50, line_vec.y + 1, line_vec.w, line_vec.h - line_vec.y - 1);
|
||||||
|
|
||||||
// nvgSave(vg);
|
// nvgSave(vg);
|
||||||
// nvgScissor(vg, line_vec.x, line_vec.y, line_vec.w - line_vec.x, line_vec.h - line_vec.y); // clip
|
// nvgScissor(vg, line_vec.x, line_vec.y, line_vec.w - line_vec.x, line_vec.h - line_vec.y); // clip
|
||||||
// ON_SCOPE_EXIT(nvgRestore(vg));
|
// ON_SCOPE_EXIT(nvgRestore(vg));
|
||||||
|
|
||||||
|
gfx::drawRect(vg, grid_vec, theme->GetColour(ThemeEntryID_GRID));
|
||||||
DrawIcon(vg, m_banner, m_entry.image.image ? m_entry.image : m_default_icon, banner_vec, false);
|
DrawIcon(vg, m_banner, m_entry.image.image ? m_entry.image : m_default_icon, banner_vec, false);
|
||||||
DrawIcon(vg, m_entry.image, m_default_icon, icon_vec);
|
DrawIcon(vg, m_entry.image, m_default_icon, icon_vec);
|
||||||
|
|
||||||
// gfx::drawImage(vg, icon_vec, m_entry.image.image);
|
|
||||||
constexpr float text_start_x = icon_vec.x;// - 10;
|
constexpr float text_start_x = icon_vec.x;// - 10;
|
||||||
float text_start_y = 218 + line_vec.y;
|
float text_start_y = 218 + line_vec.y;
|
||||||
const float text_inc_y = 32;
|
const float text_inc_y = 32;
|
||||||
const float font_size = 20;
|
const float font_size = 20;
|
||||||
|
|
||||||
// gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "%s", m_entry.name.c_str());
|
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "version: %s"_i18n.c_str(), m_entry.version.c_str());
|
||||||
// gfx::drawTextBox(vg, text_start_x - 20, text_start_y, font_size, icon_vec.w + 20*2, theme->elements[ThemeEntryID_TEXT].colour, m_entry.description.c_str(), NVG_ALIGN_CENTER);
|
|
||||||
// text_start_y += text_inc_y * 2.0;
|
|
||||||
|
|
||||||
// gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "author: %s", m_entry.author.c_str());
|
|
||||||
// text_start_y += text_inc_y;
|
|
||||||
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "version: %s"_i18n.c_str(), m_entry.version.c_str());
|
|
||||||
text_start_y += text_inc_y;
|
text_start_y += text_inc_y;
|
||||||
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "updated: %s"_i18n.c_str(), m_entry.updated.c_str());
|
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "updated: %s"_i18n.c_str(), m_entry.updated.c_str());
|
||||||
text_start_y += text_inc_y;
|
text_start_y += text_inc_y;
|
||||||
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "category: %s"_i18n.c_str(), m_entry.category.c_str());
|
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "category: %s"_i18n.c_str(), m_entry.category.c_str());
|
||||||
text_start_y += text_inc_y;
|
text_start_y += text_inc_y;
|
||||||
// gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "license: %s", m_entry.license.c_str());
|
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "extracted: %.2f MiB"_i18n.c_str(), (double)m_entry.extracted / 1024.0);
|
||||||
// text_start_y += text_inc_y;
|
|
||||||
// gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "title: %s", m_entry.title.c_str());
|
|
||||||
// text_start_y += text_inc_y;
|
|
||||||
// gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "filesize: %.2f MiB", (double)m_entry.filesize / 1024.0);
|
|
||||||
// text_start_y += text_inc_y;
|
|
||||||
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "extracted: %.2f MiB"_i18n.c_str(), (double)m_entry.extracted / 1024.0);
|
|
||||||
text_start_y += text_inc_y;
|
text_start_y += text_inc_y;
|
||||||
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "app_dls: %s"_i18n.c_str(), AppDlToStr(m_entry.app_dls).c_str());
|
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "app_dls: %s"_i18n.c_str(), AppDlToStr(m_entry.app_dls).c_str());
|
||||||
text_start_y += text_inc_y;
|
text_start_y += text_inc_y;
|
||||||
|
|
||||||
// for (const auto& option : m_options) {
|
// todo: rewrite this mess and use list
|
||||||
const auto& text_col = theme->elements[ThemeEntryID_TEXT].colour;
|
|
||||||
|
|
||||||
constexpr float mm = 0;//20;
|
constexpr float mm = 0;//20;
|
||||||
constexpr Vec4 block{968.f + mm, 110.f, 256.f - mm*2, 60.f};
|
constexpr Vec4 block{968.f + mm, 110.f, 256.f - mm*2, 60.f};
|
||||||
constexpr float text_xoffset{15.f};
|
|
||||||
const float x = block.x;
|
const float x = block.x;
|
||||||
float y = 1.f + text_start_y + (text_inc_y * 3) ;
|
float y = 1.f + text_start_y + (text_inc_y * 3) ;
|
||||||
const float h = block.h;
|
const float h = block.h;
|
||||||
@@ -825,15 +740,10 @@ void EntryMenu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
auto text_id = ThemeEntryID_TEXT;
|
auto text_id = ThemeEntryID_TEXT;
|
||||||
if (m_index == i) {
|
if (m_index == i) {
|
||||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
|
gfx::drawRectOutline(vg, theme, 4.f, Vec4{x, y, w, h});
|
||||||
} else {
|
|
||||||
// if (i == m_index_offset) {
|
|
||||||
// gfx::drawRect(vg, x, y, w, 1.f, text_col);
|
|
||||||
// }
|
|
||||||
// gfx::drawRect(vg, x, y + h, w, 1.f, text_col);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gfx::drawTextArgs(vg, x + w / 2, y + h / 2, 22, NVG_ALIGN_MIDDLE | NVG_ALIGN_CENTER, theme->elements[ThemeEntryID_TEXT].colour, option.display_text.c_str());
|
gfx::drawTextArgs(vg, x + w / 2, y + h / 2, 22, NVG_ALIGN_MIDDLE | NVG_ALIGN_CENTER, theme->GetColour(text_id), option.display_text.c_str());
|
||||||
y -= block.h + 18;
|
y -= block.h + 18;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -863,6 +773,7 @@ void EntryMenu::UpdateOptions() {
|
|||||||
return InstallApp(pbox, m_entry);
|
return InstallApp(pbox, m_entry);
|
||||||
}, [this](bool success){
|
}, [this](bool success){
|
||||||
if (success) {
|
if (success) {
|
||||||
|
App::Notify("Downloaded "_i18n + m_entry.title);
|
||||||
m_entry.status = EntryStatus::Installed;
|
m_entry.status = EntryStatus::Installed;
|
||||||
m_menu.SetDirty();
|
m_menu.SetDirty();
|
||||||
UpdateOptions();
|
UpdateOptions();
|
||||||
@@ -875,6 +786,7 @@ void EntryMenu::UpdateOptions() {
|
|||||||
return UninstallApp(pbox, m_entry);
|
return UninstallApp(pbox, m_entry);
|
||||||
}, [this](bool success){
|
}, [this](bool success){
|
||||||
if (success) {
|
if (success) {
|
||||||
|
App::Notify("Removed "_i18n + m_entry.title);
|
||||||
m_entry.status = EntryStatus::Get;
|
m_entry.status = EntryStatus::Get;
|
||||||
m_menu.SetDirty();
|
m_menu.SetDirty();
|
||||||
UpdateOptions();
|
UpdateOptions();
|
||||||
@@ -913,7 +825,7 @@ void EntryMenu::UpdateOptions() {
|
|||||||
SetIndex(0);
|
SetIndex(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntryMenu::SetIndex(std::size_t index) {
|
void EntryMenu::SetIndex(s64 index) {
|
||||||
m_index = index;
|
m_index = index;
|
||||||
const auto option = m_options[m_index];
|
const auto option = m_options[m_index];
|
||||||
if (option.confirm_text.empty()) {
|
if (option.confirm_text.empty()) {
|
||||||
@@ -941,8 +853,6 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
|
|||||||
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");
|
||||||
|
|
||||||
// m_span = m_entries;
|
|
||||||
|
|
||||||
this->SetActions(
|
this->SetActions(
|
||||||
std::make_pair(Button::RIGHT, Action{[this](){
|
std::make_pair(Button::RIGHT, Action{[this](){
|
||||||
if (m_entries_current.empty()) {
|
if (m_entries_current.empty()) {
|
||||||
@@ -967,41 +877,23 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
|
|||||||
}
|
}
|
||||||
}}),
|
}}),
|
||||||
std::make_pair(Button::DOWN, Action{[this](){
|
std::make_pair(Button::DOWN, Action{[this](){
|
||||||
if (ScrollHelperDown(m_index, m_start, 3, 9, m_entries_current.size())) {
|
if (m_list->ScrollDown(m_index, 3, m_entries_current.size())) {
|
||||||
SetIndex(m_index);
|
SetIndex(m_index);
|
||||||
}
|
}
|
||||||
}}),
|
}}),
|
||||||
std::make_pair(Button::UP, Action{[this](){
|
std::make_pair(Button::UP, Action{[this](){
|
||||||
if (m_entries_current.empty()) {
|
if (m_list->ScrollUp(m_index, 3, m_entries_current.size())) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_index >= 3) {
|
|
||||||
SetIndex(m_index - 3);
|
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
|
||||||
if (m_index < m_start ) {
|
|
||||||
// log_write("moved up\n");
|
|
||||||
m_start -= 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::R2, Action{(u8)ActionType::HELD, [this](){
|
|
||||||
if (ScrollHelperDown(m_index, m_start, 9, 9, m_entries_current.size())) {
|
|
||||||
SetIndex(m_index);
|
SetIndex(m_index);
|
||||||
}
|
}
|
||||||
}}),
|
}}),
|
||||||
std::make_pair(Button::L2, Action{(u8)ActionType::HELD, [this](){
|
std::make_pair(Button::R2, Action{[this](){
|
||||||
if (m_entries.empty()) {
|
if (m_list->ScrollDown(m_index, 9, m_entries_current.size())) {
|
||||||
return;
|
SetIndex(m_index);
|
||||||
}
|
}
|
||||||
|
}}),
|
||||||
if (m_index >= 9) {
|
std::make_pair(Button::L2, Action{[this](){
|
||||||
SetIndex(m_index - 9);
|
if (m_list->ScrollUp(m_index, 9, m_entries_current.size())) {
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
SetIndex(m_index);
|
||||||
while (m_index < m_start) {
|
|
||||||
// log_write("moved up\n");
|
|
||||||
m_start -= 3;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}),
|
}}),
|
||||||
std::make_pair(Button::A, Action{"Info"_i18n, [this](){
|
std::make_pair(Button::A, Action{"Info"_i18n, [this](){
|
||||||
@@ -1032,20 +924,20 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
|
|||||||
sort_items.push_back("Alphabetical"_i18n);
|
sort_items.push_back("Alphabetical"_i18n);
|
||||||
|
|
||||||
SidebarEntryArray::Items order_items;
|
SidebarEntryArray::Items order_items;
|
||||||
order_items.push_back("Decending"_i18n);
|
order_items.push_back("Descending"_i18n);
|
||||||
order_items.push_back("Ascending"_i18n);
|
order_items.push_back("Ascending"_i18n);
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Filter"_i18n, filter_items, [this, filter_items](std::size_t& index_out){
|
options->Add(std::make_shared<SidebarEntryArray>("Filter"_i18n, filter_items, [this, filter_items](s64& index_out){
|
||||||
SetFilter((Filter)index_out);
|
SetFilter((Filter)index_out);
|
||||||
}, (std::size_t)m_filter));
|
}, (s64)m_filter));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](std::size_t& index_out){
|
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
|
||||||
SetSort((SortType)index_out);
|
SetSort((SortType)index_out);
|
||||||
}, (std::size_t)m_sort));
|
}, (s64)m_sort));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](std::size_t& index_out){
|
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](s64& index_out){
|
||||||
SetOrder((OrderType)index_out);
|
SetOrder((OrderType)index_out);
|
||||||
}, (std::size_t)m_order));
|
}, (s64)m_order));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Search"_i18n, [this](){
|
options->Add(std::make_shared<SidebarEntryCallback>("Search"_i18n, [this](){
|
||||||
std::string out;
|
std::string out;
|
||||||
@@ -1058,52 +950,13 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
|
|||||||
);
|
);
|
||||||
|
|
||||||
m_repo_download_state = ImageDownloadState::Progress;
|
m_repo_download_state = ImageDownloadState::Progress;
|
||||||
#if 0
|
curl::Api().ToFileAsync(
|
||||||
DownloadMemoryAsync(URL_JSON, [this](std::vector<u8>& data, bool success){
|
curl::Url{URL_JSON},
|
||||||
if (success) {
|
curl::Path{REPO_PATH},
|
||||||
repo_json = data;
|
curl::Flags{curl::Flag_Cache},
|
||||||
repo_json.push_back('\0');
|
curl::StopToken{this->GetToken()},
|
||||||
m_repo_download_state = ImageDownloadState::Done;
|
curl::OnComplete{[this](auto& result){
|
||||||
if (HasFocus()) {
|
if (result.success) {
|
||||||
ScanHomebrew();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
m_repo_download_state = ImageDownloadState::Failed;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
#else
|
|
||||||
FsTimeStampRaw time_stamp{};
|
|
||||||
u64 current_time{};
|
|
||||||
bool download_file = false;
|
|
||||||
if (R_SUCCEEDED(fs.GetFsOpenResult())) {
|
|
||||||
fs.GetFileTimeStampRaw(REPO_PATH, &time_stamp);
|
|
||||||
timeGetCurrentTime(TimeType_Default, ¤t_time);
|
|
||||||
}
|
|
||||||
|
|
||||||
// this fails if we don't have the file or on fw < 3.0.0
|
|
||||||
if (!time_stamp.is_valid) {
|
|
||||||
download_file = true;
|
|
||||||
} else {
|
|
||||||
// check the date, if older than 1hour, then fetch new file
|
|
||||||
// this relaxes the spam to their server, don't want to fetch repo
|
|
||||||
// every time the user opens the app!
|
|
||||||
const auto time_file = time_stamp.created;
|
|
||||||
const auto time_cur = current_time;
|
|
||||||
const auto day = 60 * 60;
|
|
||||||
if (time_file > time_cur || time_cur - time_file >= day) {
|
|
||||||
log_write("repo.json expired, downloading new! time_file: %zu time_cur: %zu\n", time_file, time_cur);
|
|
||||||
download_file = true;
|
|
||||||
} else {
|
|
||||||
log_write("repo.json not expired! time_file: %zu time_cur: %zu\n", time_file, time_cur);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: remove me soon
|
|
||||||
// download_file = true;
|
|
||||||
|
|
||||||
if (download_file) {
|
|
||||||
DownloadFileAsync(URL_JSON, REPO_PATH, "", [this](std::vector<u8>& data, bool success){
|
|
||||||
if (success) {
|
|
||||||
m_repo_download_state = ImageDownloadState::Done;
|
m_repo_download_state = ImageDownloadState::Done;
|
||||||
if (HasFocus()) {
|
if (HasFocus()) {
|
||||||
ScanHomebrew();
|
ScanHomebrew();
|
||||||
@@ -1111,16 +964,16 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
|
|||||||
} else {
|
} else {
|
||||||
m_repo_download_state = ImageDownloadState::Failed;
|
m_repo_download_state = ImageDownloadState::Failed;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
} else {
|
});
|
||||||
m_repo_download_state = ImageDownloadState::Done;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
m_filter = (Filter)ini_getl(INI_SECTION, "filter", m_filter, App::CONFIG_PATH);
|
m_filter = (Filter)ini_getl(INI_SECTION, "filter", m_filter, App::CONFIG_PATH);
|
||||||
m_sort = (SortType)ini_getl(INI_SECTION, "sort", m_sort, App::CONFIG_PATH);
|
m_sort = (SortType)ini_getl(INI_SECTION, "sort", m_sort, App::CONFIG_PATH);
|
||||||
m_order = (OrderType)ini_getl(INI_SECTION, "order", m_order, App::CONFIG_PATH);
|
m_order = (OrderType)ini_getl(INI_SECTION, "order", m_order, App::CONFIG_PATH);
|
||||||
|
|
||||||
|
const Vec4 v{75, 110, 370, 155};
|
||||||
|
const Vec2 pad{10, 10};
|
||||||
|
m_list = std::make_unique<List>(3, 9, m_pos, v, pad);
|
||||||
Sort();
|
Sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1130,112 +983,132 @@ Menu::~Menu() {
|
|||||||
|
|
||||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||||
MenuBase::Update(controller, touch);
|
MenuBase::Update(controller, touch);
|
||||||
|
m_list->OnUpdate(controller, touch, m_entries_current.size(), [this](auto i) {
|
||||||
|
if (m_index == i) {
|
||||||
|
FireAction(Button::A);
|
||||||
|
} else {
|
||||||
|
App::PlaySoundEffect(SoundEffect_Focus);
|
||||||
|
SetIndex(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||||
MenuBase::Draw(vg, theme);
|
MenuBase::Draw(vg, theme);
|
||||||
|
|
||||||
if (m_entries.empty()) {
|
if (m_entries.empty()) {
|
||||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::YELLOW, "Loading..."_i18n.c_str());
|
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Loading..."_i18n.c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_entries_current.empty()) {
|
if (m_entries_current.empty()) {
|
||||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::YELLOW, "Empty!"_i18n.c_str());
|
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Empty!"_i18n.c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const u64 SCROLL = m_start;
|
// max images per frame, in order to not hit io / gpu too hard.
|
||||||
const u64 max_entry_display = 9;
|
const int image_load_max = 2;
|
||||||
const u64 nro_total = m_entries_current.size();
|
int image_load_count = 0;
|
||||||
const u64 cursor_pos = m_index;
|
|
||||||
|
|
||||||
// only draw scrollbar if needed
|
m_list->Draw(vg, theme, m_entries_current.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
|
||||||
if (nro_total > max_entry_display) {
|
const auto& [x, y, w, h] = v;
|
||||||
const auto scrollbar_size = 500.f;
|
const auto index = m_entries_current[pos];
|
||||||
const auto sb_h = 3.f / (float)nro_total * scrollbar_size;
|
auto& e = m_entries[index];
|
||||||
const auto sb_y = SCROLL / 3.f;
|
auto& image = e.image;
|
||||||
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
|
|
||||||
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * 2) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (u64 i = 0, pos = SCROLL, y = 110, w = 370, h = 155; pos < nro_total && i < max_entry_display; y += h + 10) {
|
// try and load cached image.
|
||||||
for (u64 j = 0, x = 75; j < 3 && pos < nro_total && i < max_entry_display; j++, i++, pos++, x += w + 10) {
|
if (image_load_count < image_load_max && !image.image && !image.tried_cache) {
|
||||||
const auto index = m_entries_current[pos];
|
image.tried_cache = true;
|
||||||
auto& e = m_entries[index];
|
image.cached = EntryLoadImageFile(BuildIconCachePath(e), image);
|
||||||
|
if (image.cached) {
|
||||||
// lazy load image
|
image_load_count++;
|
||||||
if (!e.image.image) {
|
|
||||||
switch (e.image.state) {
|
|
||||||
case ImageDownloadState::None: {
|
|
||||||
const auto path = BuildIconCachePath(e);
|
|
||||||
if (fs::FsNativeSd().FileExists(path)) {
|
|
||||||
EntryLoadImageFile(path, e.image);
|
|
||||||
} else {
|
|
||||||
const auto url = BuildIconUrl(e);
|
|
||||||
e.image.state = ImageDownloadState::Progress;
|
|
||||||
DownloadFileAsync(url, path, "", [this, index](std::vector<u8>& data, bool success) {
|
|
||||||
if (success) {
|
|
||||||
m_entries[index].image.state = ImageDownloadState::Done;
|
|
||||||
} else {
|
|
||||||
m_entries[index].image.state = ImageDownloadState::Failed;
|
|
||||||
log_write("failed to download image\n");
|
|
||||||
}
|
|
||||||
}, nullptr, DownloadPriority::High);
|
|
||||||
}
|
|
||||||
} break;
|
|
||||||
case ImageDownloadState::Progress: {
|
|
||||||
|
|
||||||
} break;
|
|
||||||
case ImageDownloadState::Done: {
|
|
||||||
EntryLoadImageFile(BuildIconCachePath(e), e.image);
|
|
||||||
} break;
|
|
||||||
case ImageDownloadState::Failed: {
|
|
||||||
} break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto text_id = ThemeEntryID_TEXT;
|
|
||||||
if (pos == cursor_pos) {
|
|
||||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
|
||||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
|
|
||||||
} else {
|
|
||||||
DrawElement(x, y, w, h, ThemeEntryID_GRID);
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr double image_scale = 256.0 / 115.0;
|
|
||||||
// const float image_size = 256 / image_scale;
|
|
||||||
// const float image_size_h = 150 / image_scale;
|
|
||||||
DrawIcon(vg, e.image, m_default_image, x + 20, y + 20, 115, 115, true, image_scale);
|
|
||||||
// gfx::drawImage(vg, x + 20, y + 20, image_size, image_size_h, e.image.image ? e.image.image : m_default_image);
|
|
||||||
|
|
||||||
nvgSave(vg);
|
|
||||||
nvgScissor(vg, x, y, w - 30.f, h); // clip
|
|
||||||
{
|
|
||||||
const float font_size = 18;
|
|
||||||
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.title.c_str());
|
|
||||||
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.author.c_str());
|
|
||||||
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.version.c_str());
|
|
||||||
}
|
|
||||||
nvgRestore(vg);
|
|
||||||
|
|
||||||
float i_size = 22;
|
|
||||||
switch (e.status) {
|
|
||||||
case EntryStatus::Get:
|
|
||||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image);
|
|
||||||
break;
|
|
||||||
case EntryStatus::Installed:
|
|
||||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image);
|
|
||||||
break;
|
|
||||||
case EntryStatus::Local:
|
|
||||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image);
|
|
||||||
break;
|
|
||||||
case EntryStatus::Update:
|
|
||||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// lazy load image
|
||||||
|
if (!image.image || image.cached) {
|
||||||
|
switch (image.state) {
|
||||||
|
case ImageDownloadState::None: {
|
||||||
|
const auto path = BuildIconCachePath(e);
|
||||||
|
const auto url = BuildIconUrl(e);
|
||||||
|
image.state = ImageDownloadState::Progress;
|
||||||
|
curl::Api().ToFileAsync(
|
||||||
|
curl::Url{url},
|
||||||
|
curl::Path{path},
|
||||||
|
curl::Flags{curl::Flag_Cache},
|
||||||
|
curl::StopToken{this->GetToken()},
|
||||||
|
curl::OnComplete{[this, &image](auto& result) {
|
||||||
|
if (result.success) {
|
||||||
|
image.state = ImageDownloadState::Done;
|
||||||
|
// data hasn't changed
|
||||||
|
if (result.code == 304) {
|
||||||
|
image.cached = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
image.state = ImageDownloadState::Failed;
|
||||||
|
log_write("failed to download image\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} break;
|
||||||
|
case ImageDownloadState::Progress: {
|
||||||
|
|
||||||
|
} break;
|
||||||
|
case ImageDownloadState::Done: {
|
||||||
|
if (image_load_count < image_load_max) {
|
||||||
|
image.cached = false;
|
||||||
|
if (!EntryLoadImageFile(BuildIconCachePath(e), e.image)) {
|
||||||
|
image.state = ImageDownloadState::Failed;
|
||||||
|
} else {
|
||||||
|
image_load_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case ImageDownloadState::Failed: {
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto text_id = ThemeEntryID_TEXT;
|
||||||
|
if (pos == m_index) {
|
||||||
|
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||||
|
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||||
|
} else {
|
||||||
|
DrawElement(x, y, w, h, ThemeEntryID_GRID);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr double image_scale = 256.0 / 115.0;
|
||||||
|
// const float image_size = 256 / image_scale;
|
||||||
|
// const float image_size_h = 150 / image_scale;
|
||||||
|
DrawIcon(vg, e.image, m_default_image, x + 20, y + 20, 115, 115, true, image_scale);
|
||||||
|
// gfx::drawImage(vg, x + 20, y + 20, image_size, image_size_h, image.image ? image.image : m_default_image);
|
||||||
|
|
||||||
|
nvgSave(vg);
|
||||||
|
nvgIntersectScissor(vg, v.x, v.y, w - 30.f, h); // clip
|
||||||
|
{
|
||||||
|
const float font_size = 18;
|
||||||
|
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.title.c_str());
|
||||||
|
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.author.c_str());
|
||||||
|
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.version.c_str());
|
||||||
|
}
|
||||||
|
nvgRestore(vg);
|
||||||
|
|
||||||
|
float i_size = 22;
|
||||||
|
switch (e.status) {
|
||||||
|
case EntryStatus::Get:
|
||||||
|
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image);
|
||||||
|
break;
|
||||||
|
case EntryStatus::Installed:
|
||||||
|
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image);
|
||||||
|
break;
|
||||||
|
case EntryStatus::Local:
|
||||||
|
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image);
|
||||||
|
break;
|
||||||
|
case EntryStatus::Update:
|
||||||
|
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Menu::OnFocusGained() {
|
void Menu::OnFocusGained() {
|
||||||
@@ -1264,21 +1137,16 @@ void Menu::OnFocusGained() {
|
|||||||
if (m_dirty) {
|
if (m_dirty) {
|
||||||
m_dirty = false;
|
m_dirty = false;
|
||||||
const auto& current_entry = m_entries[m_entries_current[m_index]];
|
const auto& current_entry = m_entries[m_entries_current[m_index]];
|
||||||
// m_start = 0;
|
|
||||||
// m_index = 0;
|
|
||||||
log_write("\nold index: %zu start: %zu\n", m_index, m_start);
|
|
||||||
// old index: 19 start: 12
|
|
||||||
Sort();
|
Sort();
|
||||||
|
|
||||||
for (u32 i = 0; i < m_entries_current.size(); i++) {
|
for (u32 i = 0; i < m_entries_current.size(); i++) {
|
||||||
if (current_entry.name == m_entries[m_entries_current[i]].name) {
|
if (current_entry.name == m_entries[m_entries_current[i]].name) {
|
||||||
SetIndex(i);
|
SetIndex(i);
|
||||||
if (i >= 9) {
|
if (i >= 9) {
|
||||||
m_start = (i - 9) / 3 * 3 + 3;
|
m_list->SetYoff((((i - 9) + 3) / 3) * m_list->GetMaxY());
|
||||||
} else {
|
} else {
|
||||||
m_start = 0;
|
m_list->SetYoff(0);
|
||||||
}
|
}
|
||||||
log_write("\nnew index: %zu start: %zu\n", m_index, m_start);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1286,10 +1154,10 @@ void Menu::OnFocusGained() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Menu::SetIndex(std::size_t index) {
|
void Menu::SetIndex(s64 index) {
|
||||||
m_index = index;
|
m_index = index;
|
||||||
if (!m_index) {
|
if (!m_index) {
|
||||||
m_start = 0;
|
m_list->SetYoff(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
this->SetSubHeading(std::to_string(m_index + 1) + " / " + std::to_string(m_entries_current.size()));
|
this->SetSubHeading(std::to_string(m_index + 1) + " / " + std::to_string(m_entries_current.size()));
|
||||||
@@ -1392,7 +1260,7 @@ void Menu::Sort() {
|
|||||||
case SortType_Updated: {
|
case SortType_Updated: {
|
||||||
if (lhs.updated_num == rhs.updated_num) {
|
if (lhs.updated_num == rhs.updated_num) {
|
||||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
||||||
} else if (m_order == OrderType_Decending) {
|
} else if (m_order == OrderType_Descending) {
|
||||||
return lhs.updated_num > rhs.updated_num;
|
return lhs.updated_num > rhs.updated_num;
|
||||||
} else {
|
} else {
|
||||||
return lhs.updated_num < rhs.updated_num;
|
return lhs.updated_num < rhs.updated_num;
|
||||||
@@ -1401,7 +1269,7 @@ void Menu::Sort() {
|
|||||||
case SortType_Downloads: {
|
case SortType_Downloads: {
|
||||||
if (lhs.app_dls == rhs.app_dls) {
|
if (lhs.app_dls == rhs.app_dls) {
|
||||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
||||||
} else if (m_order == OrderType_Decending) {
|
} else if (m_order == OrderType_Descending) {
|
||||||
return lhs.app_dls > rhs.app_dls;
|
return lhs.app_dls > rhs.app_dls;
|
||||||
} else {
|
} else {
|
||||||
return lhs.app_dls < rhs.app_dls;
|
return lhs.app_dls < rhs.app_dls;
|
||||||
@@ -1410,14 +1278,14 @@ void Menu::Sort() {
|
|||||||
case SortType_Size: {
|
case SortType_Size: {
|
||||||
if (lhs.extracted == rhs.extracted) {
|
if (lhs.extracted == rhs.extracted) {
|
||||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
||||||
} else if (m_order == OrderType_Decending) {
|
} else if (m_order == OrderType_Descending) {
|
||||||
return lhs.extracted > rhs.extracted;
|
return lhs.extracted > rhs.extracted;
|
||||||
} else {
|
} else {
|
||||||
return lhs.extracted < rhs.extracted;
|
return lhs.extracted < rhs.extracted;
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
case SortType_Alphabetical: {
|
case SortType_Alphabetical: {
|
||||||
if (m_order == OrderType_Decending) {
|
if (m_order == OrderType_Descending) {
|
||||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
||||||
} else {
|
} else {
|
||||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) > 0;
|
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) > 0;
|
||||||
@@ -1484,9 +1352,10 @@ void Menu::SetSearch(const std::string& term) {
|
|||||||
SetFilter(m_filter);
|
SetFilter(m_filter);
|
||||||
SetIndex(m_entry_search_jump_back);
|
SetIndex(m_entry_search_jump_back);
|
||||||
if (m_entry_search_jump_back >= 9) {
|
if (m_entry_search_jump_back >= 9) {
|
||||||
m_start = (m_entry_search_jump_back - 9) / 3 * 3 + 3;
|
m_list->SetYoff(0);
|
||||||
|
m_list->SetYoff((((m_entry_search_jump_back - 9) + 3) / 3) * m_list->GetMaxY());
|
||||||
} else {
|
} else {
|
||||||
m_start = 0;
|
m_list->SetYoff(0);
|
||||||
}
|
}
|
||||||
}});
|
}});
|
||||||
|
|
||||||
@@ -1517,11 +1386,12 @@ void Menu::SetAuthor() {
|
|||||||
} else {
|
} else {
|
||||||
SetFilter(m_filter);
|
SetFilter(m_filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
SetIndex(m_entry_author_jump_back);
|
SetIndex(m_entry_author_jump_back);
|
||||||
if (m_entry_author_jump_back >= 9) {
|
if (m_entry_author_jump_back >= 9) {
|
||||||
m_start = (m_entry_author_jump_back - 9) / 3 * 3 + 3;
|
m_list->SetYoff((((m_entry_author_jump_back - 9) + 3) / 3) * m_list->GetMaxY());
|
||||||
} else {
|
} else {
|
||||||
m_start = 0;
|
m_list->SetYoff(0);
|
||||||
}
|
}
|
||||||
}});
|
}});
|
||||||
|
|
||||||
|
|||||||
526
sphaira/source/ui/menus/ghdl.cpp
Normal file
@@ -0,0 +1,526 @@
|
|||||||
|
#include "ui/menus/ghdl.hpp"
|
||||||
|
#include "ui/sidebar.hpp"
|
||||||
|
#include "ui/option_box.hpp"
|
||||||
|
#include "ui/popup_list.hpp"
|
||||||
|
#include "ui/progress_box.hpp"
|
||||||
|
#include "ui/error_box.hpp"
|
||||||
|
|
||||||
|
#include "log.hpp"
|
||||||
|
#include "app.hpp"
|
||||||
|
#include "ui/nvg_util.hpp"
|
||||||
|
#include "fs.hpp"
|
||||||
|
#include "defines.hpp"
|
||||||
|
#include "image.hpp"
|
||||||
|
#include "download.hpp"
|
||||||
|
#include "i18n.hpp"
|
||||||
|
#include "yyjson_helper.hpp"
|
||||||
|
|
||||||
|
#include <minIni.h>
|
||||||
|
#include <minizip/unzip.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace sphaira::ui::menu::gh {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto CACHE_PATH = "/switch/sphaira/cache/github";
|
||||||
|
|
||||||
|
auto GenerateApiUrl(const Entry& e) {
|
||||||
|
if (e.tag == "latest") {
|
||||||
|
return "https://api.github.com/repos/" + e.owner + "/" + e.repo + "/releases/latest";
|
||||||
|
} else {
|
||||||
|
return "https://api.github.com/repos/" + e.owner + "/" + e.repo + "/releases/tags/" + e.tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto apiBuildAssetCache(const std::string& url) -> fs::FsPath {
|
||||||
|
fs::FsPath path;
|
||||||
|
std::snprintf(path, sizeof(path), "%s/%u.json", CACHE_PATH, crc32Calculate(url.data(), url.size()));
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void from_json(yyjson_val* json, AssetEntry& e) {
|
||||||
|
JSON_OBJ_ITR(
|
||||||
|
JSON_SET_STR(name);
|
||||||
|
JSON_SET_STR(path);
|
||||||
|
JSON_SET_STR(pre_install_message);
|
||||||
|
JSON_SET_STR(post_install_message);
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void from_json(const fs::FsPath& path, Entry& e) {
|
||||||
|
JSON_INIT_VEC_FILE(path, nullptr, nullptr);
|
||||||
|
JSON_OBJ_ITR(
|
||||||
|
JSON_SET_STR(url);
|
||||||
|
JSON_SET_STR(owner);
|
||||||
|
JSON_SET_STR(repo);
|
||||||
|
JSON_SET_STR(tag);
|
||||||
|
JSON_SET_STR(pre_install_message);
|
||||||
|
JSON_SET_STR(post_install_message);
|
||||||
|
JSON_SET_ARR_OBJ(assets);
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void from_json(yyjson_val* json, GhApiAsset& e) {
|
||||||
|
JSON_OBJ_ITR(
|
||||||
|
JSON_SET_STR(name);
|
||||||
|
JSON_SET_STR(content_type);
|
||||||
|
JSON_SET_UINT(size);
|
||||||
|
JSON_SET_UINT(download_count);
|
||||||
|
JSON_SET_STR(browser_download_url);
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void from_json(const fs::FsPath& path, GhApiEntry& e) {
|
||||||
|
JSON_INIT_VEC_FILE(path, nullptr, nullptr);
|
||||||
|
JSON_OBJ_ITR(
|
||||||
|
JSON_SET_STR(tag_name);
|
||||||
|
JSON_SET_STR(name);
|
||||||
|
JSON_SET_ARR_OBJ(assets);
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto DownloadApp(ProgressBox* pbox, const GhApiAsset& gh_asset, const AssetEntry* entry) -> bool {
|
||||||
|
static const fs::FsPath temp_file{"/switch/sphaira/cache/github/ghdl.temp"};
|
||||||
|
constexpr auto chunk_size = 1024 * 512; // 512KiB
|
||||||
|
|
||||||
|
fs::FsNativeSd fs;
|
||||||
|
R_TRY_RESULT(fs.GetFsOpenResult(), false);
|
||||||
|
ON_SCOPE_EXIT(fs.DeleteFile(temp_file));
|
||||||
|
|
||||||
|
if (gh_asset.browser_download_url.empty()) {
|
||||||
|
log_write("failed to find asset\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. download the asset
|
||||||
|
if (!pbox->ShouldExit()) {
|
||||||
|
pbox->NewTransfer("Downloading "_i18n + gh_asset.name);
|
||||||
|
log_write("starting download: %s\n", gh_asset.browser_download_url.c_str());
|
||||||
|
|
||||||
|
if (!curl::Api().ToFile(
|
||||||
|
curl::Url{gh_asset.browser_download_url},
|
||||||
|
curl::Path{temp_file},
|
||||||
|
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||||
|
).success){
|
||||||
|
log_write("error with download\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::FsPath root_path{"/"};
|
||||||
|
if (entry && !entry->path.empty()) {
|
||||||
|
root_path = entry->path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. extract the zip / file
|
||||||
|
if (gh_asset.content_type.find("zip") != gh_asset.content_type.npos) {
|
||||||
|
log_write("found zip\n");
|
||||||
|
auto zfile = unzOpen64(temp_file);
|
||||||
|
if (!zfile) {
|
||||||
|
log_write("failed to open zip: %s\n", temp_file.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;
|
||||||
|
fs::FsPath file_path;
|
||||||
|
if (UNZ_OK != unzGetCurrentFileInfo64(zfile, &info, file_path, sizeof(file_path), 0, 0, 0, 0)) {
|
||||||
|
log_write("failed to get current info\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
file_path = fs::AppendPath(root_path, file_path);
|
||||||
|
|
||||||
|
Result rc;
|
||||||
|
if (file_path[strlen(file_path) -1] == '/') {
|
||||||
|
if (R_FAILED(rc = fs.CreateDirectoryRecursively(file_path)) && rc != FsError_PathAlreadyExists) {
|
||||||
|
log_write("failed to create folder: %s 0x%04X\n", file_path.s, rc);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (R_FAILED(rc = fs.CreateDirectoryRecursivelyWithPath(file_path)) && rc != FsError_PathAlreadyExists) {
|
||||||
|
log_write("failed to create folder: %s 0x%04X\n", file_path.s, rc);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
|
||||||
|
if (bytes_read <= 0) {
|
||||||
|
log_write("failed to read zip file: %s\n", file_path.s);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fs.CreateDirectoryRecursivelyWithPath(root_path);
|
||||||
|
fs.DeleteFile(root_path);
|
||||||
|
if (R_FAILED(fs.RenameFile(temp_file, root_path))) {
|
||||||
|
log_write("failed to rename file: %s -> %s\n", temp_file.s, root_path.s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log_write("success\n");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto DownloadAssetJson(ProgressBox* pbox, const std::string& url, GhApiEntry& out) -> bool {
|
||||||
|
// 1. download the json
|
||||||
|
if (!pbox->ShouldExit()) {
|
||||||
|
pbox->NewTransfer("Downloading json"_i18n);
|
||||||
|
log_write("starting download\n");
|
||||||
|
|
||||||
|
const auto path = apiBuildAssetCache(url);
|
||||||
|
|
||||||
|
const auto result = curl::Api().ToFile(
|
||||||
|
curl::Url{url},
|
||||||
|
curl::Path{path},
|
||||||
|
curl::OnProgress{pbox->OnDownloadProgressCallback()},
|
||||||
|
curl::Flags{curl::Flag_Cache},
|
||||||
|
curl::Header{
|
||||||
|
{ "Accept", "application/vnd.github+json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
log_write("json empty\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
from_json(result.path, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !out.assets.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Menu::Menu() : MenuBase{"GitHub"_i18n} {
|
||||||
|
fs::FsNativeSd().CreateDirectoryRecursively(CACHE_PATH);
|
||||||
|
|
||||||
|
this->SetActions(
|
||||||
|
std::make_pair(Button::DOWN, Action{[this](){
|
||||||
|
if (m_list->ScrollDown(m_index, 1, m_entries.size())) {
|
||||||
|
SetIndex(m_index);
|
||||||
|
}
|
||||||
|
}}),
|
||||||
|
std::make_pair(Button::UP, Action{[this](){
|
||||||
|
if (m_list->ScrollUp(m_index, 1, m_entries.size())) {
|
||||||
|
SetIndex(m_index);
|
||||||
|
}
|
||||||
|
}}),
|
||||||
|
std::make_pair(Button::DPAD_RIGHT, Action{[this](){
|
||||||
|
if (m_list->ScrollDown(m_index, 8, m_entries.size())) {
|
||||||
|
SetIndex(m_index);
|
||||||
|
}
|
||||||
|
}}),
|
||||||
|
std::make_pair(Button::DPAD_LEFT, Action{[this](){
|
||||||
|
if (m_list->ScrollUp(m_index, 8, m_entries.size())) {
|
||||||
|
SetIndex(m_index);
|
||||||
|
}
|
||||||
|
}}),
|
||||||
|
|
||||||
|
std::make_pair(Button::A, Action{"Download"_i18n, [this](){
|
||||||
|
if (m_entries.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hack
|
||||||
|
static GhApiEntry gh_entry;
|
||||||
|
gh_entry = {};
|
||||||
|
|
||||||
|
App::Push(std::make_shared<ProgressBox>("Downloading "_i18n + GetEntry().repo, [this](auto pbox){
|
||||||
|
return DownloadAssetJson(pbox, GenerateApiUrl(GetEntry()), gh_entry);
|
||||||
|
}, [this](bool success){
|
||||||
|
if (success) {
|
||||||
|
const auto& assets = GetEntry().assets;
|
||||||
|
PopupList::Items asset_items;
|
||||||
|
std::vector<const AssetEntry*> asset_ptr;
|
||||||
|
std::vector<GhApiAsset> api_assets;
|
||||||
|
bool using_name = false;
|
||||||
|
|
||||||
|
for (auto&p : gh_entry.assets) {
|
||||||
|
bool found = false;
|
||||||
|
|
||||||
|
for (auto& e : assets) {
|
||||||
|
if (!e.name.empty()) {
|
||||||
|
using_name = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.name.find(e.name) != p.name.npos) {
|
||||||
|
found = true;
|
||||||
|
asset_ptr.emplace_back(&e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!using_name || found) {
|
||||||
|
asset_items.emplace_back(p.name);
|
||||||
|
api_assets.emplace_back(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
App::Push(std::make_shared<PopupList>("Select asset to download for "_i18n + GetEntry().repo, asset_items, [this, api_assets, asset_ptr](auto op_index){
|
||||||
|
if (!op_index) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto index = *op_index;
|
||||||
|
const auto& asset_entry = api_assets[index];
|
||||||
|
const AssetEntry* ptr{};
|
||||||
|
auto pre_install_message = GetEntry().pre_install_message;
|
||||||
|
if (asset_ptr.size()) {
|
||||||
|
ptr = asset_ptr[index];
|
||||||
|
if (!ptr->pre_install_message.empty()) {
|
||||||
|
pre_install_message = ptr->pre_install_message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto func = [this, &asset_entry, ptr](){
|
||||||
|
App::Push(std::make_shared<ProgressBox>("Downloading "_i18n + GetEntry().repo, [this, &asset_entry, ptr](auto pbox){
|
||||||
|
return DownloadApp(pbox, asset_entry, ptr);
|
||||||
|
}, [this, ptr](bool success){
|
||||||
|
if (success) {
|
||||||
|
App::Notify("Downloaded "_i18n + GetEntry().repo);
|
||||||
|
auto post_install_message = GetEntry().post_install_message;
|
||||||
|
if (ptr && !ptr->post_install_message.empty()) {
|
||||||
|
post_install_message = ptr->post_install_message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!post_install_message.empty()) {
|
||||||
|
App::Push(std::make_shared<OptionBox>(post_install_message, "OK"_i18n));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 2));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!pre_install_message.empty()) {
|
||||||
|
App::Push(std::make_shared<OptionBox>(
|
||||||
|
pre_install_message,
|
||||||
|
"Back"_i18n, "Download"_i18n, 1, [this, func](auto op_index){
|
||||||
|
if (op_index && *op_index) {
|
||||||
|
func();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
func();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, 2));
|
||||||
|
}}),
|
||||||
|
|
||||||
|
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||||
|
SetPop();
|
||||||
|
}})
|
||||||
|
);
|
||||||
|
|
||||||
|
const Vec4 v{75, GetY() + 1.f + 42.f, 1220.f-45.f*2, 60};
|
||||||
|
m_list = std::make_unique<List>(1, 8, m_pos, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu::~Menu() {
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||||
|
MenuBase::Update(controller, touch);
|
||||||
|
m_list->OnUpdate(controller, touch, m_entries.size(), [this](auto i) {
|
||||||
|
if (m_index == i) {
|
||||||
|
FireAction(Button::A);
|
||||||
|
} else {
|
||||||
|
App::PlaySoundEffect(SoundEffect_Focus);
|
||||||
|
SetIndex(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||||
|
MenuBase::Draw(vg, theme);
|
||||||
|
|
||||||
|
const auto& text_col = theme->GetColour(ThemeEntryID_TEXT);
|
||||||
|
|
||||||
|
if (m_entries.empty()) {
|
||||||
|
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Empty..."_i18n.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr float text_xoffset{15.f};
|
||||||
|
|
||||||
|
m_list->Draw(vg, theme, m_entries.size(), [this, text_col](auto* vg, auto* theme, auto v, auto i) {
|
||||||
|
const auto& [x, y, w, h] = v;
|
||||||
|
auto& e = m_entries[i];
|
||||||
|
|
||||||
|
auto text_id = ThemeEntryID_TEXT;
|
||||||
|
if (m_index == i) {
|
||||||
|
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||||
|
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||||
|
} else {
|
||||||
|
if (i != m_entries.size() - 1) {
|
||||||
|
gfx::drawRect(vg, x, y + h, w, 1.f, theme->GetColour(ThemeEntryID_LINE_SEPARATOR));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nvgSave(vg);
|
||||||
|
nvgIntersectScissor(vg, x + text_xoffset, y, w-(x+text_xoffset+50), h);
|
||||||
|
gfx::drawTextArgs(vg, x + text_xoffset, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s By %s", e.repo.c_str(), e.owner.c_str());
|
||||||
|
nvgRestore(vg);
|
||||||
|
|
||||||
|
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "version: %s", e.tag.c_str());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::OnFocusGained() {
|
||||||
|
MenuBase::OnFocusGained();
|
||||||
|
if (m_entries.empty()) {
|
||||||
|
Scan();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::SetIndex(s64 index) {
|
||||||
|
m_index = index;
|
||||||
|
if (!m_index) {
|
||||||
|
m_list->SetYoff(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetTitleSubHeading(m_entries[m_index].json_path);
|
||||||
|
UpdateSubheading();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::Scan() {
|
||||||
|
m_entries.clear();
|
||||||
|
|
||||||
|
// load from romfs first
|
||||||
|
if (R_SUCCEEDED(romfsInit())) {
|
||||||
|
LoadEntriesFromPath("romfs:/github/");
|
||||||
|
romfsExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// then load custom entries
|
||||||
|
LoadEntriesFromPath("/config/sphaira/github/");
|
||||||
|
Sort();
|
||||||
|
SetIndex(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::LoadEntriesFromPath(const fs::FsPath& path) {
|
||||||
|
auto dir = opendir(path);
|
||||||
|
if (!dir) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ON_SCOPE_EXIT(closedir(dir));
|
||||||
|
|
||||||
|
while (auto d = readdir(dir)) {
|
||||||
|
if (d->d_name[0] == '.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d->d_type != DT_REG) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto ext = std::strrchr(d->d_name, '.');
|
||||||
|
if (!ext || strcasecmp(ext, ".json")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entry entry{};
|
||||||
|
const auto full_path = fs::AppendPath(path, d->d_name);
|
||||||
|
from_json(full_path, entry);
|
||||||
|
|
||||||
|
// parse owner and author from url (if needed).
|
||||||
|
if (!entry.url.empty()) {
|
||||||
|
const auto s = entry.url.substr(std::strlen("https://github.com/"));
|
||||||
|
const auto it = s.find('/');
|
||||||
|
if (it != s.npos) {
|
||||||
|
entry.owner = s.substr(0, it);
|
||||||
|
entry.repo = s.substr(it + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that we have a owner and repo
|
||||||
|
if (entry.owner.empty() || entry.repo.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.tag.empty()) {
|
||||||
|
entry.tag = "latest";
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.json_path = full_path;
|
||||||
|
m_entries.emplace_back(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::Sort() {
|
||||||
|
const auto sorter = [this](Entry& lhs, Entry& rhs) -> bool {
|
||||||
|
// handle fallback if multiple entries are added with the same name
|
||||||
|
// used for forks of a project.
|
||||||
|
// in the rare case of the user adding the same owner and repo,
|
||||||
|
// fallback to the filepath, which *is* unqiue
|
||||||
|
auto r = strcasecmp(lhs.repo.c_str(), rhs.repo.c_str());
|
||||||
|
if (!r) {
|
||||||
|
r = strcasecmp(lhs.owner.c_str(), rhs.owner.c_str());
|
||||||
|
if (!r) {
|
||||||
|
r = strcasecmp(lhs.json_path, rhs.json_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r < 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::sort(m_entries.begin(), m_entries.end(), sorter);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::UpdateSubheading() {
|
||||||
|
const auto index = m_entries.empty() ? 0 : m_index + 1;
|
||||||
|
this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::ui::menu::gh
|
||||||
@@ -20,7 +20,7 @@ namespace {
|
|||||||
auto GenerateStarPath(const fs::FsPath& nro_path) -> fs::FsPath {
|
auto GenerateStarPath(const fs::FsPath& nro_path) -> fs::FsPath {
|
||||||
fs::FsPath out{};
|
fs::FsPath out{};
|
||||||
const auto dilem = std::strrchr(nro_path.s, '/');
|
const auto dilem = std::strrchr(nro_path.s, '/');
|
||||||
std::snprintf(out, sizeof(out), "%.*s.%s.star", dilem - nro_path.s + 1, nro_path.s, dilem + 1);
|
std::snprintf(out, sizeof(out), "%.*s.%s.star", int(dilem - nro_path.s + 1), nro_path.s, dilem + 1);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,27 +43,23 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
|||||||
}
|
}
|
||||||
}}),
|
}}),
|
||||||
std::make_pair(Button::DOWN, Action{[this](){
|
std::make_pair(Button::DOWN, Action{[this](){
|
||||||
if (m_index < (m_entries.size() - 1)) {
|
if (m_list->ScrollDown(m_index, 3, m_entries.size())) {
|
||||||
if (m_index < (m_entries.size() - 3)) {
|
SetIndex(m_index);
|
||||||
SetIndex(m_index + 3);
|
|
||||||
} else {
|
|
||||||
SetIndex(m_entries.size() - 1);
|
|
||||||
}
|
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
|
||||||
if (m_index - m_start >= 9) {
|
|
||||||
log_write("moved down\n");
|
|
||||||
m_start += 3;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}),
|
}}),
|
||||||
std::make_pair(Button::UP, Action{[this](){
|
std::make_pair(Button::UP, Action{[this](){
|
||||||
if (m_index >= 3) {
|
if (m_list->ScrollUp(m_index, 3, m_entries.size())) {
|
||||||
SetIndex(m_index - 3);
|
SetIndex(m_index);
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
}
|
||||||
if (m_index < m_start ) {
|
}}),
|
||||||
// log_write("moved up\n");
|
std::make_pair(Button::R2, Action{[this](){
|
||||||
m_start -= 3;
|
if (m_list->ScrollDown(m_index, 9, m_entries.size())) {
|
||||||
}
|
SetIndex(m_index);
|
||||||
|
}
|
||||||
|
}}),
|
||||||
|
std::make_pair(Button::L2, Action{[this](){
|
||||||
|
if (m_list->ScrollUp(m_index, 9, m_entries.size())) {
|
||||||
|
SetIndex(m_index);
|
||||||
}
|
}
|
||||||
}}),
|
}}),
|
||||||
std::make_pair(Button::A, Action{"Launch"_i18n, [this](){
|
std::make_pair(Button::A, Action{"Launch"_i18n, [this](){
|
||||||
@@ -87,15 +83,15 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
|||||||
sort_items.push_back("Size (Star)"_i18n);
|
sort_items.push_back("Size (Star)"_i18n);
|
||||||
|
|
||||||
SidebarEntryArray::Items order_items;
|
SidebarEntryArray::Items order_items;
|
||||||
order_items.push_back("Decending"_i18n);
|
order_items.push_back("Descending"_i18n);
|
||||||
order_items.push_back("Ascending"_i18n);
|
order_items.push_back("Ascending"_i18n);
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](std::size_t& index_out){
|
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
|
||||||
m_sort.Set(index_out);
|
m_sort.Set(index_out);
|
||||||
SortAndFindLastFile();
|
SortAndFindLastFile();
|
||||||
}, m_sort.Get()));
|
}, m_sort.Get()));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](std::size_t& index_out){
|
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](s64& index_out){
|
||||||
m_order.Set(index_out);
|
m_order.Set(index_out);
|
||||||
SortAndFindLastFile();
|
SortAndFindLastFile();
|
||||||
}, m_order.Get()));
|
}, m_order.Get()));
|
||||||
@@ -145,6 +141,10 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
|||||||
}
|
}
|
||||||
}})
|
}})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const Vec4 v{75, 110, 370, 155};
|
||||||
|
const Vec2 pad{10, 10};
|
||||||
|
m_list = std::make_unique<List>(3, 9, m_pos, v, pad);
|
||||||
}
|
}
|
||||||
|
|
||||||
Menu::~Menu() {
|
Menu::~Menu() {
|
||||||
@@ -157,68 +157,71 @@ Menu::~Menu() {
|
|||||||
|
|
||||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||||
MenuBase::Update(controller, touch);
|
MenuBase::Update(controller, touch);
|
||||||
|
m_list->OnUpdate(controller, touch, m_entries.size(), [this](auto i) {
|
||||||
|
if (m_index == i) {
|
||||||
|
FireAction(Button::A);
|
||||||
|
} else {
|
||||||
|
App::PlaySoundEffect(SoundEffect_Focus);
|
||||||
|
SetIndex(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||||
MenuBase::Draw(vg, theme);
|
MenuBase::Draw(vg, theme);
|
||||||
|
|
||||||
const u64 SCROLL = m_start;
|
// max images per frame, in order to not hit io / gpu too hard.
|
||||||
const u64 max_entry_display = 9;
|
const int image_load_max = 2;
|
||||||
const u64 nro_total = m_entries.size();
|
int image_load_count = 0;
|
||||||
const u64 cursor_pos = m_index;
|
|
||||||
fs::FsNativeSd fs;
|
|
||||||
|
|
||||||
// only draw scrollbar if needed
|
m_list->Draw(vg, theme, m_entries.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
|
||||||
if (nro_total > max_entry_display) {
|
const auto& [x, y, w, h] = v;
|
||||||
const auto scrollbar_size = 500.f;
|
auto& e = m_entries[pos];
|
||||||
const auto sb_h = 3.f / (float)nro_total * scrollbar_size;
|
|
||||||
const auto sb_y = SCROLL / 3.f;
|
|
||||||
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
|
|
||||||
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * 2) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (u64 i = 0, pos = SCROLL, y = 110, w = 370, h = 155; pos < nro_total && i < max_entry_display; y += h + 10) {
|
// lazy load image
|
||||||
for (u64 j = 0, x = 75; j < 3 && pos < nro_total && i < max_entry_display; j++, i++, pos++, x += w + 10) {
|
if (image_load_count < image_load_max) {
|
||||||
auto& e = m_entries[pos];
|
if (!e.image && e.icon_size && e.icon_offset) {
|
||||||
|
// NOTE: it seems that images can be any size. SuperTux uses a 1024x1024
|
||||||
// lazy load image
|
// ~300Kb image, which takes a few frames to completely load.
|
||||||
if (!e.image && e.icon.empty() && e.icon_size && e.icon_offset) {
|
// really, switch-tools should handle this by resizing the image before
|
||||||
e.icon = nro_get_icon(e.path, e.icon_size, e.icon_offset);
|
// adding it to the nro, as well as validate its a valid jpeg.
|
||||||
if (!e.icon.empty()) {
|
const auto icon = nro_get_icon(e.path, e.icon_size, e.icon_offset);
|
||||||
e.image = nvgCreateImageMem(vg, 0, e.icon.data(), e.icon.size());
|
if (!icon.empty()) {
|
||||||
|
e.image = nvgCreateImageMem(vg, 0, icon.data(), icon.size());
|
||||||
|
image_load_count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto text_id = ThemeEntryID_TEXT;
|
|
||||||
if (pos == cursor_pos) {
|
|
||||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
|
||||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
|
|
||||||
} else {
|
|
||||||
DrawElement(x, y, w, h, ThemeEntryID_GRID);
|
|
||||||
}
|
|
||||||
|
|
||||||
const float image_size = 115;
|
|
||||||
gfx::drawImageRounded(vg, x + 20, y + 20, image_size, image_size, e.image);
|
|
||||||
|
|
||||||
nvgSave(vg);
|
|
||||||
nvgScissor(vg, x, y, w - 30.f, h); // clip
|
|
||||||
{
|
|
||||||
bool has_star = false;
|
|
||||||
if (IsStarEnabled()) {
|
|
||||||
if (!e.has_star.has_value()) {
|
|
||||||
e.has_star = fs.FileExists(GenerateStarPath(e.path));
|
|
||||||
}
|
|
||||||
has_star = e.has_star.value();
|
|
||||||
}
|
|
||||||
|
|
||||||
const float font_size = 18;
|
|
||||||
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s%s", has_star ? "\u2605 " : "", e.GetName());
|
|
||||||
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetAuthor());
|
|
||||||
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetDisplayVersion());
|
|
||||||
}
|
|
||||||
nvgRestore(vg);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
auto text_id = ThemeEntryID_TEXT;
|
||||||
|
if (pos == m_index) {
|
||||||
|
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||||
|
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||||
|
} else {
|
||||||
|
DrawElement(v, ThemeEntryID_GRID);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float image_size = 115;
|
||||||
|
gfx::drawImageRounded(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage());
|
||||||
|
|
||||||
|
nvgSave(vg);
|
||||||
|
nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip
|
||||||
|
{
|
||||||
|
bool has_star = false;
|
||||||
|
if (IsStarEnabled()) {
|
||||||
|
if (!e.has_star.has_value()) {
|
||||||
|
e.has_star = fs::FsNativeSd().FileExists(GenerateStarPath(e.path));
|
||||||
|
}
|
||||||
|
has_star = e.has_star.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
const float font_size = 18;
|
||||||
|
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), "%s%s", has_star ? "\u2605 " : "", e.GetName());
|
||||||
|
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetAuthor());
|
||||||
|
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetDisplayVersion());
|
||||||
|
}
|
||||||
|
nvgRestore(vg);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Menu::OnFocusGained() {
|
void Menu::OnFocusGained() {
|
||||||
@@ -228,14 +231,12 @@ void Menu::OnFocusGained() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Menu::SetIndex(std::size_t index) {
|
void Menu::SetIndex(s64 index) {
|
||||||
m_index = index;
|
m_index = index;
|
||||||
if (!m_index) {
|
if (!m_index) {
|
||||||
m_start = 0;
|
m_list->SetYoff(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& e = m_entries[m_index];
|
|
||||||
|
|
||||||
if (IsStarEnabled()) {
|
if (IsStarEnabled()) {
|
||||||
const auto star_path = GenerateStarPath(m_entries[m_index].path);
|
const auto star_path = GenerateStarPath(m_entries[m_index].path);
|
||||||
if (fs::FsNativeSd().FileExists(star_path)) {
|
if (fs::FsNativeSd().FileExists(star_path)) {
|
||||||
@@ -266,18 +267,18 @@ void Menu::SetIndex(std::size_t index) {
|
|||||||
|
|
||||||
void Menu::InstallHomebrew() {
|
void Menu::InstallHomebrew() {
|
||||||
const auto& nro = m_entries[m_index];
|
const auto& nro = m_entries[m_index];
|
||||||
InstallHomebrew(nro.path, nro.nacp, nro.icon);
|
InstallHomebrew(nro.path, nro.nacp, nro_get_icon(nro.path, nro.icon_size, nro.icon_offset));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Menu::ScanHomebrew() {
|
void Menu::ScanHomebrew() {
|
||||||
TimeStamp ts;
|
TimeStamp ts;
|
||||||
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.GetSeconds());
|
log_write("nros found: %zu time_taken: %.2f\n", m_entries.size(), ts.GetSecondsD());
|
||||||
|
|
||||||
struct IniUser {
|
struct IniUser {
|
||||||
std::vector<NroEntry>& entires;
|
std::vector<NroEntry>& entires;
|
||||||
Hbini* ini;
|
Hbini* ini{};
|
||||||
std::string last_section;
|
std::string last_section{};
|
||||||
} ini_user{ m_entries };
|
} ini_user{ m_entries };
|
||||||
|
|
||||||
ini_browse([](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
|
ini_browse([](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
|
||||||
@@ -317,11 +318,6 @@ void Menu::Sort() {
|
|||||||
fs::FsPath star_path;
|
fs::FsPath star_path;
|
||||||
for (auto& p : m_entries) {
|
for (auto& p : m_entries) {
|
||||||
p.has_star = fs.FileExists(GenerateStarPath(p.path));
|
p.has_star = fs.FileExists(GenerateStarPath(p.path));
|
||||||
if (p.has_star == true) {
|
|
||||||
log_write("found star: %s\n", p.path.s);
|
|
||||||
} else {
|
|
||||||
log_write("no star: %s\n", p.path.s);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,6 +326,22 @@ void Menu::Sort() {
|
|||||||
const auto order = m_order.Get();
|
const auto order = m_order.Get();
|
||||||
|
|
||||||
const auto sorter = [this, sort, order](const NroEntry& lhs, const NroEntry& rhs) -> bool {
|
const auto sorter = [this, sort, order](const NroEntry& lhs, const NroEntry& rhs) -> bool {
|
||||||
|
const auto name_cmp = [order](const NroEntry& lhs, const NroEntry& rhs) -> bool {
|
||||||
|
auto r = strcasecmp(lhs.GetName(), rhs.GetName());
|
||||||
|
if (!r) {
|
||||||
|
r = strcasecmp(lhs.GetAuthor(), rhs.GetAuthor());
|
||||||
|
if (!r) {
|
||||||
|
r = strcasecmp(lhs.path, rhs.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order == OrderType_Descending) {
|
||||||
|
return r < 0;
|
||||||
|
} else {
|
||||||
|
return r > 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
case SortType_UpdatedStar:
|
case SortType_UpdatedStar:
|
||||||
if (lhs.has_star.value() && !rhs.has_star.value()) {
|
if (lhs.has_star.value() && !rhs.has_star.value()) {
|
||||||
@@ -337,6 +349,7 @@ void Menu::Sort() {
|
|||||||
} else if (!lhs.has_star.value() && rhs.has_star.value()) {
|
} else if (!lhs.has_star.value() && rhs.has_star.value()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
[[fallthrough]];
|
||||||
case SortType_Updated: {
|
case SortType_Updated: {
|
||||||
auto lhs_timestamp = lhs.hbini.timestamp;
|
auto lhs_timestamp = lhs.hbini.timestamp;
|
||||||
auto rhs_timestamp = rhs.hbini.timestamp;
|
auto rhs_timestamp = rhs.hbini.timestamp;
|
||||||
@@ -348,8 +361,8 @@ void Menu::Sort() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (lhs_timestamp == rhs_timestamp) {
|
if (lhs_timestamp == rhs_timestamp) {
|
||||||
return strcasecmp(lhs.GetName(), rhs.GetName()) < 0;
|
return name_cmp(lhs, rhs);
|
||||||
} else if (order == OrderType_Decending) {
|
} else if (order == OrderType_Descending) {
|
||||||
return lhs_timestamp > rhs_timestamp;
|
return lhs_timestamp > rhs_timestamp;
|
||||||
} else {
|
} else {
|
||||||
return lhs_timestamp < rhs_timestamp;
|
return lhs_timestamp < rhs_timestamp;
|
||||||
@@ -362,10 +375,11 @@ void Menu::Sort() {
|
|||||||
} else if (!lhs.has_star.value() && rhs.has_star.value()) {
|
} else if (!lhs.has_star.value() && rhs.has_star.value()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
[[fallthrough]];
|
||||||
case SortType_Size: {
|
case SortType_Size: {
|
||||||
if (lhs.size == rhs.size) {
|
if (lhs.size == rhs.size) {
|
||||||
return strcasecmp(lhs.GetName(), rhs.GetName()) < 0;
|
return name_cmp(lhs, rhs);
|
||||||
} else if (order == OrderType_Decending) {
|
} else if (order == OrderType_Descending) {
|
||||||
return lhs.size > rhs.size;
|
return lhs.size > rhs.size;
|
||||||
} else {
|
} else {
|
||||||
return lhs.size < rhs.size;
|
return lhs.size < rhs.size;
|
||||||
@@ -378,12 +392,9 @@ void Menu::Sort() {
|
|||||||
} else if (!lhs.has_star.value() && rhs.has_star.value()) {
|
} else if (!lhs.has_star.value() && rhs.has_star.value()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
[[fallthrough]];
|
||||||
case SortType_Alphabetical: {
|
case SortType_Alphabetical: {
|
||||||
if (order == OrderType_Decending) {
|
return name_cmp(lhs, rhs);
|
||||||
return strcasecmp(lhs.GetName(), rhs.GetName()) < 0;
|
|
||||||
} else {
|
|
||||||
return strcasecmp(lhs.GetName(), rhs.GetName()) > 0;
|
|
||||||
}
|
|
||||||
} break;
|
} break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,9 +420,9 @@ void Menu::SortAndFindLastFile() {
|
|||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
// guesstimate where the position is
|
// guesstimate where the position is
|
||||||
if (index >= 9) {
|
if (index >= 9) {
|
||||||
m_start = (index - 9) / 3 * 3 + 3;
|
m_list->SetYoff((((index - 9) + 3) / 3) * m_list->GetMaxY());
|
||||||
} else {
|
} else {
|
||||||
m_start = 0;
|
m_list->SetYoff(0);
|
||||||
}
|
}
|
||||||
SetIndex(index);
|
SetIndex(index);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,44 +139,44 @@ Menu::Menu() : MenuBase{"Irs"_i18n} {
|
|||||||
format_str.emplace_back("20x15"_i18n);
|
format_str.emplace_back("20x15"_i18n);
|
||||||
}
|
}
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Controller"_i18n, controller_str, [this](std::size_t& index){
|
options->Add(std::make_shared<SidebarEntryArray>("Controller"_i18n, controller_str, [this](s64& index){
|
||||||
irsStopImageProcessor(m_entries[m_index].m_handle);
|
irsStopImageProcessor(m_entries[m_index].m_handle);
|
||||||
m_index = index;
|
m_index = index;
|
||||||
UpdateConfig(&m_config);
|
UpdateConfig(&m_config);
|
||||||
}, m_index));
|
}, m_index));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Rotation"_i18n, rotation_str, [this](std::size_t& index){
|
options->Add(std::make_shared<SidebarEntryArray>("Rotation"_i18n, rotation_str, [this](s64& index){
|
||||||
m_rotation = (Rotation)index;
|
m_rotation = (Rotation)index;
|
||||||
}, m_rotation));
|
}, m_rotation));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Colour"_i18n, colour_str, [this](std::size_t& index){
|
options->Add(std::make_shared<SidebarEntryArray>("Colour"_i18n, colour_str, [this](s64& index){
|
||||||
m_colour = (Colour)index;
|
m_colour = (Colour)index;
|
||||||
updateColourArray();
|
updateColourArray();
|
||||||
}, m_colour));
|
}, m_colour));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Light Target"_i18n, light_target_str, [this](std::size_t& index){
|
options->Add(std::make_shared<SidebarEntryArray>("Light Target"_i18n, light_target_str, [this](s64& index){
|
||||||
m_config.light_target = index;
|
m_config.light_target = index;
|
||||||
UpdateConfig(&m_config);
|
UpdateConfig(&m_config);
|
||||||
}, m_config.light_target));
|
}, m_config.light_target));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Gain"_i18n, gain_str, [this](std::size_t& index){
|
options->Add(std::make_shared<SidebarEntryArray>("Gain"_i18n, gain_str, [this](s64& index){
|
||||||
m_config.gain = GAIN_MIN + index;
|
m_config.gain = GAIN_MIN + index;
|
||||||
UpdateConfig(&m_config);
|
UpdateConfig(&m_config);
|
||||||
}, m_config.gain - GAIN_MIN));
|
}, m_config.gain - GAIN_MIN));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Negative Image"_i18n, is_negative_image_used_str, [this](std::size_t& index){
|
options->Add(std::make_shared<SidebarEntryArray>("Negative Image"_i18n, is_negative_image_used_str, [this](s64& index){
|
||||||
m_config.is_negative_image_used = index;
|
m_config.is_negative_image_used = index;
|
||||||
UpdateConfig(&m_config);
|
UpdateConfig(&m_config);
|
||||||
}, m_config.is_negative_image_used));
|
}, m_config.is_negative_image_used));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Format"_i18n, format_str, [this](std::size_t& index){
|
options->Add(std::make_shared<SidebarEntryArray>("Format"_i18n, format_str, [this](s64& index){
|
||||||
m_config.orig_format = index;
|
m_config.orig_format = index;
|
||||||
m_config.trimming_format = index;
|
m_config.trimming_format = index;
|
||||||
UpdateConfig(&m_config);
|
UpdateConfig(&m_config);
|
||||||
}, m_config.orig_format));
|
}, m_config.orig_format));
|
||||||
|
|
||||||
if (hosversionAtLeast(4,0,0)) {
|
if (hosversionAtLeast(4,0,0)) {
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Trimming Format"_i18n, format_str, [this](std::size_t& index){
|
options->Add(std::make_shared<SidebarEntryArray>("Trimming Format"_i18n, format_str, [this](s64& index){
|
||||||
// you cannot set trim a larger region than the source
|
// you cannot set trim a larger region than the source
|
||||||
if (index < m_config.orig_format) {
|
if (index < m_config.orig_format) {
|
||||||
index = m_config.orig_format;
|
index = m_config.orig_format;
|
||||||
@@ -258,8 +258,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
const auto scale = std::min(scale_x, scale_y);
|
const auto scale = std::min(scale_x, scale_y);
|
||||||
w = m_irs_width * scale;
|
w = m_irs_width * scale;
|
||||||
h = m_irs_height * scale;
|
h = m_irs_height * scale;
|
||||||
cx = (m_pos.x + m_pos.w / 2.0) - w / 2.0;
|
cx = (m_pos.x + m_pos.w / 2.F) - w / 2.F;
|
||||||
cy = (m_pos.y + m_pos.h / 2.0) - h / 2.0;
|
cy = (m_pos.y + m_pos.h / 2.F) - h / 2.F;
|
||||||
angle = 0;
|
angle = 0;
|
||||||
} break;
|
} break;
|
||||||
case Rotation_90: {
|
case Rotation_90: {
|
||||||
@@ -268,8 +268,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
const auto scale = std::min(scale_x, scale_y);
|
const auto scale = std::min(scale_x, scale_y);
|
||||||
w = m_irs_width * scale;
|
w = m_irs_width * scale;
|
||||||
h = m_irs_height * scale;
|
h = m_irs_height * scale;
|
||||||
cx = (m_pos.x + m_pos.w / 2.0) + h / 2.0;
|
cx = (m_pos.x + m_pos.w / 2.F) + h / 2.F;
|
||||||
cy = (m_pos.y + m_pos.h / 2.0) - w / 2.0;
|
cy = (m_pos.y + m_pos.h / 2.F) - w / 2.F;
|
||||||
angle = 90;
|
angle = 90;
|
||||||
} break;
|
} break;
|
||||||
case Rotation_180: {
|
case Rotation_180: {
|
||||||
@@ -278,8 +278,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
const auto scale = std::min(scale_x, scale_y);
|
const auto scale = std::min(scale_x, scale_y);
|
||||||
w = m_irs_width * scale;
|
w = m_irs_width * scale;
|
||||||
h = m_irs_height * scale;
|
h = m_irs_height * scale;
|
||||||
cx = (m_pos.x + m_pos.w / 2.0) + w / 2.0;
|
cx = (m_pos.x + m_pos.w / 2.F) + w / 2.F;
|
||||||
cy = (m_pos.y + m_pos.h / 2.0) + h / 2.0;
|
cy = (m_pos.y + m_pos.h / 2.F) + h / 2.F;
|
||||||
angle = 180;
|
angle = 180;
|
||||||
} break;
|
} break;
|
||||||
case Rotation_270: {
|
case Rotation_270: {
|
||||||
@@ -288,8 +288,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
const auto scale = std::min(scale_x, scale_y);
|
const auto scale = std::min(scale_x, scale_y);
|
||||||
w = m_irs_width * scale;
|
w = m_irs_width * scale;
|
||||||
h = m_irs_height * scale;
|
h = m_irs_height * scale;
|
||||||
cx = (m_pos.x + m_pos.w / 2.0) - h / 2.0;
|
cx = (m_pos.x + m_pos.w / 2.F) - h / 2.F;
|
||||||
cy = (m_pos.y + m_pos.h / 2.0) + w / 2.0;
|
cy = (m_pos.y + m_pos.h / 2.F) + w / 2.F;
|
||||||
angle = 270;
|
angle = 270;
|
||||||
} break;
|
} break;
|
||||||
}
|
}
|
||||||
@@ -382,7 +382,7 @@ void Menu::LoadDefaultConfig() {
|
|||||||
irsGetClusteringProcessorDefaultConfig(&m_clustering_config);
|
irsGetClusteringProcessorDefaultConfig(&m_clustering_config);
|
||||||
irsGetIrLedProcessorDefaultConfig(&m_led_config);
|
irsGetIrLedProcessorDefaultConfig(&m_led_config);
|
||||||
|
|
||||||
m_tera_config;
|
m_tera_config = {};
|
||||||
m_adaptive_config = {};
|
m_adaptive_config = {};
|
||||||
m_hand_config = {};
|
m_hand_config = {};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "ui/menus/main_menu.hpp"
|
#include "ui/menus/main_menu.hpp"
|
||||||
#include "ui/menus/irs_menu.hpp"
|
#include "ui/menus/irs_menu.hpp"
|
||||||
#include "ui/menus/themezer.hpp"
|
#include "ui/menus/themezer.hpp"
|
||||||
|
#include "ui/menus/ghdl.hpp"
|
||||||
|
|
||||||
#include "ui/sidebar.hpp"
|
#include "ui/sidebar.hpp"
|
||||||
#include "ui/popup_list.hpp"
|
#include "ui/popup_list.hpp"
|
||||||
@@ -22,6 +23,9 @@
|
|||||||
namespace sphaira::ui::menu::main {
|
namespace sphaira::ui::menu::main {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
constexpr const char* GITHUB_URL{"https://api.github.com/repos/ITotalJustice/sphaira/releases/latest"};
|
||||||
|
constexpr fs::FsPath CACHE_PATH{"/switch/sphaira/cache/sphaira_latest.json"};
|
||||||
|
|
||||||
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
|
||||||
@@ -34,16 +38,12 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
|
|||||||
pbox->NewTransfer("Downloading "_i18n + version);
|
pbox->NewTransfer("Downloading "_i18n + version);
|
||||||
log_write("starting download: %s\n", url.c_str());
|
log_write("starting download: %s\n", url.c_str());
|
||||||
|
|
||||||
DownloadClearCache(url);
|
if (!curl::Api().ToFile(
|
||||||
if (!DownloadFile(url, zip_out, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
|
curl::Url{url},
|
||||||
if (pbox->ShouldExit()) {
|
curl::Path{zip_out},
|
||||||
return false;
|
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||||
}
|
).success) {
|
||||||
pbox->UpdateTransfer(dlnow, dltotal);
|
|
||||||
return true;
|
|
||||||
})) {
|
|
||||||
log_write("error with download\n");
|
log_write("error with download\n");
|
||||||
// push popup error box
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
|
|||||||
if (!pbox->ShouldExit()) {
|
if (!pbox->ShouldExit()) {
|
||||||
auto zfile = unzOpen64(zip_out);
|
auto zfile = unzOpen64(zip_out);
|
||||||
if (!zfile) {
|
if (!zfile) {
|
||||||
log_write("failed to open zip: %s\n", zip_out);
|
log_write("failed to open zip: %s\n", zip_out.s);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ON_SCOPE_EXIT(unzClose(zfile));
|
ON_SCOPE_EXIT(unzClose(zfile));
|
||||||
@@ -89,33 +89,37 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
|
|||||||
file_path = fs::AppendPath("/", file_path);
|
file_path = fs::AppendPath("/", file_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (std::strstr(file_path, "sphaira.nro")) {
|
||||||
|
file_path = App::GetExePath();
|
||||||
|
}
|
||||||
|
|
||||||
Result rc;
|
Result rc;
|
||||||
if (file_path[strlen(file_path) -1] == '/') {
|
if (file_path[strlen(file_path) -1] == '/') {
|
||||||
if (R_FAILED(rc = fs.CreateDirectoryRecursively(file_path)) && rc != FsError_PathAlreadyExists) {
|
if (R_FAILED(rc = fs.CreateDirectoryRecursively(file_path)) && rc != FsError_PathAlreadyExists) {
|
||||||
log_write("failed to create folder: %s 0x%04X\n", file_path, rc);
|
log_write("failed to create folder: %s 0x%04X\n", file_path.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
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) {
|
||||||
log_write("failed to create file: %s 0x%04X\n", file_path, rc);
|
log_write("failed to create file: %s 0x%04X\n", file_path.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (R_FAILED(rc = fs.OpenFile(file_path, FsOpenMode_Write, &f))) {
|
if (R_FAILED(rc = fs.OpenFile(file_path, FsOpenMode_Write, &f))) {
|
||||||
log_write("failed to open file: %s 0x%04X\n", file_path, rc);
|
log_write("failed to open file: %s 0x%04X\n", file_path.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ON_SCOPE_EXIT(fsFileClose(&f));
|
ON_SCOPE_EXIT(fsFileClose(&f));
|
||||||
|
|
||||||
if (R_FAILED(rc = fsFileSetSize(&f, info.uncompressed_size))) {
|
if (R_FAILED(rc = fsFileSetSize(&f, info.uncompressed_size))) {
|
||||||
log_write("failed to set file size: %s 0x%04X\n", file_path, rc);
|
log_write("failed to set file size: %s 0x%04X\n", file_path.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<char> buf(chunk_size);
|
std::vector<char> buf(chunk_size);
|
||||||
u64 offset{};
|
s64 offset{};
|
||||||
while (offset < info.uncompressed_size) {
|
while (offset < info.uncompressed_size) {
|
||||||
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
|
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
|
||||||
if (bytes_read <= 0) {
|
if (bytes_read <= 0) {
|
||||||
@@ -124,7 +128,7 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (R_FAILED(rc = fsFileWrite(&f, offset, buf.data(), bytes_read, FsWriteOption_None))) {
|
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, rc);
|
log_write("failed to write file: %s 0x%04X\n", file_path.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,63 +146,70 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
MainMenu::MainMenu() {
|
MainMenu::MainMenu() {
|
||||||
DownloadMemoryAsync("https://api.github.com/repos/ITotalJustice/sphaira/releases/latest", "", [this](std::vector<u8>& data, bool success){
|
curl::Api().ToFileAsync(
|
||||||
m_update_state = UpdateState::Error;
|
curl::Url{GITHUB_URL},
|
||||||
ON_SCOPE_EXIT( log_write("update status: %u\n", (u8)m_update_state) );
|
curl::Path{CACHE_PATH},
|
||||||
|
curl::Flags{curl::Flag_Cache},
|
||||||
|
curl::StopToken{this->GetToken()},
|
||||||
|
curl::Header{
|
||||||
|
{ "Accept", "application/vnd.github+json" },
|
||||||
|
},
|
||||||
|
curl::OnComplete{[this](auto& result){
|
||||||
|
log_write("inside github download\n");
|
||||||
|
m_update_state = UpdateState::Error;
|
||||||
|
ON_SCOPE_EXIT( log_write("update status: %u\n", (u8)m_update_state) );
|
||||||
|
|
||||||
if (!success) {
|
if (!result.success) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto json = yyjson_read((const char*)data.data(), data.size(), 0);
|
auto json = yyjson_read_file(CACHE_PATH, YYJSON_READ_NOFLAG, nullptr, nullptr);
|
||||||
R_UNLESS(json, false);
|
R_UNLESS(json, false);
|
||||||
ON_SCOPE_EXIT(yyjson_doc_free(json));
|
ON_SCOPE_EXIT(yyjson_doc_free(json));
|
||||||
|
|
||||||
auto root = yyjson_doc_get_root(json);
|
auto root = yyjson_doc_get_root(json);
|
||||||
R_UNLESS(root, false);
|
R_UNLESS(root, false);
|
||||||
|
|
||||||
auto tag_key = yyjson_obj_get(root, "tag_name");
|
auto tag_key = yyjson_obj_get(root, "tag_name");
|
||||||
R_UNLESS(tag_key, false);
|
R_UNLESS(tag_key, false);
|
||||||
|
|
||||||
|
const auto version = yyjson_get_str(tag_key);
|
||||||
|
R_UNLESS(version, false);
|
||||||
|
if (std::strcmp(APP_VERSION, version) >= 0) {
|
||||||
|
m_update_state = UpdateState::None;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto body_key = yyjson_obj_get(root, "body");
|
||||||
|
R_UNLESS(body_key, false);
|
||||||
|
|
||||||
|
const auto body = yyjson_get_str(body_key);
|
||||||
|
R_UNLESS(body, false);
|
||||||
|
|
||||||
|
auto assets = yyjson_obj_get(root, "assets");
|
||||||
|
R_UNLESS(assets, false);
|
||||||
|
|
||||||
|
auto idx0 = yyjson_arr_get(assets, 0);
|
||||||
|
R_UNLESS(idx0, false);
|
||||||
|
|
||||||
|
auto url_key = yyjson_obj_get(idx0, "browser_download_url");
|
||||||
|
R_UNLESS(url_key, false);
|
||||||
|
|
||||||
|
const auto url = yyjson_get_str(url_key);
|
||||||
|
R_UNLESS(url, false);
|
||||||
|
|
||||||
|
m_update_version = version;
|
||||||
|
m_update_url = url;
|
||||||
|
m_update_description = body;
|
||||||
|
m_update_state = UpdateState::Update;
|
||||||
|
log_write("found url: %s\n", url);
|
||||||
|
log_write("found body: %s\n", body);
|
||||||
|
App::Notify("Update avaliable: "_i18n + m_update_version);
|
||||||
|
|
||||||
const auto version = yyjson_get_str(tag_key);
|
|
||||||
R_UNLESS(version, false);
|
|
||||||
if (std::strcmp(APP_VERSION, version) >= 0) {
|
|
||||||
m_update_state = UpdateState::None;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto body_key = yyjson_obj_get(root, "body");
|
|
||||||
R_UNLESS(body_key, false);
|
|
||||||
|
|
||||||
const auto body = yyjson_get_str(body_key);
|
|
||||||
R_UNLESS(body, false);
|
|
||||||
|
|
||||||
auto assets = yyjson_obj_get(root, "assets");
|
|
||||||
R_UNLESS(assets, false);
|
|
||||||
|
|
||||||
auto idx0 = yyjson_arr_get(assets, 0);
|
|
||||||
R_UNLESS(idx0, false);
|
|
||||||
|
|
||||||
auto url_key = yyjson_obj_get(idx0, "browser_download_url");
|
|
||||||
R_UNLESS(url_key, false);
|
|
||||||
|
|
||||||
const auto url = yyjson_get_str(url_key);
|
|
||||||
R_UNLESS(url, false);
|
|
||||||
|
|
||||||
m_update_version = version;
|
|
||||||
m_update_url = url;
|
|
||||||
m_update_description = body;
|
|
||||||
m_update_state = UpdateState::Update;
|
|
||||||
log_write("found url: %s\n", url);
|
|
||||||
log_write("found body: %s\n", body);
|
|
||||||
App::Notify("Update avaliable: "_i18n + m_update_version);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
AddOnLPress();
|
|
||||||
AddOnRPress();
|
|
||||||
|
|
||||||
this->SetActions(
|
this->SetActions(
|
||||||
std::make_pair(Button::START, Action{App::Exit}),
|
std::make_pair(Button::START, Action{App::Exit}),
|
||||||
std::make_pair(Button::Y, Action{"Menu"_i18n, [this](){
|
std::make_pair(Button::Y, Action{"Menu"_i18n, [this](){
|
||||||
@@ -219,9 +230,8 @@ MainMenu::MainMenu() {
|
|||||||
language_items.push_back("Portuguese"_i18n);
|
language_items.push_back("Portuguese"_i18n);
|
||||||
language_items.push_back("Russian"_i18n);
|
language_items.push_back("Russian"_i18n);
|
||||||
language_items.push_back("Swedish"_i18n);
|
language_items.push_back("Swedish"_i18n);
|
||||||
|
language_items.push_back("Vietnamese"_i18n);
|
||||||
|
|
||||||
options->AddHeader("Header"_i18n);
|
|
||||||
options->AddSpacer();
|
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [this](){
|
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [this](){
|
||||||
SidebarEntryArray::Items theme_items{};
|
SidebarEntryArray::Items theme_items{};
|
||||||
const auto theme_meta = App::GetThemeMetaList();
|
const auto theme_meta = App::GetThemeMetaList();
|
||||||
@@ -232,7 +242,7 @@ MainMenu::MainMenu() {
|
|||||||
auto options = std::make_shared<Sidebar>("Theme Options"_i18n, Sidebar::Side::LEFT);
|
auto options = std::make_shared<Sidebar>("Theme Options"_i18n, Sidebar::Side::LEFT);
|
||||||
ON_SCOPE_EXIT(App::Push(options));
|
ON_SCOPE_EXIT(App::Push(options));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Select Theme"_i18n, theme_items, [this, theme_items](std::size_t& index_out){
|
options->Add(std::make_shared<SidebarEntryArray>("Select Theme"_i18n, theme_items, [this, theme_items](s64& index_out){
|
||||||
App::SetTheme(index_out);
|
App::SetTheme(index_out);
|
||||||
}, App::GetThemeIndex()));
|
}, App::GetThemeIndex()));
|
||||||
|
|
||||||
@@ -243,6 +253,10 @@ MainMenu::MainMenu() {
|
|||||||
options->Add(std::make_shared<SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [this](bool& enable){
|
options->Add(std::make_shared<SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [this](bool& enable){
|
||||||
App::SetThemeMusicEnable(enable);
|
App::SetThemeMusicEnable(enable);
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [this](bool& enable){
|
||||||
|
App::Set12HourTimeEnable(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Network"_i18n, [this](){
|
options->Add(std::make_shared<SidebarEntryCallback>("Network"_i18n, [this](){
|
||||||
@@ -270,11 +284,8 @@ MainMenu::MainMenu() {
|
|||||||
m_update_state = UpdateState::None;
|
m_update_state = UpdateState::None;
|
||||||
App::Notify("Updated to "_i18n + m_update_version);
|
App::Notify("Updated to "_i18n + m_update_version);
|
||||||
App::Push(std::make_shared<OptionBox>(
|
App::Push(std::make_shared<OptionBox>(
|
||||||
"Restart Sphaira?"_i18n,
|
"Press OK to restart Sphaira"_i18n, "OK"_i18n, [](auto){
|
||||||
"Back"_i18n, "Restart"_i18n, 1, [this](auto op_index){
|
App::ExitRestart();
|
||||||
if (op_index && *op_index) {
|
|
||||||
App::ExitRestart();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
@@ -285,9 +296,9 @@ MainMenu::MainMenu() {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Language"_i18n, language_items, [this](std::size_t& index_out){
|
options->Add(std::make_shared<SidebarEntryArray>("Language"_i18n, language_items, [this](s64& index_out){
|
||||||
App::SetLanguage(index_out);
|
App::SetLanguage(index_out);
|
||||||
}, (std::size_t)App::GetLanguage()));
|
}, (s64)App::GetLanguage()));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Misc"_i18n, [this](){
|
options->Add(std::make_shared<SidebarEntryCallback>("Misc"_i18n, [this](){
|
||||||
auto options = std::make_shared<Sidebar>("Misc Options"_i18n, Sidebar::Side::LEFT);
|
auto options = std::make_shared<Sidebar>("Misc Options"_i18n, Sidebar::Side::LEFT);
|
||||||
@@ -297,6 +308,10 @@ MainMenu::MainMenu() {
|
|||||||
App::Push(std::make_shared<menu::themezer::Menu>());
|
App::Push(std::make_shared<menu::themezer::Menu>());
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryCallback>("GitHub"_i18n, [](){
|
||||||
|
App::Push(std::make_shared<menu::gh::Menu>());
|
||||||
|
}));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Irs"_i18n, [](){
|
options->Add(std::make_shared<SidebarEntryCallback>("Irs"_i18n, [](){
|
||||||
App::Push(std::make_shared<menu::irs::Menu>());
|
App::Push(std::make_shared<menu::irs::Menu>());
|
||||||
}));
|
}));
|
||||||
@@ -316,6 +331,11 @@ MainMenu::MainMenu() {
|
|||||||
install_items.push_back("System memory"_i18n);
|
install_items.push_back("System memory"_i18n);
|
||||||
install_items.push_back("microSD card"_i18n);
|
install_items.push_back("microSD card"_i18n);
|
||||||
|
|
||||||
|
SidebarEntryArray::Items text_scroll_speed_items;
|
||||||
|
text_scroll_speed_items.push_back("Slow"_i18n);
|
||||||
|
text_scroll_speed_items.push_back("Normal"_i18n);
|
||||||
|
text_scroll_speed_items.push_back("Fast"_i18n);
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [this](bool& enable){
|
options->Add(std::make_shared<SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [this](bool& enable){
|
||||||
App::SetLogEnable(enable);
|
App::SetLogEnable(enable);
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
@@ -328,13 +348,17 @@ MainMenu::MainMenu() {
|
|||||||
App::SetInstallEnable(enable);
|
App::SetInstallEnable(enable);
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Install location"_i18n, install_items, [this](std::size_t& index_out){
|
options->Add(std::make_shared<SidebarEntryArray>("Install location"_i18n, install_items, [this](s64& index_out){
|
||||||
App::SetInstallSdEnable(index_out);
|
App::SetInstallSdEnable(index_out);
|
||||||
}, (std::size_t)App::GetInstallSdEnable()));
|
}, (s64)App::GetInstallSdEnable()));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [this](bool& enable){
|
options->Add(std::make_shared<SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [this](bool& enable){
|
||||||
App::SetInstallPrompt(enable);
|
App::SetInstallPrompt(enable);
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [this](s64& index_out){
|
||||||
|
App::SetTextScrollSpeed(index_out);
|
||||||
|
}, (s64)App::GetTextScrollSpeed()));
|
||||||
}));
|
}));
|
||||||
}})
|
}})
|
||||||
);
|
);
|
||||||
@@ -344,6 +368,8 @@ MainMenu::MainMenu() {
|
|||||||
m_app_store_menu = std::make_shared<appstore::Menu>(m_homebrew_menu->GetHomebrewList());
|
m_app_store_menu = std::make_shared<appstore::Menu>(m_homebrew_menu->GetHomebrewList());
|
||||||
m_current_menu = m_homebrew_menu;
|
m_current_menu = m_homebrew_menu;
|
||||||
|
|
||||||
|
AddOnLRPress();
|
||||||
|
|
||||||
for (auto [button, action] : m_actions) {
|
for (auto [button, action] : m_actions) {
|
||||||
m_current_menu->SetAction(button, action);
|
m_current_menu->SetAction(button, action);
|
||||||
}
|
}
|
||||||
@@ -363,11 +389,11 @@ void MainMenu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
|
|
||||||
void MainMenu::OnFocusGained() {
|
void MainMenu::OnFocusGained() {
|
||||||
Widget::OnFocusGained();
|
Widget::OnFocusGained();
|
||||||
this->SetHidden(false);
|
|
||||||
m_current_menu->OnFocusGained();
|
m_current_menu->OnFocusGained();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainMenu::OnFocusLost() {
|
void MainMenu::OnFocusLost() {
|
||||||
|
Widget::OnFocusLost();
|
||||||
m_current_menu->OnFocusLost();
|
m_current_menu->OnFocusLost();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,17 +402,11 @@ void MainMenu::OnLRPress(std::shared_ptr<MenuBase> menu, Button b) {
|
|||||||
if (m_current_menu == m_homebrew_menu) {
|
if (m_current_menu == m_homebrew_menu) {
|
||||||
m_current_menu = menu;
|
m_current_menu = menu;
|
||||||
RemoveAction(b);
|
RemoveAction(b);
|
||||||
if (b == Button::L) {
|
|
||||||
AddOnRPress();
|
|
||||||
} else {
|
|
||||||
AddOnLPress();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
m_current_menu = m_homebrew_menu;
|
m_current_menu = m_homebrew_menu;
|
||||||
AddOnRPress();
|
|
||||||
AddOnLPress();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AddOnLRPress();
|
||||||
m_current_menu->OnFocusGained();
|
m_current_menu->OnFocusGained();
|
||||||
|
|
||||||
for (auto [button, action] : m_actions) {
|
for (auto [button, action] : m_actions) {
|
||||||
@@ -394,18 +414,20 @@ void MainMenu::OnLRPress(std::shared_ptr<MenuBase> menu, Button b) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainMenu::AddOnLPress() {
|
void MainMenu::AddOnLRPress() {
|
||||||
const auto label = m_current_menu == m_homebrew_menu ? "Files" : "Apps";
|
if (m_current_menu != m_filebrowser_menu) {
|
||||||
SetAction(Button::L, Action{i18n::get(label), [this]{
|
const auto label = m_current_menu == m_homebrew_menu ? "Files" : "Apps";
|
||||||
OnLRPress(m_filebrowser_menu, Button::L);
|
SetAction(Button::L, Action{i18n::get(label), [this]{
|
||||||
}});
|
OnLRPress(m_filebrowser_menu, Button::L);
|
||||||
}
|
}});
|
||||||
|
}
|
||||||
|
|
||||||
void MainMenu::AddOnRPress() {
|
if (m_current_menu != m_app_store_menu) {
|
||||||
const auto label = m_current_menu == m_homebrew_menu ? "Store" : "Apps";
|
const auto label = m_current_menu == m_homebrew_menu ? "Store" : "Apps";
|
||||||
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_app_store_menu, Button::R);
|
||||||
}});
|
}});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace sphaira::ui::menu::main
|
} // namespace sphaira::ui::menu::main
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ namespace sphaira::ui::menu {
|
|||||||
MenuBase::MenuBase(std::string title) : m_title{title} {
|
MenuBase::MenuBase(std::string title) : m_title{title} {
|
||||||
// this->SetParent(this);
|
// this->SetParent(this);
|
||||||
this->SetPos(30, 87, 1220 - 30, 646 - 87);
|
this->SetPos(30, 87, 1220 - 30, 646 - 87);
|
||||||
m_applet_type = appletGetAppletType();
|
|
||||||
SetAction(Button::START, Action{App::Exit});
|
SetAction(Button::START, Action{App::Exit});
|
||||||
|
UpdateVars();
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuBase::~MenuBase() {
|
MenuBase::~MenuBase() {
|
||||||
@@ -18,30 +18,17 @@ MenuBase::~MenuBase() {
|
|||||||
|
|
||||||
void MenuBase::Update(Controller* controller, TouchInfo* touch) {
|
void MenuBase::Update(Controller* controller, TouchInfo* touch) {
|
||||||
Widget::Update(controller, touch);
|
Widget::Update(controller, touch);
|
||||||
|
|
||||||
|
// update every second.
|
||||||
|
if (m_poll_timestamp.GetSeconds() >= 1) {
|
||||||
|
UpdateVars();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
|
void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
|
||||||
DrawElement(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, ThemeEntryID_BACKGROUND);
|
DrawElement(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, ThemeEntryID_BACKGROUND);
|
||||||
Widget::Draw(vg, theme);
|
Widget::Draw(vg, theme);
|
||||||
|
|
||||||
u32 battery_percetange{};
|
|
||||||
|
|
||||||
PsmChargerType charger_type{};
|
|
||||||
NifmInternetConnectionType type{};
|
|
||||||
NifmInternetConnectionStatus status{};
|
|
||||||
u32 strength{};
|
|
||||||
u32 ip{};
|
|
||||||
|
|
||||||
const auto t = time(NULL);
|
|
||||||
struct tm tm{};
|
|
||||||
localtime_r(&t, &tm);
|
|
||||||
|
|
||||||
// todo: app thread poll every 1s and this query the result
|
|
||||||
psmGetBatteryChargePercentage(&battery_percetange);
|
|
||||||
psmGetChargerType(&charger_type);
|
|
||||||
nifmGetInternetConnectionStatus(&type, &strength, &status);
|
|
||||||
nifmGetCurrentIpAddress(&ip);
|
|
||||||
|
|
||||||
const float start_y = 70;
|
const float start_y = 70;
|
||||||
const float font_size = 22;
|
const float font_size = 22;
|
||||||
const float spacing = 30;
|
const float spacing = 30;
|
||||||
@@ -51,37 +38,41 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
|
|
||||||
nvgFontSize(vg, font_size);
|
nvgFontSize(vg, font_size);
|
||||||
|
|
||||||
#define draw(...) \
|
#define draw(colour, ...) \
|
||||||
gfx::textBounds(vg, 0, 0, bounds, __VA_ARGS__); \
|
gfx::textBounds(vg, 0, 0, bounds, __VA_ARGS__); \
|
||||||
start_x -= bounds[2] - bounds[0]; \
|
start_x -= bounds[2] - bounds[0]; \
|
||||||
gfx::drawTextArgs(vg, start_x, start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->elements[ThemeEntryID_TEXT].colour, __VA_ARGS__); \
|
gfx::drawTextArgs(vg, start_x, start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(colour), __VA_ARGS__); \
|
||||||
start_x -= spacing;
|
start_x -= spacing;
|
||||||
|
|
||||||
// draw("version %s", APP_VERSION);
|
// draw("version %s", APP_VERSION);
|
||||||
draw("%u\uFE6A", battery_percetange);
|
draw(ThemeEntryID_TEXT, "%u\uFE6A", m_battery_percetange);
|
||||||
draw("%02u:%02u:%02u", tm.tm_hour, tm.tm_min, tm.tm_sec);
|
|
||||||
if (ip) {
|
if (App::Get12HourTimeEnable()) {
|
||||||
draw("%u.%u.%u.%u", ip&0xFF, (ip>>8)&0xFF, (ip>>16)&0xFF, (ip>>24)&0xFF);
|
draw(ThemeEntryID_TEXT, "%02u:%02u:%02u %s", (m_tm.tm_hour == 0 || m_tm.tm_hour == 12) ? 12 : m_tm.tm_hour % 12, m_tm.tm_min, m_tm.tm_sec, (m_tm.tm_hour < 12) ? "AM" : "PM");
|
||||||
} else {
|
} else {
|
||||||
draw(("No Internet"_i18n).c_str());
|
draw(ThemeEntryID_TEXT, "%02u:%02u:%02u", m_tm.tm_hour, m_tm.tm_min, m_tm.tm_sec);
|
||||||
}
|
}
|
||||||
if (m_applet_type == AppletType_LibraryApplet || m_applet_type == AppletType_SystemApplet) {
|
|
||||||
draw(("[Applet Mode]"_i18n).c_str());
|
if (m_ip) {
|
||||||
|
draw(ThemeEntryID_TEXT, "%u.%u.%u.%u", m_ip&0xFF, (m_ip>>8)&0xFF, (m_ip>>16)&0xFF, (m_ip>>24)&0xFF);
|
||||||
|
} else {
|
||||||
|
draw(ThemeEntryID_TEXT, ("No Internet"_i18n).c_str());
|
||||||
|
}
|
||||||
|
if (!App::IsApplication()) {
|
||||||
|
draw(ThemeEntryID_ERROR, ("[Applet Mode]"_i18n).c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
#undef draw
|
#undef draw
|
||||||
|
|
||||||
gfx::drawRect(vg, 30.f, 86.f, 1220.f, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
|
gfx::drawRect(vg, 30.f, 86.f, 1220.f, 1.f, theme->GetColour(ThemeEntryID_LINE));
|
||||||
gfx::drawRect(vg, 30.f, 646.0f, 1220.f, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
|
gfx::drawRect(vg, 30.f, 646.0f, 1220.f, 1.f, theme->GetColour(ThemeEntryID_LINE));
|
||||||
|
|
||||||
nvgFontSize(vg, 28);
|
nvgFontSize(vg, 28);
|
||||||
gfx::textBounds(vg, 0, 0, bounds, m_title.c_str());
|
gfx::textBounds(vg, 0, 0, bounds, m_title.c_str());
|
||||||
gfx::drawTextArgs(vg, 80, start_y, 28.f, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str());
|
gfx::drawTextArgs(vg, 80, start_y, 28.f, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT), m_title.c_str());
|
||||||
gfx::drawTextArgs(vg, 80 + (bounds[2] - bounds[0]) + 10, start_y, 16, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->elements[ThemeEntryID_TEXT].colour, m_title_sub_heading.c_str());
|
gfx::drawTextArgs(vg, 80 + (bounds[2] - bounds[0]) + 10, start_y, 16, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), m_title_sub_heading.c_str());
|
||||||
|
|
||||||
// gfx::drawTextArgs(vg, 80, 65, 28.f, NVG_ALIGN_LEFT, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str());
|
gfx::drawTextArgs(vg, 80, 685.f, 18, NVG_ALIGN_LEFT, theme->GetColour(ThemeEntryID_TEXT), "%s", m_sub_heading.c_str());
|
||||||
// gfx::drawTextArgs(vg, 80, 680.f, 18, NVG_ALIGN_LEFT, theme->elements[ThemeEntryID_TEXT].colour, "%s", m_sub_heading.c_str());
|
|
||||||
gfx::drawTextArgs(vg, 80, 685.f, 18, NVG_ALIGN_LEFT, theme->elements[ThemeEntryID_TEXT].colour, "%s", m_sub_heading.c_str());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuBase::SetTitle(std::string title) {
|
void MenuBase::SetTitle(std::string title) {
|
||||||
@@ -96,4 +87,24 @@ void MenuBase::SetSubHeading(std::string sub_heading) {
|
|||||||
m_sub_heading = sub_heading;
|
m_sub_heading = sub_heading;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MenuBase::UpdateVars() {
|
||||||
|
m_tm = {};
|
||||||
|
m_poll_timestamp = {};
|
||||||
|
m_battery_percetange = {};
|
||||||
|
m_charger_type = {};
|
||||||
|
m_type = {};
|
||||||
|
m_status = {};
|
||||||
|
m_strength = {};
|
||||||
|
m_ip = {};
|
||||||
|
|
||||||
|
const auto t = time(NULL);
|
||||||
|
localtime_r(&t, &m_tm);
|
||||||
|
psmGetBatteryChargePercentage(&m_battery_percetange);
|
||||||
|
psmGetChargerType(&m_charger_type);
|
||||||
|
nifmGetInternetConnectionStatus(&m_type, &m_strength, &m_status);
|
||||||
|
nifmGetCurrentIpAddress(&m_ip);
|
||||||
|
|
||||||
|
m_poll_timestamp.Update();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace sphaira::ui::menu
|
} // namespace sphaira::ui::menu
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
#include "i18n.hpp"
|
#include "i18n.hpp"
|
||||||
|
|
||||||
#include <minIni.h>
|
#include <minIni.h>
|
||||||
#include <nanovg/stb_image.h>
|
#include <stb_image.h>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <minizip/unzip.h>
|
#include <minizip/unzip.h>
|
||||||
#include <yyjson.h>
|
#include <yyjson.h>
|
||||||
@@ -53,9 +53,11 @@ constexpr const char* REQUEST_ORDER[]{
|
|||||||
// https://api.themezer.net/?query=query($nsfw:Boolean,$page:Int,$limit:Int,$sort:String,$order:String,$query:String,$creators:[String!]){packList(nsfw:$nsfw,page:$page,limit:$limit,sort:$sort,order:$order,query:$query,creators:$creators){id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count,themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}}}&variables={"nsfw":false,"page":1,"limit":10,"sort":"updated","order":"desc","query":null,"creators":["695065006068334622"]}
|
// https://api.themezer.net/?query=query($nsfw:Boolean,$page:Int,$limit:Int,$sort:String,$order:String,$query:String,$creators:[String!]){packList(nsfw:$nsfw,page:$page,limit:$limit,sort:$sort,order:$order,query:$query,creators:$creators){id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count,themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}}}&variables={"nsfw":false,"page":1,"limit":10,"sort":"updated","order":"desc","query":null,"creators":["695065006068334622"]}
|
||||||
|
|
||||||
// i know, this is cursed
|
// i know, this is cursed
|
||||||
|
// todo: send actual POST request rather than GET.
|
||||||
auto apiBuildUrlListInternal(const Config& e, bool is_pack) -> std::string {
|
auto apiBuildUrlListInternal(const Config& e, bool is_pack) -> std::string {
|
||||||
std::string api = "https://api.themezer.net/?query=query";
|
std::string api = "https://api.themezer.net/?query=query";
|
||||||
std::string fields = "{id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count";
|
// std::string fields = "{id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count";
|
||||||
|
std::string fields = "{id,creator{id,display_name},details{name}";
|
||||||
const char* boolarr[2] = { "false", "true" };
|
const char* boolarr[2] = { "false", "true" };
|
||||||
|
|
||||||
std::string cmd;
|
std::string cmd;
|
||||||
@@ -65,7 +67,8 @@ auto apiBuildUrlListInternal(const Config& e, bool is_pack) -> std::string {
|
|||||||
|
|
||||||
if (is_pack) {
|
if (is_pack) {
|
||||||
cmd = "packList";
|
cmd = "packList";
|
||||||
fields += ",themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}";
|
// fields += ",themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}";
|
||||||
|
fields += ",themes{id,preview{thumb}}";
|
||||||
} else {
|
} else {
|
||||||
cmd = "themeList";
|
cmd = "themeList";
|
||||||
p0 += ",$target:String";
|
p0 += ",$target:String";
|
||||||
@@ -89,7 +92,9 @@ auto apiBuildUrlListInternal(const Config& e, bool is_pack) -> std::string {
|
|||||||
json += ",\"query\":\"" + e.query + "\"";
|
json += ",\"query\":\"" + e.query + "\"";
|
||||||
}
|
}
|
||||||
|
|
||||||
return api+"("+p0+"){"+cmd+"("+p1+")"+fields+"}}&variables={"+json+"}";
|
json = curl::EscapeString('{'+json+'}');
|
||||||
|
|
||||||
|
return api+"("+p0+"){"+cmd+"("+p1+")"+fields+"}}&variables="+json;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto apiBuildUrlDownloadInternal(const std::string& id, bool is_pack) -> std::string {
|
auto apiBuildUrlDownloadInternal(const std::string& id, bool is_pack) -> std::string {
|
||||||
@@ -99,47 +104,33 @@ auto apiBuildUrlDownloadInternal(const std::string& id, bool is_pack) -> std::st
|
|||||||
// https://api.themezer.net/?query=query{downloadPack(id:"11"){filename,url,mimetype}}
|
// https://api.themezer.net/?query=query{downloadPack(id:"11"){filename,url,mimetype}}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto apiBuildUrlDownloadTheme(const ThemeEntry& e) -> std::string {
|
|
||||||
return apiBuildUrlDownloadInternal(e.id, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto apiBuildUrlDownloadPack(const PackListEntry& e) -> std::string {
|
auto apiBuildUrlDownloadPack(const PackListEntry& e) -> std::string {
|
||||||
return apiBuildUrlDownloadInternal(e.id, true);
|
return apiBuildUrlDownloadInternal(e.id, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto apiBuildFilePack(const PackListEntry& e) -> fs::FsPath {
|
|
||||||
fs::FsPath path;
|
|
||||||
std::snprintf(path, sizeof(path), "%s/%s By %s/", THEME_FOLDER, e.details.name.c_str(), e.creator.display_name.c_str());
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto apiBuildUrlPack(const PackListEntry& e) -> std::string {
|
|
||||||
char url[2048];
|
|
||||||
std::snprintf(url, sizeof(url), "https://api.themezer.net/?query=query($id:String!){pack(id:$id){id,creator{display_name},details{name,description},last_updated,categories,dl_count,like_count,themes{id,details{name},layout{id,details{name}},categories,target,preview{original,thumb},last_updated,dl_count,like_count}}}&variables={\"id\":\"%s\"}", e.id.c_str());
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto apiBuildUrlThemeList(const Config& e) -> std::string {
|
|
||||||
return apiBuildUrlListInternal(e, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto apiBuildUrlListPacks(const Config& e) -> std::string {
|
auto apiBuildUrlListPacks(const Config& e) -> std::string {
|
||||||
return apiBuildUrlListInternal(e, true);
|
return apiBuildUrlListInternal(e, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto apiBuildListPacksCache(const Config& e) -> fs::FsPath {
|
||||||
|
fs::FsPath path;
|
||||||
|
std::snprintf(path, sizeof(path), "%s/%u_page.json", CACHE_PATH, e.page);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
auto apiBuildIconCache(const ThemeEntry& e) -> fs::FsPath {
|
auto apiBuildIconCache(const ThemeEntry& e) -> fs::FsPath {
|
||||||
fs::FsPath path;
|
fs::FsPath path;
|
||||||
std::snprintf(path, sizeof(path), "%s/%s_thumb.jpg", CACHE_PATH, e.id.c_str());
|
std::snprintf(path, sizeof(path), "%s/%s_thumb.jpg", CACHE_PATH, e.id.c_str());
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto loadThemeImage(ThemeEntry& e) -> void {
|
auto loadThemeImage(ThemeEntry& e) -> bool {
|
||||||
auto& image = e.preview.lazy_image;
|
auto& image = e.preview.lazy_image;
|
||||||
|
|
||||||
// already have the image
|
// already have the image
|
||||||
if (e.preview.lazy_image.image) {
|
if (e.preview.lazy_image.image) {
|
||||||
// log_write("warning, tried to load image: %s when already loaded\n", path.c_str());
|
// log_write("warning, tried to load image: %s when already loaded\n", path.c_str());
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
auto vg = App::GetVg();
|
auto vg = App::GetVg();
|
||||||
|
|
||||||
@@ -148,43 +139,23 @@ auto loadThemeImage(ThemeEntry& e) -> void {
|
|||||||
|
|
||||||
const auto path = apiBuildIconCache(e);
|
const auto path = apiBuildIconCache(e);
|
||||||
if (R_FAILED(fs.read_entire_file(path, image_buf))) {
|
if (R_FAILED(fs.read_entire_file(path, image_buf))) {
|
||||||
e.preview.lazy_image.state = ImageDownloadState::Failed;
|
log_write("failed to load image from file: %s\n", path.s);
|
||||||
} else {
|
} else {
|
||||||
int channels_in_file;
|
int channels_in_file;
|
||||||
auto buf = stbi_load_from_memory(image_buf.data(), image_buf.size(), &image.w, &image.h, &channels_in_file, 4);
|
auto buf = stbi_load_from_memory(image_buf.data(), image_buf.size(), &image.w, &image.h, &channels_in_file, 4);
|
||||||
if (buf) {
|
if (buf) {
|
||||||
ON_SCOPE_EXIT(stbi_image_free(buf));
|
ON_SCOPE_EXIT(stbi_image_free(buf));
|
||||||
std::memcpy(image.first_pixel, buf, sizeof(image.first_pixel));
|
|
||||||
image.image = nvgCreateImageRGBA(vg, image.w, image.h, 0, buf);
|
image.image = nvgCreateImageRGBA(vg, image.w, image.h, 0, buf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!image.image) {
|
if (!image.image) {
|
||||||
image.state = ImageDownloadState::Failed;
|
log_write("failed to load image from file: %s\n", path.s);
|
||||||
log_write("failed to load image from file: %s\n", path);
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// log_write("loaded image from file: %s\n", path);
|
// log_write("loaded image from file: %s\n", path);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto ScrollHelperDown(u64& index, u64& start, u64 step, u64 max, u64 size) -> bool {
|
|
||||||
if (size && index < (size - 1)) {
|
|
||||||
if (index < (size - step)) {
|
|
||||||
index = index + step;
|
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
|
||||||
} else {
|
|
||||||
index = size - 1;
|
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
|
||||||
}
|
|
||||||
if (index - start >= max) {
|
|
||||||
log_write("moved down\n");
|
|
||||||
start += step;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void from_json(yyjson_val* json, Creator& e) {
|
void from_json(yyjson_val* json, Creator& e) {
|
||||||
@@ -197,35 +168,18 @@ void from_json(yyjson_val* json, Creator& e) {
|
|||||||
void from_json(yyjson_val* json, Details& e) {
|
void from_json(yyjson_val* json, Details& e) {
|
||||||
JSON_OBJ_ITR(
|
JSON_OBJ_ITR(
|
||||||
JSON_SET_STR(name);
|
JSON_SET_STR(name);
|
||||||
JSON_SET_STR(description);
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void from_json(yyjson_val* json, Preview& e) {
|
void from_json(yyjson_val* json, Preview& e) {
|
||||||
JSON_OBJ_ITR(
|
JSON_OBJ_ITR(
|
||||||
JSON_SET_STR(original);
|
|
||||||
JSON_SET_STR(thumb);
|
JSON_SET_STR(thumb);
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void from_json(yyjson_val* json, DownloadPack& e) {
|
|
||||||
JSON_OBJ_ITR(
|
|
||||||
JSON_SET_STR(filename);
|
|
||||||
JSON_SET_STR(url);
|
|
||||||
JSON_SET_STR(mimetype);
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void from_json(yyjson_val* json, ThemeEntry& e) {
|
void from_json(yyjson_val* json, ThemeEntry& e) {
|
||||||
JSON_OBJ_ITR(
|
JSON_OBJ_ITR(
|
||||||
JSON_SET_STR(id);
|
JSON_SET_STR(id);
|
||||||
JSON_SET_OBJ(creator);
|
|
||||||
JSON_SET_OBJ(details);
|
|
||||||
JSON_SET_STR(last_updated);
|
|
||||||
JSON_SET_UINT(dl_count);
|
|
||||||
JSON_SET_UINT(like_count);
|
|
||||||
JSON_SET_ARR_STR(categories);
|
|
||||||
JSON_SET_STR(target);
|
|
||||||
JSON_SET_OBJ(preview);
|
JSON_SET_OBJ(preview);
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -235,10 +189,6 @@ void from_json(yyjson_val* json, PackListEntry& e) {
|
|||||||
JSON_SET_STR(id);
|
JSON_SET_STR(id);
|
||||||
JSON_SET_OBJ(creator);
|
JSON_SET_OBJ(creator);
|
||||||
JSON_SET_OBJ(details);
|
JSON_SET_OBJ(details);
|
||||||
JSON_SET_STR(last_updated);
|
|
||||||
JSON_SET_ARR_STR(categories);
|
|
||||||
JSON_SET_UINT(dl_count);
|
|
||||||
JSON_SET_UINT(like_count);
|
|
||||||
JSON_SET_ARR_OBJ(themes);
|
JSON_SET_ARR_OBJ(themes);
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -254,7 +204,6 @@ void from_json(yyjson_val* json, Pagination& e) {
|
|||||||
|
|
||||||
void from_json(const std::vector<u8>& data, DownloadPack& e) {
|
void from_json(const std::vector<u8>& data, DownloadPack& e) {
|
||||||
JSON_INIT_VEC(data, "data");
|
JSON_INIT_VEC(data, "data");
|
||||||
// JSON_GET_OBJ("downloadTheme");
|
|
||||||
JSON_GET_OBJ("downloadPack");
|
JSON_GET_OBJ("downloadPack");
|
||||||
JSON_OBJ_ITR(
|
JSON_OBJ_ITR(
|
||||||
JSON_SET_STR(filename);
|
JSON_SET_STR(filename);
|
||||||
@@ -263,8 +212,8 @@ void from_json(const std::vector<u8>& data, DownloadPack& e) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void from_json(const std::vector<u8>& data, PackList& e) {
|
void from_json(const fs::FsPath& path, PackList& e) {
|
||||||
JSON_INIT_VEC(data, "data");
|
JSON_INIT_VEC_FILE(path, "data", nullptr);
|
||||||
JSON_OBJ_ITR(
|
JSON_OBJ_ITR(
|
||||||
JSON_SET_ARR_OBJ(packList);
|
JSON_SET_ARR_OBJ(packList);
|
||||||
JSON_SET_OBJ(pagination);
|
JSON_SET_OBJ(pagination);
|
||||||
@@ -272,7 +221,7 @@ void from_json(const std::vector<u8>& data, PackList& e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
|
auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
|
||||||
static fs::FsPath zip_out{"/switch/sphaira/cache/themezer/temp.zip"};
|
static const fs::FsPath zip_out{"/switch/sphaira/cache/themezer/temp.zip"};
|
||||||
constexpr auto chunk_size = 1024 * 512; // 512KiB
|
constexpr auto chunk_size = 1024 * 512; // 512KiB
|
||||||
|
|
||||||
fs::FsNativeSd fs;
|
fs::FsNativeSd fs;
|
||||||
@@ -287,22 +236,17 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
|
|||||||
|
|
||||||
const auto url = apiBuildUrlDownloadPack(entry);
|
const auto url = apiBuildUrlDownloadPack(entry);
|
||||||
log_write("using url: %s\n", url.c_str());
|
log_write("using url: %s\n", url.c_str());
|
||||||
DownloadClearCache(url);
|
const auto result = curl::Api().ToMemory(
|
||||||
const auto data = DownloadMemory(url, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
|
curl::Url{url},
|
||||||
if (pbox->ShouldExit()) {
|
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||||
return false;
|
);
|
||||||
}
|
|
||||||
pbox->UpdateTransfer(dlnow, dltotal);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data.empty()) {
|
if (!result.success || result.data.empty()) {
|
||||||
log_write("error with download: %s\n", url.c_str());
|
log_write("error with download: %s\n", url.c_str());
|
||||||
// push popup error box
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
from_json(data, download_pack);
|
from_json(result.data, download_pack);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. download the zip
|
// 2. download the zip
|
||||||
@@ -310,16 +254,11 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
|
|||||||
pbox->NewTransfer("Downloading "_i18n + entry.details.name);
|
pbox->NewTransfer("Downloading "_i18n + entry.details.name);
|
||||||
log_write("starting download: %s\n", download_pack.url.c_str());
|
log_write("starting download: %s\n", download_pack.url.c_str());
|
||||||
|
|
||||||
DownloadClearCache(download_pack.url);
|
if (!curl::Api().ToFile(
|
||||||
if (!DownloadFile(download_pack.url, zip_out, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
|
curl::Url{download_pack.url},
|
||||||
if (pbox->ShouldExit()) {
|
curl::Path{zip_out},
|
||||||
return false;
|
curl::OnProgress{pbox->OnDownloadProgressCallback()}).success) {
|
||||||
}
|
|
||||||
pbox->UpdateTransfer(dlnow, dltotal);
|
|
||||||
return true;
|
|
||||||
})) {
|
|
||||||
log_write("error with download\n");
|
log_write("error with download\n");
|
||||||
// push popup error box
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,14 +267,14 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
|
|||||||
|
|
||||||
// create directories
|
// create directories
|
||||||
fs::FsPath dir_path;
|
fs::FsPath dir_path;
|
||||||
std::snprintf(dir_path, sizeof(dir_path), "%s/%s - By %s", THEME_FOLDER, entry.details.name.c_str(), entry.creator.display_name.c_str());
|
std::snprintf(dir_path, sizeof(dir_path), "%s/%s - By %s", THEME_FOLDER.s, entry.details.name.c_str(), entry.creator.display_name.c_str());
|
||||||
fs.CreateDirectoryRecursively(dir_path);
|
fs.CreateDirectoryRecursively(dir_path);
|
||||||
|
|
||||||
// 3. extract the zip
|
// 3. extract the zip
|
||||||
if (!pbox->ShouldExit()) {
|
if (!pbox->ShouldExit()) {
|
||||||
auto zfile = unzOpen64(zip_out);
|
auto zfile = unzOpen64(zip_out);
|
||||||
if (!zfile) {
|
if (!zfile) {
|
||||||
log_write("failed to open zip: %s\n", zip_out);
|
log_write("failed to open zip: %s\n", zip_out.s);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ON_SCOPE_EXIT(unzClose(zfile));
|
ON_SCOPE_EXIT(unzClose(zfile));
|
||||||
@@ -370,24 +309,24 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
|
|||||||
|
|
||||||
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) {
|
||||||
log_write("failed to create file: %s 0x%04X\n", file_path, rc);
|
log_write("failed to create file: %s 0x%04X\n", file_path.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (R_FAILED(rc = fs.OpenFile(file_path, FsOpenMode_Write, &f))) {
|
if (R_FAILED(rc = fs.OpenFile(file_path, FsOpenMode_Write, &f))) {
|
||||||
log_write("failed to open file: %s 0x%04X\n", file_path, rc);
|
log_write("failed to open file: %s 0x%04X\n", file_path.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ON_SCOPE_EXIT(fsFileClose(&f));
|
ON_SCOPE_EXIT(fsFileClose(&f));
|
||||||
|
|
||||||
if (R_FAILED(rc = fsFileSetSize(&f, info.uncompressed_size))) {
|
if (R_FAILED(rc = fsFileSetSize(&f, info.uncompressed_size))) {
|
||||||
log_write("failed to set file size: %s 0x%04X\n", file_path, rc);
|
log_write("failed to set file size: %s 0x%04X\n", file_path.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<char> buf(chunk_size);
|
std::vector<char> buf(chunk_size);
|
||||||
u64 offset{};
|
s64 offset{};
|
||||||
while (offset < info.uncompressed_size) {
|
while (offset < info.uncompressed_size) {
|
||||||
if (pbox->ShouldExit()) {
|
if (pbox->ShouldExit()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -400,7 +339,7 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (R_FAILED(rc = fsFileWrite(&f, offset, buf.data(), bytes_read, FsWriteOption_None))) {
|
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, rc);
|
log_write("failed to write file: %s 0x%04X\n", file_path.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,6 +362,8 @@ LazyImage::~LazyImage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||||
|
fs::FsNativeSd().CreateDirectoryRecursively(CACHE_PATH);
|
||||||
|
|
||||||
SetAction(Button::B, Action{"Back"_i18n, [this]{
|
SetAction(Button::B, Action{"Back"_i18n, [this]{
|
||||||
SetPop();
|
SetPop();
|
||||||
}});
|
}});
|
||||||
@@ -445,7 +386,25 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
|||||||
}}),
|
}}),
|
||||||
std::make_pair(Button::DOWN, Action{[this](){
|
std::make_pair(Button::DOWN, Action{[this](){
|
||||||
const auto& page = m_pages[m_page_index];
|
const auto& page = m_pages[m_page_index];
|
||||||
if (ScrollHelperDown(m_index, m_start, 3, 6, page.m_packList.size())) {
|
if (m_list->ScrollDown(m_index, 3, page.m_packList.size())) {
|
||||||
|
SetIndex(m_index);
|
||||||
|
}
|
||||||
|
}}),
|
||||||
|
std::make_pair(Button::UP, Action{[this](){
|
||||||
|
const auto& page = m_pages[m_page_index];
|
||||||
|
if (m_list->ScrollUp(m_index, 3, page.m_packList.size())) {
|
||||||
|
SetIndex(m_index);
|
||||||
|
}
|
||||||
|
}}),
|
||||||
|
std::make_pair(Button::R2, Action{[this](){
|
||||||
|
const auto& page = m_pages[m_page_index];
|
||||||
|
if (m_list->ScrollDown(m_index, 6, page.m_packList.size())) {
|
||||||
|
SetIndex(m_index);
|
||||||
|
}
|
||||||
|
}}),
|
||||||
|
std::make_pair(Button::L2, Action{[this](){
|
||||||
|
const auto& page = m_pages[m_page_index];
|
||||||
|
if (m_list->ScrollUp(m_index, 6, page.m_packList.size())) {
|
||||||
SetIndex(m_index);
|
SetIndex(m_index);
|
||||||
}
|
}
|
||||||
}}),
|
}}),
|
||||||
@@ -461,12 +420,10 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
|||||||
|
|
||||||
App::Push(std::make_shared<ProgressBox>("Installing "_i18n + entry.details.name, [this, &entry](auto pbox){
|
App::Push(std::make_shared<ProgressBox>("Installing "_i18n + entry.details.name, [this, &entry](auto pbox){
|
||||||
return InstallTheme(pbox, entry);
|
return InstallTheme(pbox, entry);
|
||||||
}, [this](bool success){
|
}, [this, &entry](bool success){
|
||||||
// if (success) {
|
if (success) {
|
||||||
// m_entry.status = EntryStatus::Installed;
|
App::Notify("Downloaded "_i18n + entry.details.name);
|
||||||
// m_menu.SetDirty();
|
}
|
||||||
// UpdateOptions();
|
|
||||||
// }
|
|
||||||
}, 2));
|
}, 2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -492,14 +449,14 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
|||||||
InvalidateAllPages();
|
InvalidateAllPages();
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](std::size_t& 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) {
|
||||||
m_sort.Set(index_out);
|
m_sort.Set(index_out);
|
||||||
InvalidateAllPages();
|
InvalidateAllPages();
|
||||||
}
|
}
|
||||||
}, m_sort.Get()));
|
}, m_sort.Get()));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](std::size_t& index_out){
|
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](s64& index_out){
|
||||||
if (m_order.Get() != index_out) {
|
if (m_order.Get() != index_out) {
|
||||||
m_order.Set(index_out);
|
m_order.Set(index_out);
|
||||||
InvalidateAllPages();
|
InvalidateAllPages();
|
||||||
@@ -526,16 +483,6 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}}),
|
}}),
|
||||||
std::make_pair(Button::UP, Action{[this](){
|
|
||||||
if (m_index >= 3) {
|
|
||||||
SetIndex(m_index - 3);
|
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
|
||||||
if (m_index < m_start ) {
|
|
||||||
// log_write("moved up\n");
|
|
||||||
m_start -= 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::R, Action{"Next Page"_i18n, [this](){
|
std::make_pair(Button::R, Action{"Next Page"_i18n, [this](){
|
||||||
m_page_index++;
|
m_page_index++;
|
||||||
if (m_page_index >= m_page_index_max) {
|
if (m_page_index >= m_page_index_max) {
|
||||||
@@ -552,6 +499,10 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
|||||||
}})
|
}})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const Vec4 v{75, 110, 350, 250};
|
||||||
|
const Vec2 pad{10, 10};
|
||||||
|
m_list = std::make_unique<List>(3, 6, m_pos, v, pad);
|
||||||
|
|
||||||
m_page_index = 0;
|
m_page_index = 0;
|
||||||
m_pages.resize(1);
|
m_pages.resize(1);
|
||||||
PackListDownload();
|
PackListDownload();
|
||||||
@@ -563,13 +514,31 @@ Menu::~Menu() {
|
|||||||
|
|
||||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||||
MenuBase::Update(controller, touch);
|
MenuBase::Update(controller, touch);
|
||||||
|
|
||||||
|
if (m_pages.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& page = m_pages[m_page_index];
|
||||||
|
if (page.m_ready != PageLoadState::Done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_list->OnUpdate(controller, touch, page.m_packList.size(), [this](auto i) {
|
||||||
|
if (m_index == i) {
|
||||||
|
FireAction(Button::A);
|
||||||
|
} else {
|
||||||
|
App::PlaySoundEffect(SoundEffect_Focus);
|
||||||
|
SetIndex(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||||
MenuBase::Draw(vg, theme);
|
MenuBase::Draw(vg, theme);
|
||||||
|
|
||||||
if (m_pages.empty()) {
|
if (m_pages.empty()) {
|
||||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::YELLOW, "Empty!"_i18n.c_str());
|
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Empty!"_i18n.c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,98 +546,105 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
|
|
||||||
switch (page.m_ready) {
|
switch (page.m_ready) {
|
||||||
case PageLoadState::None:
|
case PageLoadState::None:
|
||||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::YELLOW, "Not Ready..."_i18n.c_str());
|
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Not Ready..."_i18n.c_str());
|
||||||
return;
|
return;
|
||||||
case PageLoadState::Loading:
|
case PageLoadState::Loading:
|
||||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::YELLOW, "Loading"_i18n.c_str());
|
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Loading"_i18n.c_str());
|
||||||
return;
|
return;
|
||||||
case PageLoadState::Done:
|
case PageLoadState::Done:
|
||||||
break;
|
break;
|
||||||
case PageLoadState::Error:
|
case PageLoadState::Error:
|
||||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::YELLOW, "Error loading page!"_i18n.c_str());
|
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Error loading page!"_i18n.c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const u64 SCROLL = m_start;
|
// max images per frame, in order to not hit io / gpu too hard.
|
||||||
const u64 max_entry_display = 9;
|
const int image_load_max = 2;
|
||||||
const u64 nro_total = page.m_packList.size();// m_entries_current.size();
|
int image_load_count = 0;
|
||||||
const u64 cursor_pos = m_index;
|
|
||||||
|
|
||||||
// only draw scrollbar if needed
|
m_list->Draw(vg, theme, page.m_packList.size(), [this, &page, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
|
||||||
if (nro_total > max_entry_display) {
|
const auto& [x, y, w, h] = v;
|
||||||
const auto scrollbar_size = 500.f;
|
auto& e = page.m_packList[pos];
|
||||||
const auto sb_h = 3.f / (float)(nro_total + 3) * scrollbar_size;
|
|
||||||
const auto sb_y = SCROLL / 3.f;
|
|
||||||
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
|
|
||||||
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * 2) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
|
|
||||||
}
|
|
||||||
|
|
||||||
nvgSave(vg);
|
auto text_id = ThemeEntryID_TEXT;
|
||||||
nvgScissor(vg, 30, 87, 1220 - 30, 646 - 87); // clip
|
if (pos == m_index) {
|
||||||
|
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||||
|
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||||
|
} else {
|
||||||
|
DrawElement(x, y, w, h, ThemeEntryID_GRID);
|
||||||
|
}
|
||||||
|
|
||||||
for (u64 i = 0, pos = SCROLL, y = 110, w = 350, h = 250; pos < nro_total && i < max_entry_display; y += h + 10) {
|
const float xoff = (350 - 320) / 2;
|
||||||
for (u64 j = 0, x = 75; j < 3 && pos < nro_total && i < max_entry_display; j++, i++, pos++, x += w + 10) {
|
|
||||||
const auto index = pos;
|
|
||||||
auto& e = page.m_packList[index];
|
|
||||||
|
|
||||||
auto text_id = ThemeEntryID_TEXT;
|
// lazy load image
|
||||||
if (pos == cursor_pos) {
|
if (e.themes.size()) {
|
||||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
auto& theme = e.themes[0];
|
||||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
|
auto& image = e.themes[0].preview.lazy_image;
|
||||||
} else {
|
|
||||||
DrawElement(x, y, w, h, ThemeEntryID_GRID);
|
|
||||||
}
|
|
||||||
|
|
||||||
const float xoff = (350 - 320) / 2;
|
// try and load cached image.
|
||||||
const float yoff = (350 - 320) / 2;
|
if (image_load_count < image_load_max && !image.image && !image.tried_cache) {
|
||||||
|
image.tried_cache = true;
|
||||||
// lazy load image
|
image.cached = loadThemeImage(theme);
|
||||||
if (e.themes.size()) {
|
if (image.cached) {
|
||||||
auto& theme = e.themes[0];
|
image_load_count++;
|
||||||
auto& image = e.themes[0].preview.lazy_image;
|
|
||||||
if (!image.image) {
|
|
||||||
switch (image.state) {
|
|
||||||
case ImageDownloadState::None: {
|
|
||||||
const auto path = apiBuildIconCache(theme);
|
|
||||||
log_write("downloading theme!: %s\n", path);
|
|
||||||
|
|
||||||
if (fs::FsNativeSd().FileExists(path)) {
|
|
||||||
loadThemeImage(theme);
|
|
||||||
} else {
|
|
||||||
const auto url = theme.preview.thumb;
|
|
||||||
log_write("downloading url: %s\n", url.c_str());
|
|
||||||
image.state = ImageDownloadState::Progress;
|
|
||||||
DownloadFileAsync(url, path, "", [this, index, &image](std::vector<u8>& data, bool success) {
|
|
||||||
if (success) {
|
|
||||||
image.state = ImageDownloadState::Done;
|
|
||||||
log_write("downloaded themezer image\n");
|
|
||||||
} else {
|
|
||||||
image.state = ImageDownloadState::Failed;
|
|
||||||
log_write("failed to download image\n");
|
|
||||||
}
|
|
||||||
}, nullptr, DownloadPriority::High);
|
|
||||||
}
|
|
||||||
} break;
|
|
||||||
case ImageDownloadState::Progress: {
|
|
||||||
|
|
||||||
} break;
|
|
||||||
case ImageDownloadState::Done: {
|
|
||||||
loadThemeImage(theme);
|
|
||||||
} break;
|
|
||||||
case ImageDownloadState::Failed: {
|
|
||||||
} break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
gfx::drawImageRounded(vg, x + xoff, y, 320, 180, image.image);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gfx::drawTextArgs(vg, x + xoff, y + 180 + 20, 18, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s", e.details.name.c_str());
|
if (!image.image || image.cached) {
|
||||||
gfx::drawTextArgs(vg, x + xoff, y + 180 + 55, 18, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s", e.creator.display_name.c_str());
|
switch (image.state) {
|
||||||
}
|
case ImageDownloadState::None: {
|
||||||
}
|
const auto path = apiBuildIconCache(theme);
|
||||||
|
log_write("downloading theme!: %s\n", path.s);
|
||||||
|
|
||||||
nvgRestore(vg);
|
const auto url = theme.preview.thumb;
|
||||||
|
log_write("downloading url: %s\n", url.c_str());
|
||||||
|
image.state = ImageDownloadState::Progress;
|
||||||
|
curl::Api().ToFileAsync(
|
||||||
|
curl::Url{url},
|
||||||
|
curl::Path{path},
|
||||||
|
curl::Flags{curl::Flag_Cache},
|
||||||
|
curl::StopToken{this->GetToken()},
|
||||||
|
curl::OnComplete{[this, &image](auto& result) {
|
||||||
|
if (result.success) {
|
||||||
|
image.state = ImageDownloadState::Done;
|
||||||
|
// data hasn't changed
|
||||||
|
if (result.code == 304) {
|
||||||
|
image.cached = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
image.state = ImageDownloadState::Failed;
|
||||||
|
log_write("failed to download image\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} break;
|
||||||
|
case ImageDownloadState::Progress: {
|
||||||
|
|
||||||
|
} break;
|
||||||
|
case ImageDownloadState::Done: {
|
||||||
|
image.cached = false;
|
||||||
|
if (!loadThemeImage(theme)) {
|
||||||
|
image.state = ImageDownloadState::Failed;
|
||||||
|
} else {
|
||||||
|
image_load_count++;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case ImageDownloadState::Failed: {
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gfx::drawImageRounded(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage());
|
||||||
|
}
|
||||||
|
|
||||||
|
nvgSave(vg);
|
||||||
|
nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip
|
||||||
|
{
|
||||||
|
gfx::drawTextArgs(vg, x + xoff, y + 180 + 20, 18, NVG_ALIGN_LEFT, theme->GetColour(text_id), "%s", e.details.name.c_str());
|
||||||
|
gfx::drawTextArgs(vg, x + xoff, y + 180 + 55, 18, NVG_ALIGN_LEFT, theme->GetColour(text_id), "%s", e.creator.display_name.c_str());
|
||||||
|
}
|
||||||
|
nvgRestore(vg);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Menu::OnFocusGained() {
|
void Menu::OnFocusGained() {
|
||||||
@@ -691,7 +667,7 @@ void Menu::PackListDownload() {
|
|||||||
SetSubHeading(subheading);
|
SetSubHeading(subheading);
|
||||||
|
|
||||||
m_index = 0;
|
m_index = 0;
|
||||||
m_start = 0;
|
m_list->SetYoff(0);
|
||||||
|
|
||||||
// already downloaded
|
// already downloaded
|
||||||
if (m_pages[m_page_index].m_ready != PageLoadState::None) {
|
if (m_pages[m_page_index].m_ready != PageLoadState::None) {
|
||||||
@@ -706,39 +682,43 @@ void Menu::PackListDownload() {
|
|||||||
config.order_index = m_order.Get();
|
config.order_index = m_order.Get();
|
||||||
config.nsfw = m_nsfw.Get();
|
config.nsfw = m_nsfw.Get();
|
||||||
const auto packList_url = apiBuildUrlListPacks(config);
|
const auto packList_url = apiBuildUrlListPacks(config);
|
||||||
const auto themeList_url = apiBuildUrlThemeList(config);
|
const auto packlist_path = apiBuildListPacksCache(config);
|
||||||
|
|
||||||
log_write("\npackList_url: %s\n\n", packList_url.c_str());
|
log_write("\npackList_url: %s\n\n", packList_url.c_str());
|
||||||
log_write("\nthemeList_url: %s\n\n", themeList_url.c_str());
|
|
||||||
|
|
||||||
DownloadClearCache(packList_url);
|
curl::Api().ToFileAsync(
|
||||||
DownloadMemoryAsync(packList_url, "", [this, page_index](std::vector<u8>& data, bool success){
|
curl::Url{packList_url},
|
||||||
log_write("got themezer data\n");
|
curl::Path{packlist_path},
|
||||||
if (!success) {
|
curl::Flags{curl::Flag_Cache},
|
||||||
|
curl::StopToken{this->GetToken()},
|
||||||
|
curl::OnComplete{[this, page_index](auto& result){
|
||||||
|
log_write("got themezer data\n");
|
||||||
|
if (!result.success) {
|
||||||
|
auto& page = m_pages[page_index-1];
|
||||||
|
page.m_ready = PageLoadState::Error;
|
||||||
|
log_write("failed to get themezer data...\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PackList a;
|
||||||
|
from_json(result.path, a);
|
||||||
|
|
||||||
|
m_pages.resize(a.pagination.page_count);
|
||||||
auto& page = m_pages[page_index-1];
|
auto& page = m_pages[page_index-1];
|
||||||
page.m_ready = PageLoadState::Error;
|
|
||||||
log_write("failed to get themezer data...\n");
|
page.m_packList = a.packList;
|
||||||
return;
|
page.m_pagination = a.pagination;
|
||||||
|
page.m_ready = PageLoadState::Done;
|
||||||
|
m_page_index_max = a.pagination.page_count;
|
||||||
|
|
||||||
|
char subheading[128];
|
||||||
|
std::snprintf(subheading, sizeof(subheading), "Page %zu / %zu"_i18n.c_str(), m_page_index+1, m_page_index_max);
|
||||||
|
SetSubHeading(subheading);
|
||||||
|
|
||||||
|
log_write("a.pagination.page: %zu\n", a.pagination.page);
|
||||||
|
log_write("a.pagination.page_count: %zu\n", a.pagination.page_count);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
PackList a;
|
|
||||||
from_json(data, a);
|
|
||||||
|
|
||||||
m_pages.resize(a.pagination.page_count);
|
|
||||||
auto& page = m_pages[page_index-1];
|
|
||||||
|
|
||||||
page.m_packList = a.packList;
|
|
||||||
page.m_pagination = a.pagination;
|
|
||||||
page.m_ready = PageLoadState::Done;
|
|
||||||
m_page_index_max = a.pagination.page_count;
|
|
||||||
|
|
||||||
char subheading[128];
|
|
||||||
std::snprintf(subheading, sizeof(subheading), "Page %zu / %zu"_i18n.c_str(), m_page_index+1, m_page_index_max);
|
|
||||||
SetSubHeading(subheading);
|
|
||||||
|
|
||||||
log_write("a.pagination.page: %u\n", a.pagination.page);
|
|
||||||
log_write("a.pagination.page_count: %u\n", a.pagination.page_count);
|
|
||||||
}, nullptr, DownloadPriority::High);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace sphaira::ui::menu::themezer
|
} // namespace sphaira::ui::menu::themezer
|
||||||
|
|||||||
@@ -11,10 +11,6 @@ NotifEntry::NotifEntry(std::string text, Side side)
|
|||||||
, m_side{side} {
|
, m_side{side} {
|
||||||
}
|
}
|
||||||
|
|
||||||
auto NotifEntry::OnLayoutChange() -> void {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
auto NotifEntry::Draw(NVGcontext* vg, Theme* theme, float y) -> bool {
|
auto NotifEntry::Draw(NVGcontext* vg, Theme* theme, float y) -> bool {
|
||||||
m_pos.y = y;
|
m_pos.y = y;
|
||||||
Draw(vg, theme);
|
Draw(vg, theme);
|
||||||
@@ -23,15 +19,9 @@ auto NotifEntry::Draw(NVGcontext* vg, Theme* theme, float y) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto NotifEntry::Draw(NVGcontext* vg, Theme* theme) -> void {
|
auto NotifEntry::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||||
auto overlay_col = theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour;
|
auto text_col = theme->GetColour(ThemeEntryID_TEXT);
|
||||||
auto selected_col = theme->elements[ThemeEntryID_SELECTED].colour;
|
|
||||||
auto text_col = theme->elements[ThemeEntryID_TEXT].colour;
|
|
||||||
float font_size = 18.f;
|
float font_size = 18.f;
|
||||||
// overlay_col.a = 0.2f;
|
|
||||||
// selected_col.a = 0.2f;
|
|
||||||
// text_col.a = 0.2f;
|
|
||||||
|
|
||||||
// auto vg = App::GetVg();
|
|
||||||
if (!m_bounds_measured) {
|
if (!m_bounds_measured) {
|
||||||
m_bounds_measured = true;
|
m_bounds_measured = true;
|
||||||
m_pos.w = 320.f;
|
m_pos.w = 320.f;
|
||||||
@@ -53,15 +43,10 @@ auto NotifEntry::Draw(NVGcontext* vg, Theme* theme) -> void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gfx::drawRectOutline(vg, 4.f, overlay_col, m_pos, selected_col);
|
gfx::drawRectOutline(vg, theme, 4.f, m_pos);
|
||||||
gfx::drawText(vg, Vec2{m_pos.x + (m_pos.w / 2.f), m_pos.y + (m_pos.h / 2.f)}, font_size, text_col, m_text.c_str(), NVG_ALIGN_MIDDLE | NVG_ALIGN_CENTER);
|
gfx::drawText(vg, Vec2{m_pos.x + (m_pos.w / 2.f), m_pos.y + (m_pos.h / 2.f)}, font_size, text_col, m_text.c_str(), NVG_ALIGN_MIDDLE | NVG_ALIGN_CENTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto NotifMananger::OnLayoutChange() -> void {
|
|
||||||
mutexLock(&m_mutex);
|
|
||||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
|
||||||
}
|
|
||||||
|
|
||||||
auto NotifMananger::Draw(NVGcontext* vg, Theme* theme) -> void {
|
auto NotifMananger::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||||
mutexLock(&m_mutex);
|
mutexLock(&m_mutex);
|
||||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
namespace sphaira::ui::gfx {
|
namespace sphaira::ui::gfx {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
static 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"},
|
||||||
std::pair{Button::X, "\uE0E2"},
|
std::pair{Button::X, "\uE0E2"},
|
||||||
@@ -33,67 +33,45 @@ static constexpr std::array buttons = {
|
|||||||
std::pair{Button::R3, "\uE105"},
|
std::pair{Button::R3, "\uE105"},
|
||||||
};
|
};
|
||||||
|
|
||||||
#define F(a) (a/255.f) // turn range 0-255 to 0.f-1.f range
|
|
||||||
constexpr std::array COLOURS = {
|
|
||||||
std::pair<Colour, NVGcolor>{Colour::BLACK, { F(45.f), F(45.f), F(45.f), F(255.f) }},
|
|
||||||
std::pair<Colour, NVGcolor>{Colour::LIGHT_BLACK, { F(50.f), F(50.f), F(50.f), F(255.f) }},
|
|
||||||
std::pair<Colour, NVGcolor>{Colour::SILVER, { F(128.f), F(128.f), F(128.f), F(255.f) }},
|
|
||||||
std::pair<Colour, NVGcolor>{Colour::DARK_GREY, { F(70.f), F(70.f), F(70.f), F(255.f) }},
|
|
||||||
std::pair<Colour, NVGcolor>{Colour::GREY, { F(77.f), F(77.f), F(77.f), F(255.f) }},
|
|
||||||
std::pair<Colour, NVGcolor>{Colour::WHITE, { F(251.f), F(251.f), F(251.f), F(255.f) }},
|
|
||||||
std::pair<Colour, NVGcolor>{Colour::CYAN, { F(0.f), F(255.f), F(200.f), F(255.f) }},
|
|
||||||
std::pair<Colour, NVGcolor>{Colour::TEAL, { F(143.f), F(253.f), F(252.f), F(255.f) }},
|
|
||||||
std::pair<Colour, NVGcolor>{Colour::BLUE, { F(36.f), F(141.f), F(199.f), F(255.f) }},
|
|
||||||
std::pair<Colour, NVGcolor>{Colour::LIGHT_BLUE, { F(26.f), F(188.f), F(252.f), F(255.f) }},
|
|
||||||
std::pair<Colour, NVGcolor>{Colour::YELLOW, { F(255.f), F(177.f), F(66.f), F(255.f) }},
|
|
||||||
std::pair<Colour, NVGcolor>{Colour::RED, { F(250.f), F(90.f), F(58.f), F(255.f) }}
|
|
||||||
};
|
|
||||||
#undef F
|
|
||||||
|
|
||||||
// NEW ---------------------
|
// NEW ---------------------
|
||||||
inline void drawRectIntenal(NVGcontext* vg, const Vec4& vec, const NVGcolor& c, bool rounded) {
|
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, bool rounded) {
|
||||||
nvgBeginPath(vg);
|
nvgBeginPath(vg);
|
||||||
if (rounded) {
|
if (rounded) {
|
||||||
nvgRoundedRect(vg, vec.x, vec.y, vec.w, vec.h, 15);
|
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
|
||||||
} else {
|
} else {
|
||||||
nvgRect(vg, vec.x, vec.y, vec.w, vec.h);
|
nvgRect(vg, v.x, v.y, v.w, v.h);
|
||||||
}
|
}
|
||||||
nvgFillColor(vg, c);
|
nvgFillColor(vg, c);
|
||||||
nvgFill(vg);
|
nvgFill(vg);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void drawRectIntenal(NVGcontext* vg, const Vec4& vec, const NVGpaint& p, bool rounded) {
|
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGpaint& p, bool rounded) {
|
||||||
nvgBeginPath(vg);
|
nvgBeginPath(vg);
|
||||||
if (rounded) {
|
if (rounded) {
|
||||||
nvgRoundedRect(vg, vec.x, vec.y, vec.w, vec.h, 15);
|
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
|
||||||
} else {
|
} else {
|
||||||
nvgRect(vg, vec.x, vec.y, vec.w, vec.h);
|
nvgRect(vg, v.x, v.y, v.w, v.h);
|
||||||
}
|
}
|
||||||
nvgFillPaint(vg, p);
|
nvgFillPaint(vg, p);
|
||||||
nvgFill(vg);
|
nvgFill(vg);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void drawRectOutlineInternal(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGcolor& c) {
|
void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, const Vec4& v) {
|
||||||
float gradientX, gradientY, color;
|
float gradientX, gradientY, color;
|
||||||
getHighlightAnimation(&gradientX, &gradientY, &color);
|
getHighlightAnimation(&gradientX, &gradientY, &color);
|
||||||
|
|
||||||
const auto strokeWidth = 5.0;
|
const auto strokeWidth = 5.F;
|
||||||
auto v2 = vec;
|
auto v2 = v;
|
||||||
v2.x -= strokeWidth / 2.0;
|
v2.x -= strokeWidth / 2.F;
|
||||||
v2.y -= strokeWidth / 2.0;
|
v2.y -= strokeWidth / 2.F;
|
||||||
v2.w += strokeWidth;
|
v2.w += strokeWidth;
|
||||||
v2.h += strokeWidth;
|
v2.h += strokeWidth;
|
||||||
const auto corner_radius = 0.5;
|
const auto corner_radius = 0.5F;
|
||||||
|
|
||||||
nvgSave(vg);
|
const auto shadow_width = 2.F;
|
||||||
nvgResetScissor(vg);
|
const auto shadow_offset = 10.F;
|
||||||
|
const auto shadow_feather = 10.F;
|
||||||
// const auto stroke_width = 5.0f;
|
const auto shadow_opacity = 128.F;
|
||||||
// const auto shadow_corner_radius = 6.0f;
|
|
||||||
const auto shadow_width = 2.0f;
|
|
||||||
const auto shadow_offset = 10.0f;
|
|
||||||
const auto shadow_feather = 10.0f;
|
|
||||||
const auto shadow_opacity = 128.0f;
|
|
||||||
|
|
||||||
// Shadow
|
// Shadow
|
||||||
NVGpaint shadowPaint = nvgBoxGradient(vg,
|
NVGpaint shadowPaint = nvgBoxGradient(vg,
|
||||||
@@ -110,8 +88,8 @@ inline void drawRectOutlineInternal(NVGcontext* vg, float size, const NVGcolor&
|
|||||||
nvgFillPaint(vg, shadowPaint);
|
nvgFillPaint(vg, shadowPaint);
|
||||||
nvgFill(vg);
|
nvgFill(vg);
|
||||||
|
|
||||||
const auto color1 = nvgRGB(25, 138, 198);
|
const auto color1 = theme->GetColour(ThemeEntryID_HIGHLIGHT_1);
|
||||||
const auto color2 = nvgRGB(137, 241, 242);
|
const auto color2 = theme->GetColour(ThemeEntryID_HIGHLIGHT_2);
|
||||||
const auto borderColor = nvgRGBAf(color2.r, color2.g, color2.b, 0.5);
|
const auto borderColor = nvgRGBAf(color2.r, color2.g, color2.b, 0.5);
|
||||||
const auto transparent = nvgRGBA(0, 0, 0, 0);
|
const auto transparent = nvgRGBA(0, 0, 0, 0);
|
||||||
|
|
||||||
@@ -147,54 +125,23 @@ inline void drawRectOutlineInternal(NVGcontext* vg, float size, const NVGcolor&
|
|||||||
nvgStrokeWidth(vg, strokeWidth);
|
nvgStrokeWidth(vg, strokeWidth);
|
||||||
nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
|
nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
|
||||||
nvgStroke(vg);
|
nvgStroke(vg);
|
||||||
|
|
||||||
drawRectIntenal(vg, {vec.x-size,vec.y-size,vec.w+(size*2.f),vec.h+(size * 2.f)}, pulsationColor, false);
|
|
||||||
drawRectIntenal(vg, vec, c, true);
|
|
||||||
nvgBeginPath(vg);
|
|
||||||
nvgRoundedRect(vg, vec.x, vec.y, vec.w, vec.h, corner_radius);
|
|
||||||
nvgFillColor(vg, c);
|
|
||||||
nvgFill(vg);
|
|
||||||
|
|
||||||
nvgRestore(vg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void drawRectOutlineInternal(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGpaint& p) {
|
void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, const Vec4& v, const NVGcolor& c) {
|
||||||
float gradientX, gradientY, color;
|
const auto corner_radius = 0.5;
|
||||||
getHighlightAnimation(&gradientX, &gradientY, &color);
|
drawRectOutlineInternal(vg, theme, size, v);
|
||||||
|
|
||||||
NVGcolor pulsationColor = nvgRGBAf((color * out_col.r) + (1 - color) * out_col.r,
|
|
||||||
(color * out_col.g) + (1 - color) * out_col.g,
|
|
||||||
(color * out_col.b) + (1 - color) * out_col.b,
|
|
||||||
out_col.a);
|
|
||||||
|
|
||||||
drawRectIntenal(vg, {vec.x-size,vec.y-size,vec.w+(size*2.f),vec.h+(size * 2.f)}, pulsationColor, false);
|
|
||||||
drawRectIntenal(vg, vec, p, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void drawTriangleInternal(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor& c) {
|
|
||||||
nvgBeginPath(vg);
|
nvgBeginPath(vg);
|
||||||
nvgMoveTo(vg, aX, aY);
|
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, corner_radius);
|
||||||
nvgLineTo(vg, bX, bY);
|
|
||||||
nvgLineTo(vg, cX, cY);
|
|
||||||
nvgFillColor(vg, c);
|
nvgFillColor(vg, c);
|
||||||
nvgFill(vg);
|
nvgFill(vg);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void drawTriangleInternal(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p) {
|
void drawTextIntenal(NVGcontext* vg, const Vec2& v, float size, const char* str, const char* end, int align, const NVGcolor& c) {
|
||||||
nvgBeginPath(vg);
|
|
||||||
nvgMoveTo(vg, aX, aY);
|
|
||||||
nvgLineTo(vg, bX, bY);
|
|
||||||
nvgLineTo(vg, cX, cY);
|
|
||||||
nvgFillPaint(vg, p);
|
|
||||||
nvgFill(vg);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void drawTextIntenal(NVGcontext* vg, Vec2 vec, float size, const char* str, const char* end, int align, const NVGcolor& c) {
|
|
||||||
nvgBeginPath(vg);
|
nvgBeginPath(vg);
|
||||||
nvgFontSize(vg, size);
|
nvgFontSize(vg, size);
|
||||||
nvgTextAlign(vg, align);
|
nvgTextAlign(vg, align);
|
||||||
nvgFillColor(vg, c);
|
nvgFillColor(vg, c);
|
||||||
nvgText(vg, vec.x, vec.y, str, end);
|
nvgText(vg, v.x, v.y, str, end);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -208,15 +155,6 @@ const char* getButton(const Button want) {
|
|||||||
std::unreachable();
|
std::unreachable();
|
||||||
}
|
}
|
||||||
|
|
||||||
NVGcolor getColour(Colour want) {
|
|
||||||
for (auto& [key, val] : COLOURS) {
|
|
||||||
if (key == want) {
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::unreachable();
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawTextArgs(NVGcontext* vg, float x, float y, float size, int align, const NVGcolor& c, const char* str, ...) {
|
void drawTextArgs(NVGcontext* vg, float x, float y, float size, int align, const NVGcolor& c, const char* str, ...) {
|
||||||
std::va_list v;
|
std::va_list v;
|
||||||
va_start(v, str);
|
va_start(v, str);
|
||||||
@@ -226,21 +164,7 @@ void drawTextArgs(NVGcontext* vg, float x, float y, float size, int align, const
|
|||||||
drawText(vg, x, y, size, buffer, nullptr, align, c);
|
drawText(vg, x, y, size, buffer, nullptr, align, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void drawImageInternal(NVGcontext* vg, Vec4 v, int texture, int rounded = 0) {
|
void drawImage(NVGcontext* vg, const Vec4& v, int texture) {
|
||||||
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
|
|
||||||
// drawRect(vg, x, y, w, h, paint);
|
|
||||||
nvgBeginPath(vg);
|
|
||||||
// nvgRect(vg, x, y, w, h);
|
|
||||||
if (rounded == 0) {
|
|
||||||
nvgRect(vg, v.x, v.y, v.w, v.h);
|
|
||||||
} else {
|
|
||||||
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
|
|
||||||
}
|
|
||||||
nvgFillPaint(vg, paint);
|
|
||||||
nvgFill(vg);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawImage(NVGcontext* vg, Vec4 v, int texture) {
|
|
||||||
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
|
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
|
||||||
drawRect(vg, v, paint, false);
|
drawRect(vg, v, paint, false);
|
||||||
}
|
}
|
||||||
@@ -249,7 +173,7 @@ void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture)
|
|||||||
drawImage(vg, Vec4(x, y, w, h), texture);
|
drawImage(vg, Vec4(x, y, w, h), texture);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawImageRounded(NVGcontext* vg, Vec4 v, int texture) {
|
void drawImageRounded(NVGcontext* vg, const Vec4& v, int texture) {
|
||||||
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
|
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
|
||||||
nvgBeginPath(vg);
|
nvgBeginPath(vg);
|
||||||
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
|
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
|
||||||
@@ -261,7 +185,7 @@ void drawImageRounded(NVGcontext* vg, float x, float y, float w, float h, int te
|
|||||||
drawImageRounded(vg, Vec4(x, y, w, h), texture);
|
drawImageRounded(vg, Vec4(x, y, w, h), texture);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, 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) {
|
||||||
nvgBeginPath(vg);
|
nvgBeginPath(vg);
|
||||||
nvgFontSize(vg, size);
|
nvgFontSize(vg, size);
|
||||||
nvgTextAlign(vg, align);
|
nvgTextAlign(vg, align);
|
||||||
@@ -269,14 +193,6 @@ void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, NVGc
|
|||||||
nvgTextBox(vg, x, y, bound, str, end);
|
nvgTextBox(vg, x, y, bound, str, end);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, NVGcolor&& c, const char* str, int align, const char* end) {
|
|
||||||
drawTextBox(vg, x, y, size, bound, c, str, align, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, Colour c, const char* str, int align, const char* end) {
|
|
||||||
drawTextBox(vg, x, y, size, bound, getColour(c), str, align, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
void textBounds(NVGcontext* vg, float x, float y, float *bounds, const char* str, ...) {
|
void textBounds(NVGcontext* vg, float x, float y, float *bounds, const char* str, ...) {
|
||||||
char buf[0x100];
|
char buf[0x100];
|
||||||
va_list v;
|
va_list v;
|
||||||
@@ -289,222 +205,89 @@ void textBounds(NVGcontext* vg, float x, float y, float *bounds, const char* str
|
|||||||
// NEW-----------
|
// NEW-----------
|
||||||
|
|
||||||
void dimBackground(NVGcontext* vg) {
|
void dimBackground(NVGcontext* vg) {
|
||||||
// drawRectIntenal(vg, {0.f,0.f,1280.f,720.f}, nvgRGBA(30,30,30,180));
|
drawRectIntenal(vg, {0.f,0.f,SCREEN_WIDTH,SCREEN_HEIGHT}, nvgRGBA(0, 0, 0, 180), false);
|
||||||
// drawRectIntenal(vg, {0.f,0.f,1920.f,1080.f}, nvgRGBA(20, 20, 20, 225), false);
|
|
||||||
drawRectIntenal(vg, {0.f,0.f,1920.f,1080.f}, nvgRGBA(0, 0, 0, 220), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, Colour c, bool rounded) {
|
|
||||||
drawRectIntenal(vg, {x,y,w,h}, getColour(c), rounded);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawRect(NVGcontext* vg, Vec4 vec, Colour c, bool rounded) {
|
|
||||||
drawRectIntenal(vg, vec, getColour(c), rounded);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, bool rounded) {
|
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, bool rounded) {
|
||||||
drawRectIntenal(vg, {x,y,w,h}, c, rounded);
|
drawRectIntenal(vg, {x,y,w,h}, c, rounded);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor&& c, bool rounded) {
|
void drawRect(NVGcontext* vg, const Vec4& v, const NVGcolor& c, bool rounded) {
|
||||||
drawRectIntenal(vg, {x,y,w,h}, c, rounded);
|
drawRectIntenal(vg, v, c, rounded);
|
||||||
}
|
|
||||||
|
|
||||||
void drawRect(NVGcontext* vg, Vec4 vec, const NVGcolor& c, bool rounded) {
|
|
||||||
drawRectIntenal(vg, vec, c, rounded);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawRect(NVGcontext* vg, Vec4 vec, const NVGcolor&& c, bool rounded) {
|
|
||||||
drawRectIntenal(vg, vec, c, rounded);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint& p, bool rounded) {
|
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint& p, bool rounded) {
|
||||||
drawRectIntenal(vg, {x,y,w,h}, p, rounded);
|
drawRectIntenal(vg, {x,y,w,h}, p, rounded);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint&& p, bool rounded) {
|
void drawRect(NVGcontext* vg, const Vec4& v, const NVGpaint& p, bool rounded) {
|
||||||
drawRectIntenal(vg, {x,y,w,h}, p, rounded);
|
drawRectIntenal(vg, v, p, rounded);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawRect(NVGcontext* vg, Vec4 vec, const NVGpaint& p, bool rounded) {
|
void drawRectOutline(NVGcontext* vg, const Theme* theme, float size, float x, float y, float w, float h) {
|
||||||
drawRectIntenal(vg, vec, p, rounded);
|
drawRectOutlineInternal(vg, theme, size, {x,y,w,h}, theme->GetColour(ThemeEntryID_SELECTED_BACKGROUND));
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawRect(NVGcontext* vg, Vec4 vec, const NVGpaint&& p, bool rounded) {
|
void drawRectOutline(NVGcontext* vg, const Theme* theme, float size, const Vec4& v) {
|
||||||
drawRectIntenal(vg, vec, p, rounded);
|
drawRectOutlineInternal(vg, theme, size, v, theme->GetColour(ThemeEntryID_SELECTED_BACKGROUND));
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, float x, float y, float w, float h, Colour c) {
|
|
||||||
drawRectOutlineInternal(vg, size, out_col, {x,y,w,h}, getColour(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, Colour c) {
|
|
||||||
drawRectOutlineInternal(vg, size, out_col, vec, getColour(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, float x, float y, float w, float h, const NVGcolor& c) {
|
|
||||||
drawRectOutlineInternal(vg, size, out_col, {x,y,w,h}, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, float x, float y, float w, float h, const NVGcolor&& c) {
|
|
||||||
drawRectOutlineInternal(vg, size, out_col, {x,y,w,h}, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGcolor& c) {
|
|
||||||
drawRectOutlineInternal(vg, size, out_col, vec, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGcolor&& c) {
|
|
||||||
drawRectOutlineInternal(vg, size, out_col, vec, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, float x, float y, float w, float h, const NVGpaint& p) {
|
|
||||||
drawRectOutlineInternal(vg, size, out_col, {x,y,w,h}, p);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, float x, float y, float w, float h, const NVGpaint&& p) {
|
|
||||||
drawRectOutlineInternal(vg, size, out_col, {x,y,w,h}, p);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGpaint& p) {
|
|
||||||
drawRectOutlineInternal(vg, size, out_col, vec, p);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawRectOutline(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGpaint&& p) {
|
|
||||||
drawRectOutlineInternal(vg, size, out_col, vec, p);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, Colour c) {
|
|
||||||
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, getColour(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor& c) {
|
|
||||||
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor&& c) {
|
|
||||||
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p) {
|
|
||||||
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, p);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint&& p) {
|
|
||||||
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, p);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawText(NVGcontext* vg, float x, float y, float size, const char* str, const char* end, int align, Colour c) {
|
|
||||||
drawTextIntenal(vg, {x,y}, size, str, end, align, getColour(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawText(NVGcontext* vg, float x, float y, float size, Colour c, const char* str, int align, const char* end) {
|
|
||||||
drawTextIntenal(vg, {x,y}, size, str, end, align, getColour(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawText(NVGcontext* vg, Vec2 vec, float size, const char* str, const char* end, int align, Colour c) {
|
|
||||||
drawTextIntenal(vg, vec, size, str, end, align, getColour(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawText(NVGcontext* vg, Vec2 vec, float size, Colour c, const char* str, int align, const char* end) {
|
|
||||||
drawTextIntenal(vg, vec, size, str, end, align, getColour(c));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawText(NVGcontext* vg, float x, float y, float size, const char* str, const char* end, int align, const NVGcolor& c) {
|
void drawText(NVGcontext* vg, float x, float y, float size, const char* str, const char* end, int align, const NVGcolor& c) {
|
||||||
drawTextIntenal(vg, {x,y}, size, str, end, align, c);
|
drawTextIntenal(vg, {x,y}, size, str, end, align, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawText(NVGcontext* vg, float x, float y, float size, const char* str, const char* end, int align, const NVGcolor&& c) {
|
|
||||||
drawTextIntenal(vg, {x,y}, size, str, end, align, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawText(NVGcontext* vg, float x, float y, float size, const NVGcolor& c, const char* str, int align, const char* end) {
|
void drawText(NVGcontext* vg, float x, float y, float size, const NVGcolor& c, const char* str, int align, const char* end) {
|
||||||
drawTextIntenal(vg, {x,y}, size, str, end, align, c);
|
drawTextIntenal(vg, {x,y}, size, str, end, align, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawText(NVGcontext* vg, float x, float y, float size, const NVGcolor&& c, const char* str, int align, const char* end) {
|
void drawText(NVGcontext* vg, const Vec2& v, float size, const char* str, const char* end, int align, const NVGcolor& c) {
|
||||||
drawTextIntenal(vg, {x,y}, size, str, end, align, c);
|
drawTextIntenal(vg, v, size, str, end, align, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawText(NVGcontext* vg, Vec2 vec, float size, const char* str, const char* end, int align, const NVGcolor& c) {
|
void drawText(NVGcontext* vg, const Vec2& v, float size, const NVGcolor& c, const char* str, int align, const char* end) {
|
||||||
drawTextIntenal(vg, vec, size, str, end, align, c);
|
drawTextIntenal(vg, v, size, str, end, align, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawText(NVGcontext* vg, Vec2 vec, float size, const char* str, const char* end, int align, const NVGcolor&& c) {
|
void drawScrollbar(NVGcontext* vg, const Theme* theme, float x, float y, float h, u32 index_off, u32 count, u32 max_per_page) {
|
||||||
drawTextIntenal(vg, vec, size, str, end, align, c);
|
const s64 SCROLL = index_off;
|
||||||
}
|
const s64 max_entry_display = max_per_page;
|
||||||
|
const s64 entry_total = count;
|
||||||
|
const float scc2 = 8.0;
|
||||||
|
const float scw = 2.0;
|
||||||
|
|
||||||
void drawText(NVGcontext* vg, Vec2 vec, float size, const NVGcolor& c, const char* str, int align, const char* end) {
|
// only draw scrollbar if needed
|
||||||
drawTextIntenal(vg, vec, size, str, end, align, c);
|
if (entry_total > max_entry_display) {
|
||||||
}
|
const float sb_h = 1.f / (float)entry_total * h;
|
||||||
|
const float sb_y = SCROLL;
|
||||||
void drawText(NVGcontext* vg, Vec2 vec, float size, const NVGcolor&& c, const char* str, int align, const char* end) {
|
gfx::drawRect(vg, x, y, scc2, h, theme->GetColour(ThemeEntryID_SCROLLBAR_BACKGROUND), false);
|
||||||
drawTextIntenal(vg, vec, size, str, end, align, c);
|
gfx::drawRect(vg, x + scw, y + scw + sb_h * sb_y, scc2 - scw * 2, sb_h * float(max_entry_display) - scw * 2, theme->GetColour(ThemeEntryID_SCROLLBAR), false);
|
||||||
}
|
|
||||||
|
|
||||||
void drawTextArgs(NVGcontext* vg, float x, float y, float size, int align, Colour c, const char* str, ...) {
|
|
||||||
std::va_list v;
|
|
||||||
va_start(v, str);
|
|
||||||
char buffer[0x100];
|
|
||||||
std::vsnprintf(buffer, sizeof(buffer), str, v);
|
|
||||||
va_end(v);
|
|
||||||
drawTextIntenal(vg, {x, y}, size, buffer, nullptr, align, getColour(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawButton(NVGcontext* vg, float x, float y, float size, Button button) {
|
|
||||||
drawText(vg, x, y, size, getButton(button), nullptr, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, getColour(Colour::WHITE));
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawButtons(NVGcontext* vg, const Widget::Actions& _actions, const NVGcolor& c, float start_x) {
|
|
||||||
nvgBeginPath(vg);
|
|
||||||
nvgFontSize(vg, 24.f);
|
|
||||||
nvgTextAlign(vg, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP);
|
|
||||||
nvgFillColor(vg, c);
|
|
||||||
|
|
||||||
float x = start_x;
|
|
||||||
const float y = 675.f;
|
|
||||||
float bounds[4]{};
|
|
||||||
|
|
||||||
// swaps L/R position, idc how shit this is, it's called once per frame.
|
|
||||||
std::vector<std::pair<Button, Action>> actions;
|
|
||||||
actions.reserve(_actions.size());
|
|
||||||
|
|
||||||
for (const auto a: _actions) {
|
|
||||||
// swap
|
|
||||||
if (a.first == Button::R && actions.size() && actions.back().first == Button::L) {
|
|
||||||
const auto s = actions.back();
|
|
||||||
actions.back() = a;
|
|
||||||
actions.emplace_back(s);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
actions.emplace_back(a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& [button, action] : actions) {
|
|
||||||
if (action.IsHidden() || action.m_hint.empty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
nvgFontSize(vg, 20.f);
|
|
||||||
nvgTextBounds(vg, x, y, action.m_hint.c_str(), nullptr, bounds);
|
|
||||||
auto len = bounds[2] - bounds[0];
|
|
||||||
nvgText(vg, x, y, action.m_hint.c_str(), nullptr);
|
|
||||||
|
|
||||||
x -= len + 8.f;
|
|
||||||
nvgFontSize(vg, 26.f);
|
|
||||||
nvgTextBounds(vg, x, y - 7.f, getButton(button), nullptr, bounds);
|
|
||||||
len = bounds[2] - bounds[0];
|
|
||||||
nvgText(vg, x, y - 4.f, getButton(button), nullptr);
|
|
||||||
x -= len + 34.f;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// from gc installer
|
void drawScrollbar(NVGcontext* vg, const Theme* theme, u32 index_off, u32 count, u32 max_per_page) {
|
||||||
void drawDimBackground(NVGcontext* vg) {
|
drawScrollbar(vg, theme, SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200, index_off, count, max_per_page);
|
||||||
// drawRect(vg, 0, 0, 1920, 1080, nvgRGBA(20, 20, 20, 225));
|
}
|
||||||
drawRect(vg, 0, 0, 1920, 1080, nvgRGBA(0, 0, 0, 220));
|
|
||||||
|
void drawScrollbar2(NVGcontext* vg, const Theme* theme, float x, float y, float h, s64 index_off, s64 count, s64 row, s64 page) {
|
||||||
|
// round up
|
||||||
|
if (count % row) {
|
||||||
|
count = count + (row - count % row);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float scc2 = 6.0;
|
||||||
|
const float scw = 2.0;
|
||||||
|
|
||||||
|
// only draw scrollbar if needed
|
||||||
|
if (count > page) {
|
||||||
|
const float sb_h = 1.f / (float)count * h;
|
||||||
|
const float sb_y = index_off;
|
||||||
|
gfx::drawRect(vg, x, y, scc2, h, theme->GetColour(ThemeEntryID_SCROLLBAR_BACKGROUND), false);
|
||||||
|
gfx::drawRect(vg, x + scw, y + scw + sb_h * sb_y, scc2 - scw * 2, sb_h * float(page) - scw * 2, theme->GetColour(ThemeEntryID_SCROLLBAR), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawScrollbar2(NVGcontext* vg, const Theme* theme, s64 index_off, s64 count, s64 row, s64 page) {
|
||||||
|
drawScrollbar2(vg, theme, SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200, index_off, count, row, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
#define HIGHLIGHT_SPEED 350.0
|
#define HIGHLIGHT_SPEED 350.0
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ OptionBoxEntry::OptionBoxEntry(const std::string& text, Vec4 pos)
|
|||||||
|
|
||||||
auto OptionBoxEntry::Draw(NVGcontext* vg, Theme* theme) -> void {
|
auto OptionBoxEntry::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||||
if (m_selected) {
|
if (m_selected) {
|
||||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
|
gfx::drawRectOutline(vg, theme, 4.f, m_pos);
|
||||||
gfx::drawText(vg, m_text_pos, 26.f, theme->elements[ThemeEntryID_TEXT_SELECTED].colour, m_text.c_str(), NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
|
gfx::drawText(vg, m_text_pos, 26.f, theme->GetColour(ThemeEntryID_TEXT_SELECTED), m_text.c_str(), NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
|
||||||
} else {
|
} else {
|
||||||
gfx::drawText(vg, m_text_pos, 26.f, theme->elements[ThemeEntryID_TEXT].colour, m_text.c_str(), NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
|
gfx::drawText(vg, m_text_pos, 26.f, theme->GetColour(ThemeEntryID_TEXT), m_text.c_str(), NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ OptionBox::OptionBox(const std::string& message, const Option& a, const Option&
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, std::size_t index, Callback cb)
|
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, s64 index, Callback cb)
|
||||||
: m_message{message}
|
: m_message{message}
|
||||||
, m_callback{cb} {
|
, m_callback{cb} {
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ OptionBox::OptionBox(const std::string& message, const Option& a, const Option&
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, std::size_t index, Callback cb)
|
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, s64 index, Callback cb)
|
||||||
: m_message{message}
|
: m_message{message}
|
||||||
, m_callback{cb} {
|
, m_callback{cb} {
|
||||||
|
|
||||||
@@ -79,65 +79,59 @@ OptionBox::OptionBox(const std::string& message, const Option& a, const Option&
|
|||||||
auto OptionBox::Update(Controller* controller, TouchInfo* touch) -> void {
|
auto OptionBox::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||||
Widget::Update(controller, touch);
|
Widget::Update(controller, touch);
|
||||||
|
|
||||||
// if (!controller->GotDown(Button::ANY_HORIZONTAL)) {
|
if (touch->is_clicked) {
|
||||||
// return;
|
for (s64 i = 0; i < m_entries.size(); i++) {
|
||||||
// }
|
auto& e = m_entries[i];
|
||||||
|
if (touch->in_range(e.GetPos())) {
|
||||||
// const auto old_index = m_index;
|
SetIndex(i);
|
||||||
|
FireAction(Button::A);
|
||||||
// if (controller->GotDown(Button::LEFT) && m_index) {
|
break;
|
||||||
// m_index--;
|
}
|
||||||
// } else if (controller->GotDown(Button::RIGHT) && m_index < (m_entries.size() - 1)) {
|
}
|
||||||
// m_index++;
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// if (old_index != m_index) {
|
|
||||||
// m_entries[old_index].Selected(false);
|
|
||||||
// m_entries[m_index].Selected(true);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
auto OptionBox::OnLayoutChange() -> void {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto OptionBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
auto OptionBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||||
const float padding = 15;
|
const float padding = 15;
|
||||||
gfx::dimBackground(vg);
|
gfx::dimBackground(vg);
|
||||||
gfx::drawRect(vg, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
|
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP));
|
||||||
|
|
||||||
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->elements[ThemeEntryID_TEXT].colour, m_message.c_str(), NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
|
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);
|
||||||
nvgRestore(vg);
|
nvgRestore(vg);
|
||||||
|
|
||||||
gfx::drawRect(vg, m_spacer_line, theme->elements[ThemeEntryID_TEXT].colour);
|
gfx::drawRect(vg, m_spacer_line, theme->GetColour(ThemeEntryID_LINE_SEPARATOR));
|
||||||
|
|
||||||
for (auto&p: m_entries) {
|
for (auto&p: m_entries) {
|
||||||
p.Draw(vg, theme);
|
p.Draw(vg, theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto OptionBox::Setup(std::size_t index) -> void {
|
auto OptionBox::OnFocusGained() noexcept -> void {
|
||||||
m_index = std::min(m_entries.size() - 1, index);
|
Widget::OnFocusGained();
|
||||||
|
SetHidden(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto OptionBox::OnFocusLost() noexcept -> void {
|
||||||
|
Widget::OnFocusLost();
|
||||||
|
SetHidden(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto OptionBox::Setup(s64 index) -> void {
|
||||||
|
m_index = std::min<s64>(m_entries.size() - 1, index);
|
||||||
m_entries[m_index].Selected(true);
|
m_entries[m_index].Selected(true);
|
||||||
m_spacer_line = Vec4{m_pos.x, m_pos.y + 220.f - 2.f, m_pos.w, 2.f};
|
m_spacer_line = Vec4{m_pos.x, m_pos.y + 220.f - 2.f, m_pos.w, 2.f};
|
||||||
|
|
||||||
SetActions(
|
SetActions(
|
||||||
std::make_pair(Button::LEFT, Action{[this](){
|
std::make_pair(Button::LEFT, Action{[this](){
|
||||||
if (m_index) {
|
if (m_index) {
|
||||||
m_entries[m_index].Selected(false);
|
SetIndex(m_index - 1);
|
||||||
m_index--;
|
|
||||||
m_entries[m_index].Selected(true);
|
|
||||||
App::PlaySoundEffect(SoundEffect_Focus);
|
|
||||||
}
|
}
|
||||||
}}),
|
}}),
|
||||||
std::make_pair(Button::RIGHT, Action{[this](){
|
std::make_pair(Button::RIGHT, Action{[this](){
|
||||||
if (m_index < (m_entries.size() - 1)) {
|
if (m_index < (m_entries.size() - 1)) {
|
||||||
m_entries[m_index].Selected(false);
|
SetIndex(m_index + 1);
|
||||||
m_index++;
|
|
||||||
m_entries[m_index].Selected(true);
|
|
||||||
App::PlaySoundEffect(SoundEffect_Focus);
|
|
||||||
}
|
}
|
||||||
}}),
|
}}),
|
||||||
std::make_pair(Button::A, Action{[this](){
|
std::make_pair(Button::A, Action{[this](){
|
||||||
@@ -151,4 +145,12 @@ auto OptionBox::Setup(std::size_t index) -> void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OptionBox::SetIndex(s64 index) {
|
||||||
|
if (m_index != index) {
|
||||||
|
m_entries[m_index].Selected(false);
|
||||||
|
m_index = index;
|
||||||
|
m_entries[m_index].Selected(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace sphaira::ui
|
} // namespace sphaira::ui
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
#include "ui/option_list.hpp"
|
|
||||||
#include "app.hpp"
|
|
||||||
#include "ui/nvg_util.hpp"
|
|
||||||
#include "i18n.hpp"
|
|
||||||
|
|
||||||
namespace sphaira::ui {
|
|
||||||
|
|
||||||
OptionList::OptionList(Options options)
|
|
||||||
: m_options{std::move(options)} {
|
|
||||||
SetAction(Button::A, Action{"Select"_i18n, [this](){
|
|
||||||
const auto& [_, func] = m_options[m_index];
|
|
||||||
func();
|
|
||||||
SetPop();
|
|
||||||
}});
|
|
||||||
|
|
||||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
|
||||||
SetPop();
|
|
||||||
}});
|
|
||||||
}
|
|
||||||
|
|
||||||
auto OptionList::Update(Controller* controller, TouchInfo* touch) -> void {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
auto OptionList::OnLayoutChange() -> void {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
auto OptionList::Draw(NVGcontext* vg, Theme* theme) -> void {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace sphaira::ui
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
namespace sphaira::ui {
|
namespace sphaira::ui {
|
||||||
|
|
||||||
PopupList::PopupList(std::string title, Items items, std::string& index_str_ref, std::size_t& index_ref)
|
PopupList::PopupList(std::string title, Items items, std::string& index_str_ref, s64& index_ref)
|
||||||
: PopupList{std::move(title), std::move(items), Callback{}, index_ref} {
|
: PopupList{std::move(title), std::move(items), Callback{}, index_ref} {
|
||||||
|
|
||||||
m_callback = [&index_str_ref, &index_ref, this](auto op_idx) {
|
m_callback = [&index_str_ref, &index_ref, this](auto op_idx) {
|
||||||
@@ -22,7 +22,9 @@ PopupList::PopupList(std::string title, Items items, std::string& index_ref)
|
|||||||
const auto it = std::find(m_items.cbegin(), m_items.cend(), index_ref);
|
const auto it = std::find(m_items.cbegin(), m_items.cend(), index_ref);
|
||||||
if (it != m_items.cend()) {
|
if (it != m_items.cend()) {
|
||||||
m_index = std::distance(m_items.cbegin(), it);
|
m_index = std::distance(m_items.cbegin(), it);
|
||||||
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h);
|
if (m_index >= 6) {
|
||||||
|
m_list->SetYoff((m_index - 5) * m_list->GetMaxY());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_callback = [&index_ref, this](auto op_idx) {
|
m_callback = [&index_ref, this](auto op_idx) {
|
||||||
@@ -32,7 +34,7 @@ PopupList::PopupList(std::string title, Items items, std::string& index_ref)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
PopupList::PopupList(std::string title, Items items, std::size_t& index_ref)
|
PopupList::PopupList(std::string title, Items items, s64& index_ref)
|
||||||
: PopupList{std::move(title), std::move(items), Callback{}, index_ref} {
|
: PopupList{std::move(title), std::move(items), Callback{}, index_ref} {
|
||||||
|
|
||||||
m_callback = [&index_ref, this](auto op_idx) {
|
m_callback = [&index_ref, this](auto op_idx) {
|
||||||
@@ -47,28 +49,29 @@ PopupList::PopupList(std::string title, Items items, Callback cb, std::string in
|
|||||||
|
|
||||||
const auto it = std::find(m_items.cbegin(), m_items.cend(), index);
|
const auto it = std::find(m_items.cbegin(), m_items.cend(), index);
|
||||||
if (it != m_items.cend()) {
|
if (it != m_items.cend()) {
|
||||||
m_index = std::distance(m_items.cbegin(), it);
|
SetIndex(std::distance(m_items.cbegin(), it));
|
||||||
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h);
|
if (m_index >= 6) {
|
||||||
|
m_list->SetYoff((m_index - 5) * m_list->GetMaxY());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PopupList::PopupList(std::string title, Items items, Callback cb, std::size_t index)
|
PopupList::PopupList(std::string title, Items items, Callback cb, s64 index)
|
||||||
: m_title{std::move(title)}
|
: m_title{std::move(title)}
|
||||||
, m_items{std::move(items)}
|
, m_items{std::move(items)}
|
||||||
, m_callback{cb}
|
, m_callback{cb}
|
||||||
, m_index{index} {
|
, m_index{index} {
|
||||||
|
this->SetActions(
|
||||||
m_pos.w = 1280.f;
|
std::make_pair(Button::DOWN, Action{[this](){
|
||||||
const float a = std::min(405.f, (60.f * static_cast<float>(m_items.size())));
|
if (m_list->ScrollDown(m_index, 1, m_items.size())) {
|
||||||
m_pos.h = 80.f + 140.f + a;
|
SetIndex(m_index);
|
||||||
m_pos.y = 720.f - m_pos.h;
|
}
|
||||||
m_line_top = m_pos.y + 70.f;
|
}}),
|
||||||
m_line_bottom = 720.f - 73.f;
|
std::make_pair(Button::UP, Action{[this](){
|
||||||
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h);
|
if (m_list->ScrollUp(m_index, 1, m_items.size())) {
|
||||||
|
SetIndex(m_index);
|
||||||
m_scrollbar.Setup(Vec4{1220.f, m_line_top, 1.f, m_line_bottom - m_line_top}, m_block.h, m_items.size());
|
}
|
||||||
|
}}),
|
||||||
SetActions(
|
|
||||||
std::make_pair(Button::A, Action{"Select"_i18n, [this](){
|
std::make_pair(Button::A, Action{"Select"_i18n, [this](){
|
||||||
if (m_callback) {
|
if (m_callback) {
|
||||||
m_callback(m_index);
|
m_callback(m_index);
|
||||||
@@ -76,85 +79,71 @@ PopupList::PopupList(std::string title, Items items, Callback cb, std::size_t in
|
|||||||
SetPop();
|
SetPop();
|
||||||
}}),
|
}}),
|
||||||
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||||
if (m_callback) {
|
|
||||||
m_callback(std::nullopt);
|
|
||||||
}
|
|
||||||
SetPop();
|
SetPop();
|
||||||
}})
|
}})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
m_pos.w = 1280.f;
|
||||||
|
const float a = std::min(370.f, (60.f * static_cast<float>(m_items.size())));
|
||||||
|
m_pos.h = 80.f + 140.f + a;
|
||||||
|
m_pos.y = 720.f - m_pos.h;
|
||||||
|
m_line_top = m_pos.y + 70.f;
|
||||||
|
m_line_bottom = 720.f - 73.f;
|
||||||
|
|
||||||
|
Vec4 v{m_block};
|
||||||
|
v.y = m_line_top + 1.f + 42.f;
|
||||||
|
const Vec4 pos{0, m_line_top, 1280.f, m_line_bottom - m_line_top};
|
||||||
|
m_list = std::make_unique<List>(1, 6, pos, v);
|
||||||
|
m_list->SetScrollBarPos(1250, m_line_top + 20, m_line_bottom - m_line_top - 40);
|
||||||
|
|
||||||
|
if (m_index >= 6) {
|
||||||
|
m_list->SetYoff((m_index - 5) * m_list->GetMaxY());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto PopupList::Update(Controller* controller, TouchInfo* touch) -> void {
|
auto PopupList::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||||
Widget::Update(controller, touch);
|
Widget::Update(controller, touch);
|
||||||
|
m_list->OnUpdate(controller, touch, m_items.size(), [this](auto i) {
|
||||||
if (!controller->GotDown(Button::ANY_VERTICAL)) {
|
SetIndex(i);
|
||||||
return;
|
FireAction(Button::A);
|
||||||
}
|
});
|
||||||
|
|
||||||
const auto old_index = m_index;
|
|
||||||
|
|
||||||
if (controller->GotDown(Button::DOWN) && m_index < (m_items.size() - 1)) {
|
|
||||||
m_index++;
|
|
||||||
m_selected_y += m_block.h;
|
|
||||||
} else if (controller->GotDown(Button::UP) && m_index != 0) {
|
|
||||||
m_index--;
|
|
||||||
m_selected_y -= m_block.h;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (old_index != m_index) {
|
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
|
||||||
OnLayoutChange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto PopupList::OnLayoutChange() -> void {
|
|
||||||
if ((m_selected_y + m_block.h) > m_line_bottom) {
|
|
||||||
m_selected_y -= m_block.h;
|
|
||||||
m_index_offset++;
|
|
||||||
m_scrollbar.Move(ScrollBar::Direction::DOWN);
|
|
||||||
} else if (m_selected_y <= m_line_top) {
|
|
||||||
m_selected_y += m_block.h;
|
|
||||||
m_index_offset--;
|
|
||||||
m_scrollbar.Move(ScrollBar::Direction::UP);
|
|
||||||
}
|
|
||||||
// LOG("sely: %.2f, index_off: %lu\n", m_selected_y, m_index_offset);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void {
|
auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||||
gfx::dimBackground(vg);
|
gfx::dimBackground(vg);
|
||||||
gfx::drawRect(vg, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
|
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP));
|
||||||
gfx::drawText(vg, m_pos + m_title_pos, 24.f, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str());
|
gfx::drawText(vg, m_pos + m_title_pos, 24.f, theme->GetColour(ThemeEntryID_TEXT), m_title.c_str());
|
||||||
gfx::drawRect(vg, 30.f, m_line_top, m_line_width, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
|
gfx::drawRect(vg, 30.f, m_line_top, m_line_width, 1.f, theme->GetColour(ThemeEntryID_LINE));
|
||||||
gfx::drawRect(vg, 30.f, m_line_bottom, m_line_width, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
|
gfx::drawRect(vg, 30.f, m_line_bottom, m_line_width, 1.f, theme->GetColour(ThemeEntryID_LINE));
|
||||||
|
|
||||||
// todo: cleanup
|
m_list->Draw(vg, theme, m_items.size(), [this](auto* vg, auto* theme, auto v, auto i) {
|
||||||
const float x = m_block.x;
|
const auto& [x, y, w, h] = v;
|
||||||
float y = m_line_top + 1.f + 42.f;
|
|
||||||
const float h = m_block.h;
|
|
||||||
const float w = m_block.w;
|
|
||||||
|
|
||||||
nvgSave(vg);
|
|
||||||
nvgScissor(vg, 0, m_line_top, 1280.f, m_line_bottom - m_line_top);
|
|
||||||
|
|
||||||
for (std::size_t i = m_index_offset; i < m_items.size(); ++i) {
|
|
||||||
if (m_index == i) {
|
if (m_index == i) {
|
||||||
gfx::drawRect(vg, x - 4.f, y - 4.f, w + 8.f, h + 8.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour);
|
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||||
gfx::drawRect(vg, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
|
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));
|
||||||
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->elements[ThemeEntryID_TEXT_SELECTED].colour);
|
|
||||||
} else {
|
} else {
|
||||||
gfx::drawRect(vg, x, y, w, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
|
if (i != m_items.size() - 1) {
|
||||||
gfx::drawRect(vg, x, y + h, w, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
|
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->elements[ThemeEntryID_TEXT].colour);
|
}
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
y += h;
|
});
|
||||||
if (y > m_line_bottom) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nvgRestore(vg);
|
|
||||||
|
|
||||||
m_scrollbar.Draw(vg, theme);
|
|
||||||
Widget::Draw(vg, theme);
|
Widget::Draw(vg, theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto PopupList::OnFocusGained() noexcept -> void {
|
||||||
|
Widget::OnFocusGained();
|
||||||
|
SetHidden(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto PopupList::OnFocusLost() noexcept -> void {
|
||||||
|
Widget::OnFocusLost();
|
||||||
|
SetHidden(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PopupList::SetIndex(s64 index) {
|
||||||
|
m_index = index;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace sphaira::ui
|
} // namespace sphaira::ui
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback,
|
|||||||
m_pos.h = 430.f;
|
m_pos.h = 430.f;
|
||||||
m_pos.x = 255;
|
m_pos.x = 255;
|
||||||
m_pos.y = 145;
|
m_pos.y = 145;
|
||||||
145 + 430; // 575, 200, 420
|
|
||||||
|
|
||||||
m_pos.w = 770.f;
|
m_pos.w = 770.f;
|
||||||
m_pos.h = 295.f;
|
m_pos.h = 295.f;
|
||||||
@@ -52,9 +51,7 @@ ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
ProgressBox::~ProgressBox() {
|
ProgressBox::~ProgressBox() {
|
||||||
mutexLock(&m_mutex);
|
m_stop_source.request_stop();
|
||||||
m_exit_requested = true;
|
|
||||||
mutexUnlock(&m_mutex);
|
|
||||||
|
|
||||||
if (R_FAILED(threadWaitForExit(&m_thread))) {
|
if (R_FAILED(threadWaitForExit(&m_thread))) {
|
||||||
log_write("failed to join thread\n");
|
log_write("failed to join thread\n");
|
||||||
@@ -83,7 +80,7 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
|||||||
mutexUnlock(&m_mutex);
|
mutexUnlock(&m_mutex);
|
||||||
|
|
||||||
gfx::dimBackground(vg);
|
gfx::dimBackground(vg);
|
||||||
gfx::drawRect(vg, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
|
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP));
|
||||||
|
|
||||||
// The pop up shape.
|
// The pop up shape.
|
||||||
// const Vec4 box = { 255, 145, 770, 430 };
|
// const Vec4 box = { 255, 145, 770, 430 };
|
||||||
@@ -93,19 +90,15 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
|||||||
|
|
||||||
// shapes.
|
// shapes.
|
||||||
if (offset && size) {
|
if (offset && size) {
|
||||||
gfx::drawRect(vg, prog_bar, gfx::Colour::SILVER);
|
gfx::drawRect(vg, prog_bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND));
|
||||||
const u32 percentage = ((double)offset / (double)size) * 100.0;
|
const u32 percentage = ((double)offset / (double)size) * 100.0;
|
||||||
gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((float)offset / (float)size) * prog_bar.w, prog_bar.h, gfx::Colour::CYAN);
|
gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((float)offset / (float)size) * prog_bar.w, prog_bar.h, theme->GetColour(ThemeEntryID_PROGRESSBAR));
|
||||||
// gfx::drawTextArgs(vg, prog_bar.x + 85, prog_bar.y + 40, 20, 0, gfx::Colour::WHITE, "%u%%", percentage);
|
gfx::drawTextArgs(vg, prog_bar.x + prog_bar.w + 10, prog_bar.y, 20, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%u%%", percentage);
|
||||||
gfx::drawTextArgs(vg, prog_bar.x + prog_bar.w + 10, prog_bar.y, 20, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, gfx::Colour::WHITE, "%u%%", percentage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gfx::drawTextArgs(vg, center_x, m_pos.y + 60, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::WHITE, title.c_str());
|
gfx::drawTextArgs(vg, center_x, m_pos.y + 60, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), title.c_str());
|
||||||
// gfx::drawTextArgs(vg, center_x, 260, 20, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::SILVER, "Please do not remove the gamecard or");
|
|
||||||
// gfx::drawTextArgs(vg, center_x, 295, 20, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::SILVER, "power off the system whilst installing.");
|
|
||||||
// gfx::drawTextArgs(vg, center_x, 360, 20, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::WHITE, "%.2f MiB/s", 24.0);
|
|
||||||
if (!transfer.empty()) {
|
if (!transfer.empty()) {
|
||||||
gfx::drawTextArgs(vg, center_x, prog_bar.y - 15 - 20 * 1.5, 20, NVG_ALIGN_CENTER, gfx::Colour::WHITE, "%s", transfer.c_str());
|
gfx::drawTextArgs(vg, center_x, prog_bar.y - 15 - 20 * 1.5F, 20, NVG_ALIGN_CENTER, theme->GetColour(ThemeEntryID_TEXT), "%s", transfer.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +112,7 @@ auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
|
|||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ProgressBox::UpdateTransfer(u64 offset, u64 size) -> ProgressBox& {
|
auto ProgressBox::UpdateTransfer(s64 offset, s64 size) -> ProgressBox& {
|
||||||
mutexLock(&m_mutex);
|
mutexLock(&m_mutex);
|
||||||
m_size = size;
|
m_size = size;
|
||||||
m_offset = offset;
|
m_offset = offset;
|
||||||
@@ -129,16 +122,11 @@ auto ProgressBox::UpdateTransfer(u64 offset, u64 size) -> ProgressBox& {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ProgressBox::RequestExit() {
|
void ProgressBox::RequestExit() {
|
||||||
mutexLock(&m_mutex);
|
m_stop_source.request_stop();
|
||||||
m_exit_requested = true;
|
|
||||||
mutexUnlock(&m_mutex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ProgressBox::ShouldExit() -> bool {
|
auto ProgressBox::ShouldExit() -> bool {
|
||||||
mutexLock(&m_mutex);
|
return m_stop_source.stop_requested();
|
||||||
const auto exit_requested = m_exit_requested;
|
|
||||||
mutexUnlock(&m_mutex);
|
|
||||||
return exit_requested;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_path) -> Result {
|
auto ProgressBox::CopyFile(const fs::FsPath& src_path, const fs::FsPath& dst_path) -> Result {
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ namespace sphaira::ui {
|
|||||||
ScrollableText::ScrollableText(const std::string& text, float x, float y, float y_clip, float w, float font_size)
|
ScrollableText::ScrollableText(const std::string& text, float x, float y, float y_clip, float w, float font_size)
|
||||||
: m_font_size{font_size}
|
: m_font_size{font_size}
|
||||||
, m_y_off_base{y}
|
, m_y_off_base{y}
|
||||||
, m_y_off{y}
|
|
||||||
, m_clip_y{y_clip}
|
, m_clip_y{y_clip}
|
||||||
, m_end_w{w}
|
, m_end_w{w}
|
||||||
|
, m_y_off{y}
|
||||||
{
|
{
|
||||||
SetActions(
|
SetActions(
|
||||||
std::make_pair(Button::LS_DOWN, Action{[this](){
|
std::make_pair(Button::LS_DOWN, Action{[this](){
|
||||||
@@ -94,17 +94,15 @@ void ScrollableText::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
const auto sb_h = 1.f / max_index * scrollbar_size;
|
const auto sb_h = 1.f / max_index * scrollbar_size;
|
||||||
const auto in_clip = m_clip_y / m_step - 1;
|
const auto in_clip = m_clip_y / m_step - 1;
|
||||||
const auto sb_y = m_index;
|
const auto sb_y = m_index;
|
||||||
// gfx::drawRect(vg, banner_vec.x+banner_vec.w-20, m_y_off_base, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
|
gfx::drawRect(vg, banner_vec.w, m_y_off_base, 10, scrollbar_size, theme->GetColour(ThemeEntryID_SCROLLBAR_BACKGROUND));
|
||||||
// gfx::drawRect(vg, banner_vec.x+banner_vec.w-20+2, m_y_off_base + sb_h * sb_y, 10-4, sb_h + (sb_h * in_clip) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
|
gfx::drawRect(vg, banner_vec.w+2, m_y_off_base + sb_h * sb_y, 10-4, sb_h + (sb_h * in_clip) - 4, theme->GetColour(ThemeEntryID_SCROLLBAR));
|
||||||
gfx::drawRect(vg, banner_vec.w, m_y_off_base, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
|
|
||||||
gfx::drawRect(vg, banner_vec.w+2, m_y_off_base + sb_h * sb_y, 10-4, sb_h + (sb_h * in_clip) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nvgSave(vg);
|
nvgSave(vg);
|
||||||
nvgScissor(vg, 0, m_y_off_base - m_font_size, 1280, m_clip_y + m_font_size); // clip
|
nvgIntersectScissor(vg, 0, m_y_off_base - m_font_size, 1280, m_clip_y + m_font_size); // clip
|
||||||
|
|
||||||
nvgTextLineHeight(App::GetVg(), 1.7);
|
nvgTextLineHeight(App::GetVg(), 1.7);
|
||||||
gfx::drawTextBox(vg, banner_vec.x + 40, m_y_off, m_font_size, m_bounds[2] - m_bounds[0], theme->elements[ThemeEntryID_TEXT].colour, m_text.c_str());
|
gfx::drawTextBox(vg, banner_vec.x + 40, m_y_off, m_font_size, m_bounds[2] - m_bounds[0], theme->GetColour(ThemeEntryID_TEXT), m_text.c_str());
|
||||||
nvgRestore(vg);
|
nvgRestore(vg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
#include "ui/scrollbar.hpp"
|
|
||||||
#include "ui/nvg_util.hpp"
|
|
||||||
|
|
||||||
namespace sphaira::ui {
|
|
||||||
|
|
||||||
ScrollBar::ScrollBar(Vec4 bounds, float entry_height, std::size_t entries)
|
|
||||||
: m_bounds{bounds}
|
|
||||||
, m_entries{entries}
|
|
||||||
, m_entry_height{entry_height} {
|
|
||||||
Setup();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto ScrollBar::OnLayoutChange() -> void {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
auto ScrollBar::Draw(NVGcontext* vg, Theme* theme) -> void {
|
|
||||||
if (m_should_draw) {
|
|
||||||
gfx::drawRect(vg, m_pos, gfx::Colour::RED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto ScrollBar::Setup(Vec4 bounds, float entry_height, std::size_t entries) -> void {
|
|
||||||
m_bounds = bounds;
|
|
||||||
m_entry_height = entry_height;
|
|
||||||
m_entries = entries;
|
|
||||||
Setup();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto ScrollBar::Setup() -> void {
|
|
||||||
m_bounds.y += 5.f;
|
|
||||||
m_bounds.h -= 10.f;
|
|
||||||
|
|
||||||
const float total_size = (m_entry_height) * static_cast<float>(m_entries);
|
|
||||||
if (total_size > m_bounds.h) {
|
|
||||||
m_step_size = total_size / m_entries;
|
|
||||||
m_pos.x = m_bounds.x;
|
|
||||||
m_pos.y = m_bounds.y;
|
|
||||||
m_pos.w = 2.f;
|
|
||||||
m_pos.h = total_size - m_bounds.h;
|
|
||||||
m_should_draw = true;
|
|
||||||
// LOG("total size: %.2f\n", total_size);
|
|
||||||
// LOG("step size: %.2f\n", m_step_size);
|
|
||||||
// LOG("pos y: %.2f\n", m_pos.y);
|
|
||||||
// LOG("pos h: %.2f\n", m_pos.h);
|
|
||||||
} else {
|
|
||||||
// LOG("not big enough for scroll total: %.2f bounds: %.2f\n", total_size, bounds.h);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto ScrollBar::Move(Direction direction) -> void {
|
|
||||||
switch (direction) {
|
|
||||||
case Direction::DOWN:
|
|
||||||
if (m_index < (m_entries - 1)) {
|
|
||||||
m_index++;
|
|
||||||
m_pos.y += m_step_size;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Direction::UP:
|
|
||||||
if (m_index != 0) {
|
|
||||||
m_index--;
|
|
||||||
m_pos.y -= m_step_size;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace sphaira::ui
|
|
||||||
@@ -7,13 +7,20 @@
|
|||||||
namespace sphaira::ui {
|
namespace sphaira::ui {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
struct SidebarSpacer : SidebarEntryBase {
|
auto GetTextScrollSpeed() -> float {
|
||||||
|
switch (App::GetTextScrollSpeed()) {
|
||||||
|
case 0: return 0.5;
|
||||||
|
default: case 1: return 1.0;
|
||||||
|
case 2: return 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
};
|
auto DistanceBetweenY(Vec4 va, Vec4 vb) -> Vec4 {
|
||||||
|
return Vec4{
|
||||||
struct SidebarHeader : SidebarEntryBase {
|
va.x, va.y,
|
||||||
|
va.w, vb.y - va.y
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -25,14 +32,7 @@ SidebarEntryBase::SidebarEntryBase(std::string&& title)
|
|||||||
auto SidebarEntryBase::Draw(NVGcontext* vg, Theme* theme) -> void {
|
auto SidebarEntryBase::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||||
// draw spacers or highlight box if in focus (selected)
|
// draw spacers or highlight box if in focus (selected)
|
||||||
if (HasFocus()) {
|
if (HasFocus()) {
|
||||||
gfx::drawRect(vg, m_pos, nvgRGB(50,50,50));
|
gfx::drawRectOutline(vg, theme, 4.f, m_pos);
|
||||||
gfx::drawRect(vg, m_pos, nvgRGB(0,0,0));
|
|
||||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
|
|
||||||
// gfx::drawRect(vg, m_pos.x - 4.f, m_pos.y - 4.f, m_pos.w + 8.f, m_pos.h + 8.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour);
|
|
||||||
// gfx::drawRect(vg, m_pos.x, m_pos.y, m_pos.w, m_pos.h, theme->elements[ThemeEntryID_SELECTED].colour);
|
|
||||||
} else {
|
|
||||||
gfx::drawRect(vg, m_pos.x, m_pos.y, m_pos.w, 1.f, nvgRGB(81, 81, 81)); // spacer
|
|
||||||
gfx::drawRect(vg, m_pos.x, m_pos.y + m_pos.h, m_pos.w, 1.f, nvgRGB(81, 81, 81)); // spacer
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,16 +61,16 @@ auto SidebarEntryBool::Draw(NVGcontext* vg, Theme* theme) -> void {
|
|||||||
SidebarEntryBase::Draw(vg, theme);
|
SidebarEntryBase::Draw(vg, theme);
|
||||||
|
|
||||||
// if (HasFocus()) {
|
// if (HasFocus()) {
|
||||||
// gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT_SELECTED].colour, m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
// gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->GetColour(ThemeEntryID_TEXT_SELECTED), m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
||||||
// } else {
|
// } else {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->GetColour(ThemeEntryID_TEXT), m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
||||||
|
|
||||||
if (m_option == true) {
|
if (m_option == true) {
|
||||||
gfx::drawText(vg, Vec2{m_pos.x + m_pos.w - 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT_SELECTED].colour, m_true_str.c_str(), NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
|
gfx::drawText(vg, Vec2{m_pos.x + m_pos.w - 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->GetColour(ThemeEntryID_TEXT_SELECTED), m_true_str.c_str(), NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
|
||||||
} else { // text info
|
} else { // text info
|
||||||
gfx::drawText(vg, Vec2{m_pos.x + m_pos.w - 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT].colour, m_false_str.c_str(), NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
|
gfx::drawText(vg, Vec2{m_pos.x + m_pos.w - 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->GetColour(ThemeEntryID_TEXT), m_false_str.c_str(), NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,9 +91,9 @@ auto SidebarEntryCallback::Draw(NVGcontext* vg, Theme* theme) -> void {
|
|||||||
SidebarEntryBase::Draw(vg, theme);
|
SidebarEntryBase::Draw(vg, theme);
|
||||||
|
|
||||||
// if (HasFocus()) {
|
// if (HasFocus()) {
|
||||||
// gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT_SELECTED].colour, m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
// gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->GetColour(ThemeEntryID_TEXT_SELECTED), m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
||||||
// } else {
|
// } else {
|
||||||
gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->GetColour(ThemeEntryID_TEXT), m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ SidebarEntryArray::SidebarEntryArray(std::string title, Items items, Callback cb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SidebarEntryArray::SidebarEntryArray(std::string title, Items items, Callback cb, std::size_t index)
|
SidebarEntryArray::SidebarEntryArray(std::string title, Items items, Callback cb, s64 index)
|
||||||
: SidebarEntryBase{std::forward<std::string>(title)}
|
: SidebarEntryBase{std::forward<std::string>(title)}
|
||||||
, m_items{std::move(items)}
|
, m_items{std::move(items)}
|
||||||
, m_callback{cb}
|
, m_callback{cb}
|
||||||
@@ -155,11 +155,59 @@ auto SidebarEntryArray::Draw(NVGcontext* vg, Theme* theme) -> void {
|
|||||||
SidebarEntryBase::Draw(vg, theme);
|
SidebarEntryBase::Draw(vg, theme);
|
||||||
|
|
||||||
const auto& text_entry = m_items[m_index];
|
const auto& text_entry = m_items[m_index];
|
||||||
// const auto& colour = HasFocus() ? theme->elements[ThemeEntryID_TEXT_SELECTED].colour : theme->elements[ThemeEntryID_TEXT].colour;
|
|
||||||
const auto& colour = theme->elements[ThemeEntryID_TEXT].colour;
|
|
||||||
|
|
||||||
gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, colour, m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
// scrolling text
|
||||||
gfx::drawText(vg, Vec2{m_pos.x + m_pos.w - 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->elements[ThemeEntryID_TEXT_SELECTED].colour, text_entry.c_str(), NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
|
// todo: move below in a flexible class and use it for all text drawing.
|
||||||
|
float bounds[4];
|
||||||
|
nvgFontSize(vg, 20);
|
||||||
|
nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
||||||
|
nvgTextBounds(vg, 0, 0, m_title.c_str(), nullptr, bounds);
|
||||||
|
const float start_x = bounds[2] + 50;
|
||||||
|
const float max_off = m_pos.w - start_x - 15.f;
|
||||||
|
|
||||||
|
auto value_str = m_items[m_index];
|
||||||
|
nvgTextBounds(vg, 0, 0, value_str.c_str(), nullptr, bounds);
|
||||||
|
|
||||||
|
if (HasFocus()) {
|
||||||
|
const auto scroll_amount = GetTextScrollSpeed();
|
||||||
|
if (bounds[2] > max_off) {
|
||||||
|
value_str += " ";
|
||||||
|
nvgTextBounds(vg, 0, 0, value_str.c_str(), nullptr, bounds);
|
||||||
|
|
||||||
|
if (!m_text_yoff) {
|
||||||
|
m_tick++;
|
||||||
|
if (m_tick >= 90) {
|
||||||
|
m_tick = 0;
|
||||||
|
m_text_yoff += scroll_amount;
|
||||||
|
}
|
||||||
|
} else if (bounds[2] > m_text_yoff) {
|
||||||
|
m_text_yoff += std::min(scroll_amount, bounds[2] - m_text_yoff);
|
||||||
|
} else {
|
||||||
|
m_text_yoff = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
value_str += text_entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Vec2 key_text_pos{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)};
|
||||||
|
gfx::drawText(vg, key_text_pos, 20.f, theme->GetColour(ThemeEntryID_TEXT), m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
||||||
|
|
||||||
|
nvgSave(vg);
|
||||||
|
const float xpos = m_pos.x + m_pos.w - 15.f - std::min(max_off, bounds[2]);
|
||||||
|
nvgIntersectScissor(vg, xpos, GetY(), max_off, GetH());
|
||||||
|
const Vec2 value_text_pos{xpos - m_text_yoff, m_pos.y + (m_pos.h / 2.f)};
|
||||||
|
gfx::drawText(vg, value_text_pos, 20.f, theme->GetColour(ThemeEntryID_TEXT_SELECTED), value_str.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
||||||
|
nvgRestore(vg);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SidebarEntryArray::OnFocusGained() noexcept -> void {
|
||||||
|
Widget::OnFocusGained();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SidebarEntryArray::OnFocusLost() noexcept -> void {
|
||||||
|
Widget::OnFocusLost();
|
||||||
|
m_text_yoff = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Sidebar::Sidebar(std::string title, Side side, Items&& items)
|
Sidebar::Sidebar(std::string title, Side side, Items&& items)
|
||||||
@@ -191,24 +239,12 @@ Sidebar::Sidebar(std::string title, std::string sub, Side side, Items&& items)
|
|||||||
m_title_pos = Vec2{m_pos.x + 30.f, m_pos.y + 40.f};
|
m_title_pos = Vec2{m_pos.x + 30.f, m_pos.y + 40.f};
|
||||||
m_base_pos = Vec4{GetX() + 30.f, GetY() + 170.f, m_pos.w - (30.f * 2.f), 70.f};
|
m_base_pos = Vec4{GetX() + 30.f, GetY() + 170.f, m_pos.w - (30.f * 2.f), 70.f};
|
||||||
|
|
||||||
// each item has it's own Action, but we take over B
|
// set button positions
|
||||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
SetUiButtonPos({m_pos.x + m_pos.w - 60.f, 675});
|
||||||
SetPop();
|
|
||||||
}});
|
|
||||||
|
|
||||||
m_selected_y = m_base_pos.y;
|
const Vec4 pos = DistanceBetweenY(m_top_bar, m_bottom_bar);
|
||||||
|
m_list = std::make_unique<List>(1, 6, pos, m_base_pos);
|
||||||
if (!m_items.empty()) {
|
m_list->SetScrollBarPos(GetX() + GetW() - 20, m_base_pos.y - 10, pos.h - m_base_pos.y + 48);
|
||||||
// setup positions
|
|
||||||
m_selected_y = m_base_pos.y;
|
|
||||||
// for (auto&p : m_items) {
|
|
||||||
// p->SetPos(m_base_pos);
|
|
||||||
// m_base_pos.y += m_base_pos.h;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // give focus to first entry.
|
|
||||||
// m_items[m_index]->OnFocusGained();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Sidebar::Sidebar(std::string title, std::string sub, Side side)
|
Sidebar::Sidebar(std::string title, std::string sub, Side side)
|
||||||
@@ -217,77 +253,44 @@ Sidebar::Sidebar(std::string title, std::string sub, Side side)
|
|||||||
|
|
||||||
|
|
||||||
auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void {
|
auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||||
m_items[m_index]->Update(controller, touch);
|
|
||||||
Widget::Update(controller, touch);
|
Widget::Update(controller, touch);
|
||||||
|
|
||||||
|
// if touched out of bounds, pop the sidebar and all widgets below it.
|
||||||
|
if (touch->is_clicked && !touch->in_range(GetPos())) {
|
||||||
|
App::PopToMenu();
|
||||||
|
} else {
|
||||||
|
m_list->OnUpdate(controller, touch, m_items.size(), [this](auto i) {
|
||||||
|
SetIndex(i);
|
||||||
|
FireAction(Button::A);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (m_items[m_index]->ShouldPop()) {
|
if (m_items[m_index]->ShouldPop()) {
|
||||||
SetPop();
|
SetPop();
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto old_index = m_index;
|
|
||||||
if (controller->GotDown(Button::ANY_DOWN) && m_index < (m_items.size() - 1)) {
|
|
||||||
m_index++;
|
|
||||||
m_selected_y += m_box_size.y;
|
|
||||||
} else if (controller->GotDown(Button::ANY_UP) && m_index != 0) {
|
|
||||||
m_index--;
|
|
||||||
m_selected_y -= m_box_size.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we moved
|
|
||||||
if (m_index != old_index) {
|
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
|
||||||
m_items[old_index]->OnFocusLost();
|
|
||||||
m_items[m_index]->OnFocusGained();
|
|
||||||
|
|
||||||
// move offset
|
|
||||||
if ((m_selected_y + m_box_size.y) >= m_bottom_bar.y) {
|
|
||||||
m_selected_y -= m_box_size.y;
|
|
||||||
m_index_offset++;
|
|
||||||
// LOG("move down\n");
|
|
||||||
} else if (m_selected_y <= m_top_bar.y) {
|
|
||||||
// LOG("move up sely %.2f top %.2f\n", m_selected_y, m_top_bar.y);
|
|
||||||
m_selected_y += m_box_size.y;
|
|
||||||
m_index_offset--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto DistanceBetweenY(Vec4 va, Vec4 vb) -> Vec4 {
|
|
||||||
return Vec4{
|
|
||||||
va.x, va.y,
|
|
||||||
va.w, vb.y - va.y
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Sidebar::Draw(NVGcontext* vg, Theme* theme) -> void {
|
auto Sidebar::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||||
gfx::drawRect(vg, m_pos, nvgRGBA(0, 0, 0, 220));
|
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_SIDEBAR));
|
||||||
gfx::drawText(vg, m_title_pos, m_title_size, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str());
|
gfx::drawText(vg, m_title_pos, m_title_size, theme->GetColour(ThemeEntryID_TEXT), m_title.c_str());
|
||||||
if (!m_sub.empty()) {
|
if (!m_sub.empty()) {
|
||||||
gfx::drawTextArgs(vg, m_pos.x + m_pos.w - 30.f, m_title_pos.y + 10.f, 18, NVG_ALIGN_TOP | NVG_ALIGN_RIGHT, theme->elements[ThemeEntryID_TEXT].colour, m_sub.c_str());
|
gfx::drawTextArgs(vg, m_pos.x + m_pos.w - 30.f, m_title_pos.y + 10.f, 16, NVG_ALIGN_TOP | NVG_ALIGN_RIGHT, theme->GetColour(ThemeEntryID_TEXT_INFO), m_sub.c_str());
|
||||||
}
|
}
|
||||||
gfx::drawRect(vg, m_top_bar, theme->elements[ThemeEntryID_TEXT].colour);
|
gfx::drawRect(vg, m_top_bar, theme->GetColour(ThemeEntryID_LINE));
|
||||||
gfx::drawRect(vg, m_bottom_bar, theme->elements[ThemeEntryID_TEXT].colour);
|
gfx::drawRect(vg, m_bottom_bar, theme->GetColour(ThemeEntryID_LINE));
|
||||||
|
|
||||||
const auto dist = DistanceBetweenY(m_top_bar, m_bottom_bar);
|
Widget::Draw(vg, theme);
|
||||||
nvgSave(vg);
|
|
||||||
nvgScissor(vg, dist.x, dist.y, dist.w, dist.h);
|
|
||||||
|
|
||||||
// for (std::size_t i = m_index_offset; i < m_items.size(); ++i) {
|
m_list->Draw(vg, theme, m_items.size(), [this](auto* vg, auto* theme, auto v, auto i) {
|
||||||
// m_items[i]->Draw(vg, theme);
|
const auto& [x, y, w, h] = v;
|
||||||
// }
|
|
||||||
|
|
||||||
for (auto&p : m_items) {
|
if (i != m_items.size() - 1) {
|
||||||
p->Draw(vg, theme);
|
gfx::drawRect(vg, x, y + h, w, 1.f, theme->GetColour(ThemeEntryID_LINE_SEPARATOR));
|
||||||
}
|
}
|
||||||
|
|
||||||
nvgRestore(vg);
|
m_items[i]->SetY(y);
|
||||||
|
m_items[i]->Draw(vg, theme);
|
||||||
// draw the buttons. fetch the actions from current item and insert into array.
|
});
|
||||||
Actions draw_actions{m_actions};
|
|
||||||
const auto& actions_ref = m_items[m_index]->GetActions();
|
|
||||||
draw_actions.insert(actions_ref.cbegin(), actions_ref.cend());
|
|
||||||
|
|
||||||
gfx::drawButtons(vg, draw_actions, theme->elements[ThemeEntryID_TEXT].colour, m_pos.x + m_pos.w - 60.f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Sidebar::OnFocusGained() noexcept -> void {
|
auto Sidebar::OnFocusGained() noexcept -> void {
|
||||||
@@ -303,23 +306,51 @@ auto Sidebar::OnFocusLost() noexcept -> void {
|
|||||||
void Sidebar::Add(std::shared_ptr<SidebarEntryBase> entry) {
|
void Sidebar::Add(std::shared_ptr<SidebarEntryBase> entry) {
|
||||||
m_items.emplace_back(entry);
|
m_items.emplace_back(entry);
|
||||||
m_items.back()->SetPos(m_base_pos);
|
m_items.back()->SetPos(m_base_pos);
|
||||||
m_base_pos.y += m_base_pos.h;
|
|
||||||
|
|
||||||
// for (auto&p : m_items) {
|
|
||||||
// p->SetPos(base_pos);
|
|
||||||
// m_base_pos.y += m_base_pos.h;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// give focus to first entry.
|
// give focus to first entry.
|
||||||
m_items[m_index]->OnFocusGained();
|
if (m_items.size() == 1) {
|
||||||
|
m_items[m_index]->OnFocusGained();
|
||||||
|
SetupButtons();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Sidebar::AddSpacer() {
|
void Sidebar::SetIndex(s64 index) {
|
||||||
|
// if we moved
|
||||||
|
if (m_index != index) {
|
||||||
|
m_items[m_index]->OnFocusLost();
|
||||||
|
m_index = index;
|
||||||
|
m_items[m_index]->OnFocusGained();
|
||||||
|
SetupButtons();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Sidebar::AddHeader(std::string name) {
|
void Sidebar::SetupButtons() {
|
||||||
|
RemoveActions();
|
||||||
|
|
||||||
|
// add entry actions
|
||||||
|
for (const auto& [button, action] : m_items[m_index]->GetActions()) {
|
||||||
|
SetAction(button, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add default actions, overriding if needed.
|
||||||
|
this->SetActions(
|
||||||
|
std::make_pair(Button::DOWN, Action{[this](){
|
||||||
|
auto index = m_index;
|
||||||
|
if (m_list->ScrollDown(index, 1, m_items.size())) {
|
||||||
|
SetIndex(index);
|
||||||
|
}
|
||||||
|
}}),
|
||||||
|
std::make_pair(Button::UP, Action{[this](){
|
||||||
|
auto index = m_index;
|
||||||
|
if (m_list->ScrollUp(index, 1, m_items.size())) {
|
||||||
|
SetIndex(index);
|
||||||
|
}
|
||||||
|
}}),
|
||||||
|
// each item has it's own Action, but we take over B
|
||||||
|
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||||
|
SetPop();
|
||||||
|
}})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace sphaira::ui
|
} // namespace sphaira::ui
|
||||||
|
|||||||