Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
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
10
.github/workflows/build_presets.yml
vendored
@@ -1,10 +1,6 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -12,7 +8,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
preset: [Release, RelWithDebInfo, MinSizeRel, Debug]
|
||||
preset: [MinSizeRel]
|
||||
runs-on: ${{ matrix.os }}
|
||||
container: devkitpro/devkita64:latest
|
||||
|
||||
@@ -24,7 +20,7 @@ jobs:
|
||||
|
||||
- name: Configure CMake
|
||||
run: |
|
||||
cmake --preset ${{ matrix.preset }}
|
||||
cmake --preset ${{ matrix.preset }} -DUSE_VFS_GC=0
|
||||
|
||||
- name: Build
|
||||
run: cmake --build --preset ${{ matrix.preset }} --parallel 4
|
||||
|
||||
@@ -4,6 +4,8 @@ A homebrew menu for the switch.
|
||||
|
||||
[See the gbatemp thread for more details / discussion](https://gbatemp.net/threads/sphaira-hbmenu-replacement.664523/).
|
||||
|
||||
[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
|
||||
|
||||
| | |
|
||||
@@ -26,7 +28,8 @@ please include:
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
8
assets/romfs/github/ftpsrv.json
Normal file
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
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
3
assets/romfs/github/untitled.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"url": "https://github.com/ITotalJustice/untitled"
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
"Details": "Details",
|
||||
"Update": "Update",
|
||||
"Remove": "Entfernen",
|
||||
"Restore": "Wiederherstellen",
|
||||
"Download": "Download",
|
||||
"Next Page": "Nächste Seite",
|
||||
"Prev Page": "Vorherige Seite",
|
||||
@@ -26,6 +27,12 @@
|
||||
"Star": "Favorit",
|
||||
"System memory": "System-Speicher",
|
||||
"microSD card": "microSD-Karte",
|
||||
"Sd": "SD",
|
||||
"Image System memory": "System-Speicher Bild",
|
||||
"Image microSD card": "microSD-Karten Bild",
|
||||
"Slow": "Langsam",
|
||||
"Normal": "Normal",
|
||||
"Fast": "Schnell",
|
||||
"Yes": "Ja",
|
||||
"No": "Nein",
|
||||
"Enabled": "Aktiviert",
|
||||
@@ -46,7 +53,7 @@
|
||||
"Alphabetical (Star)": "Alphabetisch (Favoriten)",
|
||||
"Likes": "Likes",
|
||||
"ID": "ID",
|
||||
"Decending": "Absteigend",
|
||||
"Descending": "Absteigend",
|
||||
"Descending (down)": "Absteigend",
|
||||
"Desc": "Abst.",
|
||||
"Ascending": "Aufsteigend",
|
||||
@@ -54,7 +61,6 @@
|
||||
"Asc": "Aufst.",
|
||||
|
||||
"Menu Options": "Menü-Optionen",
|
||||
"Header": "Header",
|
||||
"Theme": "Theme",
|
||||
"Theme Options": "Theme-Optionen",
|
||||
"Select Theme": "Theme auswählen",
|
||||
@@ -62,6 +68,8 @@
|
||||
"Music": "Musik",
|
||||
"Network": "Netzwerk",
|
||||
"Network Options": "Netzwerk-Optionen",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink verbunden",
|
||||
"Nxlink Upload": "Nxlink Upload",
|
||||
@@ -82,6 +90,7 @@
|
||||
"Portuguese": "Português",
|
||||
"Russian": "Русский",
|
||||
"Swedish": "Svenska",
|
||||
"Vietnamese": "Vietnamese",
|
||||
"Logging": "Logging",
|
||||
"Replace hbmenu on exit": "hbmenu beim Beenden ersetzen",
|
||||
"Misc": "Sonstiges",
|
||||
@@ -90,6 +99,7 @@
|
||||
"Install forwarders": "Forwarder installieren",
|
||||
"Install location": "Installationsort",
|
||||
"Show install warning": "Installationswarnung anzeigen",
|
||||
"Text scroll speed": "Textlaufgeschwindigkeit",
|
||||
|
||||
"FileBrowser": "Datei-Browser",
|
||||
"%zd files": "%zd Dateien",
|
||||
@@ -112,6 +122,8 @@
|
||||
"Create Folder": "Ordner erstellen",
|
||||
"Set Folder Name": "Ordnernamen eingeben",
|
||||
"View as text (unfinished)": "Als Text anzeigen (Beta)",
|
||||
"Ignore read only": "Schreibschutz ignorieren",
|
||||
"Mount": "Einbinden",
|
||||
"Empty...": "Leer...",
|
||||
"Open with DayBreak?": "Mit DayBreak öffnen?",
|
||||
"Launch ": "Starten ",
|
||||
@@ -199,6 +211,10 @@
|
||||
"Bad Page": "Ungültige Seite",
|
||||
"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 ",
|
||||
"Uninstalling ": "Deinstalliere ",
|
||||
"Deleting ": "Lösche ",
|
||||
@@ -211,6 +227,8 @@
|
||||
"Copying ": "Kopiere ",
|
||||
"Trying to load ": "Lade ",
|
||||
"Downloading ": "Lade herunter ",
|
||||
"Downloaded ": "Heruntergeladen ",
|
||||
"Removed ": "Entfernt ",
|
||||
"Checking MD5": "Prüfe MD5",
|
||||
"Loading...": "Lade...",
|
||||
"Loading": "Lade",
|
||||
@@ -220,11 +238,18 @@
|
||||
"Update avaliable: ": "Update verfügbar: ",
|
||||
"Download update: ": "Update herunterladen: ",
|
||||
"Updated to ": "Aktualisiert auf ",
|
||||
"Press OK to restart Sphaira": "OK drücken um Sphaira neuzustarten",
|
||||
"Restart Sphaira?": "Sphaira neustarten?",
|
||||
"Failed to download update": "Update-Download fehlgeschlagen",
|
||||
"Restore hbmenu?": "hbmenu wiederherstellen?",
|
||||
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Konnte /switch/hbmenu.nro nicht finden\nBitte hbmenu über den AppStore neu installieren",
|
||||
"Failed to restore hbmenu, please re-download hbmenu": "Wiederherstellung fehlgeschlagen, bitte hbmenu neu herunterladen",
|
||||
"Failed to restore hbmenu, using sphaira instead": "Wiederherstellung fehlgeschlagen, verwende stattdessen Sphaira",
|
||||
"Restored hbmenu, closing sphaira": "hbmenu wiederhergestellt, Sphaira wird beendet",
|
||||
"Restored hbmenu": "hbmenu wiederhergestellt",
|
||||
"Delete Selected files?": "Ausgewählte Dateien löschen?",
|
||||
"Completely remove ": "Vollständig entfernen ",
|
||||
"Are you sure you want to delete ": "Wirklich löschen ",
|
||||
"Are you sure you wish to cancel?": "Wirklich abbrechen?",
|
||||
"If this message appears repeatedly, please open an issue.": "Bei wiederholtem Auftreten bitte Issue erstellen."
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
"Details": "Details",
|
||||
"Update": "Update",
|
||||
"Remove": "Remove",
|
||||
"Restore": "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": "Sd",
|
||||
"Image System memory": "Image System memory",
|
||||
"Image microSD card": "Image microSD card",
|
||||
"Slow": "Slow",
|
||||
"Normal": "Normal",
|
||||
"Fast": "Fast",
|
||||
"Yes": "Yes",
|
||||
"No": "No",
|
||||
"Enabled": "Enabled",
|
||||
@@ -46,7 +53,7 @@
|
||||
"Alphabetical (Star)": "Alphabetical (Star)",
|
||||
"Likes": "Likes",
|
||||
"ID": "ID",
|
||||
"Decending": "Decending",
|
||||
"Descending": "Descending",
|
||||
"Descending (down)": "Descending (down)",
|
||||
"Desc": "Desc",
|
||||
"Ascending": "Ascending",
|
||||
@@ -54,7 +61,6 @@
|
||||
"Asc": "Asc",
|
||||
|
||||
"Menu Options": "Menu Options",
|
||||
"Header": "Header",
|
||||
"Theme": "Theme",
|
||||
"Theme Options": "Theme Options",
|
||||
"Select Theme": "Select Theme",
|
||||
@@ -62,6 +68,8 @@
|
||||
"Music": "Music",
|
||||
"Network": "Network",
|
||||
"Network Options": "Network Options",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink Connected",
|
||||
"Nxlink Upload": "Nxlink Upload",
|
||||
@@ -82,6 +90,7 @@
|
||||
"Portuguese": "Português",
|
||||
"Russian": "Русский",
|
||||
"Swedish": "Svenska",
|
||||
"Vietnamese": "Vietnamese",
|
||||
"Logging": "Logging",
|
||||
"Replace hbmenu on exit": "Replace hbmenu on exit",
|
||||
"Misc": "Misc",
|
||||
@@ -90,6 +99,7 @@
|
||||
"Install forwarders": "Install forwarders",
|
||||
"Install location": "Install location",
|
||||
"Show install warning": "Show install warning",
|
||||
"Text scroll speed": "Text scroll speed",
|
||||
|
||||
"FileBrowser": "FileBrowser",
|
||||
"%zd files": "%zd files",
|
||||
@@ -112,6 +122,8 @@
|
||||
"Create Folder": "Create Folder",
|
||||
"Set Folder Name": "Set Folder Name",
|
||||
"View as text (unfinished)": "View as text (unfinished)",
|
||||
"Ignore read only": "Ignore read only",
|
||||
"Mount": "Mount",
|
||||
"Empty...": "Empty...",
|
||||
"Open with DayBreak?": "Open with DayBreak?",
|
||||
"Launch ": "Launch ",
|
||||
@@ -199,6 +211,10 @@
|
||||
"Bad Page": "Bad Page",
|
||||
"Download theme?": "Download theme?",
|
||||
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Downloading json",
|
||||
"Select asset to download for ": "Select asset to download for ",
|
||||
|
||||
"Installing ": "Installing ",
|
||||
"Uninstalling ": "Uninstalling ",
|
||||
"Deleting ": "Deleting ",
|
||||
@@ -211,6 +227,8 @@
|
||||
"Copying ": "Copying ",
|
||||
"Trying to load ": "Trying to load ",
|
||||
"Downloading ": "Downloading ",
|
||||
"Downloaded ": "Downloaded ",
|
||||
"Removed ": "Removed ",
|
||||
"Checking MD5": "Checking MD5",
|
||||
"Loading...": "Loading...",
|
||||
"Loading": "Loading",
|
||||
@@ -220,8 +238,15 @@
|
||||
"Update avaliable: ": "Update avaliable: ",
|
||||
"Download update: ": "Download update: ",
|
||||
"Updated to ": "Updated to ",
|
||||
"Press OK to restart Sphaira": "Press OK to restart Sphaira",
|
||||
"Restart Sphaira?": "Restart Sphaira?",
|
||||
"Failed to download update": "Failed to download update",
|
||||
"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?",
|
||||
"Completely remove ": "Completely remove ",
|
||||
"Are you sure you want to delete ": "Are you sure you want to delete ",
|
||||
|
||||
@@ -9,23 +9,30 @@
|
||||
"OK": "OK",
|
||||
"Back": "Atrás",
|
||||
"Select": "Seleccionar",
|
||||
"Open": "Abierto",
|
||||
"Open": "Abrir",
|
||||
"Launch": "Ejecutar",
|
||||
"Info": "Información",
|
||||
"Install": "Instalar",
|
||||
"Delete": "Borrar",
|
||||
"Restart": "",
|
||||
"Changelog": "Log de Cambios",
|
||||
"Restart": "Reiniciar",
|
||||
"Changelog": "Log de cambios",
|
||||
"Details": "Detalles",
|
||||
"Update": "Actualizar",
|
||||
"Remove": "Borrar",
|
||||
"Restore": "Restaurar",
|
||||
"Download": "Descargar",
|
||||
"Next Page": "Página siguiente",
|
||||
"Prev Page": "Página anterior",
|
||||
"Unstar": "",
|
||||
"Star": "",
|
||||
"System memory": "",
|
||||
"microSD card": "",
|
||||
"Unstar": "Quitar favorito",
|
||||
"Star": "Favorito",
|
||||
"System memory": "Memoria de sistema",
|
||||
"microSD card": "microSD",
|
||||
"Sd": "SD",
|
||||
"Image System memory": "Imagen memoria interna",
|
||||
"Image microSD card": "Imagen tarjeta microSD",
|
||||
"Slow": "Lento",
|
||||
"Normal": "Normal",
|
||||
"Fast": "Rápido",
|
||||
"Yes": "Sí",
|
||||
"No": "No",
|
||||
"Enabled": "Activado",
|
||||
@@ -38,36 +45,37 @@
|
||||
"Order": "Orden",
|
||||
"Search": "Buscar",
|
||||
"Updated": "Actualizado",
|
||||
"Updated (Star)": "Actualizado (Star)",
|
||||
"Updated (Star)": "Actualizado (favorito)",
|
||||
"Downloads": "Descargas",
|
||||
"Size": "Tamaño",
|
||||
"Size (Star)": "Tamaño (Star)",
|
||||
"Size (Star)": "Tamaño (favorito)",
|
||||
"Alphabetical": "Alfabético",
|
||||
"Alphabetical (Star)": "Alfabético (Star)",
|
||||
"Alphabetical (Star)": "Alfabético (favorito)",
|
||||
"Likes": "Me Gusta",
|
||||
"ID": "ID",
|
||||
"Decending": "Descendente",
|
||||
"Descending": "Descendente",
|
||||
"Descending (down)": "Descendente (abajo)",
|
||||
"Desc": "Descendente",
|
||||
"Ascending": "Ascendente",
|
||||
"Ascending (Up)": "Ascendente (arriba)",
|
||||
"Asc": "Ascendente",
|
||||
|
||||
"Menu Options": "Opciones de Menú",
|
||||
"Header": "Encabezamiento",
|
||||
"Menu Options": "Opciones de menú",
|
||||
"Theme": "Tema",
|
||||
"Theme Options": "Opciones de Tema",
|
||||
"Select Theme": "Seleccionar Tema",
|
||||
"Theme Options": "Opciones de tema",
|
||||
"Select Theme": "Seleccionar tema",
|
||||
"Shuffle": "Barajar",
|
||||
"Music": "Música",
|
||||
"Network": "Red",
|
||||
"Network Options": "Opciones de Red",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink Conectado",
|
||||
"Nxlink Upload": "Nxlink Subida",
|
||||
"Nxlink Finished": "Nxlink Finalizado",
|
||||
"Switch-Handheld!": "",
|
||||
"Switch-Docked!": "",
|
||||
"Network Options": "Opciones de red",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "NXlink",
|
||||
"Nxlink Connected": "NXlink conectado",
|
||||
"Nxlink Upload": "NXlink subida",
|
||||
"Nxlink Finished": "NXlink finalizado",
|
||||
"Switch-Handheld!": "¡Switch-Modo-Portátil!",
|
||||
"Switch-Docked!": "¡Switch-Modo-TV!",
|
||||
"Language": "Idioma",
|
||||
"Auto": "Automático",
|
||||
"English": "English",
|
||||
@@ -82,43 +90,47 @@
|
||||
"Portuguese": "Português",
|
||||
"Russian": "Русский",
|
||||
"Swedish": "Svenska",
|
||||
"Logging": "Explotación florestal",
|
||||
"Replace hbmenu on exit": "Reemplazar hbmenu al salir",
|
||||
"Vietnamese": "Vietnamese",
|
||||
"Logging": "Registro",
|
||||
"Replace hbmenu on exit": "Reemplazar hbmenu",
|
||||
"Misc": "Varios",
|
||||
"Misc Options": "Opciones varias",
|
||||
"Web": "Web",
|
||||
"Install forwarders": "",
|
||||
"Install location": "",
|
||||
"Show install warning": "",
|
||||
"Install forwarders": "Instalar forwarders",
|
||||
"Install location": "Dispositivo de instalación",
|
||||
"Show install warning": "Precaución de instalación",
|
||||
"Text scroll speed": "Velocidad de scroll",
|
||||
|
||||
"FileBrowser": "Explorador de Archivos",
|
||||
"%zd files": "%zd files",
|
||||
"%zd dirs": "%zd dirs",
|
||||
"File Options": "Opciones de Tema",
|
||||
"Show Hidden": "Mostrar Oculto",
|
||||
"FileBrowser": "Explorador de archivos",
|
||||
"%zd files": "%zd archivos",
|
||||
"%zd dirs": "%zd carpetas",
|
||||
"File Options": "Opciones de archivo",
|
||||
"Show Hidden": "Mostrar archivos ocultos",
|
||||
"Folders First": "Carpetas primero",
|
||||
"Hidden Last": "Oculto último",
|
||||
"Cut": "Cortar ",
|
||||
"Hidden Last": "Ocultos al final",
|
||||
"Cut": "Cortar",
|
||||
"Copy": "Copiar",
|
||||
"Paste": "Pegar",
|
||||
"Paste ": "Pegar ",
|
||||
" file(s)?": " ¿archivo(s)?",
|
||||
"Rename": "Renombrar",
|
||||
"Set New File Name": "Establecer Nuevo Nombre de Archivo",
|
||||
"Set New File Name": "Establecer nuevo nombre de archivo",
|
||||
"Advanced": "Avanzado",
|
||||
"Advanced Options": "Opciones Avanzadas",
|
||||
"Advanced Options": "Opciones avanzadas",
|
||||
"Create File": "Crear archivo",
|
||||
"Set File Name": "Establecer Nombre de Archivo",
|
||||
"Set File Name": "Establecer nombre de archivo",
|
||||
"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)",
|
||||
"Ignore read only": "Ignorar sólo lectura",
|
||||
"Mount": "Montar",
|
||||
"Empty...": "Vacío...",
|
||||
"Open with DayBreak?": "Abrir con DayBreak",
|
||||
"Launch ": "",
|
||||
"Launch option for: ": "Opción de ejecución para: ",
|
||||
"Select launcher for: ": "",
|
||||
"Open with DayBreak?": "¿Abrir con DayBreak?",
|
||||
"Launch ": "Abrir ",
|
||||
"Launch option for: ": "Opción de abrir con: ",
|
||||
"Select launcher for: ": "Seleccionar abrir con: ",
|
||||
|
||||
"Homebrew": "Honebrew",
|
||||
"Homebrew": "Homebrew",
|
||||
"Homebrew Options": "Opciones de Homebrew",
|
||||
"Hide Sphaira": "Ocultar Sphaira",
|
||||
"Install Forwarder": "Instalar Forwarder",
|
||||
@@ -128,16 +140,16 @@
|
||||
"Creating Control": "Creando Control",
|
||||
"Creating Meta": "Creando Meta",
|
||||
"Writing Nca": "Creando NCA",
|
||||
"Updating ncm databse": "Actualizando base de datos ncm ",
|
||||
"Pushing application record": "",
|
||||
"Updating ncm databse": "Actualizando base de datos ncm",
|
||||
"Pushing application record": "Registro de aplicación",
|
||||
"Installed!": "¡Instalado!",
|
||||
"Failed to install forwarder": "Fallo al instalar forwarder",
|
||||
"Unstarred ": "",
|
||||
"Starred ": "",
|
||||
"Unstarred ": "Quitar Favorito",
|
||||
"Starred ": "Favorito",
|
||||
|
||||
"AppStore": "AppStore",
|
||||
"AppStore": "Tienda",
|
||||
"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",
|
||||
"Games": "Juegos",
|
||||
"Emulators": "Emuladores",
|
||||
@@ -145,60 +157,64 @@
|
||||
"Themes": "Temas",
|
||||
"Legacy": "Legado",
|
||||
"version: %s": "version: %s",
|
||||
"updated: %s": "updated: %s",
|
||||
"category: %s": "category: %s",
|
||||
"extracted: %.2f MiB": "extracted: %.2f MiB",
|
||||
"updated: %s": "actualizado: %s",
|
||||
"category: %s": "categoría: %s",
|
||||
"extracted: %.2f MiB": "extraído: %.2f MiB",
|
||||
"app_dls: %s": "app_dls: %s",
|
||||
"More by Author": "Mostrar mas del Autor",
|
||||
"Leave Feedback": "Dejar Mensaje",
|
||||
|
||||
"Irs": "IRS",
|
||||
"Ambient Noise Level: ": "Nivel de Ruido",
|
||||
"Ambient Noise Level: ": "Nivel de Ruido Ambiente",
|
||||
"Controller": "Control",
|
||||
"Pad ": "Almohadilla ",
|
||||
"Pad ": "GamePad ",
|
||||
" (Available)": " (Disponible)",
|
||||
" (Unsupported)": "",
|
||||
" (Unsupported)": "(No Compatible)",
|
||||
" (Unconnected)": " (Desconectado)",
|
||||
"HandHeld": "Portátil",
|
||||
"Rotation": "Rotación",
|
||||
"0 (Sideways)": "0 (De Lado)",
|
||||
"90 (Flat)": "90 (Plano)",
|
||||
"180 (-Sideways)": "180 (-De Lado)",
|
||||
"270 (Upside down)": "270 (Al Revés)",
|
||||
"0 (Sideways)": "0° (De lado)",
|
||||
"90 (Flat)": "90° (Plano)",
|
||||
"180 (-Sideways)": "180° (De lado)",
|
||||
"270 (Upside down)": "270° (Al revés)",
|
||||
"Colour": "Color",
|
||||
"Grey": "Gris",
|
||||
"Ironbow": "Paleta Térmica",
|
||||
"Ironbow": "Paleta térmica",
|
||||
"Green": "Verde",
|
||||
"Red": "Rojo",
|
||||
"Blue": "Azul",
|
||||
"Light Target": "Objetivo de Luz",
|
||||
"Light Target": "Objetivo de luz",
|
||||
"All leds": "Todos los leds",
|
||||
"Bright group": "Grupo brillante",
|
||||
"Bright group": "Grupo brillo",
|
||||
"Dim group": "Grupo tenue",
|
||||
"None": "Ninguno",
|
||||
"Gain": "Ganancia",
|
||||
"Negative Image": "Imagen Negativa",
|
||||
"Normal image": "Imagen Normal",
|
||||
"Negative image": "Imagen Negativa",
|
||||
"Negative Image": "Imagen negativa",
|
||||
"Normal image": "Imagen normal",
|
||||
"Negative image": "Imagen negativa",
|
||||
"Format": "Formato",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"Trimming Format": "Formato de Recorte",
|
||||
"External Light Filter": "Filtro de Luz Externa",
|
||||
"Load Default": "Cargar Predeterminado",
|
||||
"Trimming Format": "Formato de recorte",
|
||||
"External Light Filter": "Filtro de luz externa",
|
||||
"Load Default": "Cargar predeterminado",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Opciones de Themezer",
|
||||
"Nsfw": "NSFW",
|
||||
"Page": "Página",
|
||||
"Page %zu / %zu": "Page %zu / %zu",
|
||||
"Page %zu / %zu": "Pág. %zu / %zu",
|
||||
"Enter Page Number": "Ingresar Número de Página",
|
||||
"Bad Page": "Página Errónea",
|
||||
"Download theme?": "¿Descargar Tema?",
|
||||
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Descargando json",
|
||||
"Select asset to download for ": "Seleccionar recurso a descargar para ",
|
||||
|
||||
"Installing ": "Instalando ",
|
||||
"Uninstalling ": "Desinstalando ",
|
||||
"Deleting ": "Borrando ",
|
||||
@@ -209,22 +225,31 @@
|
||||
"Scanning ": "Escaneando ",
|
||||
"Creating ": "Creando ",
|
||||
"Copying ": "Copiando ",
|
||||
"Trying to load ": "",
|
||||
"Trying to load ": "Intentando cargar ",
|
||||
"Downloading ": "Descargando ",
|
||||
"Checking MD5": "Chqueando MD5",
|
||||
"Downloaded ": "Descargado ",
|
||||
"Removed ": "Removido ",
|
||||
"Checking MD5": "Chequeando MD5",
|
||||
"Loading...": "Cargando...",
|
||||
"Loading": "Cargando",
|
||||
"Empty!": "¡Vacío!",
|
||||
"Not Ready...": "No Listo Aún...",
|
||||
"Not Ready...": "No listo aún...",
|
||||
"Error loading page!": "¡Error cargando la página!",
|
||||
"Update avaliable: ": "Actualización disponible: ",
|
||||
"Download update: ": "Descargar actualización: ",
|
||||
"Updated to ": "",
|
||||
"Restart Sphaira?": "",
|
||||
"Updated to ": "Actualizado a ",
|
||||
"Press OK to restart Sphaira": "Presiona OK para reiniciar sphaira",
|
||||
"Restart Sphaira?": "¿Reiniciar sphaira?",
|
||||
"Failed to download update": "Fallo al descargar actualización",
|
||||
"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",
|
||||
"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?",
|
||||
"If this message appears repeatedly, please open an issue.": ""
|
||||
}
|
||||
"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",
|
||||
"Update": "Mise à jour",
|
||||
"Remove": "Supprimer",
|
||||
"Restore": "Restaurer",
|
||||
"Download": "Télécharger",
|
||||
"Next Page": "Page Suiv.",
|
||||
"Prev Page": "Page Préc.",
|
||||
@@ -26,6 +27,12 @@
|
||||
"Star": "Ajouter aux favories",
|
||||
"System memory": "Mémoire système",
|
||||
"microSD card": "Carte microSD",
|
||||
"Sd": "Sd",
|
||||
"Image System memory": "Image de la mémoire System",
|
||||
"Image microSD card": "Image de la Carte microSD",
|
||||
"Slow": "Lent",
|
||||
"Normal": "Normal",
|
||||
"Fast": "Rapide",
|
||||
"Yes": "Oui",
|
||||
"No": "Non",
|
||||
"Enabled": "Activé(e)",
|
||||
@@ -46,7 +53,7 @@
|
||||
"Alphabetical (Star)": "Alphabétique (Favories)",
|
||||
"Likes": "Likes",
|
||||
"ID": "ID",
|
||||
"Decending": "Décroissant",
|
||||
"Descending": "Décroissant",
|
||||
"Descending (down)": "Décroissant",
|
||||
"Desc": "Décroissant",
|
||||
"Ascending": "Croissant",
|
||||
@@ -54,7 +61,6 @@
|
||||
"Asc": "Croissant",
|
||||
|
||||
"Menu Options": "Options des Menus",
|
||||
"Header": "En-tête",
|
||||
"Theme": "Thème",
|
||||
"Theme Options": "Options de Thème",
|
||||
"Select Theme": "Choisir un Thème",
|
||||
@@ -62,6 +68,8 @@
|
||||
"Music": "Musique",
|
||||
"Network": "Réseau",
|
||||
"Network Options": "Options Réseau",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink Connecté",
|
||||
"Nxlink Upload": "Nxlink téléversement",
|
||||
@@ -82,6 +90,7 @@
|
||||
"Portuguese": "Português",
|
||||
"Russian": "Русский",
|
||||
"Swedish": "Svenska",
|
||||
"Vietnamese": "Vietnamese",
|
||||
"Logging": "Journalisation",
|
||||
"Replace hbmenu on exit": "Remplacer hbmenu quand quitté",
|
||||
"Misc": "Divers",
|
||||
@@ -90,6 +99,7 @@
|
||||
"Install forwarders": "Installer les Forwarders",
|
||||
"Install location": "Emplacement d'installation",
|
||||
"Show install warning": "Afficher l'avertissement d'installation",
|
||||
"Text scroll speed": "Vitesse de défilement du texte",
|
||||
|
||||
"FileBrowser": "Explorateur de Fichiers",
|
||||
"%zd files": "%zd fichiers",
|
||||
@@ -112,6 +122,8 @@
|
||||
"Create Folder": "Créer un Dossier",
|
||||
"Set Folder Name": "Nommer Le Dossier",
|
||||
"View as text (unfinished)": "Afficher sous forme de texte (inachevé)",
|
||||
"Ignore read only": "Ignorer lecture seule",
|
||||
"Mount": "Monter",
|
||||
"Empty...": "Vide...",
|
||||
"Open with DayBreak?": "Ouvrir avec DayBreak?",
|
||||
"Launch ": "Lancer ",
|
||||
@@ -199,6 +211,10 @@
|
||||
"Bad Page": "Page inexistante",
|
||||
"Download theme?": "Télécharger le thème?",
|
||||
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Téléchargement du json",
|
||||
"Select asset to download for ": "Sélectionner l'asset pour télécharger ",
|
||||
|
||||
"Installing ": "Installation ",
|
||||
"Uninstalling ": "Désinstallation ",
|
||||
"Deleting ": "Suppression ",
|
||||
@@ -211,6 +227,8 @@
|
||||
"Copying ": "Copie ",
|
||||
"Trying to load ": "Tente de charger ",
|
||||
"Downloading ": "Téléchargement ",
|
||||
"Downloaded ": "Téléchargé",
|
||||
"Removed ": "Supprimé ",
|
||||
"Checking MD5": "Vérification MD5",
|
||||
"Loading...": "Chargement...",
|
||||
"Loading": "Chargement",
|
||||
@@ -220,11 +238,18 @@
|
||||
"Update avaliable: ": "Mise à jour disponible: ",
|
||||
"Download update: ": "Télécharger la mise à jour: ",
|
||||
"Updated to ": "Mis à jour vers ",
|
||||
"Press OK to restart Sphaira": "Appuyez sur OK pour redémarrer Sphaira",
|
||||
"Restart Sphaira?": "Redémarrer Sphaira?",
|
||||
"Failed to download update": "Echec du téléchargement de la mise à jour",
|
||||
"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?",
|
||||
"Completely remove ": "Supprimer totalement ",
|
||||
"Are you sure you want to delete ": "Êtes-vous sûr de vouloir supprimer ",
|
||||
"Are you sure you wish to cancel?": "Souhaitez-vous vraiment annuler?",
|
||||
"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]",
|
||||
"No Internet": "Niente Internet",
|
||||
"Files": "",
|
||||
"Apps": "",
|
||||
"Store": "",
|
||||
"Files": "File",
|
||||
"Apps": "App",
|
||||
"Store": "Store",
|
||||
"Menu": "Menu",
|
||||
"Options": "Opzioni",
|
||||
"OK": "",
|
||||
"OK": "OK",
|
||||
"Back": "Indietro",
|
||||
"Select": "",
|
||||
"Select": "Seleziona",
|
||||
"Open": "Apri",
|
||||
"Launch": "Lancia",
|
||||
"Info": "Informazioni",
|
||||
"Install": "Installa",
|
||||
"Delete": "Elimina",
|
||||
"Restart": "",
|
||||
"Changelog": "",
|
||||
"Details": "",
|
||||
"Update": "",
|
||||
"Remove": "",
|
||||
"Restart": "Riavvia",
|
||||
"Changelog": "Patch notes",
|
||||
"Details": "Dettagli",
|
||||
"Update": "Aggiorna",
|
||||
"Remove": "Rimuovi",
|
||||
"Restore": "Ripristina",
|
||||
"Download": "Download",
|
||||
"Next Page": "Pagina successiva",
|
||||
"Prev Page": "Pagina precedente",
|
||||
"Unstar": "",
|
||||
"Star": "",
|
||||
"System memory": "",
|
||||
"microSD card": "",
|
||||
"Unstar": "Rimuovi dai preferiti",
|
||||
"Star": "Aggiungi ai preferiti",
|
||||
"System memory": "Memoria di sistema",
|
||||
"microSD card": "Scheda microSD",
|
||||
"Sd": "SD",
|
||||
"Image System memory": "Immagine memoria di sistema",
|
||||
"Image microSD card": "Immagine scheda microSD",
|
||||
"Slow": "",
|
||||
"Normal": "",
|
||||
"Fast": "",
|
||||
"Yes": "Sì",
|
||||
"No": "No",
|
||||
"Enabled": "Abilitato",
|
||||
@@ -40,13 +47,13 @@
|
||||
"Updated": "Aggiornato",
|
||||
"Updated (Star)": "",
|
||||
"Downloads": "Download",
|
||||
"Size": "Misurare",
|
||||
"Size (Star)": "",
|
||||
"Size": "Dimensione",
|
||||
"Size (Star)": "Dimensione (Preferiti)",
|
||||
"Alphabetical": "Alfabetico",
|
||||
"Alphabetical (Star)": "",
|
||||
"Likes": "",
|
||||
"ID": "",
|
||||
"Decending": "Decrescente",
|
||||
"Alphabetical (Star)": "Alfabetico (Preferiti)",
|
||||
"Likes": "Mi Piace",
|
||||
"ID": "ID",
|
||||
"Descending": "Decrescente",
|
||||
"Descending (down)": "Decrescente",
|
||||
"Desc": "Decrescente",
|
||||
"Ascending": "Crescente",
|
||||
@@ -54,7 +61,6 @@
|
||||
"Asc": "Crescente",
|
||||
|
||||
"Menu Options": "Opzioni menu",
|
||||
"Header": "Intestazione",
|
||||
"Theme": "Tema",
|
||||
"Theme Options": "Opzioni tema",
|
||||
"Select Theme": "Seleziona tema",
|
||||
@@ -62,14 +68,16 @@
|
||||
"Music": "Musica",
|
||||
"Network": "Rete",
|
||||
"Network Options": "Opzioni di rete",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "",
|
||||
"Nxlink Upload": "",
|
||||
"Nxlink Finished": "",
|
||||
"Switch-Handheld!": "",
|
||||
"Switch-Docked!": "",
|
||||
"Nxlink Connected": "Nxlink connesso",
|
||||
"Nxlink Upload": "Nxlink upload",
|
||||
"Nxlink Finished": "Nxlink finito",
|
||||
"Switch-Handheld!": "Switch Portatile",
|
||||
"Switch-Docked!": "Switch Dock",
|
||||
"Language": "Lingua",
|
||||
"Auto": "",
|
||||
"Auto": "Auto",
|
||||
"English": "English",
|
||||
"Japanese": "日本語",
|
||||
"French": "Français",
|
||||
@@ -82,14 +90,16 @@
|
||||
"Portuguese": "Português",
|
||||
"Russian": "Русский",
|
||||
"Swedish": "Svenska",
|
||||
"Vietnamese": "Vietnamese",
|
||||
"Logging": "Logging",
|
||||
"Replace hbmenu on exit": "Sostituisci hbmenu all'uscita",
|
||||
"Misc": "Varie",
|
||||
"Misc Options": "Opzioni varie",
|
||||
"Web": "Rete",
|
||||
"Install forwarders": "",
|
||||
"Install location": "",
|
||||
"Show install warning": "",
|
||||
"Install forwarders": "Installa forwarder",
|
||||
"Install location": "Installa posizione",
|
||||
"Show install warning": "Mostra avvertimento installazione",
|
||||
"Text scroll speed": "",
|
||||
|
||||
"FileBrowser": "FileBrowser",
|
||||
"%zd files": "%zd files",
|
||||
@@ -100,23 +110,25 @@
|
||||
"Hidden Last": "Ultimo nascosto",
|
||||
"Cut": "Taglia",
|
||||
"Copy": "Copia",
|
||||
"Paste": "",
|
||||
"Paste ": "",
|
||||
" file(s)?": "",
|
||||
"Paste": "Incolla",
|
||||
"Paste ": "Incolla ",
|
||||
" file(s)?": "(i)file?",
|
||||
"Rename": "Rinomina",
|
||||
"Set New File Name": "",
|
||||
"Set New File Name": "Imposta nuovo nome",
|
||||
"Advanced": "Avanzato",
|
||||
"Advanced Options": "Opzioni avanzate",
|
||||
"Create File": "Crea file",
|
||||
"Set File Name": "",
|
||||
"Set File Name": "Imposta nome",
|
||||
"Create Folder": "Crea cartella",
|
||||
"Set Folder Name": "",
|
||||
"Set Folder Name": "Imposta nome",
|
||||
"View as text (unfinished)": "Visualizza come testo (non finito)",
|
||||
"Empty...": "",
|
||||
"Open with DayBreak?": "",
|
||||
"Launch ": "",
|
||||
"Launch option for: ": "",
|
||||
"Select launcher for: ": "",
|
||||
"Ignore read only": "Ignora read only",
|
||||
"Mount": "Monta",
|
||||
"Empty...": "Vuoto...",
|
||||
"Open with DayBreak?": "Vuoi aprire con Daybreak?",
|
||||
"Launch ": "Lancia",
|
||||
"Launch option for: ": "Lancia opzione per",
|
||||
"Select launcher for: ": "Scegli launcher per",
|
||||
|
||||
"Homebrew": "Homebrew",
|
||||
"Homebrew Options": "Opzioni Homebrew",
|
||||
@@ -199,6 +211,10 @@
|
||||
"Bad Page": "",
|
||||
"Download theme?": "",
|
||||
|
||||
"GitHub": "",
|
||||
"Downloading json": "",
|
||||
"Select asset to download for ": "",
|
||||
|
||||
"Installing ": "",
|
||||
"Uninstalling ": "",
|
||||
"Deleting ": "",
|
||||
@@ -211,6 +227,8 @@
|
||||
"Copying ": "",
|
||||
"Trying to load ": "",
|
||||
"Downloading ": "",
|
||||
"Downloaded ": "",
|
||||
"Removed ": "",
|
||||
"Checking MD5": "",
|
||||
"Loading...": "",
|
||||
"Loading": "",
|
||||
@@ -220,8 +238,15 @@
|
||||
"Update avaliable: ": "",
|
||||
"Download update: ": "",
|
||||
"Updated to ": "",
|
||||
"Press OK to restart Sphaira": "",
|
||||
"Restart Sphaira?": "",
|
||||
"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?": "",
|
||||
"Completely remove ": "",
|
||||
"Are you sure you want to delete ": "Sei sicuro di voler eliminare? ",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"Details": "詳細",
|
||||
"Update": "アップデート",
|
||||
"Remove": "除去",
|
||||
"Restore": "復元",
|
||||
"Download": "ダウンロード",
|
||||
"Next Page": "次のページ",
|
||||
"Prev Page": "前のページ",
|
||||
@@ -26,6 +27,12 @@
|
||||
"Star": "お気に入り",
|
||||
"System memory": "システムメモリ",
|
||||
"microSD card": "SDメモリーカード",
|
||||
"Sd": "SDメモリーカード",
|
||||
"Image System memory": "システムメモリイメージ",
|
||||
"Image microSD card": "SDイメージ",
|
||||
"Slow": "遅い",
|
||||
"Normal": "普通",
|
||||
"Fast": "速い",
|
||||
"Yes": "はい",
|
||||
"No": "いいえ",
|
||||
"Enabled": "",
|
||||
@@ -46,7 +53,7 @@
|
||||
"Alphabetical (Star)": "アルファベット順(お気に入り)",
|
||||
"Likes": "いいね順",
|
||||
"ID": "デベロッパー順",
|
||||
"Decending": "降順",
|
||||
"Descending": "降順",
|
||||
"Descending (down)": "降順",
|
||||
"Desc": "降順",
|
||||
"Ascending": "上昇",
|
||||
@@ -54,7 +61,6 @@
|
||||
"Asc": "上昇",
|
||||
|
||||
"Menu Options": "メニュー設定",
|
||||
"Header": "ヘッダー",
|
||||
"Theme": "テーマ",
|
||||
"Theme Options": "テーマ設定",
|
||||
"Select Theme": "テーマを選ぶ",
|
||||
@@ -62,6 +68,8 @@
|
||||
"Music": "BGM",
|
||||
"Network": "ネットワーク",
|
||||
"Network Options": "ネットワーク設定",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink 接続",
|
||||
"Nxlink Upload": "Nxlink アップロード",
|
||||
@@ -82,6 +90,7 @@
|
||||
"Portuguese": "Português",
|
||||
"Russian": "Русский",
|
||||
"Swedish": "Svenska",
|
||||
"Vietnamese": "Vietnamese",
|
||||
"Logging": "ログの取得",
|
||||
"Replace hbmenu on exit": "終了時に hbmenu を置き換える",
|
||||
"Misc": "その他",
|
||||
@@ -90,6 +99,7 @@
|
||||
"Install forwarders": "Forwarderのインストール機能",
|
||||
"Install location": "インストール経路",
|
||||
"Show install warning": "警告文を示す",
|
||||
"Text scroll speed": "流れる文字の速さ",
|
||||
|
||||
"FileBrowser": "ファイルブラウザ",
|
||||
"%zd files": "%zd個のファイル",
|
||||
@@ -112,6 +122,8 @@
|
||||
"Create Folder": "フォルダーの作成",
|
||||
"Set Folder Name": "名前を入力",
|
||||
"View as text (unfinished)": "テキストとして表示 (未完成)",
|
||||
"Ignore read only": "読み取り専用を無視する",
|
||||
"Mount": "マウント",
|
||||
"Empty...": "このフォルダーは空です",
|
||||
"Open with DayBreak?": "DayBreakで開きますか?",
|
||||
"Launch ": "起動しますか",
|
||||
@@ -199,6 +211,10 @@
|
||||
"Bad Page": "ページが見つかりません",
|
||||
"Download theme?": "テーマをインストールしますか?",
|
||||
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "JSONからダウンロード",
|
||||
"Select asset to download for ": "ダウンロードアイテムを選択 ",
|
||||
|
||||
"Installing ": "インストール中 ",
|
||||
"Uninstalling ": "アンインストール中 ",
|
||||
"Deleting ": "削除中 ",
|
||||
@@ -211,6 +227,8 @@
|
||||
"Copying ": "コピー中 ",
|
||||
"Trying to load ": "サムネイルを取得中 ",
|
||||
"Downloading ": "ダウンロード中 ",
|
||||
"Downloaded ": "ダウンロード完了 ",
|
||||
"Removed ": "除去完了 ",
|
||||
"Checking MD5": "MD5を確認中 ",
|
||||
"Loading...": "ロード中",
|
||||
"Loading": "ロード中",
|
||||
@@ -220,8 +238,15 @@
|
||||
"Update avaliable: ": "アップデート可能: ",
|
||||
"Download update: ": "アップデートをダウンロード: ",
|
||||
"Updated to ": "アップデート: ",
|
||||
"Press OK to restart Sphaira": "確認ボタンを押してSphairaを再起動",
|
||||
"Restart Sphaira?": "Sphairaを再起動しますか?",
|
||||
"Failed to download update": "アップデートのダウンロード失敗",
|
||||
"Restore hbmenu?": "hbmenuに戻しますか?",
|
||||
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "/switch/hbmemu.nroが見つかりません\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?": "本当に削除しますか?",
|
||||
"Completely remove ": "除去しますか ",
|
||||
"Are you sure you want to delete ": "消去してもよろしいですか ",
|
||||
|
||||
@@ -1,230 +1,255 @@
|
||||
{
|
||||
"[Applet Mode]": "[애플릿 모드]",
|
||||
"No Internet": "네트워크 연결 없음",
|
||||
"Files": "파일 탐색기",
|
||||
"Apps": "홈브류",
|
||||
"Store": "앱스토어",
|
||||
"Menu": "메뉴",
|
||||
"Options": "설정",
|
||||
"OK": "확인",
|
||||
"Back": "뒤로",
|
||||
"Select": "선택",
|
||||
"Open": "열기",
|
||||
"Launch": "실행",
|
||||
"Info": "정보",
|
||||
"Install": "설치",
|
||||
"Delete": "삭제",
|
||||
"Restart": "재시작",
|
||||
"Changelog": "변경 내역",
|
||||
"Details": "상세",
|
||||
"Update": "업데이트",
|
||||
"Remove": "제거",
|
||||
"Download": "다운로드",
|
||||
"Next Page": "다음 페이지",
|
||||
"Prev Page": "이전 페이지",
|
||||
"Unstar": "즐겨찾기 해제",
|
||||
"Star": "즐겨찾기",
|
||||
"System memory": "낸드 저장소",
|
||||
"microSD card": "SD 카드",
|
||||
"Yes": "예",
|
||||
"No": "아니요",
|
||||
"Enabled": "",
|
||||
"Disabled": "",
|
||||
|
||||
"Sort By": "정렬",
|
||||
"Sort Options": "정렬 설정",
|
||||
"Filter": "필터",
|
||||
"Sort": "분류",
|
||||
"Order": "정렬",
|
||||
"Search": "검색",
|
||||
"Updated": "업데이트순",
|
||||
"Updated (Star)": "업데이트순 (즐겨찾기)",
|
||||
"Downloads": "다운로드순",
|
||||
"Size": "크기순",
|
||||
"Size (Star)": "크기순 (즐겨찾기)",
|
||||
"Alphabetical": "알파벳순",
|
||||
"Alphabetical (Star)": "알파벳순 (즐겨찾기)",
|
||||
"Likes": "좋아요순",
|
||||
"ID": "작성자순",
|
||||
"Decending": "내림차순",
|
||||
"Descending (down)": "내림차순",
|
||||
"Desc": "내림차순",
|
||||
"Ascending": "오름차순",
|
||||
"Ascending (Up)": "오름차순",
|
||||
"Asc": "오름차순",
|
||||
|
||||
"Menu Options": "메뉴",
|
||||
"Header": "헤더",
|
||||
"Theme": "테마",
|
||||
"Theme Options": "테마 설정",
|
||||
"Select Theme": "테마 선택",
|
||||
"Shuffle": "셔플",
|
||||
"Music": "BGM",
|
||||
"Network": "네트워크",
|
||||
"Network Options": "네트워크 설정",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink 연결됨",
|
||||
"Nxlink Upload": "Nxlink 업로드",
|
||||
"Nxlink Finished": "Nxlink 종료됨",
|
||||
"Switch-Handheld!": "휴대모드로 전환되었습니다!",
|
||||
"Switch-Docked!": "독 모드로 전환되었습니다!",
|
||||
"Language": "언어",
|
||||
"Auto": "자동",
|
||||
"English": "English",
|
||||
"Japanese": "日本語",
|
||||
"French": "Français",
|
||||
"German": "Deutsch",
|
||||
"Italian": "Italiano",
|
||||
"Spanish": "Español",
|
||||
"Chinese": "中文",
|
||||
"Korean": "한국어",
|
||||
"Dutch": "Dutch",
|
||||
"Portuguese": "Português",
|
||||
"Russian": "Русский",
|
||||
"Swedish": "Svenska",
|
||||
"Logging": "로깅",
|
||||
"Replace hbmenu on exit": "hbmenu sphaira 교체",
|
||||
"Misc": "기타",
|
||||
"Misc Options": "기타",
|
||||
"Web": "웹 브라우저",
|
||||
"Install forwarders": "바로가기 설치 기능",
|
||||
"Install location": "설치 경로",
|
||||
"Show install warning": "바로가기 설치 경고문 표시",
|
||||
|
||||
"FileBrowser": "파일 탐색기",
|
||||
"%zd files": "%zd개의 파일",
|
||||
"%zd dirs": "%zd개의 폴더",
|
||||
"File Options": "파일 설정",
|
||||
"Show Hidden": "숨겨진 항목 표시",
|
||||
"Folders First": "폴더 우선 정렬",
|
||||
"Hidden Last": "숨겨진 항목 후순 정렬",
|
||||
"Cut": "잘라내기",
|
||||
"Copy": "복사",
|
||||
"Paste": "붙여넣기",
|
||||
"Paste ": " ",
|
||||
" file(s)?": "개 항목을 붙여넣으시겠습니까?",
|
||||
"Rename": "이름 바꾸기",
|
||||
"Set New File Name": "새 파일명 입력",
|
||||
"Advanced": "고급",
|
||||
"Advanced Options": "고급 설정",
|
||||
"Create File": "파일 생성",
|
||||
"Set File Name": "파일명 입력",
|
||||
"Create Folder": "새 폴더",
|
||||
"Set Folder Name": "폴더명 입력",
|
||||
"View as text (unfinished)": "텍스트로 보기 (미완성)",
|
||||
"Empty...": "비어있습니다...",
|
||||
"Open with DayBreak?": "DayBreak로 여시겠습니까?",
|
||||
"Launch ": "실행하시겠습니까 ",
|
||||
"Launch option for: ": "실행 설정: ",
|
||||
"Select launcher for: ": "실행 런처: ",
|
||||
|
||||
"Homebrew": "홈브류",
|
||||
"Homebrew Options": "홈브류 설정",
|
||||
"Hide Sphaira": "Sphaira 숨기기",
|
||||
"Install Forwarder": "바로가기 설치",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "주의: 시스낸드에서 바로가기 설치시 BAN 위험이 있습니다!",
|
||||
"Installing Forwarder": "바로가기 설치중...",
|
||||
"Creating Program": "프로그램 작성중...",
|
||||
"Creating Control": "컨트롤 작성중...",
|
||||
"Creating Meta": "메타 작성중...",
|
||||
"Writing Nca": "Nca 쓰는중...",
|
||||
"Updating ncm databse": "ncm 데이터베이스 업데이트중...",
|
||||
"Pushing application record": "응용 프로그램 기록 푸시중...",
|
||||
"Installed!": "설치 완료!",
|
||||
"Failed to install forwarder": "바로가기 설치 실패",
|
||||
"Unstarred ": "즐겨찾기 해제: ",
|
||||
"Starred ": "즐겨찾기 적용: ",
|
||||
|
||||
"AppStore": "앱스토어",
|
||||
"Filter: %s | Sort: %s | Order: %s": "필터: %s | 분류: %s | 정렬: %s",
|
||||
"AppStore Options": "앱스토어 설정",
|
||||
"All": "전체",
|
||||
"Games": "게임",
|
||||
"Emulators": "에뮬레이터",
|
||||
"Tools": "도구",
|
||||
"Themes": "테마",
|
||||
"Legacy": "레거시",
|
||||
"version: %s": "버전: %s",
|
||||
"updated: %s": "갱신일: %s",
|
||||
"category: %s": "카테고리: %s",
|
||||
"extracted: %.2f MiB": "용량: %.2f MiB",
|
||||
"app_dls: %s": "다운로드 횟수: %s",
|
||||
"More by Author": "개발자의 다른 앱 더보기",
|
||||
"Leave Feedback": "피드백 남기기",
|
||||
|
||||
"Irs": "Joy-Con IR 카메라",
|
||||
"Ambient Noise Level: ": "노이즈 레벨: ",
|
||||
"Controller": "컨트롤러",
|
||||
"Pad ": "조이콘 ",
|
||||
" (Available)": " (사용 가능)",
|
||||
" (Unsupported)": " (지원 안됨)",
|
||||
" (Unconnected)": " (연결 없음)",
|
||||
"HandHeld": "- 본체 연결",
|
||||
"Rotation": "화면 회전",
|
||||
"0 (Sideways)": "0 (좌회전)",
|
||||
"90 (Flat)": "90 (정방향)",
|
||||
"180 (-Sideways)": "180 (우회전)",
|
||||
"270 (Upside down)": "270 (역전)",
|
||||
"Colour": "컬러 팔레트",
|
||||
"Grey": "그레이",
|
||||
"Ironbow": "아이언보우",
|
||||
"Green": "그린",
|
||||
"Red": "레드",
|
||||
"Blue": "블루",
|
||||
"Light Target": "반사 표적",
|
||||
"All leds": "모든 LED 켜기",
|
||||
"Bright group": "Bright LED 켜기",
|
||||
"Dim group": "Dim LED 켜기",
|
||||
"None": "LED 끄기",
|
||||
"Gain": "대비",
|
||||
"Negative Image": "화상 이미지",
|
||||
"Normal image": "일반",
|
||||
"Negative image": "반전",
|
||||
"Format": "해상도",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"Trimming Format": "트리밍 해상도",
|
||||
"External Light Filter": "외부 조명 필터",
|
||||
"Load Default": "기본값으로 설정",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Themezer 설정",
|
||||
"Nsfw": "선정성 테마",
|
||||
"Page": "페이지",
|
||||
"Page %zu / %zu": "페이지 %zu / %zu",
|
||||
"Enter Page Number": "페이지 번호 입력",
|
||||
"Bad Page": "잘못된 페이지",
|
||||
"Download theme?": "테마를 내려받으시겠습니까?",
|
||||
|
||||
"Installing ": "설치중... ",
|
||||
"Uninstalling ": "설치 제거중... ",
|
||||
"Deleting ": "삭제중... ",
|
||||
"Deleting": "삭제중...",
|
||||
"Pasting ": "붙여넣는중... ",
|
||||
"Pasting": "붙여넣는중...",
|
||||
"Removing ": "제거중... ",
|
||||
"Scanning ": "스캔중... ",
|
||||
"Creating ": "작성중... ",
|
||||
"Copying ": "복사중... ",
|
||||
"Trying to load ": "썸네일 받아오는 중... ",
|
||||
"Downloading ": "다운로드중... ",
|
||||
"Checking MD5": "MD5 확인중... ",
|
||||
"Loading...": "로딩중...",
|
||||
"Loading": "로딩중...",
|
||||
"Empty!": "찾을 수 없습니다!",
|
||||
"Not Ready...": "준비되지 않았습니다...",
|
||||
"Error loading page!": "페이지 로딩 오류!",
|
||||
"Update avaliable: ": "업데이트 가능: ",
|
||||
"Download update: ": "업데이트 다운로드: ",
|
||||
"Updated to ": "업데이트: ",
|
||||
"Restart Sphaira?": "Sphaira를 재시작 하시겠습니까?",
|
||||
"Failed to download update": "업데이트 다운로드 실패!",
|
||||
"Delete Selected files?": "정말 삭제하시겠습니까?",
|
||||
"Completely remove ": "제거하시겠습니까 ",
|
||||
"Are you sure you want to delete ": "정말 삭제하시겠습니까 ",
|
||||
"Are you sure you wish to cancel?": "정말 취소하시겠습니까?",
|
||||
"If this message appears repeatedly, please open an issue.": "해당 메시지가 반복해서 나타나는 경우, 이슈를 열어주세요."
|
||||
{
|
||||
"[Applet Mode]": "[ 애플릿 모드 ]",
|
||||
"No Internet": "인터넷 연결 없음",
|
||||
"Files": "파일 탐색기",
|
||||
"Apps": "홈브류",
|
||||
"Store": "앱스토어",
|
||||
"Menu": "메뉴",
|
||||
"Options": "설정",
|
||||
"OK": "확인",
|
||||
"Back": "뒤로",
|
||||
"Select": "선택",
|
||||
"Open": "열기",
|
||||
"Launch": "실행",
|
||||
"Info": "정보",
|
||||
"Install": "설치",
|
||||
"Delete": "삭제",
|
||||
"Restart": "재시작",
|
||||
"Changelog": "변경 내역",
|
||||
"Details": "상세",
|
||||
"Update": "업데이트",
|
||||
"Remove": "제거",
|
||||
"Restore": "복원",
|
||||
"Download": "다운로드",
|
||||
"Next Page": "다음 페이지",
|
||||
"Prev Page": "이전 페이지",
|
||||
"Unstar": "즐겨찾기 해제",
|
||||
"Star": "즐겨찾기",
|
||||
"System memory": "낸드 저장소",
|
||||
"microSD card": "SD 카드",
|
||||
"Sd": "SD 카드",
|
||||
"Image System memory": "낸드 이미지",
|
||||
"Image microSD card": "SD 이미지",
|
||||
"Slow": "느림",
|
||||
"Normal": "보통",
|
||||
"Fast": "빠름",
|
||||
"Yes": "예",
|
||||
"No": "아니요",
|
||||
"Enabled": "",
|
||||
"Disabled": "",
|
||||
|
||||
"Sort By": "정렬",
|
||||
"Sort Options": "정렬 옵션",
|
||||
"Filter": "필터",
|
||||
"Sort": "분류",
|
||||
"Order": "정렬",
|
||||
"Search": "검색",
|
||||
"Updated": "업데이트순",
|
||||
"Updated (Star)": "업데이트순 (즐겨찾기)",
|
||||
"Downloads": "다운로드순",
|
||||
"Size": "크기순",
|
||||
"Size (Star)": "크기순 (즐겨찾기)",
|
||||
"Alphabetical": "알파벳순",
|
||||
"Alphabetical (Star)": "알파벳순 (즐겨찾기)",
|
||||
"Likes": "좋아요순",
|
||||
"ID": "ID순",
|
||||
"Descending": "내림차순",
|
||||
"Descending (down)": "내림차순",
|
||||
"Desc": "내림차순",
|
||||
"Ascending": "오름차순",
|
||||
"Ascending (Up)": "오름차순",
|
||||
"Asc": "오름차순",
|
||||
|
||||
"Menu Options": "메뉴",
|
||||
"Theme": "테마",
|
||||
"Theme Options": "테마 옵션",
|
||||
"Select Theme": "테마 선택",
|
||||
"Shuffle": "셔플",
|
||||
"Music": "BGM",
|
||||
"Network": "네트워크",
|
||||
"Network Options": "네트워크 옵션",
|
||||
"Ftp": "FTP (무선)",
|
||||
"Mtp": "MTP (유선)",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink 연결됨",
|
||||
"Nxlink Upload": "Nxlink 업로드",
|
||||
"Nxlink Finished": "Nxlink 종료됨",
|
||||
"Switch-Handheld!": "휴대모드로 전환됨!",
|
||||
"Switch-Docked!": "독 모드로 전환됨!",
|
||||
"Language": "언어",
|
||||
"Auto": "자동",
|
||||
"English": "English",
|
||||
"Japanese": "日本語",
|
||||
"French": "Français",
|
||||
"German": "Deutsch",
|
||||
"Italian": "Italiano",
|
||||
"Spanish": "Español",
|
||||
"Chinese": "中文",
|
||||
"Korean": "한국어",
|
||||
"Dutch": "Dutch",
|
||||
"Portuguese": "Português",
|
||||
"Russian": "Русский",
|
||||
"Swedish": "Svenska",
|
||||
"Vietnamese": "Vietnamese",
|
||||
"Logging": "로깅",
|
||||
"Replace hbmenu on exit": "종료 시 hbmenu 교체",
|
||||
"Misc": "기타",
|
||||
"Misc Options": "기타 옵션",
|
||||
"Web": "웹 브라우저",
|
||||
"Install forwarders": "바로가기 설치",
|
||||
"Install location": "설치 위치",
|
||||
"Show install warning": "경고 메시지",
|
||||
"Text scroll speed": "흐르는 텍스트 속도",
|
||||
|
||||
"FileBrowser": "파일 탐색기",
|
||||
"%zd files": "%zd 개 파일",
|
||||
"%zd dirs": "%zd 개 폴더",
|
||||
"File Options": "파일 옵션",
|
||||
"Show Hidden": "숨겨진 항목 표시",
|
||||
"Folders First": "폴더 우선 정렬",
|
||||
"Hidden Last": "숨겨진 항목 후순 정렬",
|
||||
"Cut": "잘라내기",
|
||||
"Copy": "복사",
|
||||
"Paste": "붙여넣기",
|
||||
"Paste ": " ",
|
||||
" file(s)?": "개 항목을 붙여넣을까요?",
|
||||
"Rename": "이름 바꾸기",
|
||||
"Set New File Name": "새 파일명 입력",
|
||||
"Advanced": "고급",
|
||||
"Advanced Options": "고급 옵션",
|
||||
"Create File": "새 파일",
|
||||
"Set File Name": "파일명 입력",
|
||||
"Create Folder": "새 폴더",
|
||||
"Set Folder Name": "폴더명 입력",
|
||||
"View as text (unfinished)": "텍스트로 보기 (미완성)",
|
||||
"Ignore read only": "읽기 전용 설정 무시",
|
||||
"Mount": "마운트",
|
||||
"Empty...": "비어있음...",
|
||||
"Open with DayBreak?": "DayBreak로 열까요?",
|
||||
"Launch ": "실행할까요 ",
|
||||
"Launch option for: ": "실행 옵션: ",
|
||||
"Select launcher for: ": "실행 런처: ",
|
||||
|
||||
"Homebrew": "홈브류",
|
||||
"Homebrew Options": "홈브류 옵션",
|
||||
"Hide Sphaira": "Sphaira 숨기기",
|
||||
"Install Forwarder": "바로가기 설치",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "경고: 시스낸드에 바로가기 설치 시, 밴 위험이 있습니다!",
|
||||
"Installing Forwarder": "바로가기 설치",
|
||||
"Creating Program": "프로그램 생성",
|
||||
"Creating Control": "컨트롤 생성",
|
||||
"Creating Meta": "메타 생성",
|
||||
"Writing Nca": "Nca 쓰기",
|
||||
"Updating ncm databse": "Ncm 데이터베이스 업데이트",
|
||||
"Pushing application record": "응용 프로그램 기록 푸싱",
|
||||
"Installed!": "설치 완료!",
|
||||
"Failed to install forwarder": "바로가기 설치 실패",
|
||||
"Unstarred ": "즐겨찾기 해제: ",
|
||||
"Starred ": "즐겨찾기 적용: ",
|
||||
|
||||
"AppStore": "앱스토어",
|
||||
"Filter: %s | Sort: %s | Order: %s": "필터: %s | 분류: %s | 정렬: %s",
|
||||
"AppStore Options": "앱스토어 옵션",
|
||||
"All": "모두",
|
||||
"Games": "게임",
|
||||
"Emulators": "에뮬레이터",
|
||||
"Tools": "도구",
|
||||
"Themes": "테마",
|
||||
"Legacy": "레거시",
|
||||
"version: %s": "버전: %s",
|
||||
"updated: %s": "업데이트: %s",
|
||||
"category: %s": "카테고리: %s",
|
||||
"extracted: %.2f MiB": "용량: %.2f MiB",
|
||||
"app_dls: %s": "다운로드 횟수: %s",
|
||||
"More by Author": "개발자의 다른 앱 더 보기",
|
||||
"Leave Feedback": "피드백 남기기",
|
||||
|
||||
"Irs": "조이콘 적외선 카메라",
|
||||
"Ambient Noise Level: ": "주변 노이즈 레벨: ",
|
||||
"Controller": "컨트롤러",
|
||||
"Pad ": "조이콘 ",
|
||||
" (Available)": " (사용 가능)",
|
||||
" (Unsupported)": " (지원 안됨)",
|
||||
" (Unconnected)": " (연결 없음)",
|
||||
"HandHeld": "본체 연결",
|
||||
"Rotation": "화면 회전",
|
||||
"0 (Sideways)": "반시계방향 90° 회전",
|
||||
"90 (Flat)": "정방향",
|
||||
"180 (-Sideways)": "시계방향 90° 회전",
|
||||
"270 (Upside down)": "상하반전",
|
||||
"Colour": "색상",
|
||||
"Grey": "회색",
|
||||
"Ironbow": "아이언보우",
|
||||
"Green": "초록색",
|
||||
"Red": "빨간색",
|
||||
"Blue": "파란색",
|
||||
"Light Target": "반사 표적",
|
||||
"All leds": "모든 LED 켜기",
|
||||
"Bright group": "Bright LED 켜기",
|
||||
"Dim group": "Dim LED 켜기",
|
||||
"None": "LED 끄기",
|
||||
"Gain": "대비",
|
||||
"Negative Image": "화상 이미지",
|
||||
"Normal image": "일반",
|
||||
"Negative image": "반전",
|
||||
"Format": "해상도",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"Trimming Format": "트리밍 해상도",
|
||||
"External Light Filter": "외부 조명 필터",
|
||||
"Load Default": "기본값으로 설정",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Themezer 옵션",
|
||||
"Nsfw": "선정성 테마",
|
||||
"Page": "페이지",
|
||||
"Page %zu / %zu": "페이지 %zu / %zu",
|
||||
"Enter Page Number": "페이지 번호 입력",
|
||||
"Bad Page": "잘못된 페이지",
|
||||
"Download theme?": "테마를 다운로드할까요?",
|
||||
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "JSON에서 다운로드",
|
||||
"Select asset to download for ": "다운로드 아이템: ",
|
||||
|
||||
"Installing ": "설치 ",
|
||||
"Uninstalling ": "설치 제거 ",
|
||||
"Deleting ": "삭제 ",
|
||||
"Deleting": "삭제",
|
||||
"Pasting ": "붙여넣기 ",
|
||||
"Pasting": "붙여넣기",
|
||||
"Removing ": "제거 ",
|
||||
"Scanning ": "스캔 ",
|
||||
"Creating ": "생성 ",
|
||||
"Copying ": "복사 ",
|
||||
"Trying to load ": "썸네일 받아오는 중... ",
|
||||
"Downloading ": "다운로드 ",
|
||||
"Downloaded ": "다운로드 완료: ",
|
||||
"Removed ": "제거 됨: ",
|
||||
"Checking MD5": "MD5 확인",
|
||||
"Loading...": "로딩 중...",
|
||||
"Loading": "로딩 중...",
|
||||
"Empty!": "찾을 수 없습니다!",
|
||||
"Not Ready...": "준비되지 않음...",
|
||||
"Error loading page!": "페이지 로딩 오류!",
|
||||
"Update avaliable: ": "업데이트 가능: ",
|
||||
"Download update: ": "업데이트 다운로드: ",
|
||||
"Updated to ": "업데이트: ",
|
||||
"Press OK to restart Sphaira": "확인 버튼 입력하여 Sphaira 재시작",
|
||||
"Restart Sphaira?": "Sphaira를 재시작할까요?",
|
||||
"Failed to download update": "업데이트 다운로드 실패함",
|
||||
"Restore hbmenu?": "hbmenu로 교체할까요?",
|
||||
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "/switch/hbmemu.nro를 찾을 수 없습니다\n앱스토어에서 다시 설치하세요",
|
||||
"Failed to restore hbmenu, please re-download hbmenu": "hbmenu 교체 실패함, hbmenu를 다시 다운로드하세요",
|
||||
"Failed to restore hbmenu, using sphaira instead": "hbmenu 교체 실패함, sphaira를 계속 사용합니다",
|
||||
"Restored hbmenu, closing sphaira": "hbmenu로 교체됨, sphaira를 종료합니다",
|
||||
"Restored hbmenu": "hbmenu로 교체됨",
|
||||
"Delete Selected files?": "선택한 파일을 삭제할까요?",
|
||||
"Completely remove ": "정말 삭제할까요 ",
|
||||
"Are you sure you want to delete ": "정말 삭제할까요 ",
|
||||
"Are you sure you wish to cancel?": "정말 취소할까요?",
|
||||
"If this message appears repeatedly, please open an issue.": "해당 메시지가 반복해서 나타나는 경우, 이슈를 등록하세요."
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
"Details": "",
|
||||
"Update": "",
|
||||
"Remove": "",
|
||||
"Restore": "",
|
||||
"Download": "Downloaden",
|
||||
"Next Page": "Volgende pagina",
|
||||
"Prev Page": "Vorige pagina",
|
||||
@@ -26,6 +27,12 @@
|
||||
"Star": "",
|
||||
"System memory": "",
|
||||
"microSD card": "",
|
||||
"Sd": "",
|
||||
"Image System memory": "",
|
||||
"Image microSD card": "",
|
||||
"Slow": "",
|
||||
"Normal": "",
|
||||
"Fast": "",
|
||||
"Yes": "Ja",
|
||||
"No": "Nee",
|
||||
"Enabled": "Ingeschakeld",
|
||||
@@ -46,7 +53,7 @@
|
||||
"Alphabetical (Star)": "",
|
||||
"Likes": "",
|
||||
"ID": "",
|
||||
"Decending": "Aflopend",
|
||||
"Descending": "Aflopend",
|
||||
"Descending (down)": "Aflopend",
|
||||
"Desc": "Aflopend",
|
||||
"Ascending": "Oplopend",
|
||||
@@ -54,7 +61,6 @@
|
||||
"Asc": "Oplopend",
|
||||
|
||||
"Menu Options": "Menu-opties",
|
||||
"Header": "Koptekst",
|
||||
"Theme": "Thema",
|
||||
"Theme Options": "Thema Opties",
|
||||
"Select Theme": "Selecteer Thema",
|
||||
@@ -62,6 +68,8 @@
|
||||
"Music": "Muziek",
|
||||
"Network": "Netwerk",
|
||||
"Network Options": "Netwerkopties",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "",
|
||||
"Nxlink Upload": "",
|
||||
@@ -82,6 +90,7 @@
|
||||
"Portuguese": "Português",
|
||||
"Russian": "Русский",
|
||||
"Swedish": "Svenska",
|
||||
"Vietnamese": "Vietnamese",
|
||||
"Logging": "Loggen",
|
||||
"Replace hbmenu on exit": "Vervang hbmenu bij afsluiten",
|
||||
"Misc": "Diversen",
|
||||
@@ -90,6 +99,7 @@
|
||||
"Install forwarders": "",
|
||||
"Install location": "",
|
||||
"Show install warning": "",
|
||||
"Text scroll speed": "",
|
||||
|
||||
"FileBrowser": "Bestandsbrowser",
|
||||
"%zd files": "%zd files",
|
||||
@@ -112,6 +122,8 @@
|
||||
"Create Folder": "Map maken",
|
||||
"Set Folder Name": "",
|
||||
"View as text (unfinished)": "Bekijk als tekst (onvoltooid)",
|
||||
"Ignore read only": "",
|
||||
"Mount": "",
|
||||
"Empty...": "",
|
||||
"Open with DayBreak?": "",
|
||||
"Launch ": "",
|
||||
@@ -199,6 +211,10 @@
|
||||
"Bad Page": "",
|
||||
"Download theme?": "",
|
||||
|
||||
"GitHub": "",
|
||||
"Downloading json": "",
|
||||
"Select asset to download for ": "",
|
||||
|
||||
"Installing ": "",
|
||||
"Uninstalling ": "",
|
||||
"Deleting ": "",
|
||||
@@ -211,6 +227,8 @@
|
||||
"Copying ": "",
|
||||
"Trying to load ": "",
|
||||
"Downloading ": "",
|
||||
"Downloaded ": "",
|
||||
"Removed ": "",
|
||||
"Checking MD5": "",
|
||||
"Loading...": "",
|
||||
"Loading": "",
|
||||
@@ -220,8 +238,15 @@
|
||||
"Update avaliable: ": "",
|
||||
"Download update: ": "",
|
||||
"Updated to ": "",
|
||||
"Press OK to restart Sphaira": "",
|
||||
"Restart Sphaira?": "",
|
||||
"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?": "",
|
||||
"Completely remove ": "",
|
||||
"Are you sure you want to delete ": "Weet u zeker dat u wilt verwijderen ",
|
||||
|
||||
@@ -1,75 +1,83 @@
|
||||
{
|
||||
"[Applet Mode]": "[Modo Applet]",
|
||||
"No Internet": "Sem Internet",
|
||||
"Files": "",
|
||||
"Apps": "",
|
||||
"Store": "",
|
||||
"Files": "Arquivos",
|
||||
"Apps": "Aplicativos",
|
||||
"Store": "Loja",
|
||||
"Menu": "Menu",
|
||||
"Options": "Opções",
|
||||
"OK": "",
|
||||
"OK": "OK",
|
||||
"Back": "Voltar",
|
||||
"Select": "",
|
||||
"Select": "Selecionar",
|
||||
"Open": "Abrir",
|
||||
"Launch": "Iniciar",
|
||||
"Info": "Informações",
|
||||
"Install": "Instalar",
|
||||
"Delete": "Excluir",
|
||||
"Restart": "",
|
||||
"Changelog": "",
|
||||
"Details": "",
|
||||
"Update": "",
|
||||
"Remove": "",
|
||||
"Download": "Download",
|
||||
"Restart": "Reiniciar",
|
||||
"Changelog": "Alterações",
|
||||
"Details": "Detalhes",
|
||||
"Update": "Atualizar",
|
||||
"Remove": "Remover",
|
||||
"Restore": "Restaurar",
|
||||
"Download": "Baixar",
|
||||
"Next Page": "Próxima página",
|
||||
"Prev Page": "Página anterior",
|
||||
"Unstar": "",
|
||||
"Star": "",
|
||||
"System memory": "",
|
||||
"microSD card": "",
|
||||
"Unstar": "Desfavoritar",
|
||||
"Star": "Favoritar",
|
||||
"System memory": "Memória do console",
|
||||
"microSD card": "Cartão microSD",
|
||||
"Sd": "SD",
|
||||
"Image System memory": "Imagem (memória do console)",
|
||||
"Image microSD card": "Imagem (cartão microSD)",
|
||||
"Slow": "Lenta",
|
||||
"Normal": "Normal",
|
||||
"Fast": "Rápida",
|
||||
"Yes": "Sim",
|
||||
"No": "Não",
|
||||
"Enabled": "Habilitado",
|
||||
"Disabled": "Desabilitado",
|
||||
"Enabled": "Sim",
|
||||
"Disabled": "Não",
|
||||
|
||||
"Sort By": "Ordenar por",
|
||||
"Sort Options": "Opções de classificação",
|
||||
"Sort By": "Ordernar/Organizar",
|
||||
"Sort Options": "Ordernar/Organizar",
|
||||
"Filter": "Filtro",
|
||||
"Sort": "Organizar",
|
||||
"Sort": "Organizar por",
|
||||
"Order": "Ordem",
|
||||
"Search": "Procurar",
|
||||
"Search": "Buscar",
|
||||
"Updated": "Atualizado",
|
||||
"Updated (Star)": "",
|
||||
"Downloads": "Downloads",
|
||||
"Updated (Star)": "Atualizado (favoritos)",
|
||||
"Downloads": "Nº de downloads",
|
||||
"Size": "Tamanho",
|
||||
"Size (Star)": "",
|
||||
"Alphabetical": "Alfabético",
|
||||
"Alphabetical (Star)": "",
|
||||
"Likes": "",
|
||||
"ID": "",
|
||||
"Decending": "Decrescente",
|
||||
"Descending (down)": "Decrescente",
|
||||
"Desc": "Decrescente",
|
||||
"Size (Star)": "Tamanho (favoritos)",
|
||||
"Alphabetical": "Ordem alfabética",
|
||||
"Alphabetical (Star)": "Ordem alfabética (favoritos)",
|
||||
"Likes": "Nº de curtidas",
|
||||
"ID": "ID",
|
||||
"Descending": "Decrescente",
|
||||
"Descending (down)": "Decrescente (baixo)",
|
||||
"Desc": "Decr.",
|
||||
"Ascending": "Ascendente",
|
||||
"Ascending (Up)": "Ascendente",
|
||||
"Asc": "Ascendente",
|
||||
"Ascending (Up)": "Ascendente (cima)",
|
||||
"Asc": "Asc.",
|
||||
|
||||
"Menu Options": "Opções do menu",
|
||||
"Header": "Cabeçalho",
|
||||
"Theme": "Tema",
|
||||
"Theme Options": "Opções de tema",
|
||||
"Select Theme": "Selecionar tema",
|
||||
"Shuffle": "Embaralhar",
|
||||
"Select Theme": "Tema atual",
|
||||
"Shuffle": "Embaralhar temas",
|
||||
"Music": "Música",
|
||||
"Network": "Rede",
|
||||
"Network Options": "Opções de rede",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "",
|
||||
"Nxlink Upload": "",
|
||||
"Nxlink Finished": "",
|
||||
"Switch-Handheld!": "",
|
||||
"Switch-Docked!": "",
|
||||
"Nxlink Connected": "Nxlink conectado",
|
||||
"Nxlink Upload": "Envio Nxlink",
|
||||
"Nxlink Finished": "Nxlink finalizado",
|
||||
"Switch-Handheld!": "Switch-Portátil",
|
||||
"Switch-Docked!": "Switch-Docado",
|
||||
"Language": "Idioma",
|
||||
"Auto": "",
|
||||
"Auto": "Automático",
|
||||
"English": "English",
|
||||
"Japanese": "日本語",
|
||||
"French": "Français",
|
||||
@@ -82,96 +90,100 @@
|
||||
"Portuguese": "Português",
|
||||
"Russian": "Русский",
|
||||
"Swedish": "Svenska",
|
||||
"Logging": "Logging",
|
||||
"Replace hbmenu on exit": "Substitua hbmenu ao sair",
|
||||
"Vietnamese": "Vietnamese",
|
||||
"Logging": "Registro de depuração",
|
||||
"Replace hbmenu on exit": "Substituir hbmenu ao sair",
|
||||
"Misc": "Diversos",
|
||||
"Misc Options": "Opções diversas",
|
||||
"Web": "Rede",
|
||||
"Install forwarders": "",
|
||||
"Install location": "",
|
||||
"Show install warning": "",
|
||||
"Web": "Navegador de internet",
|
||||
"Install forwarders": "Instalar forwarders",
|
||||
"Install location": "Local de instalação",
|
||||
"Show install warning": "Mostrar aviso de instalação",
|
||||
"Text scroll speed": "Rolagem do texto",
|
||||
|
||||
"FileBrowser": "Navegador de arquivos",
|
||||
"%zd files": "%zd files",
|
||||
"%zd dirs": "%zd dirs",
|
||||
"FileBrowser": "Arquivos",
|
||||
"%zd files": "%zd arquivo(s)",
|
||||
"%zd dirs": "%zd diretório(s)",
|
||||
"File Options": "Opções de arquivo",
|
||||
"Show Hidden": "Mostrar oculto",
|
||||
"Show Hidden": "Mostrar ocultos",
|
||||
"Folders First": "Pastas primeiro",
|
||||
"Hidden Last": "Oculto por último",
|
||||
"Cut": "Cortar",
|
||||
"Hidden Last": "Ocultos por último",
|
||||
"Cut": "Recortar",
|
||||
"Copy": "Copiar",
|
||||
"Paste": "",
|
||||
"Paste ": "",
|
||||
" file(s)?": "",
|
||||
"Paste": "Colar",
|
||||
"Paste ": "Colar ",
|
||||
" file(s)?": " arquivo(s)?",
|
||||
"Rename": "Renomear",
|
||||
"Set New File Name": "",
|
||||
"Set New File Name": "Defina o nome do novo arquivo",
|
||||
"Advanced": "Avançado",
|
||||
"Advanced Options": "Criar arquivo",
|
||||
"Advanced Options": "Opções avançadas",
|
||||
"Create File": "Criar arquivo",
|
||||
"Set File Name": "",
|
||||
"Set File Name": "Defina o nome do arquivo",
|
||||
"Create Folder": "Criar pasta",
|
||||
"Set Folder Name": "",
|
||||
"Set Folder Name": "Defina o nome da pasta",
|
||||
"View as text (unfinished)": "Ver como texto (inacabado)",
|
||||
"Empty...": "",
|
||||
"Open with DayBreak?": "",
|
||||
"Launch ": "",
|
||||
"Launch option for: ": "",
|
||||
"Select launcher for: ": "",
|
||||
"Ignore read only": "Ignorar somente leitura",
|
||||
"Mount": "Montar",
|
||||
"Empty...": "Vazio...",
|
||||
"Open with DayBreak?": "Abrir com DayBreak?",
|
||||
"Launch ": "Iniciar ",
|
||||
"Launch option for: ": "Opções de inicialização para: ",
|
||||
"Select launcher for: ": "Selecionar launcher para: ",
|
||||
|
||||
"Homebrew": "Homebrew",
|
||||
"Homebrew Options": "Opções do Homebrew",
|
||||
"Hide Sphaira": "Esconder Sphaira",
|
||||
"Homebrew": "Aplicativos",
|
||||
"Homebrew Options": "Opções do aplicativo",
|
||||
"Hide Sphaira": "Esconder sphaira",
|
||||
"Install Forwarder": "Instalar forwarder",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Isso pode resultar em um banimento!",
|
||||
"Installing Forwarder": "",
|
||||
"Creating Program": "",
|
||||
"Creating Control": "",
|
||||
"Creating Meta": "",
|
||||
"Writing Nca": "",
|
||||
"Updating ncm databse": "",
|
||||
"Pushing application record": "",
|
||||
"Installed!": "",
|
||||
"Failed to install forwarder": "",
|
||||
"Unstarred ": "",
|
||||
"Starred ": "",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Instalar forwarders pode\nresultar em um banimento!",
|
||||
"Installing Forwarder": "Instalando forwarder",
|
||||
"Creating Program": "Criando Program",
|
||||
"Creating Control": "Criando Control",
|
||||
"Creating Meta": "Criando Meta",
|
||||
"Writing Nca": "Escrevendo NCA",
|
||||
"Updating ncm databse": "Atualizando base de dados NCM",
|
||||
"Pushing application record": "Aplicando registro do aplicativo",
|
||||
"Installed!": "Instalado!",
|
||||
"Failed to install forwarder": "Falha ao instalar forwarder",
|
||||
"Unstarred ": "Desfavoritado ",
|
||||
"Starred ": "Favoritado ",
|
||||
|
||||
"AppStore": "",
|
||||
"Filter: %s | Sort: %s | Order: %s": "Filtro: %s | Organizar: %s | Ordem: %s",
|
||||
"AppStore Options": "Opções da AppStore",
|
||||
"AppStore": "Loja",
|
||||
"Filter: %s | Sort: %s | Order: %s": "Filtro: %s | Por: %s | Ordem: %s",
|
||||
"AppStore Options": "Opções da loja",
|
||||
"All": "Todos",
|
||||
"Games": "Jogos",
|
||||
"Emulators": "Emuladores",
|
||||
"Tools": "Ferramentas",
|
||||
"Themes": "Temas",
|
||||
"Legacy": "Legado",
|
||||
"version: %s": "version: %s",
|
||||
"updated: %s": "updated: %s",
|
||||
"category: %s": "category: %s",
|
||||
"extracted: %.2f MiB": "extracted: %.2f MiB",
|
||||
"app_dls: %s": "app_dls: %s",
|
||||
"More by Author": "",
|
||||
"Leave Feedback": "",
|
||||
"version: %s": "versão: %s",
|
||||
"updated: %s": "atualizado: %s",
|
||||
"category: %s": "categoria: %s",
|
||||
"extracted: %.2f MiB": "tam. extraído: %.2f MiB",
|
||||
"app_dls: %s": "downloads: %s",
|
||||
"More by Author": "Mais deste autor",
|
||||
"Leave Feedback": "Deixar um feedback",
|
||||
|
||||
"Irs": "Irs",
|
||||
"Ambient Noise Level: ": "",
|
||||
"Irs": "IRS",
|
||||
"Ambient Noise Level: ": "Nível de ruído ambiente: ",
|
||||
"Controller": "Controle",
|
||||
"Pad ": "Pad ",
|
||||
" (Available)": " (Disponível)",
|
||||
" (Unsupported)": "",
|
||||
" (Unconnected)": " (Desconectado)",
|
||||
" (Available)": " (disponível)",
|
||||
" (Unsupported)": "(não suportado)",
|
||||
" (Unconnected)": " (desconectado)",
|
||||
"HandHeld": "Portátil",
|
||||
"Rotation": "Rotação",
|
||||
"0 (Sideways)": "0 (Lateralmente)",
|
||||
"0 (Sideways)": "0 (lateralmente)",
|
||||
"90 (Flat)": "90 (plano)",
|
||||
"180 (-Sideways)": "180 (-Lateralmente)",
|
||||
"270 (Upside down)": "270 (De cabeça para baixo)",
|
||||
"180 (-Sideways)": "180 (-lateralmente)",
|
||||
"270 (Upside down)": "270 (de cabeça para baixo)",
|
||||
"Colour": "Cor",
|
||||
"Grey": "Cinza",
|
||||
"Ironbow": "Arco de ferro",
|
||||
"Green": "Verde",
|
||||
"Red": "Vermelho",
|
||||
"Blue": "Azul",
|
||||
"Light Target": "Alvo leve",
|
||||
"Light Target": "Alvo de luz",
|
||||
"All leds": "Todos os LEDs",
|
||||
"Bright group": "Grupo claro",
|
||||
"Dim group": "Grupo escuro",
|
||||
@@ -180,51 +192,64 @@
|
||||
"Negative Image": "Imagem negativa",
|
||||
"Normal image": "Imagem normal",
|
||||
"Negative image": "Imagem negativa",
|
||||
"Format": "Formatar",
|
||||
"Format": "Formato",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"Trimming Format": "Formato de corte",
|
||||
"External Light Filter": "Filtro de luz externo",
|
||||
"External Light Filter": "Filtro de luz externa",
|
||||
"Load Default": "Carregar padrão",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "",
|
||||
"Nsfw": "",
|
||||
"Page": "",
|
||||
"Page %zu / %zu": "Page %zu / %zu",
|
||||
"Enter Page Number": "",
|
||||
"Bad Page": "",
|
||||
"Download theme?": "",
|
||||
"Themezer Options": "Opções do Themezer",
|
||||
"Nsfw": "Temas 18+ (NSFW)",
|
||||
"Page": "Ir para página",
|
||||
"Page %zu / %zu": "Página %zu / %zu",
|
||||
"Enter Page Number": "Número da página",
|
||||
"Bad Page": "Página inválida",
|
||||
"Download theme?": "Baixar tema?",
|
||||
|
||||
"Installing ": "",
|
||||
"Uninstalling ": "",
|
||||
"Deleting ": "",
|
||||
"Deleting": "",
|
||||
"Pasting ": "",
|
||||
"Pasting": "",
|
||||
"Removing ": "",
|
||||
"Scanning ": "",
|
||||
"Creating ": "",
|
||||
"Copying ": "",
|
||||
"Trying to load ": "",
|
||||
"Downloading ": "",
|
||||
"Checking MD5": "",
|
||||
"Loading...": "",
|
||||
"Loading": "",
|
||||
"Empty!": "",
|
||||
"Not Ready...": "",
|
||||
"Error loading page!": "",
|
||||
"Update avaliable: ": "",
|
||||
"Download update: ": "",
|
||||
"Updated to ": "",
|
||||
"Restart Sphaira?": "",
|
||||
"Failed to download update": "",
|
||||
"Delete Selected files?": "",
|
||||
"Completely remove ": "",
|
||||
"Are you sure you want to delete ": "Excluir ",
|
||||
"Are you sure you wish to cancel?": "",
|
||||
"If this message appears repeatedly, please open an issue.": ""
|
||||
}
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Baixando JSON",
|
||||
"Select asset to download for ": "Selecione o recurso para baixar em ",
|
||||
|
||||
"Installing ": "Instalando ",
|
||||
"Uninstalling ": "Desinstalando ",
|
||||
"Deleting ": "Excluindo ",
|
||||
"Deleting": "Excluindo",
|
||||
"Pasting ": "Colando ",
|
||||
"Pasting": "Colando ",
|
||||
"Removing ": "Removendo ",
|
||||
"Scanning ": "Analisando ",
|
||||
"Creating ": "Criando ",
|
||||
"Copying ": "Copiando ",
|
||||
"Trying to load ": "Tentando carregar ",
|
||||
"Downloading ": "Baixando ",
|
||||
"Downloaded ": "Baixado ",
|
||||
"Removed ": "Removido ",
|
||||
"Checking MD5": "Checando MD5",
|
||||
"Loading...": "Carregando...",
|
||||
"Loading": "Carregando",
|
||||
"Empty!": "Vazio",
|
||||
"Not Ready...": "Não está pronto...",
|
||||
"Error loading page!": "Erro ao carregar página!",
|
||||
"Update avaliable: ": "Atualização disponível: ",
|
||||
"Download update: ": "Baixar autalização: ",
|
||||
"Updated to ": "Atualizado para ",
|
||||
"Press OK to restart Sphaira": "Selecione OK para reiniciar o sphaira",
|
||||
"Restart Sphaira?": "Reiniciar sphaira?",
|
||||
"Failed to download update": "Falha ao baixar a atualização",
|
||||
"Restore hbmenu?": "Restaurar hbmenu?",
|
||||
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Falha ao buscar /switch/hbmenu.nro\nUse a AppStore para reinstalar o hbmenu",
|
||||
"Failed to restore hbmenu, please re-download hbmenu": "Falha ao restaurar o hbmenu, baixe o hbmenu novamente",
|
||||
"Failed to restore hbmenu, using sphaira instead": "Falha ao restaurar hbmenu, usando sphaira",
|
||||
"Restored hbmenu, closing sphaira": "hbmenu restaurado, fechando sphaira",
|
||||
"Restored hbmenu": "hbmenu restaurado",
|
||||
"Delete Selected files?": "Excluir os arquivos selecionados?",
|
||||
"Completely remove ": "Remover completamente ",
|
||||
"Are you sure you want to delete ": "Você tem certeza que quer excluir ",
|
||||
"Are you sure you wish to cancel?": "Você tem certeza que quer cancelar?",
|
||||
"If this message appears repeatedly, please open an issue.": "Se esta mensagem aparecer repetidamente, abra um issue."
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"Details": "",
|
||||
"Update": "",
|
||||
"Remove": "",
|
||||
"Restore": "",
|
||||
"Download": "Скачать",
|
||||
"Next Page": "Следующая страница",
|
||||
"Prev Page": "Предыдущая страница",
|
||||
@@ -26,6 +27,12 @@
|
||||
"Star": "",
|
||||
"System memory": "",
|
||||
"microSD card": "",
|
||||
"Sd": "",
|
||||
"Image System memory": "",
|
||||
"Image microSD card": "",
|
||||
"Slow": "",
|
||||
"Normal": "",
|
||||
"Fast": "",
|
||||
"Yes": "Да",
|
||||
"No": "Нет",
|
||||
"Enabled": "Включено",
|
||||
@@ -46,7 +53,7 @@
|
||||
"Alphabetical (Star)": "",
|
||||
"Likes": "",
|
||||
"ID": "",
|
||||
"Decending": "По убыванию",
|
||||
"Descending": "По убыванию",
|
||||
"Descending (down)": "По убыванию",
|
||||
"Desc": "По убыванию",
|
||||
"Ascending": "По возрастанию",
|
||||
@@ -54,7 +61,6 @@
|
||||
"Asc": "По возрастанию",
|
||||
|
||||
"Menu Options": "Параметры меню",
|
||||
"Header": "Заголовок",
|
||||
"Theme": "Тема",
|
||||
"Theme Options": "Параметры темы",
|
||||
"Select Theme": "Выберите тему",
|
||||
@@ -62,6 +68,8 @@
|
||||
"Music": "Музыка",
|
||||
"Network": "Сеть",
|
||||
"Network Options": "Параметры сети",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "",
|
||||
"Nxlink Upload": "",
|
||||
@@ -82,6 +90,7 @@
|
||||
"Portuguese": "Português",
|
||||
"Russian": "Русский",
|
||||
"Swedish": "Svenska",
|
||||
"Vietnamese": "Vietnamese",
|
||||
"Logging": "Журналирование",
|
||||
"Replace hbmenu on exit": "Заменить hbmenu при выходе",
|
||||
"Misc": "Прочее",
|
||||
@@ -90,6 +99,7 @@
|
||||
"Install forwarders": "",
|
||||
"Install location": "",
|
||||
"Show install warning": "",
|
||||
"Text scroll speed": "",
|
||||
|
||||
"FileBrowser": "Файловый менеджер",
|
||||
"%zd files": "%zd files",
|
||||
@@ -112,6 +122,8 @@
|
||||
"Create Folder": "Создать папку",
|
||||
"Set Folder Name": "",
|
||||
"View as text (unfinished)": "Посмотреть как текст (незакончено)",
|
||||
"Ignore read only": "",
|
||||
"Mount": "",
|
||||
"Empty...": "",
|
||||
"Open with DayBreak?": "",
|
||||
"Launch ": "",
|
||||
@@ -199,6 +211,10 @@
|
||||
"Bad Page": "",
|
||||
"Download theme?": "",
|
||||
|
||||
"GitHub": "",
|
||||
"Downloading json": "",
|
||||
"Select asset to download for ": "",
|
||||
|
||||
"Installing ": "",
|
||||
"Uninstalling ": "",
|
||||
"Deleting ": "",
|
||||
@@ -211,6 +227,8 @@
|
||||
"Copying ": "",
|
||||
"Trying to load ": "",
|
||||
"Downloading ": "",
|
||||
"Downloaded ": "",
|
||||
"Removed ": "",
|
||||
"Checking MD5": "",
|
||||
"Loading...": "",
|
||||
"Loading": "",
|
||||
@@ -220,8 +238,15 @@
|
||||
"Update avaliable: ": "",
|
||||
"Download update: ": "",
|
||||
"Updated to ": "",
|
||||
"Press OK to restart Sphaira": "",
|
||||
"Restart Sphaira?": "",
|
||||
"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?": "",
|
||||
"Completely remove ": "",
|
||||
"Are you sure you want to delete ": "Вы уверены, что хотите удалить ",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"[Applet Mode]": "[Applet-läge]",
|
||||
"No Internet": "Ingen internetanslutning",
|
||||
"No Internet": "Ingen Internetanslutning",
|
||||
"Files": "Filer",
|
||||
"Apps": "Appar",
|
||||
"Store": "Butik",
|
||||
@@ -14,47 +14,53 @@
|
||||
"Info": "Info",
|
||||
"Install": "Installera",
|
||||
"Delete": "Radera",
|
||||
"Restart": "",
|
||||
"Restart": "Starta om",
|
||||
"Changelog": "Ändringslogg",
|
||||
"Details": "Detaljer",
|
||||
"Update": "Uppdatera",
|
||||
"Remove": "Ta bort",
|
||||
"Restore": "Återställ",
|
||||
"Download": "Ladda ner",
|
||||
"Next Page": "Nästa sida",
|
||||
"Prev Page": "Föregående sida",
|
||||
"Unstar": "",
|
||||
"Star": "",
|
||||
"System memory": "",
|
||||
"microSD card": "",
|
||||
"Unstar": "Avmarkera stjärna",
|
||||
"Star": "Markera stjärna",
|
||||
"System memory": "Systemminne",
|
||||
"microSD card": "microSD-kort",
|
||||
"Sd": "Sd",
|
||||
"Image System memory": "Avbild Systemminne",
|
||||
"Image microSD card": "Avbild microSD-kort",
|
||||
"Slow": "",
|
||||
"Normal": "",
|
||||
"Fast": "",
|
||||
"Yes": "Ja",
|
||||
"No": "Nej",
|
||||
"Enabled": "Aktiverad",
|
||||
"Disabled": "Avaktiverad",
|
||||
|
||||
"Disabled": "Inaktiverad",
|
||||
|
||||
"Sort By": "Sortera efter",
|
||||
"Sort Options": "Sorteringsalternativ",
|
||||
"Filter": "Filter",
|
||||
"Filter": "Filtrera",
|
||||
"Sort": "Sortera",
|
||||
"Order": "Ordning",
|
||||
"Search": "Sök",
|
||||
"Updated": "Uppdaterad",
|
||||
"Updated (Star)": "",
|
||||
"Updated (Star)": "Uppdaterad (Stjärna)",
|
||||
"Downloads": "Nedladdningar",
|
||||
"Size": "Storlek",
|
||||
"Size (Star)": "",
|
||||
"Size (Star)": "Storlek (Stjärna)",
|
||||
"Alphabetical": "Alfabetisk",
|
||||
"Alphabetical (Star)": "",
|
||||
"Alphabetical (Star)": "Alfabetisk (Stjärna)",
|
||||
"Likes": "Gillar",
|
||||
"ID": "ID",
|
||||
"Decending": "Fallande",
|
||||
"Descending": "Fallande",
|
||||
"Descending (down)": "Fallande (nedåt)",
|
||||
"Desc": "Fallande",
|
||||
"Desc": "Fall",
|
||||
"Ascending": "Stigande",
|
||||
"Ascending (Up)": "Stigande (uppåt)",
|
||||
"Asc": "Stigande",
|
||||
|
||||
"Asc": "Stig",
|
||||
|
||||
"Menu Options": "Menyalternativ",
|
||||
"Header": "Rubrik",
|
||||
"Theme": "Tema",
|
||||
"Theme Options": "Temaalternativ",
|
||||
"Select Theme": "Välj tema",
|
||||
@@ -62,12 +68,14 @@
|
||||
"Music": "Musik",
|
||||
"Network": "Nätverk",
|
||||
"Network Options": "Nätverksalternativ",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink ansluten",
|
||||
"Nxlink Upload": "Nxlink uppladdning",
|
||||
"Nxlink Upload": "Nxlink överför",
|
||||
"Nxlink Finished": "Nxlink klar",
|
||||
"Switch-Handheld!": "",
|
||||
"Switch-Docked!": "",
|
||||
"Switch-Handheld!": "Switch Handhållen!",
|
||||
"Switch-Docked!": "Switch Dockad!",
|
||||
"Language": "Språk",
|
||||
"Auto": "Auto",
|
||||
"English": "Engelska",
|
||||
@@ -82,15 +90,17 @@
|
||||
"Portuguese": "Portugisiska",
|
||||
"Russian": "Ryska",
|
||||
"Swedish": "Svenska",
|
||||
"Vietnamese": "Vietnamese",
|
||||
"Logging": "Loggning",
|
||||
"Replace hbmenu on exit": "Ersätt hbmenu vid avslut",
|
||||
"Misc": "Övrigt",
|
||||
"Misc Options": "Övriga alternativ",
|
||||
"Web": "Webb",
|
||||
"Install forwarders": "",
|
||||
"Install location": "",
|
||||
"Show install warning": "",
|
||||
|
||||
"Install forwarders": "Installera genvägar",
|
||||
"Install location": "Installationsplats",
|
||||
"Show install warning": "Visa installationsvarning",
|
||||
"Text scroll speed": "",
|
||||
|
||||
"FileBrowser": "Filbläddrare",
|
||||
"%zd files": "%zd filer",
|
||||
"%zd dirs": "%zd kataloger",
|
||||
@@ -112,29 +122,31 @@
|
||||
"Create Folder": "Skapa mapp",
|
||||
"Set Folder Name": "Ange mappnamn",
|
||||
"View as text (unfinished)": "Visa som text (ofärdig)",
|
||||
"Ignore read only": "Ignorera skrivskydd",
|
||||
"Mount": "Montera",
|
||||
"Empty...": "Tom...",
|
||||
"Open with DayBreak?": "Öppna med DayBreak?",
|
||||
"Launch ": "",
|
||||
"Launch ": "Starta ",
|
||||
"Launch option for: ": "Startalternativ för: ",
|
||||
"Select launcher for: ": "",
|
||||
|
||||
"Select launcher for: ": "Välj startprogram för: ",
|
||||
|
||||
"Homebrew": "Homebrew",
|
||||
"Homebrew Options": "Homebrew-alternativ",
|
||||
"Hide Sphaira": "Dölj Sphaira",
|
||||
"Install Forwarder": "Installera forwarder",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "VARNING: Att installera forwarders leder till en avstängning!",
|
||||
"Installing Forwarder": "Installerar forwarder",
|
||||
"Install Forwarder": "Installera genväg",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "VARNING: Att installera genvägar kan leda till avstängning!",
|
||||
"Installing Forwarder": "Installerar genväg",
|
||||
"Creating Program": "Skapar program",
|
||||
"Creating Control": "Skapar kontroll",
|
||||
"Creating Meta": "Skapar meta",
|
||||
"Creating Meta": "Skapar metadata",
|
||||
"Writing Nca": "Skriver Nca",
|
||||
"Updating ncm databse": "Uppdaterar ncm-databas",
|
||||
"Pushing application record": "Lägger till applikationspost",
|
||||
"Pushing application record": "Skickar programpost",
|
||||
"Installed!": "Installerad!",
|
||||
"Failed to install forwarder": "Misslyckades med att installera forwarder",
|
||||
"Unstarred ": "",
|
||||
"Starred ": "",
|
||||
|
||||
"Failed to install forwarder": "Misslyckades att installera genväg",
|
||||
"Unstarred ": "Avmarkerad ",
|
||||
"Starred ": "Markerad ",
|
||||
|
||||
"AppStore": "AppStore",
|
||||
"Filter: %s | Sort: %s | Order: %s": "Filter: %s | Sortering: %s | Ordning: %s",
|
||||
"AppStore Options": "AppStore-alternativ",
|
||||
@@ -149,21 +161,21 @@
|
||||
"category: %s": "kategori: %s",
|
||||
"extracted: %.2f MiB": "extraherad: %.2f MiB",
|
||||
"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",
|
||||
|
||||
|
||||
"Irs": "Irs",
|
||||
"Ambient Noise Level: ": "Omgivningsljudnivå: ",
|
||||
"Controller": "Kontroll",
|
||||
"Controller": "Kontroller",
|
||||
"Pad ": "Handkontroll ",
|
||||
" (Available)": " (Tillgänglig)",
|
||||
" (Unsupported)": "",
|
||||
" (Unsupported)": " (Ej stödd)",
|
||||
" (Unconnected)": " (Ej ansluten)",
|
||||
"HandHeld": "Handhållen",
|
||||
"Rotation": "Rotation",
|
||||
"0 (Sideways)": "0 (Sido)",
|
||||
"0 (Sideways)": "0 (Sidan)",
|
||||
"90 (Flat)": "90 (Platt)",
|
||||
"180 (-Sideways)": "180 (-Sido)",
|
||||
"180 (-Sideways)": "180 (-Sidan)",
|
||||
"270 (Upside down)": "270 (Upp och ner)",
|
||||
"Colour": "Färg",
|
||||
"Grey": "Grå",
|
||||
@@ -173,8 +185,8 @@
|
||||
"Blue": "Blå",
|
||||
"Light Target": "Ljusmål",
|
||||
"All leds": "Alla lysdioder",
|
||||
"Bright group": "Ljusstark grupp",
|
||||
"Dim group": "Dämpad grupp",
|
||||
"Bright group": "Ljus grupp",
|
||||
"Dim group": "Dimma grupp",
|
||||
"None": "Ingen",
|
||||
"Gain": "Förstärkning",
|
||||
"Negative Image": "Negativ bild",
|
||||
@@ -186,10 +198,10 @@
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"Trimming Format": "Trimformat",
|
||||
"External Light Filter": "Extern ljusfilter",
|
||||
"Load Default": "Ladda standardinställningar",
|
||||
|
||||
"Trimming Format": "Trimningsformat",
|
||||
"External Light Filter": "Externt ljusfilter",
|
||||
"Load Default": "Ladda standard",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Themezer-alternativ",
|
||||
"Nsfw": "Nsfw",
|
||||
@@ -198,7 +210,11 @@
|
||||
"Enter Page Number": "Ange sidnummer",
|
||||
"Bad Page": "Ogiltig sida",
|
||||
"Download theme?": "Ladda ner tema?",
|
||||
|
||||
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Laddar ner JSON",
|
||||
"Select asset to download for ": "Välj tillgång att ladda ner för ",
|
||||
|
||||
"Installing ": "Installerar ",
|
||||
"Uninstalling ": "Avinstallerar ",
|
||||
"Deleting ": "Raderar ",
|
||||
@@ -209,22 +225,31 @@
|
||||
"Scanning ": "Skannar ",
|
||||
"Creating ": "Skapar ",
|
||||
"Copying ": "Kopierar ",
|
||||
"Trying to load ": "",
|
||||
"Trying to load ": "Försöker ladda ",
|
||||
"Downloading ": "Laddar ner ",
|
||||
"Downloaded ": "Nedladdad ",
|
||||
"Removed ": "Borttagen ",
|
||||
"Checking MD5": "Kontrollerar MD5",
|
||||
"Loading...": "Laddar...",
|
||||
"Loading": "Laddar",
|
||||
"Empty!": "Tomt!",
|
||||
"Not Ready...": "Ej redo...",
|
||||
"Not Ready...": "Inte redo...",
|
||||
"Error loading page!": "Fel vid laddning av sida!",
|
||||
"Update avaliable: ": "Uppdatering tillgänglig: ",
|
||||
"Download update: ": "Ladda ner uppdatering: ",
|
||||
"Updated to ": "",
|
||||
"Restart Sphaira?": "",
|
||||
"Failed to download update": "Misslyckades med att ladda ner uppdatering",
|
||||
"Updated to ": "Uppdaterad till ",
|
||||
"Press OK to restart Sphaira": "",
|
||||
"Restart Sphaira?": "Starta om Sphaira?",
|
||||
"Failed to download update": "Misslyckades att ladda ner uppdatering",
|
||||
"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?",
|
||||
"Completely remove ": "Ta bort helt ",
|
||||
"Are you sure you want to delete ": "Är du säker på att du vill radera ",
|
||||
"Are you sure you wish to cancel?": "Är du säker på att du vill avbryta?",
|
||||
"If this message appears repeatedly, please open an issue.": ""
|
||||
}
|
||||
"If this message appears repeatedly, please open an issue.": "Om detta meddelande visas upprepade gånger, vänligen öppna en felanmälan.",
|
||||
}
|
||||
|
||||
255
assets/romfs/i18n/vi.json
Normal file
255
assets/romfs/i18n/vi.json
Normal file
@@ -0,0 +1,255 @@
|
||||
{
|
||||
"[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",
|
||||
"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?",
|
||||
"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": "信息",
|
||||
"Install": "安装",
|
||||
"Delete": "删除",
|
||||
"Restart": "",
|
||||
"Restart": "重启",
|
||||
"Changelog": "更新日志",
|
||||
"Details": "详情",
|
||||
"Update": "更新",
|
||||
"Remove": "删除",
|
||||
"Restore": "恢复",
|
||||
"Download": "下载",
|
||||
"Next Page": "下一页",
|
||||
"Prev Page": "上一页",
|
||||
@@ -26,6 +27,12 @@
|
||||
"Star": "星标",
|
||||
"System memory": "主机内存",
|
||||
"microSD card": "SD卡",
|
||||
"Sd": "SD卡",
|
||||
"Image System memory": "主机内存图像",
|
||||
"Image microSD card": "SD卡图像",
|
||||
"Slow": "",
|
||||
"Normal": "",
|
||||
"Fast": "",
|
||||
"Yes": "是",
|
||||
"No": "否",
|
||||
"Enabled": "启用",
|
||||
@@ -46,7 +53,7 @@
|
||||
"Alphabetical (Star)": "按字母顺序(星标优先)",
|
||||
"Likes": "点赞量",
|
||||
"ID": "ID",
|
||||
"Decending": "降序",
|
||||
"Descending": "降序",
|
||||
"Descending (down)": "降序",
|
||||
"Desc": "降序",
|
||||
"Ascending": "升序",
|
||||
@@ -54,7 +61,6 @@
|
||||
"Asc": "升序",
|
||||
|
||||
"Menu Options": "菜单选项",
|
||||
"Header": "标题",
|
||||
"Theme": "主题",
|
||||
"Theme Options": "主题选项",
|
||||
"Select Theme": "选择主题",
|
||||
@@ -62,12 +68,14 @@
|
||||
"Music": "音乐",
|
||||
"Network": "网络",
|
||||
"Network Options": "网络选项",
|
||||
"Nxlink": "Nxlink",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink插件提交",
|
||||
"Nxlink Connected": "Nxlink 已连接",
|
||||
"Nxlink Upload": "Nxlink 上传中",
|
||||
"Nxlink Finished": "Nxlink 已结束",
|
||||
"Switch-Handheld!": "",
|
||||
"Switch-Docked!": "",
|
||||
"Switch-Handheld!": "切换至掌机模式!",
|
||||
"Switch-Docked!": "切换至底座模式!",
|
||||
"Language": "语言",
|
||||
"Auto": "自动",
|
||||
"English": "English",
|
||||
@@ -82,14 +90,16 @@
|
||||
"Portuguese": "Português",
|
||||
"Russian": "Русский",
|
||||
"Swedish": "Svenska",
|
||||
"Vietnamese": "Vietnamese",
|
||||
"Logging": "日志",
|
||||
"Replace hbmenu on exit": "退出后用Sphaira替换hbmenu",
|
||||
"Misc": "杂项",
|
||||
"Misc Options": "杂项设置",
|
||||
"Misc": "拓展",
|
||||
"Misc Options": "拓展设置",
|
||||
"Web": "网页浏览器",
|
||||
"Install forwarders": "允许安装前端应用",
|
||||
"Install location": "安装位置",
|
||||
"Show install warning": "显示安装警告",
|
||||
"Text scroll speed": "",
|
||||
|
||||
"FileBrowser": "文件浏览",
|
||||
"%zd files": "%zd 个文件",
|
||||
@@ -112,12 +122,14 @@
|
||||
"Create Folder": "新建文件夹",
|
||||
"Set Folder Name": "输入文件夹名",
|
||||
"View as text (unfinished)": "以文本形式查看(未完善)",
|
||||
"Ignore read only": "忽略只读",
|
||||
"Mount": "挂载",
|
||||
"Empty...": "空...",
|
||||
"Open with DayBreak?": "使用DayBreak打开?",
|
||||
"Launch ": "",
|
||||
"Launch ": "启动 ",
|
||||
"Launch option for: ": "启动选项:",
|
||||
"Select launcher for: ": "",
|
||||
|
||||
"Select launcher for: ": "选择启动器用于:",
|
||||
|
||||
"Homebrew": "应用列表",
|
||||
"Homebrew Options": "应用选项",
|
||||
"Hide Sphaira": "在应用列表中隐藏Sphaira",
|
||||
@@ -132,8 +144,8 @@
|
||||
"Pushing application record": "正在推送应用记录",
|
||||
"Installed!": "安装完成!",
|
||||
"Failed to install forwarder": "前端应用安装失败",
|
||||
"Unstarred ": "",
|
||||
"Starred ": "",
|
||||
"Unstarred ": "取消星标 ",
|
||||
"Starred ": "已星标 ",
|
||||
|
||||
"AppStore": "应用商店",
|
||||
"Filter: %s | Sort: %s | Order: %s": "筛选: %s | 排序: %s | 顺序: %s",
|
||||
@@ -176,7 +188,7 @@
|
||||
"Bright group": "亮色组",
|
||||
"Dim group": "暗色组",
|
||||
"None": "无",
|
||||
"Gain": "增益",
|
||||
"Gain": "曝光",
|
||||
"Negative Image": "负片图像",
|
||||
"Normal image": "正常图像",
|
||||
"Negative image": "负片图像",
|
||||
@@ -199,6 +211,10 @@
|
||||
"Bad Page": "错误的页面",
|
||||
"Download theme?": "下载该主题?",
|
||||
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "正在下载 json",
|
||||
"Select asset to download for ": "选择要下载的资源用于 ",
|
||||
|
||||
"Installing ": "正在安装 ",
|
||||
"Uninstalling ": "正在卸载 ",
|
||||
"Deleting ": "正在删除 ",
|
||||
@@ -209,22 +225,31 @@
|
||||
"Scanning ": "正在扫描 ",
|
||||
"Creating ": "正在创建 ",
|
||||
"Copying ": "正在复制 ",
|
||||
"Trying to load ": "",
|
||||
"Trying to load ": "尝试加载 ",
|
||||
"Downloading ": "正在下载 ",
|
||||
"Downloaded ": "已下载 ",
|
||||
"Removed ": "已移除 ",
|
||||
"Checking MD5": "正在校验 MD5",
|
||||
"Loading...": "加载中...",
|
||||
"Loading": "加载中",
|
||||
"Empty!": "空!",
|
||||
"Empty!": "空空如野!",
|
||||
"Not Ready...": "尚未准备好...",
|
||||
"Error loading page!": "页面加载失败!",
|
||||
"Update avaliable: ": "有可用更新!",
|
||||
"Download update: ": "下载更新:",
|
||||
"Updated to ": "",
|
||||
"Restart Sphaira?": "",
|
||||
"Updated to ": "更新至 ",
|
||||
"Press OK to restart Sphaira": "",
|
||||
"Restart Sphaira?": "重启 Sphaira?",
|
||||
"Failed to download update": "更新下载失败",
|
||||
"Restore hbmenu?": "恢复 hbmenu?",
|
||||
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "未能找到 /switch/hbmenu.nro\n请使用应用商店重新安装 hbmenu",
|
||||
"Failed to restore hbmenu, please re-download hbmenu": "恢复 hbmenu 失败,请重新下载 hbmenu",
|
||||
"Failed to restore hbmenu, using sphaira instead": "恢复 hbmenu 失败,改用 Sphaira",
|
||||
"Restored hbmenu, closing sphaira": "已恢复 hbmenu,正在关闭 Sphaira",
|
||||
"Restored hbmenu": "已恢复 hbmenu",
|
||||
"Delete Selected files?": "删除选中的文件?",
|
||||
"Completely remove ": "彻底删除 ",
|
||||
"Are you sure you want to delete ": "您确定要删除吗 ",
|
||||
"Are you sure you wish to cancel?": "您确定要取消吗?",
|
||||
"If this message appears repeatedly, please open an issue.": ""
|
||||
}
|
||||
"If this message appears repeatedly, please open an issue.": "如果此消息反复出现,请提交一个 issue。"
|
||||
}
|
||||
|
||||
BIN
assets/romfs/theme/bubble1.png
Normal file
BIN
assets/romfs/theme/bubble1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
BIN
assets/romfs/theme/bubble2.png
Normal file
BIN
assets/romfs/theme/bubble2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
BIN
assets/romfs/theme/bubble3.png
Normal file
BIN
assets/romfs/theme/bubble3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
@@ -1,22 +1,23 @@
|
||||
[meta]
|
||||
name=Abyss
|
||||
author=TotalJustice
|
||||
version=1.0.0
|
||||
; unused currently
|
||||
preview=romfs:/theme/preview.jpg
|
||||
version=1.1.0
|
||||
inherit=romfs:/themes/base_black_theme.ini
|
||||
|
||||
[theme]
|
||||
background=0x0f111aff
|
||||
grid=0x0f115c30
|
||||
selected=0x0f115cff
|
||||
selected_overlay=0x529cffff
|
||||
text=0xffbc41ff
|
||||
text_selected=0x529cffff
|
||||
background = 0x0f111a
|
||||
grid = 0x0f115c30
|
||||
popup = 0x0f115c
|
||||
|
||||
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
|
||||
line = 0xffbc41
|
||||
line_seperator = 0xffbc41
|
||||
|
||||
text = 0xffbc41
|
||||
text_info = 0xd79f36
|
||||
text_selected = 0x529cff
|
||||
selected_background = 0x0f115c
|
||||
|
||||
scrollbar = 0x529cff
|
||||
scrollbar_background = ; hide the background
|
||||
|
||||
progressbar = 0x3250f0
|
||||
|
||||
33
assets/romfs/themes/base_black_theme.ini
Normal file
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
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]
|
||||
name=Black
|
||||
author=TotalJustice
|
||||
version=1.0.0
|
||||
preview=romfs:/theme/preview.jpg
|
||||
|
||||
[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
|
||||
version=1.1.0
|
||||
inherit=romfs:/themes/base_black_theme.ini
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
[meta]
|
||||
name=OLED Black
|
||||
author=iTotalJustice/Sanras
|
||||
version=1.0.0
|
||||
preview=romfs:/theme/preview.jpg
|
||||
author=TotalJustice/Sanras
|
||||
version=1.1.0
|
||||
inherit=romfs:/themes/base_black_theme.ini
|
||||
|
||||
[theme]
|
||||
background=0x000000ff
|
||||
cursor=romfs:/theme/cursor.png
|
||||
cursor_drag=romfs:/theme/cursor_drag.png
|
||||
grid=0x46464640
|
||||
selected=0x323232ff
|
||||
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
|
||||
background = 0x000000
|
||||
grid = 0x46464640
|
||||
popup = 0x323232
|
||||
text = 0xfbfbfb
|
||||
text_selected = 0x00ffc8
|
||||
selected_background = 0x323232
|
||||
|
||||
5
assets/romfs/themes/white_theme.ini
Normal file
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)
|
||||
|
||||
set(sphaira_VERSION 0.5.0)
|
||||
set(sphaira_VERSION 0.6.2)
|
||||
|
||||
project(sphaira
|
||||
VERSION ${sphaira_VERSION}
|
||||
@@ -45,18 +45,19 @@ add_executable(sphaira
|
||||
source/ui/menus/main_menu.cpp
|
||||
source/ui/menus/menu_base.cpp
|
||||
source/ui/menus/themezer.cpp
|
||||
source/ui/menus/ghdl.cpp
|
||||
|
||||
source/ui/error_box.cpp
|
||||
source/ui/notification.cpp
|
||||
source/ui/nvg_util.cpp
|
||||
source/ui/option_box.cpp
|
||||
source/ui/option_list.cpp
|
||||
source/ui/popup_list.cpp
|
||||
source/ui/progress_box.cpp
|
||||
source/ui/scrollable_text.cpp
|
||||
source/ui/scrollbar.cpp
|
||||
source/ui/sidebar.cpp
|
||||
source/ui/widget.cpp
|
||||
source/ui/list.cpp
|
||||
source/ui/bubbles.cpp
|
||||
|
||||
source/app.cpp
|
||||
source/download.cpp
|
||||
@@ -80,12 +81,54 @@ target_compile_definitions(sphaira PRIVATE
|
||||
-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)
|
||||
set(FETCHCONTENT_QUIET FALSE)
|
||||
|
||||
FetchContent_Declare(ftpsrv
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
|
||||
GIT_TAG 8d5a14e
|
||||
GIT_TAG 1.2.2
|
||||
SOURCE_SUBDIR NONE
|
||||
)
|
||||
|
||||
FetchContent_Declare(libhaze
|
||||
@@ -95,12 +138,12 @@ FetchContent_Declare(libhaze
|
||||
|
||||
FetchContent_Declare(libpulsar
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/switch-libpulsar.git
|
||||
GIT_TAG d729be3
|
||||
GIT_TAG de656e4
|
||||
)
|
||||
|
||||
FetchContent_Declare(nanovg
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/nanovg-deko3d.git
|
||||
GIT_TAG 1902b38
|
||||
GIT_TAG 845c9fc
|
||||
)
|
||||
|
||||
FetchContent_Declare(stb
|
||||
@@ -113,12 +156,12 @@ FetchContent_Declare(yyjson
|
||||
GIT_TAG 0.10.0
|
||||
)
|
||||
|
||||
FetchContent_Declare(minIni-sphaira
|
||||
FetchContent_Declare(minIni
|
||||
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_NX ON)
|
||||
set(MININI_USE_FLOAT OFF)
|
||||
@@ -135,34 +178,66 @@ set(NANOVG_NO_GIF ON)
|
||||
set(NANOVG_NO_HDR ON)
|
||||
set(NANOVG_NO_PIC ON)
|
||||
set(NANOVG_NO_PNM ON)
|
||||
set(NANOVG_STBI_STATIC OFF)
|
||||
set(NANOVG_STBTT_STATIC ON)
|
||||
|
||||
set(YYJSON_DISABLE_READER OFF)
|
||||
set(YYJSON_DISABLE_WRITER ON)
|
||||
set(YYJSON_DISABLE_WRITER OFF)
|
||||
set(YYJSON_DISABLE_UTILS ON)
|
||||
set(YYJSON_DISABLE_FAST_FP_CONV ON)
|
||||
set(YYJSON_DISABLE_NON_STANDARD ON)
|
||||
set(YYJSON_DISABLE_UTF8_VALIDATION ON)
|
||||
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(
|
||||
ftpsrv
|
||||
libhaze
|
||||
libpulsar
|
||||
nanovg
|
||||
stb
|
||||
minIni-sphaira
|
||||
minIni
|
||||
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
|
||||
add_library(libhaze
|
||||
${libhaze_SOURCE_DIR}/source/async_usb_server.cpp
|
||||
@@ -188,39 +263,6 @@ set_target_properties(libhaze PROPERTIES
|
||||
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)
|
||||
target_include_directories(stb INTERFACE ${stb_SOURCE_DIR})
|
||||
|
||||
@@ -240,10 +282,10 @@ set_target_properties(sphaira PROPERTIES
|
||||
)
|
||||
|
||||
target_link_libraries(sphaira PRIVATE
|
||||
ftpsrv
|
||||
ftpsrv_helper
|
||||
libhaze
|
||||
libpulsar
|
||||
minIni-sphaira
|
||||
minIni
|
||||
nanovg
|
||||
stb
|
||||
yyjson
|
||||
|
||||
@@ -25,6 +25,7 @@ enum SoundEffect {
|
||||
SoundEffect_Startup,
|
||||
SoundEffect_Install,
|
||||
SoundEffect_Error,
|
||||
SoundEffect_MAX,
|
||||
};
|
||||
|
||||
enum class LaunchType {
|
||||
@@ -33,7 +34,9 @@ enum class LaunchType {
|
||||
Forwader_Sphaira,
|
||||
};
|
||||
|
||||
// todo: why is this global???
|
||||
void DrawElement(float x, float y, float w, float h, ThemeEntryID id);
|
||||
void DrawElement(const Vec4&, ThemeEntryID id);
|
||||
|
||||
class App {
|
||||
public:
|
||||
@@ -45,6 +48,8 @@ public:
|
||||
static void ExitRestart();
|
||||
static auto GetVg() -> NVGcontext*;
|
||||
static void Push(std::shared_ptr<ui::Widget>);
|
||||
// pops all widgets above a menu
|
||||
static void PopToMenu();
|
||||
|
||||
// this is thread safe
|
||||
static void Notify(std::string text, ui::NotifEntry::Side side = ui::NotifEntry::Side::RIGHT);
|
||||
@@ -54,8 +59,10 @@ public:
|
||||
static void NotifyFlashLed();
|
||||
|
||||
static auto GetThemeMetaList() -> std::span<ThemeMeta>;
|
||||
static void SetTheme(u64 theme_index);
|
||||
static auto GetThemeIndex() -> u64;
|
||||
static void SetTheme(s64 theme_index);
|
||||
static auto GetThemeIndex() -> s64;
|
||||
|
||||
static auto GetDefaultImage(int* w = nullptr, int* h = nullptr) -> int;
|
||||
|
||||
// returns argv[0]
|
||||
static auto GetExePath() -> fs::FsPath;
|
||||
@@ -73,6 +80,7 @@ public:
|
||||
static auto GetThemeShuffleEnable() -> bool;
|
||||
static auto GetThemeMusicEnable() -> bool;
|
||||
static auto GetLanguage() -> long;
|
||||
static auto GetTextScrollSpeed() -> long;
|
||||
|
||||
static void SetMtpEnable(bool enable);
|
||||
static void SetFtpEnable(bool enable);
|
||||
@@ -85,6 +93,7 @@ public:
|
||||
static void SetThemeShuffleEnable(bool enable);
|
||||
static void SetThemeMusicEnable(bool enable);
|
||||
static void SetLanguage(long index);
|
||||
static void SetTextScrollSpeed(long index);
|
||||
|
||||
static auto Install(OwoConfig& config) -> Result;
|
||||
static auto Install(ui::ProgressBox* pbox, OwoConfig& config) -> Result;
|
||||
@@ -98,9 +107,9 @@ public:
|
||||
// void DrawElement(float x, float y, float w, float h, ui::ThemeEntryID id);
|
||||
auto LoadElementImage(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 ScanThemes(const std::string& path);
|
||||
void ScanThemeEntries();
|
||||
@@ -110,6 +119,21 @@ public:
|
||||
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:
|
||||
static constexpr inline auto CONFIG_PATH = "/config/sphaira/config.ini";
|
||||
static constexpr inline auto PLAYLOG_PATH = "/config/sphaira/playlog.ini";
|
||||
@@ -119,6 +143,7 @@ public:
|
||||
u64 m_start_timestamp{};
|
||||
u64 m_prev_timestamp{};
|
||||
fs::FsPath m_prev_last_launch{};
|
||||
int m_default_image{};
|
||||
|
||||
bool m_is_launched_via_sphaira_forwader{};
|
||||
|
||||
@@ -138,7 +163,7 @@ public:
|
||||
|
||||
Theme m_theme{};
|
||||
fs::FsPath theme_path{};
|
||||
std::size_t m_theme_index{};
|
||||
s64 m_theme_index{};
|
||||
|
||||
bool m_quit{};
|
||||
|
||||
@@ -153,9 +178,10 @@ public:
|
||||
option::OptionBool m_theme_shuffle{INI_SECTION, "theme_shuffle", false};
|
||||
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
|
||||
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[24]{};
|
||||
PLSR_PlayerSoundId m_sound_ids[SoundEffect_MAX]{};
|
||||
|
||||
private: // from nanovg decko3d example by adubbz
|
||||
static constexpr unsigned NumFramebuffers = 2;
|
||||
|
||||
@@ -1,41 +1,235 @@
|
||||
#pragma once
|
||||
|
||||
#include "fs.hpp"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <algorithm>
|
||||
#include <stop_token>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira {
|
||||
namespace sphaira::curl {
|
||||
|
||||
using DownloadCallback = std::function<void(std::vector<u8>& data, bool success)>;
|
||||
using ProgressCallback = std::function<bool(u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow)>;
|
||||
enum {
|
||||
Flag_None = 0,
|
||||
// requests to download send etag in the header.
|
||||
// the received etag is then saved on success.
|
||||
// this api is only available on downloading to file.
|
||||
Flag_Cache = 1 << 0,
|
||||
};
|
||||
|
||||
enum class DownloadPriority {
|
||||
enum class Priority {
|
||||
Normal, // gets pushed to the back of the queue
|
||||
High, // gets pushed to the front of the queue
|
||||
};
|
||||
|
||||
struct DownloadEventData {
|
||||
DownloadCallback callback;
|
||||
std::vector<u8> data;
|
||||
bool result;
|
||||
struct Api;
|
||||
struct ApiResult;
|
||||
|
||||
using Path = fs::FsPath;
|
||||
using OnComplete = std::function<void(ApiResult& result)>;
|
||||
using OnProgress = std::function<bool(u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow)>;
|
||||
using StopToken = std::stop_token;
|
||||
|
||||
struct Url {
|
||||
Url() = default;
|
||||
Url(const std::string& str) : m_str{str} {}
|
||||
std::string m_str;
|
||||
};
|
||||
|
||||
auto DownloadInit() -> bool;
|
||||
void DownloadExit();
|
||||
struct Fields {
|
||||
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
|
||||
auto DownloadMemory(const std::string& url, const std::string& post, ProgressCallback pcallback = nullptr) -> std::vector<u8>;
|
||||
auto DownloadFile(const std::string& url, const std::string& out, const std::string& post, ProgressCallback pcallback = nullptr) -> bool;
|
||||
auto ToMemory(const Api& e) -> ApiResult;
|
||||
auto ToFile(const Api& e) -> ApiResult;
|
||||
|
||||
// async functions
|
||||
// starts the downloads in a new thread, pushes an event when complete
|
||||
// then, the callback will be called on the main thread.
|
||||
// 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 ToMemoryAsync(const Api& e) -> bool;
|
||||
auto ToFileAsync(const Api& e) -> bool;
|
||||
|
||||
auto DownloadMemoryAsync(const std::string& url, const std::string& post, DownloadCallback callback, ProgressCallback pcallback = nullptr, DownloadPriority prio = DownloadPriority::Normal) -> bool;
|
||||
auto DownloadFileAsync(const std::string& url, const std::string& out, const std::string& post, DownloadCallback callback, ProgressCallback pcallback = nullptr, DownloadPriority prio = DownloadPriority::Normal) -> bool;
|
||||
struct Api {
|
||||
Api() = default;
|
||||
|
||||
void DownloadClearCache(const std::string& url);
|
||||
template <typename... Ts>
|
||||
Api(Ts&&... ts) {
|
||||
Api::set_option(std::forward<Ts>(ts)...);
|
||||
}
|
||||
|
||||
} // namespace sphaira
|
||||
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,
|
||||
HazeCallbackData,
|
||||
NxlinkCallbackData,
|
||||
DownloadEventData
|
||||
curl::DownloadEventData
|
||||
>;
|
||||
|
||||
// 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);
|
||||
|
||||
Result CreateFile(FsFileSystem* fs, const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false);
|
||||
Result CreateDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false);
|
||||
Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false);
|
||||
Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false);
|
||||
Result DeleteFile(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false);
|
||||
Result DeleteDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false);
|
||||
Result DeleteDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false);
|
||||
Result RenameFile(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 = 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 = true);
|
||||
Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
|
||||
Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
|
||||
Result DeleteFile(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
|
||||
Result DeleteDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
|
||||
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 = true);
|
||||
Result RenameDirectory(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only = true);
|
||||
Result GetEntryType(FsFileSystem* fs, const FsPath& path, FsDirEntryType* out);
|
||||
Result GetFileTimeStampRaw(FsFileSystem* fs, const FsPath& path, FsTimeStampRaw *out);
|
||||
bool FileExists(FsFileSystem* fs, const FsPath& path);
|
||||
bool DirExists(FsFileSystem* fs, const FsPath& path);
|
||||
Result read_entire_file(FsFileSystem* fs, const FsPath& path, std::vector<u8>& out);
|
||||
Result write_entire_file(FsFileSystem* fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = false);
|
||||
Result copy_entire_file(FsFileSystem* fs, const FsPath& dst, const FsPath& src, 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 = true);
|
||||
|
||||
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false);
|
||||
Result CreateDirectory(const FsPath& path, bool ignore_read_only = false);
|
||||
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = false);
|
||||
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = false);
|
||||
Result DeleteFile(const FsPath& path, bool ignore_read_only = false);
|
||||
Result DeleteDirectory(const FsPath& path, bool ignore_read_only = false);
|
||||
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = false);
|
||||
Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = false);
|
||||
Result RenameDirectory(const FsPath& src, const FsPath& dst, 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 = true);
|
||||
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = true);
|
||||
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = true);
|
||||
Result DeleteFile(const FsPath& path, bool ignore_read_only = true);
|
||||
Result DeleteDirectory(const FsPath& path, bool ignore_read_only = true);
|
||||
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = true);
|
||||
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 = true);
|
||||
Result GetEntryType(const FsPath& path, FsDirEntryType* out);
|
||||
Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out);
|
||||
bool FileExists(const FsPath& path);
|
||||
bool DirExists(const FsPath& path);
|
||||
Result read_entire_file(const FsPath& path, std::vector<u8>& out);
|
||||
Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = false);
|
||||
Result copy_entire_file(const FsPath& dst, const FsPath& src, 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 = true);
|
||||
|
||||
struct Fs {
|
||||
static constexpr inline u32 FsModule = 505;
|
||||
@@ -222,51 +222,64 @@ struct Fs {
|
||||
static constexpr inline Result ResultUnknownStdioError = MAKERESULT(FsModule, 13);
|
||||
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;
|
||||
virtual Result CreateDirectory(const FsPath& path, bool ignore_read_only = false) = 0;
|
||||
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 DeleteFile(const FsPath& path, bool ignore_read_only = false) = 0;
|
||||
virtual Result DeleteDirectory(const FsPath& path, bool ignore_read_only = false) = 0;
|
||||
virtual Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) = 0;
|
||||
virtual Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) = 0;
|
||||
virtual Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) = 0;
|
||||
Fs(bool ignore_read_only = true) : m_ignore_read_only{ignore_read_only} {}
|
||||
virtual ~Fs() = default;
|
||||
|
||||
virtual Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0) = 0;
|
||||
virtual Result CreateDirectory(const FsPath& path) = 0;
|
||||
virtual Result CreateDirectoryRecursively(const FsPath& path) = 0;
|
||||
virtual Result CreateDirectoryRecursivelyWithPath(const FsPath& path) = 0;
|
||||
virtual Result DeleteFile(const FsPath& path) = 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 GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out) = 0;
|
||||
virtual bool FileExists(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 write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = false) = 0;
|
||||
virtual Result copy_entire_file(const FsPath& dst, const FsPath& src, 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) = 0;
|
||||
|
||||
void SetIgnoreReadOnly(bool enable) {
|
||||
m_ignore_read_only = enable;
|
||||
}
|
||||
|
||||
protected:
|
||||
bool m_ignore_read_only;
|
||||
};
|
||||
|
||||
struct FsStdio : Fs {
|
||||
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false) override {
|
||||
return fs::CreateFile(path, size, option, ignore_read_only);
|
||||
FsStdio(bool ignore_read_only = true) : Fs{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 {
|
||||
return fs::CreateDirectory(path, ignore_read_only);
|
||||
Result CreateDirectory(const FsPath& path) override {
|
||||
return fs::CreateDirectory(path, m_ignore_read_only);
|
||||
}
|
||||
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::CreateDirectoryRecursively(path, ignore_read_only);
|
||||
Result CreateDirectoryRecursively(const FsPath& path) override {
|
||||
return fs::CreateDirectoryRecursively(path, m_ignore_read_only);
|
||||
}
|
||||
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::CreateDirectoryRecursivelyWithPath(path, ignore_read_only);
|
||||
Result CreateDirectoryRecursivelyWithPath(const FsPath& path) override {
|
||||
return fs::CreateDirectoryRecursivelyWithPath(path, m_ignore_read_only);
|
||||
}
|
||||
Result DeleteFile(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::DeleteFile(path, ignore_read_only);
|
||||
Result DeleteFile(const FsPath& path) override {
|
||||
return fs::DeleteFile(path, m_ignore_read_only);
|
||||
}
|
||||
Result DeleteDirectory(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::DeleteDirectory(path, ignore_read_only);
|
||||
Result DeleteDirectory(const FsPath& path) override {
|
||||
return fs::DeleteDirectory(path, m_ignore_read_only);
|
||||
}
|
||||
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::DeleteDirectoryRecursively(path, ignore_read_only);
|
||||
Result DeleteDirectoryRecursively(const FsPath& path) override {
|
||||
return fs::DeleteDirectoryRecursively(path, m_ignore_read_only);
|
||||
}
|
||||
Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) override {
|
||||
return fs::RenameFile(src, dst, ignore_read_only);
|
||||
Result RenameFile(const FsPath& src, const FsPath& dst) override {
|
||||
return fs::RenameFile(src, dst, m_ignore_read_only);
|
||||
}
|
||||
Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) override {
|
||||
return fs::RenameDirectory(src, dst, ignore_read_only);
|
||||
Result RenameDirectory(const FsPath& src, const FsPath& dst) override {
|
||||
return fs::RenameDirectory(src, dst, m_ignore_read_only);
|
||||
}
|
||||
Result GetEntryType(const FsPath& path, FsDirEntryType* out) override {
|
||||
return fs::GetEntryType(path, out);
|
||||
@@ -283,17 +296,17 @@ struct FsStdio : Fs {
|
||||
Result read_entire_file(const FsPath& path, std::vector<u8>& out) override {
|
||||
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 {
|
||||
return fs::write_entire_file(path, in, ignore_read_only);
|
||||
Result write_entire_file(const FsPath& path, const std::vector<u8>& in) override {
|
||||
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 {
|
||||
return fs::copy_entire_file(dst, src, ignore_read_only);
|
||||
Result copy_entire_file(const FsPath& dst, const FsPath& src) override {
|
||||
return fs::copy_entire_file(dst, src, m_ignore_read_only);
|
||||
}
|
||||
};
|
||||
|
||||
struct FsNative : Fs {
|
||||
FsNative() = default;
|
||||
FsNative(FsFileSystem* fs, bool own) : m_fs{*fs}, m_own{own} {}
|
||||
explicit FsNative(bool ignore_read_only = true) : Fs{ignore_read_only} {}
|
||||
explicit FsNative(FsFileSystem* fs, bool own, bool ignore_read_only = true) : Fs{ignore_read_only}, m_fs{*fs}, m_own{own} {}
|
||||
|
||||
virtual ~FsNative() {
|
||||
if (m_own) {
|
||||
@@ -355,32 +368,32 @@ struct FsNative : Fs {
|
||||
return m_open_result;
|
||||
}
|
||||
|
||||
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false) override {
|
||||
return fs::CreateFile(&m_fs, path, size, option, ignore_read_only);
|
||||
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0) override {
|
||||
return fs::CreateFile(&m_fs, path, size, option, m_ignore_read_only);
|
||||
}
|
||||
Result CreateDirectory(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::CreateDirectory(&m_fs, path, ignore_read_only);
|
||||
Result CreateDirectory(const FsPath& path) override {
|
||||
return fs::CreateDirectory(&m_fs, path, m_ignore_read_only);
|
||||
}
|
||||
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::CreateDirectoryRecursively(&m_fs, path, ignore_read_only);
|
||||
Result CreateDirectoryRecursively(const FsPath& path) override {
|
||||
return fs::CreateDirectoryRecursively(&m_fs, path, m_ignore_read_only);
|
||||
}
|
||||
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::CreateDirectoryRecursivelyWithPath(&m_fs, path, ignore_read_only);
|
||||
Result CreateDirectoryRecursivelyWithPath(const FsPath& path) override {
|
||||
return fs::CreateDirectoryRecursivelyWithPath(&m_fs, path, m_ignore_read_only);
|
||||
}
|
||||
Result DeleteFile(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::DeleteFile(&m_fs, path, ignore_read_only);
|
||||
Result DeleteFile(const FsPath& path) override {
|
||||
return fs::DeleteFile(&m_fs, path, m_ignore_read_only);
|
||||
}
|
||||
Result DeleteDirectory(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::DeleteDirectory(&m_fs, path, ignore_read_only);
|
||||
Result DeleteDirectory(const FsPath& path) override {
|
||||
return fs::DeleteDirectory(&m_fs, path, m_ignore_read_only);
|
||||
}
|
||||
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::DeleteDirectoryRecursively(&m_fs, path, ignore_read_only);
|
||||
Result DeleteDirectoryRecursively(const FsPath& path) override {
|
||||
return fs::DeleteDirectoryRecursively(&m_fs, path, m_ignore_read_only);
|
||||
}
|
||||
Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) override {
|
||||
return fs::RenameFile(&m_fs, src, dst, ignore_read_only);
|
||||
Result RenameFile(const FsPath& src, const FsPath& dst) override {
|
||||
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 {
|
||||
return fs::RenameDirectory(&m_fs, src, dst, ignore_read_only);
|
||||
Result RenameDirectory(const FsPath& src, const FsPath& dst) override {
|
||||
return fs::RenameDirectory(&m_fs, src, dst, m_ignore_read_only);
|
||||
}
|
||||
Result GetEntryType(const FsPath& path, FsDirEntryType* out) override {
|
||||
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 {
|
||||
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 {
|
||||
return fs::write_entire_file(&m_fs, path, in, ignore_read_only);
|
||||
Result write_entire_file(const FsPath& path, const std::vector<u8>& in) override {
|
||||
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 {
|
||||
return fs::copy_entire_file(&m_fs, dst, src, ignore_read_only);
|
||||
Result copy_entire_file(const FsPath& dst, const FsPath& src) override {
|
||||
return fs::copy_entire_file(&m_fs, dst, src, m_ignore_read_only);
|
||||
}
|
||||
|
||||
FsFileSystem m_fs{};
|
||||
@@ -417,43 +430,28 @@ struct FsNativeSd final : FsNative {
|
||||
};
|
||||
#else
|
||||
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;
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// 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
|
||||
|
||||
@@ -1,33 +1,8 @@
|
||||
#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 {
|
||||
|
||||
bool Init();
|
||||
void Exit();
|
||||
|
||||
} // namespace sphaira::ftpsrv
|
||||
|
||||
#endif // __cplusplus
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
#define sphaira_USE_LOG 1
|
||||
|
||||
#include <cstdarg>
|
||||
|
||||
#if sphaira_USE_LOG
|
||||
auto log_file_init() -> bool;
|
||||
auto log_nxlink_init() -> bool;
|
||||
void log_file_exit();
|
||||
void log_nxlink_exit();
|
||||
void log_write(const char* s, ...) __attribute__ ((format (printf, 1, 2)));
|
||||
void log_write_arg(const char* s, std::va_list& v);
|
||||
#else
|
||||
inline auto log_file_init() -> bool {
|
||||
return true;
|
||||
|
||||
@@ -19,7 +19,6 @@ struct NroEntry {
|
||||
s64 size{};
|
||||
NacpStruct nacp{};
|
||||
|
||||
std::vector<u8> icon{};
|
||||
u64 icon_size{};
|
||||
u64 icon_offset{};
|
||||
|
||||
@@ -76,4 +75,10 @@ auto nro_add_arg_file(std::string arg) -> std::string;
|
||||
// strips sdmc:
|
||||
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
|
||||
|
||||
10
sphaira/include/ui/bubbles.hpp
Normal file
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);
|
||||
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||
auto OnLayoutChange() -> void override;
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
|
||||
private:
|
||||
Result m_code;
|
||||
std::string m_message;
|
||||
std::string m_module_str;
|
||||
std::string m_description_str;
|
||||
Result m_code{};
|
||||
std::string m_message{};
|
||||
std::string m_module_str{};
|
||||
std::string m_description_str{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui
|
||||
|
||||
57
sphaira/include/ui/list.hpp
Normal file
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/scrollable_text.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include "nro.hpp"
|
||||
#include "fs.hpp"
|
||||
#include <span>
|
||||
@@ -27,6 +28,8 @@ struct LazyImage {
|
||||
~LazyImage();
|
||||
int image{};
|
||||
int w{}, h{};
|
||||
bool tried_cache{};
|
||||
bool cached{};
|
||||
ImageDownloadState state{ImageDownloadState::None};
|
||||
u8 first_pixel[4]{};
|
||||
};
|
||||
@@ -39,26 +42,26 @@ enum class EntryStatus {
|
||||
};
|
||||
|
||||
struct Entry {
|
||||
std::string category; // todo: lable
|
||||
std::string binary; // optional, only valid for .nro
|
||||
std::string updated; // date of update
|
||||
std::string name;
|
||||
std::string license; // optional
|
||||
std::string title; // same as name but with spaces
|
||||
std::string url; // url of repo (optional?)
|
||||
std::string description;
|
||||
std::string author;
|
||||
std::string changelog; // optional
|
||||
u64 screens; // number of screenshots
|
||||
u64 extracted; // extracted size in KiB
|
||||
std::string version;
|
||||
u64 filesize; // compressed size in KiB
|
||||
std::string details;
|
||||
u64 app_dls;
|
||||
std::string md5; // md5 of the zip
|
||||
std::string category{}; // todo: lable
|
||||
std::string binary{}; // optional, only valid for .nro
|
||||
std::string updated{}; // date of update
|
||||
std::string name{};
|
||||
std::string license{}; // optional
|
||||
std::string title{}; // same as name but with spaces
|
||||
std::string url{}; // url of repo (optional?)
|
||||
std::string description{};
|
||||
std::string author{};
|
||||
std::string changelog{}; // optional
|
||||
u64 screens{}; // number of screenshots
|
||||
u64 extracted{}; // extracted size in KiB
|
||||
std::string version{};
|
||||
u64 filesize{}; // compressed size in KiB
|
||||
std::string details{};
|
||||
u64 app_dls{};
|
||||
std::string md5{}; // md5 of the zip
|
||||
|
||||
LazyImage image;
|
||||
u32 updated_num;
|
||||
LazyImage image{};
|
||||
u32 updated_num{};
|
||||
EntryStatus status{EntryStatus::Get};
|
||||
};
|
||||
|
||||
@@ -75,7 +78,7 @@ struct EntryMenu final : MenuBase {
|
||||
// void OnFocusGained() override;
|
||||
|
||||
void ShowChangelogAction();
|
||||
void SetIndex(std::size_t index);
|
||||
void SetIndex(s64 index);
|
||||
|
||||
void UpdateOptions();
|
||||
|
||||
@@ -95,14 +98,14 @@ private:
|
||||
const LazyImage& m_default_icon;
|
||||
Menu& m_menu;
|
||||
|
||||
std::size_t m_index{}; // where i am in the array
|
||||
std::vector<Option> m_options;
|
||||
LazyImage m_banner;
|
||||
std::vector<LazyImage> m_screens;
|
||||
s64 m_index{}; // where i am in the array
|
||||
std::vector<Option> m_options{};
|
||||
LazyImage m_banner{};
|
||||
std::unique_ptr<List> m_list{};
|
||||
|
||||
std::shared_ptr<ScrollableText> m_details;
|
||||
std::shared_ptr<ScrollableText> m_changelog;
|
||||
std::shared_ptr<ScrollableText> m_detail_changelog;
|
||||
std::shared_ptr<ScrollableText> m_details{};
|
||||
std::shared_ptr<ScrollableText> m_changelog{};
|
||||
std::shared_ptr<ScrollableText> m_detail_changelog{};
|
||||
|
||||
bool m_show_changlog{};
|
||||
};
|
||||
@@ -127,39 +130,10 @@ enum SortType {
|
||||
};
|
||||
|
||||
enum OrderType {
|
||||
OrderType_Decending,
|
||||
OrderType_Descending,
|
||||
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 {
|
||||
Menu(const std::vector<NroEntry>& nro_entries);
|
||||
~Menu();
|
||||
@@ -168,7 +142,7 @@ struct Menu final : MenuBase {
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
|
||||
void SetIndex(std::size_t index);
|
||||
void SetIndex(s64 index);
|
||||
void ScanHomebrew();
|
||||
void Sort();
|
||||
|
||||
@@ -189,29 +163,29 @@ struct Menu final : MenuBase {
|
||||
|
||||
private:
|
||||
const std::vector<NroEntry>& m_nro_entries;
|
||||
std::vector<Entry> m_entries;
|
||||
std::vector<EntryMini> m_entries_index[Filter_MAX];
|
||||
std::vector<EntryMini> m_entries_index_author;
|
||||
std::vector<EntryMini> m_entries_index_search;
|
||||
std::span<EntryMini> m_entries_current;
|
||||
std::vector<Entry> m_entries{};
|
||||
std::vector<EntryMini> m_entries_index[Filter_MAX]{};
|
||||
std::vector<EntryMini> m_entries_index_author{};
|
||||
std::vector<EntryMini> m_entries_index_search{};
|
||||
std::span<EntryMini> m_entries_current{};
|
||||
|
||||
Filter m_filter{Filter::Filter_All};
|
||||
SortType m_sort{SortType::SortType_Updated};
|
||||
OrderType m_order{OrderType::OrderType_Decending};
|
||||
OrderType m_order{OrderType::OrderType_Descending};
|
||||
|
||||
std::size_t m_start{};
|
||||
std::size_t m_index{}; // where i am in the array
|
||||
LazyImage m_default_image;
|
||||
LazyImage m_update;
|
||||
LazyImage m_get;
|
||||
LazyImage m_local;
|
||||
LazyImage m_installed;
|
||||
s64 m_index{}; // where i am in the array
|
||||
LazyImage m_default_image{};
|
||||
LazyImage m_update{};
|
||||
LazyImage m_get{};
|
||||
LazyImage m_local{};
|
||||
LazyImage m_installed{};
|
||||
ImageDownloadState m_repo_download_state{ImageDownloadState::None};
|
||||
std::unique_ptr<List> m_list{};
|
||||
|
||||
std::string m_search_term;
|
||||
std::string m_author_term;
|
||||
u64 m_entry_search_jump_back{};
|
||||
u64 m_entry_author_jump_back{};
|
||||
std::string m_search_term{};
|
||||
std::string m_author_term{};
|
||||
s64 m_entry_search_jump_back{};
|
||||
s64 m_entry_author_jump_back{};
|
||||
bool m_is_search{};
|
||||
bool m_is_author{};
|
||||
bool m_dirty{}; // if set, does a sort
|
||||
|
||||
@@ -16,15 +16,15 @@ struct Menu final : MenuBase {
|
||||
|
||||
private:
|
||||
const fs::FsPath m_path;
|
||||
fs::FsNativeSd m_fs;
|
||||
FsFile m_file;
|
||||
fs::FsNativeSd m_fs{};
|
||||
FsFile m_file{};
|
||||
s64 m_file_size{};
|
||||
s64 m_file_offset{};
|
||||
|
||||
std::unique_ptr<ScrollableText> m_scroll_text;
|
||||
std::unique_ptr<ScrollableText> m_scroll_text{};
|
||||
|
||||
std::size_t m_start{};
|
||||
std::size_t m_index{}; // where i am in the array
|
||||
s64 m_start{};
|
||||
s64 m_index{}; // where i am in the array
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::fileview
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include "nro.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "option.hpp"
|
||||
@@ -9,6 +10,12 @@
|
||||
|
||||
namespace sphaira::ui::menu::filebrowser {
|
||||
|
||||
enum class FsType {
|
||||
Sd,
|
||||
ImageNand,
|
||||
ImageSd,
|
||||
};
|
||||
|
||||
enum class SelectedType {
|
||||
None,
|
||||
Copy,
|
||||
@@ -22,7 +29,7 @@ enum SortType {
|
||||
};
|
||||
|
||||
enum OrderType {
|
||||
OrderType_Decending,
|
||||
OrderType_Descending,
|
||||
OrderType_Ascending,
|
||||
};
|
||||
|
||||
@@ -79,17 +86,27 @@ struct FileEntry : FsDirectoryEntry {
|
||||
|
||||
struct FileAssocEntry {
|
||||
fs::FsPath path{}; // ini name
|
||||
std::string name; // ini name
|
||||
std::vector<std::string> ext; // list of ext
|
||||
std::vector<std::string> database; // list of systems
|
||||
std::string name{}; // ini name
|
||||
std::vector<std::string> ext{}; // list of ext
|
||||
std::vector<std::string> database{}; // list of systems
|
||||
};
|
||||
|
||||
struct LastFile {
|
||||
fs::FsPath name;
|
||||
u64 index;
|
||||
u64 offset;
|
||||
u64 entries_count;
|
||||
fs::FsPath name{};
|
||||
s64 index{};
|
||||
float offset{};
|
||||
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 {
|
||||
Menu(const std::vector<NroEntry>& nro_entries);
|
||||
~Menu();
|
||||
@@ -103,7 +120,7 @@ struct Menu final : MenuBase {
|
||||
}
|
||||
|
||||
private:
|
||||
void SetIndex(std::size_t index);
|
||||
void SetIndex(s64 index);
|
||||
void InstallForwarder();
|
||||
auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result;
|
||||
|
||||
@@ -115,7 +132,7 @@ private:
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -203,46 +220,54 @@ private:
|
||||
void OnDeleteCallback();
|
||||
void OnPasteCallback();
|
||||
void OnRenameCallback();
|
||||
auto CheckIfUpdateFolder() -> Result;
|
||||
|
||||
auto get_collection(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollection& out, bool inc_file, bool inc_dir, bool inc_size) -> Result;
|
||||
auto get_collections(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out) -> Result;
|
||||
|
||||
void SetFs(const fs::FsPath& new_path, u32 new_type);
|
||||
|
||||
private:
|
||||
static constexpr inline const char* INI_SECTION = "filebrowser";
|
||||
|
||||
const std::vector<NroEntry>& m_nro_entries;
|
||||
fs::FsPath m_path;
|
||||
std::vector<FileEntry> m_entries;
|
||||
std::vector<u32> m_entries_index; // files not including hidden
|
||||
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<fs::FsNative> m_fs{};
|
||||
FsType m_fs_type{};
|
||||
fs::FsPath m_path{};
|
||||
std::vector<FileEntry> m_entries{};
|
||||
std::vector<u32> m_entries_index{}; // files not including hidden
|
||||
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
|
||||
// show files [X]
|
||||
// show folders [X]
|
||||
// recursive (slow) [ ]
|
||||
|
||||
std::vector<FileAssocEntry> m_assoc_entries;
|
||||
std::vector<FileEntry> m_selected_files;
|
||||
std::vector<FileAssocEntry> m_assoc_entries{};
|
||||
std::vector<FileEntry> m_selected_files{};
|
||||
|
||||
// this keeps track of the highlighted file before opening a folder
|
||||
// if the user presses B to go back to the previous dir
|
||||
// this vector is popped, then, that entry is checked if it still exists
|
||||
// if it does, the index becomes that file.
|
||||
std::vector<LastFile> m_previous_highlighted_file;
|
||||
fs::FsPath m_selected_path;
|
||||
std::size_t m_index{};
|
||||
std::size_t m_index_offset{};
|
||||
std::size_t m_selected_count{};
|
||||
std::vector<LastFile> m_previous_highlighted_file{};
|
||||
fs::FsPath m_selected_path{};
|
||||
s64 m_index{};
|
||||
s64 m_selected_count{};
|
||||
SelectedType m_selected_type{SelectedType::None};
|
||||
|
||||
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Alphabetical};
|
||||
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Decending};
|
||||
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
|
||||
option::OptionBool m_show_hidden{INI_SECTION, "show_hidden", false};
|
||||
option::OptionBool m_folders_first{INI_SECTION, "folders_first", true};
|
||||
option::OptionBool m_hidden_last{INI_SECTION, "hidden_last", false};
|
||||
|
||||
option::OptionBool m_search_show_files{INI_SECTION, "search_show_files", true};
|
||||
option::OptionBool m_search_show_folders{INI_SECTION, "search_show_folders", true};
|
||||
option::OptionBool m_search_recursive{INI_SECTION, "search_recursive", false};
|
||||
option::OptionBool m_ignore_read_only{INI_SECTION, "ignore_read_only", false};
|
||||
option::OptionLong m_mount{INI_SECTION, "mount", 0};
|
||||
|
||||
bool m_loaded_assoc_entries{};
|
||||
bool m_is_update_folder{};
|
||||
|
||||
74
sphaira/include/ui/menus/ghdl.hpp
Normal file
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
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include "nro.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "option.hpp"
|
||||
@@ -17,7 +18,7 @@ enum SortType {
|
||||
};
|
||||
|
||||
enum OrderType {
|
||||
OrderType_Decending,
|
||||
OrderType_Descending,
|
||||
OrderType_Ascending,
|
||||
};
|
||||
|
||||
@@ -29,7 +30,7 @@ struct Menu final : MenuBase {
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
|
||||
void SetIndex(std::size_t index);
|
||||
void SetIndex(s64 index);
|
||||
void InstallHomebrew();
|
||||
void ScanHomebrew();
|
||||
void Sort();
|
||||
@@ -49,12 +50,12 @@ struct Menu final : MenuBase {
|
||||
private:
|
||||
static constexpr inline const char* INI_SECTION = "homebrew";
|
||||
|
||||
std::vector<NroEntry> m_entries;
|
||||
std::size_t m_start{};
|
||||
std::size_t m_index{}; // where i am in the array
|
||||
std::vector<NroEntry> m_entries{};
|
||||
s64 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_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};
|
||||
};
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ private:
|
||||
Rotation m_rotation{Rotation_90};
|
||||
Colour m_colour{Colour_Grey};
|
||||
int m_image{};
|
||||
std::size_t m_index{};
|
||||
s64 m_index{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::irs
|
||||
|
||||
@@ -28,10 +28,13 @@ struct MainMenu final : Widget {
|
||||
void OnFocusGained() override;
|
||||
void OnFocusLost() override;
|
||||
|
||||
auto IsMenu() const -> bool override {
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
void OnLRPress(std::shared_ptr<MenuBase> menu, Button b);
|
||||
void AddOnLPress();
|
||||
void AddOnRPress();
|
||||
void AddOnLRPress();
|
||||
|
||||
private:
|
||||
std::shared_ptr<homebrew::Menu> m_homebrew_menu{};
|
||||
|
||||
@@ -12,15 +12,31 @@ struct MenuBase : Widget {
|
||||
|
||||
virtual void Update(Controller* controller, TouchInfo* touch);
|
||||
virtual void Draw(NVGcontext* vg, Theme* theme);
|
||||
|
||||
auto IsMenu() const -> bool override {
|
||||
return true;
|
||||
}
|
||||
|
||||
void SetTitle(std::string title);
|
||||
void SetTitleSubHeading(std::string sub_heading);
|
||||
void SetSubHeading(std::string sub_heading);
|
||||
|
||||
private:
|
||||
std::string m_title;
|
||||
std::string m_title_sub_heading;
|
||||
std::string m_sub_heading;
|
||||
AppletType m_applet_type;
|
||||
void UpdateVars();
|
||||
|
||||
private:
|
||||
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
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "ui/scrollable_text.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include "option.hpp"
|
||||
#include <span>
|
||||
|
||||
@@ -15,28 +16,14 @@ enum class ImageDownloadState {
|
||||
};
|
||||
|
||||
struct LazyImage {
|
||||
LazyImage() = default;
|
||||
~LazyImage();
|
||||
int image{};
|
||||
int w{}, h{};
|
||||
bool tried_cache{};
|
||||
bool cached{};
|
||||
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 {
|
||||
MenuState_Normal,
|
||||
MenuState_Search,
|
||||
@@ -56,73 +43,49 @@ enum class PageLoadState {
|
||||
};
|
||||
|
||||
struct Creator {
|
||||
std::string id;
|
||||
std::string display_name;
|
||||
std::string id{};
|
||||
std::string display_name{};
|
||||
};
|
||||
|
||||
struct Details {
|
||||
std::string name;
|
||||
std::string description;
|
||||
std::string name{};
|
||||
};
|
||||
|
||||
struct Preview {
|
||||
std::string original;
|
||||
std::string thumb;
|
||||
LazyImage lazy_image;
|
||||
std::string thumb{};
|
||||
LazyImage lazy_image{};
|
||||
};
|
||||
|
||||
struct DownloadPack {
|
||||
std::string filename;
|
||||
std::string url;
|
||||
std::string mimetype;
|
||||
std::string filename{};
|
||||
std::string url{};
|
||||
std::string mimetype{};
|
||||
};
|
||||
|
||||
using DownloadTheme = DownloadPack;
|
||||
|
||||
struct ThemeEntry {
|
||||
std::string id;
|
||||
Creator creator;
|
||||
Details details;
|
||||
std::string last_updated;
|
||||
u64 dl_count;
|
||||
u64 like_count;
|
||||
std::vector<std::string> categories;
|
||||
std::string target;
|
||||
Preview preview;
|
||||
std::string id{};
|
||||
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 {
|
||||
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;
|
||||
std::string id{};
|
||||
Creator creator{};
|
||||
Details details{};
|
||||
std::vector<ThemeEntry> themes{};
|
||||
};
|
||||
|
||||
struct Pagination {
|
||||
u64 page;
|
||||
u64 limit;
|
||||
u64 page_count;
|
||||
u64 item_count;
|
||||
u64 page{};
|
||||
u64 limit{};
|
||||
u64 page_count{};
|
||||
u64 item_count{};
|
||||
};
|
||||
|
||||
struct PackList {
|
||||
std::vector<PackListEntry> packList;
|
||||
Pagination pagination;
|
||||
std::vector<PackListEntry> packList{};
|
||||
Pagination pagination{};
|
||||
};
|
||||
|
||||
struct Config {
|
||||
@@ -131,10 +94,10 @@ struct Config {
|
||||
u32 sort_index{};
|
||||
u32 order_index{};
|
||||
// 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
|
||||
// if empty, its not used
|
||||
std::string creator;
|
||||
std::string creator{};
|
||||
// defaults
|
||||
u32 page{1};
|
||||
u32 limit{18};
|
||||
@@ -160,7 +123,7 @@ struct Config {
|
||||
struct Menu; // fwd
|
||||
|
||||
struct PageEntry {
|
||||
std::vector<PackListEntry> m_packList;
|
||||
std::vector<PackListEntry> m_packList{};
|
||||
Pagination m_pagination{};
|
||||
PageLoadState m_ready{PageLoadState::None};
|
||||
};
|
||||
@@ -173,13 +136,13 @@ struct Menu final : MenuBase {
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
|
||||
void SetIndex(std::size_t index) {
|
||||
void SetIndex(s64 index) {
|
||||
m_index = index;
|
||||
if (!m_index) {
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
}
|
||||
|
||||
// void SetSearch(const std::string& term);
|
||||
// void SetAuthor();
|
||||
|
||||
void InvalidateAllPages();
|
||||
void PackListDownload();
|
||||
void OnPackListDownload();
|
||||
@@ -188,14 +151,14 @@ private:
|
||||
static constexpr inline const char* INI_SECTION = "themezer";
|
||||
static constexpr inline u32 MAX_ON_PAGE = 16; // same as website
|
||||
|
||||
std::vector<PageEntry> m_pages;
|
||||
std::size_t m_page_index{};
|
||||
std::size_t m_page_index_max{1};
|
||||
std::vector<PageEntry> m_pages{};
|
||||
s64 m_page_index{};
|
||||
s64 m_page_index_max{1};
|
||||
|
||||
std::string m_search{};
|
||||
|
||||
std::size_t m_start{};
|
||||
std::size_t m_index{}; // where i am in the array
|
||||
s64 m_index{}; // where i am in the array
|
||||
std::unique_ptr<List> m_list{};
|
||||
|
||||
// options
|
||||
option::OptionLong m_sort{INI_SECTION, "sort", 0};
|
||||
|
||||
@@ -19,13 +19,12 @@ public:
|
||||
auto IsDone() const noexcept { return m_count == 0; }
|
||||
|
||||
private:
|
||||
void OnLayoutChange() override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
|
||||
private:
|
||||
std::string m_text;
|
||||
std::string m_text{};
|
||||
std::size_t m_count{180}; // count down to zero
|
||||
Side m_side;
|
||||
Side m_side{};
|
||||
bool m_bounds_measured{};
|
||||
};
|
||||
|
||||
@@ -34,7 +33,6 @@ public:
|
||||
NotifMananger() = default;
|
||||
~NotifMananger() = default;
|
||||
|
||||
void OnLayoutChange() override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
|
||||
void Push(const NotifEntry& entry);
|
||||
@@ -49,8 +47,8 @@ private:
|
||||
void Draw(NVGcontext* vg, Theme* theme, Entries& entries);
|
||||
|
||||
private:
|
||||
Entries m_entries_left;
|
||||
Entries m_entries_right;
|
||||
Entries m_entries_left{};
|
||||
Entries m_entries_right{};
|
||||
Mutex m_mutex{};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,90 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include "nanovg.h"
|
||||
#include "ui/widget.hpp"
|
||||
#include "ui/types.hpp"
|
||||
|
||||
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*, 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*, Vec4 v, int texture);
|
||||
|
||||
auto getColour(Colour c) -> NVGcolor;
|
||||
void drawImageRounded(NVGcontext*, const Vec4& v, int texture);
|
||||
|
||||
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*, Vec4 vec, const NVGcolor& c, bool rounded = false);
|
||||
void drawRect(NVGcontext*, Vec4 vec, const NVGcolor&& c, bool rounded = false);
|
||||
void drawRect(NVGcontext*, const Vec4& v, const NVGcolor& c, bool rounded = false);
|
||||
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGpaint& p, bool rounded = false);
|
||||
void drawRect(NVGcontext*, float x, float y, float w, float h, 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 drawRect(NVGcontext*, const Vec4& v, 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*, float size, const NVGcolor& out_col, Vec4 vec, Colour 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, 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 drawRectOutline(NVGcontext*, const Theme*, float size, float x, float y, float w, float h);
|
||||
void drawRectOutline(NVGcontext*, const Theme*, float size, const Vec4& v);
|
||||
|
||||
void drawTriangle(NVGcontext*, float aX, float aY, float bX, float bY, float cX, float cY, 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 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*, Vec2 vec, 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*, 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 drawText(NVGcontext*, const Vec2& v, 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 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, 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 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 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*;
|
||||
void drawButton(NVGcontext* vg, float x, float y, float size, Button button);
|
||||
void drawButtons(NVGcontext* vg, const Widget::Actions& actions, const NVGcolor& c, float start_x = 1220.f);
|
||||
void drawScrollbar(NVGcontext*, const Theme*, u32 index_off, u32 count, u32 max_per_page);
|
||||
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 getHighlightAnimation(float* gradientX, float* gradientY, float* color);
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "types.hpp"
|
||||
#include <stop_token>
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
class Object {
|
||||
public:
|
||||
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;
|
||||
|
||||
auto GetPos() const noexcept {
|
||||
@@ -73,8 +74,14 @@ public:
|
||||
m_hidden = value;
|
||||
}
|
||||
|
||||
auto GetToken() const {
|
||||
return m_stop_source.get_token();
|
||||
}
|
||||
|
||||
protected:
|
||||
Vec4 m_pos{};
|
||||
// used for lifetime management across threads.
|
||||
std::stop_source m_stop_source{};
|
||||
bool m_hidden{false};
|
||||
};
|
||||
|
||||
|
||||
@@ -12,14 +12,13 @@ public:
|
||||
OptionBoxEntry(const std::string& text, Vec4 pos);
|
||||
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> void override {}
|
||||
auto OnLayoutChange() -> void override {}
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
|
||||
auto Selected(bool enable) -> void;
|
||||
private:
|
||||
|
||||
private:
|
||||
std::string m_text;
|
||||
std::string m_text{};
|
||||
Vec2 m_text_pos{};
|
||||
bool m_selected{false};
|
||||
};
|
||||
@@ -28,32 +27,34 @@ private:
|
||||
// todo: support upto 4 options.
|
||||
class OptionBox final : public Widget {
|
||||
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 Options = std::vector<Option>;
|
||||
|
||||
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, 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, 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 OnLayoutChange() -> void override;
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
auto OnFocusGained() noexcept -> void override;
|
||||
auto OnFocusLost() noexcept -> void override;
|
||||
|
||||
private:
|
||||
auto Setup(std::size_t index) -> void; // common setup values
|
||||
auto Setup(s64 index) -> void; // common setup values
|
||||
void SetIndex(s64 index);
|
||||
|
||||
private:
|
||||
std::string m_message;
|
||||
Callback m_callback;
|
||||
std::string m_message{};
|
||||
Callback m_callback{};
|
||||
|
||||
Vec4 m_spacer_line{};
|
||||
|
||||
std::size_t m_index{};
|
||||
std::vector<OptionBoxEntry> m_entries;
|
||||
s64 m_index{};
|
||||
std::vector<OptionBoxEntry> m_entries{};
|
||||
};
|
||||
|
||||
} // 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
|
||||
|
||||
#include "ui/widget.hpp"
|
||||
#include "ui/scrollbar.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include <optional>
|
||||
|
||||
namespace sphaira::ui {
|
||||
@@ -9,18 +9,22 @@ namespace sphaira::ui {
|
||||
class PopupList final : public Widget {
|
||||
public:
|
||||
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:
|
||||
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, 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::size_t& index_ref);
|
||||
PopupList(std::string title, Items items, s64& index_ref);
|
||||
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||
auto OnLayoutChange() -> 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:
|
||||
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_line_width{1220.f};
|
||||
|
||||
std::string m_title;
|
||||
Items m_items;
|
||||
Callback m_callback;
|
||||
std::size_t m_index; // index in list array
|
||||
std::size_t m_index_offset{}; // drawing from array start
|
||||
std::string m_title{};
|
||||
Items m_items{};
|
||||
Callback m_callback{};
|
||||
s64 m_index{}; // index in list array
|
||||
|
||||
// std::size_t& index_ref;
|
||||
// std::string& index_str_ref;
|
||||
std::unique_ptr<List> m_list{};
|
||||
|
||||
float m_selected_y{};
|
||||
float m_yoff{};
|
||||
float m_line_top{};
|
||||
float m_line_bottom{};
|
||||
ScrollBar m_scrollbar;
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui
|
||||
|
||||
@@ -22,7 +22,7 @@ struct ProgressBox final : Widget {
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
|
||||
auto NewTransfer(const std::string& transfer) -> ProgressBox&;
|
||||
auto UpdateTransfer(u64 offset, u64 size) -> ProgressBox&;
|
||||
auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
|
||||
void RequestExit();
|
||||
auto ShouldExit() -> bool;
|
||||
|
||||
@@ -30,11 +30,21 @@ struct ProgressBox final : Widget {
|
||||
auto CopyFile(const fs::FsPath& src, const fs::FsPath& dst) -> Result;
|
||||
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:
|
||||
struct ThreadData {
|
||||
ProgressBox* pbox;
|
||||
ProgressBoxCallback callback;
|
||||
bool result;
|
||||
ProgressBox* pbox{};
|
||||
ProgressBoxCallback callback{};
|
||||
bool result{};
|
||||
};
|
||||
|
||||
private:
|
||||
@@ -45,9 +55,8 @@ private:
|
||||
ProgressBoxDoneCallback m_done{};
|
||||
std::string m_title{};
|
||||
std::string m_transfer{};
|
||||
u64 m_size{};
|
||||
u64 m_offset{};
|
||||
bool m_exit_requested{};
|
||||
s64 m_size{};
|
||||
s64 m_offset{};
|
||||
};
|
||||
|
||||
// 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;
|
||||
// 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;
|
||||
float m_y_off;
|
||||
const float m_clip_y;
|
||||
const float m_end_w;
|
||||
static constexpr float m_step = 30;
|
||||
|
||||
int m_index = 0;
|
||||
const float m_font_size;
|
||||
float m_bounds[4];
|
||||
float m_y_off{};
|
||||
int m_index{};
|
||||
float m_bounds[4]{};
|
||||
};
|
||||
|
||||
} // 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
|
||||
|
||||
#include "ui/widget.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include <memory>
|
||||
|
||||
namespace sphaira::ui {
|
||||
@@ -9,11 +10,9 @@ class SidebarEntryBase : public Widget {
|
||||
public:
|
||||
SidebarEntryBase(std::string&& title);
|
||||
virtual auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
virtual auto OnLayoutChange() -> void override {}
|
||||
|
||||
protected:
|
||||
std::string m_title;
|
||||
Vec2 m_offset{};
|
||||
};
|
||||
|
||||
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, std::string true_str = "On", std::string false_str = "Off");
|
||||
|
||||
private:
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
|
||||
private:
|
||||
bool m_option;
|
||||
Callback m_callback;
|
||||
std::string m_true_str;
|
||||
@@ -50,20 +49,24 @@ class SidebarEntryArray final : public SidebarEntryBase {
|
||||
public:
|
||||
using Items = std::vector<std::string>;
|
||||
using ListCallback = std::function<void()>;
|
||||
using Callback = std::function<void(std::size_t& index)>;
|
||||
using Callback = std::function<void(s64& index)>;
|
||||
|
||||
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, std::string& index);
|
||||
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
auto OnFocusGained() noexcept -> void override;
|
||||
auto OnFocusLost() noexcept -> void override;
|
||||
|
||||
private:
|
||||
Items m_items;
|
||||
ListCallback m_list_callback;
|
||||
Callback m_callback;
|
||||
std::size_t m_index;
|
||||
s64 m_index;
|
||||
s64 m_tick{};
|
||||
float m_text_yoff{};
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
@@ -101,33 +104,30 @@ public:
|
||||
Sidebar(std::string title, std::string sub, Side side);
|
||||
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||
auto OnLayoutChange() -> void override {}
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
auto OnFocusGained() noexcept -> void override;
|
||||
auto OnFocusLost() noexcept -> void override;
|
||||
|
||||
void Add(std::shared_ptr<SidebarEntryBase> entry);
|
||||
void AddSpacer();
|
||||
void AddHeader(std::string name);
|
||||
|
||||
private:
|
||||
void SetIndex(std::size_t index);
|
||||
void SetIndex(s64 index);
|
||||
void SetupButtons();
|
||||
|
||||
private:
|
||||
std::string m_title;
|
||||
std::string m_sub;
|
||||
Side m_side;
|
||||
Items m_items;
|
||||
std::size_t m_index{};
|
||||
std::size_t m_index_offset{};
|
||||
s64 m_index{};
|
||||
|
||||
std::unique_ptr<List> m_list;
|
||||
|
||||
Vec4 m_top_bar{};
|
||||
Vec4 m_bottom_bar{};
|
||||
Vec2 m_title_pos{};
|
||||
Vec4 m_base_pos{};
|
||||
|
||||
float m_selected_y{};
|
||||
|
||||
static constexpr float m_title_size{28.f};
|
||||
// static constexpr Vec2 box_size{380.f, 70.f};
|
||||
static constexpr Vec2 m_box_size{400.f, 70.f};
|
||||
|
||||
@@ -114,15 +114,34 @@ struct [[nodiscard]] Vec4 {
|
||||
|
||||
struct TimeStamp {
|
||||
TimeStamp() {
|
||||
Update();
|
||||
}
|
||||
|
||||
void Update() {
|
||||
start = armGetSystemTick();
|
||||
}
|
||||
|
||||
auto GetNs() -> u64 {
|
||||
auto GetNs() const -> u64 {
|
||||
const auto end_ticks = armGetSystemTick();
|
||||
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();
|
||||
return ns/1000.0/1000.0/1000.0;
|
||||
}
|
||||
@@ -143,21 +162,55 @@ struct ElementEntry {
|
||||
};
|
||||
|
||||
enum ThemeEntryID {
|
||||
// colour of the background, can be an image.
|
||||
ThemeEntryID_BACKGROUND,
|
||||
|
||||
// colour of the grid background (homebrew, appstore), can be an image.
|
||||
ThemeEntryID_GRID,
|
||||
ThemeEntryID_SELECTED,
|
||||
ThemeEntryID_SELECTED_OVERLAY,
|
||||
ThemeEntryID_TEXT,
|
||||
ThemeEntryID_TEXT_SELECTED,
|
||||
// background colour of a popup.
|
||||
ThemeEntryID_POPUP,
|
||||
// colour of the error text / button.
|
||||
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_VIDEO,
|
||||
ThemeEntryID_ICON_IMAGE,
|
||||
ThemeEntryID_ICON_FILE,
|
||||
ThemeEntryID_ICON_FOLDER,
|
||||
ThemeEntryID_ICON_ZIP,
|
||||
ThemeEntryID_ICON_GAME,
|
||||
ThemeEntryID_ICON_NRO,
|
||||
|
||||
ThemeEntryID_MAX,
|
||||
@@ -167,49 +220,43 @@ struct ThemeMeta {
|
||||
std::string name;
|
||||
std::string author;
|
||||
std::string version;
|
||||
std::string ini_path;
|
||||
fs::FsPath inherit;
|
||||
fs::FsPath ini_path;
|
||||
};
|
||||
|
||||
struct Theme {
|
||||
std::string name;
|
||||
std::string author;
|
||||
std::string version;
|
||||
fs::FsPath path;
|
||||
ThemeMeta meta;
|
||||
PLSR_BFSTM music;
|
||||
ElementEntry elements[ThemeEntryID_MAX];
|
||||
|
||||
// NVGcolor background; // bg
|
||||
// NVGcolor lines; // grid lines
|
||||
// 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);
|
||||
auto GetColour(ThemeEntryID id) const {
|
||||
return elements[id].colour;
|
||||
}
|
||||
};
|
||||
|
||||
enum class TouchState {
|
||||
Start, // set when touch has started
|
||||
Touching, // set when touch is held longer than 1 frame
|
||||
Stop, // set after touch is released
|
||||
None, // set when there is no touch
|
||||
};
|
||||
// enum class TouchGesture {
|
||||
// None,
|
||||
// Tap,
|
||||
// Scroll,
|
||||
// };
|
||||
|
||||
struct TouchInfo {
|
||||
s32 initial_x;
|
||||
s32 initial_y;
|
||||
HidTouchState initial;
|
||||
HidTouchState cur;
|
||||
|
||||
s32 cur_x;
|
||||
s32 cur_y;
|
||||
auto in_range(const Vec4& v) const -> bool {
|
||||
return cur.x >= v.x && cur.x <= v.x + v.w && cur.y >= v.y && cur.y <= v.y + v.h;
|
||||
}
|
||||
|
||||
s32 prev_x;
|
||||
s32 prev_y;
|
||||
|
||||
u32 finger_id;
|
||||
auto in_range(s32 x, s32 y, s32 w, s32 h) const -> bool {
|
||||
return in_range(Vec4(x, y, w, h));
|
||||
}
|
||||
|
||||
bool is_touching;
|
||||
bool is_tap;
|
||||
bool is_scroll;
|
||||
bool is_clicked;
|
||||
bool is_end;
|
||||
};
|
||||
|
||||
enum class Button : u64 {
|
||||
@@ -276,37 +323,36 @@ inline ActionType operator|(ActionType a, ActionType b) {
|
||||
}
|
||||
|
||||
struct Action final {
|
||||
using CallbackEmpty = std::function<void()>;
|
||||
using CallbackWithBool = std::function<void(bool)>;
|
||||
using Callback = std::variant<
|
||||
std::function<void()>,
|
||||
std::function<void(bool)>
|
||||
CallbackEmpty,
|
||||
CallbackWithBool
|
||||
>;
|
||||
|
||||
Action(Callback cb) : m_type{ActionType::DOWN}, m_hint{""}, m_callback{cb}, m_hidden{true} {}
|
||||
Action(std::string hint, Callback cb) : m_type{ActionType::DOWN}, m_hint{hint}, m_callback{cb} {}
|
||||
Action(u8 type, Callback cb) : m_type{type}, m_hint{""}, m_callback{cb}, m_hidden{true} {}
|
||||
Action(u8 type, std::string hint, Callback cb) : m_type{type}, m_hint{hint}, m_callback{cb} {}
|
||||
Action(Callback cb) : Action{ActionType::DOWN, "", cb} {}
|
||||
Action(std::string hint, Callback cb) : Action{ActionType::DOWN, hint, cb} {}
|
||||
Action(u8 type, Callback cb) : Action{type, "", 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 {
|
||||
// todo: make this a visit
|
||||
switch (m_callback.index()) {
|
||||
case 0:
|
||||
std::get<0>(m_callback)();
|
||||
break;
|
||||
case 1:
|
||||
std::get<1>(m_callback)(down);
|
||||
break;
|
||||
}
|
||||
// std::visit([down, this](auto& cb){
|
||||
// cb(down);
|
||||
// }), m_callback;
|
||||
std::visit([down](auto&& arg){
|
||||
using T = std::decay_t<decltype(arg)>;
|
||||
if constexpr(std::is_same_v<T, CallbackEmpty>) {
|
||||
arg();
|
||||
} else if constexpr(std::is_same_v<T, CallbackWithBool>) {
|
||||
arg(down);
|
||||
} else {
|
||||
static_assert(false, "non-exhaustive visitor!");
|
||||
}
|
||||
}, m_callback);
|
||||
}
|
||||
|
||||
u8 m_type;
|
||||
std::string m_hint; // todo: make optional
|
||||
Callback m_callback;
|
||||
bool m_hidden{false}; // replace this optional text
|
||||
u8 m_type{};
|
||||
Callback m_callback{};
|
||||
std::string m_hint{};
|
||||
};
|
||||
|
||||
struct Controller {
|
||||
@@ -340,7 +386,7 @@ struct Controller {
|
||||
m_kup = 0;
|
||||
}
|
||||
|
||||
void UpdateButtonHeld(HidNpadButton buttons) {
|
||||
void UpdateButtonHeld(u64 buttons) {
|
||||
if (m_kdown & buttons) {
|
||||
m_step = 50;
|
||||
m_counter = 0;
|
||||
@@ -348,7 +394,7 @@ struct Controller {
|
||||
m_counter += m_step;
|
||||
|
||||
if (m_counter >= m_MAX) {
|
||||
m_kdown |= buttons;
|
||||
m_kdown |= m_kheld & buttons;
|
||||
m_counter = 0;
|
||||
m_step = std::min(m_step + 50, m_MAX_STEP);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,20 @@
|
||||
|
||||
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 {
|
||||
using Actions = std::map<Button, Action>;
|
||||
using uiButtons = std::vector<uiButton>;
|
||||
|
||||
virtual ~Widget() = default;
|
||||
|
||||
virtual void Update(Controller* controller, TouchInfo* touch);
|
||||
@@ -26,6 +39,10 @@ struct Widget : public Object {
|
||||
return m_focus;
|
||||
}
|
||||
|
||||
virtual auto IsMenu() const -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto HasAction(Button button) const -> bool;
|
||||
void SetAction(Button button, Action action);
|
||||
void SetActions(std::same_as<std::pair<Button, Action>> auto ...args) {
|
||||
@@ -45,6 +62,8 @@ struct Widget : public Object {
|
||||
m_actions.clear();
|
||||
}
|
||||
|
||||
auto FireAction(Button button, u8 type = ActionType::DOWN) -> bool;
|
||||
|
||||
void SetPop(bool pop = true) {
|
||||
m_pop = pop;
|
||||
}
|
||||
@@ -53,9 +72,14 @@ struct Widget : public Object {
|
||||
return m_pop;
|
||||
}
|
||||
|
||||
using Actions = std::map<Button, Action>;
|
||||
// using Actions = std::unordered_map<Button, Action>;
|
||||
Actions m_actions;
|
||||
auto SetUiButtonPos(Vec2 pos) {
|
||||
m_button_pos = pos;
|
||||
}
|
||||
|
||||
auto GetUiButtons() const -> uiButtons;
|
||||
|
||||
Actions m_actions{};
|
||||
Vec2 m_button_pos{1220, 675};
|
||||
bool m_focus{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); \
|
||||
e.name = str; \
|
||||
} \
|
||||
}
|
||||
} break
|
||||
|
||||
#define JSON_SET_OBJ(name) case cexprHash(#name): { \
|
||||
if (yyjson_is_obj(val)) { \
|
||||
from_json(val, e.name); \
|
||||
} \
|
||||
}
|
||||
} break
|
||||
|
||||
#define JSON_SET_UINT(name) JSON_SET_TYPE(name, uint)
|
||||
#define JSON_SET_STR(name) JSON_SET_TYPE(name, str)
|
||||
#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); \
|
||||
} \
|
||||
} \
|
||||
}
|
||||
} break
|
||||
|
||||
#define JSON_SET_ARR_OBJ2(name, member) case cexprHash(#name): { \
|
||||
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]); \
|
||||
} \
|
||||
} \
|
||||
}
|
||||
} break
|
||||
|
||||
#define JSON_SET_ARR_OBJ(name) JSON_SET_ARR_OBJ2(name, e.name)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,9 @@
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <curl/curl.h>
|
||||
#include <yyjson.h>
|
||||
|
||||
namespace sphaira {
|
||||
namespace sphaira::curl {
|
||||
namespace {
|
||||
|
||||
#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)); \
|
||||
} \
|
||||
|
||||
void DownloadThread(void* p);
|
||||
void DownloadThreadQueue(void* p);
|
||||
|
||||
#define USE_THREAD_QUEUE 1
|
||||
constexpr auto API_AGENT = "ITotalJustice";
|
||||
constexpr u64 CHUNK_SIZE = 1024*1024;
|
||||
constexpr auto MAX_THREADS = 4;
|
||||
@@ -38,42 +35,195 @@ std::atomic_bool g_running{};
|
||||
CURLSH* g_curl_share{};
|
||||
Mutex g_mutex_share[CURL_LOCK_DATA_LAST]{};
|
||||
|
||||
struct UrlCache {
|
||||
auto AddToCache(const std::string& url, bool force = false) {
|
||||
mutexLock(&mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&mutex));
|
||||
auto it = std::find(cache.begin(), cache.end(), url);
|
||||
if (it == cache.end()) {
|
||||
cache.emplace_back(url);
|
||||
struct DataStruct {
|
||||
std::vector<u8> data;
|
||||
s64 offset{};
|
||||
FsFile f{};
|
||||
s64 file_offset{};
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
if (force) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
log_write("creating new json doc\n");
|
||||
m_json = yyjson_mut_doc_new(nullptr);
|
||||
m_root = yyjson_mut_obj(m_json);
|
||||
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) {
|
||||
mutexLock(&mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&mutex));
|
||||
auto it = std::find(cache.begin(), cache.end(), url);
|
||||
if (it != cache.end()) {
|
||||
cache.erase(it);
|
||||
}
|
||||
}
|
||||
static constexpr inline fs::FsPath JSON_PATH{"/switch/sphaira/cache/cache.json"};
|
||||
static constexpr inline const char* ETAG_STR{"etag"};
|
||||
static constexpr inline const char* LAST_MODIFIED_STR{"last-modified"};
|
||||
|
||||
std::vector<std::string> cache;
|
||||
Mutex mutex{};
|
||||
};
|
||||
|
||||
struct DataStruct {
|
||||
std::vector<u8> data;
|
||||
u64 offset{};
|
||||
FsFileSystem fs{};
|
||||
FsFile f{};
|
||||
s64 file_offset{};
|
||||
Mutex m_mutex{};
|
||||
yyjson_mut_doc* m_json{};
|
||||
yyjson_mut_val* m_root{};
|
||||
std::unordered_map<std::string, Value> m_cache{};
|
||||
};
|
||||
|
||||
struct ThreadEntry {
|
||||
@@ -82,7 +232,7 @@ struct ThreadEntry {
|
||||
R_UNLESS(m_curl != nullptr, 0x1);
|
||||
|
||||
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_SUCCEED();
|
||||
}
|
||||
@@ -101,7 +251,7 @@ struct ThreadEntry {
|
||||
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");
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
@@ -109,35 +259,25 @@ struct ThreadEntry {
|
||||
if (m_in_progress) {
|
||||
return false;
|
||||
}
|
||||
m_url = url;
|
||||
m_file = file;
|
||||
m_post = post;
|
||||
m_callback = callback;
|
||||
m_pcallback = pcallback;
|
||||
m_api = api;
|
||||
m_in_progress = true;
|
||||
// log_write("started download :)\n");
|
||||
ueventSignal(&m_uevent);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ThreadFunc(void* p);
|
||||
|
||||
CURL* m_curl{};
|
||||
Thread m_thread{};
|
||||
std::string m_url{};
|
||||
std::string m_file{}; // if empty, downloads to buffer
|
||||
std::string m_post{}; // if empty, downloads to buffer
|
||||
DownloadCallback m_callback{};
|
||||
ProgressCallback m_pcallback{};
|
||||
Api m_api{};
|
||||
std::atomic_bool m_in_progress{};
|
||||
Mutex m_mutex{};
|
||||
UEvent m_uevent{};
|
||||
};
|
||||
|
||||
struct ThreadQueueEntry {
|
||||
std::string url;
|
||||
std::string file;
|
||||
std::string post;
|
||||
DownloadCallback callback;
|
||||
ProgressCallback pcallback;
|
||||
Api api;
|
||||
bool m_delete{};
|
||||
};
|
||||
|
||||
@@ -149,7 +289,7 @@ struct ThreadQueue {
|
||||
|
||||
auto Create() -> Result {
|
||||
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_SUCCEED();
|
||||
}
|
||||
@@ -160,34 +300,33 @@ struct ThreadQueue {
|
||||
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);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
ThreadQueueEntry entry{};
|
||||
entry.url = url;
|
||||
entry.file = file;
|
||||
entry.post = post;
|
||||
entry.callback = callback;
|
||||
entry.pcallback = pcallback;
|
||||
|
||||
switch (prio) {
|
||||
case DownloadPriority::Normal:
|
||||
m_entries.emplace_back(entry);
|
||||
switch (api.GetPriority()) {
|
||||
case Priority::Normal:
|
||||
m_entries.emplace_back(api);
|
||||
break;
|
||||
case DownloadPriority::High:
|
||||
m_entries.emplace_front(entry);
|
||||
case Priority::High:
|
||||
m_entries.emplace_front(api);
|
||||
break;
|
||||
}
|
||||
|
||||
ueventSignal(&m_uevent);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ThreadFunc(void* p);
|
||||
};
|
||||
|
||||
ThreadEntry g_threads[MAX_THREADS]{};
|
||||
ThreadQueue g_thread_queue;
|
||||
UrlCache g_url_cache;
|
||||
Cache g_cache;
|
||||
|
||||
void GetDownloadTempPath(fs::FsPath& buf) {
|
||||
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 {
|
||||
if (!g_running) {
|
||||
auto api = static_cast<Api*>(clientp);
|
||||
if (!g_running || api->GetToken().stop_requested()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// log_write("pcall called %u %u %u %u\n", dltotal, dlnow, ultotal, ulnow);
|
||||
auto callback = *static_cast<ProgressCallback*>(clientp);
|
||||
if (!callback(dltotal, dlnow, ultotal, ulnow)) {
|
||||
if (!api->GetOnProgress()(dltotal, dlnow, ultotal, ulnow)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -283,36 +422,65 @@ auto WriteFileCallback(void *contents, size_t size, size_t num_files, void *user
|
||||
return realsize;
|
||||
}
|
||||
|
||||
auto DownloadInternal(CURL* curl, DataStruct& chunk, ProgressCallback pcallback, const std::string& url, const std::string& file, const std::string& post) -> bool {
|
||||
fs::FsPath safe_buf;
|
||||
fs::FsPath tmp_buf;
|
||||
const bool has_file = !file.empty() && file != "";
|
||||
const bool has_post = !post.empty() && post != "";
|
||||
auto header_callback(char* b, size_t size, size_t nitems, void* userdata) -> size_t {
|
||||
auto header = static_cast<Header*>(userdata);
|
||||
const auto numbytes = size * nitems;
|
||||
|
||||
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) {
|
||||
std::strcpy(safe_buf, file.c_str());
|
||||
GetDownloadTempPath(tmp_buf);
|
||||
R_TRY_RESULT(fsOpenSdCardFileSystem(&chunk.fs), false);
|
||||
fs.CreateDirectoryRecursivelyWithPath(tmp_buf);
|
||||
|
||||
fs::CreateDirectoryRecursivelyWithPath(&chunk.fs, tmp_buf);
|
||||
|
||||
if (auto rc = fsFsCreateFile(&chunk.fs, tmp_buf, 0, 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
||||
log_write("failed to create file: %s\n", tmp_buf);
|
||||
return false;
|
||||
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);
|
||||
return {};
|
||||
}
|
||||
|
||||
if (R_FAILED(fsFsOpenFile(&chunk.fs, tmp_buf, FsOpenMode_Write|FsOpenMode_Append, &chunk.f))) {
|
||||
log_write("failed to open file: %s\n", tmp_buf);
|
||||
return false;
|
||||
if (R_FAILED(fs.OpenFile(tmp_buf, FsOpenMode_Write|FsOpenMode_Append, &chunk.f))) {
|
||||
log_write("failed to open file: %s\n", tmp_buf.s);
|
||||
return {};
|
||||
}
|
||||
|
||||
if (e.GetFlags() & Flag_Cache) {
|
||||
g_cache.get(e.GetPath(), header_in);
|
||||
}
|
||||
}
|
||||
|
||||
// reserve the first chunk
|
||||
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_FOLLOWLOCATION, 1L);
|
||||
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_SHARE, g_curl_share);
|
||||
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) {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_POSTFIELDS, post.c_str());
|
||||
log_write("setting post field: %s\n", post.c_str());
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_POSTFIELDS, e.GetFields().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.
|
||||
if (pcallback) {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &pcallback);
|
||||
if (e.GetOnProgress()) {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &e);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc2);
|
||||
} else {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc1);
|
||||
@@ -343,21 +538,34 @@ auto DownloadInternal(CURL* curl, DataStruct& chunk, ProgressCallback pcallback,
|
||||
const auto res = curl_easy_perform(curl);
|
||||
bool success = res == CURLE_OK;
|
||||
|
||||
long http_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
|
||||
if (has_file) {
|
||||
ON_SCOPE_EXIT( fs.DeleteFile(tmp_buf) );
|
||||
if (res == CURLE_OK && chunk.offset) {
|
||||
fsFileWrite(&chunk.f, chunk.file_offset, chunk.data.data(), chunk.offset, FsWriteOption_None);
|
||||
}
|
||||
|
||||
fsFileClose(&chunk.f);
|
||||
if (res != CURLE_OK) {
|
||||
fsFsDeleteFile(&chunk.fs, tmp_buf);
|
||||
} else {
|
||||
fsFsDeleteFile(&chunk.fs, safe_buf);
|
||||
fs::CreateDirectoryRecursivelyWithPath(&chunk.fs, safe_buf);
|
||||
if (R_FAILED(fsFsRenameFile(&chunk.fs, tmp_buf, safe_buf))) {
|
||||
fsFsDeleteFile(&chunk.fs, tmp_buf);
|
||||
success = false;
|
||||
|
||||
if (res == CURLE_OK) {
|
||||
if (http_code == 304) {
|
||||
log_write("cached download: %s\n", e.GetUrl().c_str());
|
||||
} else {
|
||||
log_write("un-cached download: %s code: %lu\n", e.GetUrl().c_str(), http_code);
|
||||
if (e.GetFlags() & Flag_Cache) {
|
||||
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 {
|
||||
// empty data if we failed
|
||||
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));
|
||||
return success;
|
||||
log_write("Downloaded %s %s\n", e.GetUrl().c_str(), curl_easy_strerror(res));
|
||||
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();
|
||||
if (!curl) {
|
||||
log_write("curl init failed\n");
|
||||
return false;
|
||||
return {};
|
||||
}
|
||||
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);
|
||||
while (g_running) {
|
||||
auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX);
|
||||
// log_write("woke up\n");
|
||||
if (!g_running) {
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
if (R_FAILED(rc)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DataStruct chunk;
|
||||
#if 1
|
||||
const auto result = DownloadInternal(data->m_curl, chunk, data->m_pcallback, data->m_url, data->m_file, data->m_post);
|
||||
if (g_running) {
|
||||
DownloadEventData event_data{data->m_callback, std::move(chunk.data), result};
|
||||
const auto result = DownloadInternal(data->m_curl, data->m_api);
|
||||
if (g_running && data->m_api.GetOnComplete() && !data->m_api.GetToken().stop_requested()) {
|
||||
const DownloadEventData event_data{data->m_api.GetOnComplete(), result, data->m_api.GetToken()};
|
||||
evman::push(std::move(event_data), false);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
// mutexLock(&data->m_mutex);
|
||||
// ON_SCOPE_EXIT(mutexUnlock(&data->m_mutex));
|
||||
|
||||
data->m_in_progress = false;
|
||||
// notify the queue that there's a space free
|
||||
@@ -411,7 +621,7 @@ void DownloadThread(void* p) {
|
||||
log_write("exited download thread\n");
|
||||
}
|
||||
|
||||
void DownloadThreadQueue(void* p) {
|
||||
void ThreadQueue::ThreadFunc(void* p) {
|
||||
auto data = static_cast<ThreadQueue*>(p);
|
||||
while (g_running) {
|
||||
auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX);
|
||||
@@ -444,7 +654,7 @@ void DownloadThreadQueue(void* p) {
|
||||
}
|
||||
|
||||
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");
|
||||
// mark entry for deletion
|
||||
entry.m_delete = true;
|
||||
@@ -454,10 +664,6 @@ void DownloadThreadQueue(void* p) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!g_running) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!keep_going) {
|
||||
break;
|
||||
}
|
||||
@@ -467,29 +673,14 @@ void DownloadThreadQueue(void* p) {
|
||||
for (u32 i = 0; i < pop_count; i++) {
|
||||
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");
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
auto DownloadInit() -> bool {
|
||||
auto Init() -> bool {
|
||||
if (CURLE_OK != curl_global_init(CURL_GLOBAL_DEFAULT)) {
|
||||
return false;
|
||||
}
|
||||
@@ -518,10 +709,15 @@ auto DownloadInit() -> bool {
|
||||
}
|
||||
|
||||
log_write("finished creating threads\n");
|
||||
|
||||
if (!g_cache.init()) {
|
||||
log_write("failed to init json cache\n");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DownloadExit() {
|
||||
void Exit() {
|
||||
g_running = false;
|
||||
|
||||
g_thread_queue.Close();
|
||||
@@ -536,75 +732,29 @@ void DownloadExit() {
|
||||
}
|
||||
|
||||
curl_global_cleanup();
|
||||
g_cache.exit();
|
||||
}
|
||||
|
||||
auto DownloadMemory(const std::string& url, const std::string& post, ProgressCallback pcallback) -> std::vector<u8> {
|
||||
if (g_url_cache.AddToCache(url)) {
|
||||
DataStruct chunk{};
|
||||
if (DownloadInternal(chunk, pcallback, url, "", post)) {
|
||||
return chunk.data;
|
||||
}
|
||||
auto ToMemory(const Api& e) -> ApiResult {
|
||||
if (!e.GetPath().empty()) {
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
return DownloadInternal(e);
|
||||
}
|
||||
|
||||
auto DownloadFile(const std::string& url, const std::string& out, const std::string& post, ProgressCallback pcallback) -> bool {
|
||||
if (g_url_cache.AddToCache(url)) {
|
||||
DataStruct chunk{};
|
||||
if (DownloadInternal(chunk, pcallback, url, out, post)) {
|
||||
return true;
|
||||
}
|
||||
auto ToFile(const Api& e) -> ApiResult {
|
||||
if (e.GetPath().empty()) {
|
||||
return {};
|
||||
}
|
||||
return false;
|
||||
return DownloadInternal(e);
|
||||
}
|
||||
|
||||
auto DownloadMemoryAsync(const std::string& url, const std::string& post, DownloadCallback callback, ProgressCallback pcallback, DownloadPriority prio) -> bool {
|
||||
#if USE_THREAD_QUEUE
|
||||
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 ToMemoryAsync(const Api& api) -> bool {
|
||||
return g_thread_queue.Add(api);
|
||||
}
|
||||
|
||||
auto DownloadFileAsync(const std::string& url, const std::string& out, const std::string& post, DownloadCallback callback, ProgressCallback pcallback, DownloadPriority prio) -> bool {
|
||||
#if USE_THREAD_QUEUE
|
||||
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
|
||||
auto ToFileAsync(const Api& e) -> bool {
|
||||
return g_thread_queue.Add(e);
|
||||
}
|
||||
|
||||
void DownloadClearCache(const std::string& url) {
|
||||
g_url_cache.AddToCache(url);
|
||||
g_url_cache.RemoveFromCache(url);
|
||||
}
|
||||
|
||||
} // 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) {
|
||||
log_write("failed to create folder: %s\n", path);
|
||||
log_write("failed to create folder: %s\n", path.s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& _path,
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
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());
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
#include "ftpsrv_helper.hpp"
|
||||
#include <ftpsrv.h>
|
||||
#include <ftpsrv_vfs.h>
|
||||
|
||||
#include "app.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <mutex>
|
||||
#include <algorithm>
|
||||
#include <minIni.h>
|
||||
#include <ftpsrv.h>
|
||||
#include <ftpsrv_vfs.h>
|
||||
#include <nx/vfs_nx.h>
|
||||
#include <nx/utils.h>
|
||||
|
||||
namespace {
|
||||
|
||||
const char* INI_PATH = "/config/ftpsrv/config.ini";
|
||||
FtpSrvConfig g_ftpsrv_config = {0};
|
||||
volatile bool g_should_exit = false;
|
||||
bool g_is_running{false};
|
||||
Thread g_thread;
|
||||
std::mutex g_mutex{};
|
||||
FsFileSystem* g_fs;
|
||||
|
||||
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
|
||||
sphaira::App::NotifyFlashLed();
|
||||
@@ -28,32 +29,6 @@ void ftp_progress_callback(void) {
|
||||
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) {
|
||||
while (!g_should_exit) {
|
||||
ftpsrv_init(&g_ftpsrv_config);
|
||||
@@ -77,16 +52,51 @@ bool Init() {
|
||||
return false;
|
||||
}
|
||||
|
||||
g_fs = fsdevGetDeviceFileSystem("sdmc");
|
||||
if (R_FAILED(fsdev_wrapMountSdmc())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
g_ftpsrv_config.log_callback = ftp_log_callback;
|
||||
g_ftpsrv_config.progress_callback = ftp_progress_callback;
|
||||
g_ftpsrv_config.anon = true;
|
||||
g_ftpsrv_config.timeout = 15;
|
||||
g_ftpsrv_config.port = 5000;
|
||||
g_ftpsrv_config.anon = ini_getbool("Login", "anon", 0, INI_PATH);
|
||||
int user_len = ini_gets("Login", "user", "", g_ftpsrv_config.user, sizeof(g_ftpsrv_config.user), INI_PATH);
|
||||
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;
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
@@ -108,232 +118,24 @@ void Exit() {
|
||||
g_should_exit = true;
|
||||
threadWaitForExit(&g_thread);
|
||||
threadClose(&g_thread);
|
||||
|
||||
vfs_nx_exit();
|
||||
fsdev_wrapUnmountAll();
|
||||
}
|
||||
|
||||
} // namespace sphaira::ftpsrv
|
||||
|
||||
extern "C" {
|
||||
|
||||
#define VFS_NX_BUFFER_IO 1
|
||||
|
||||
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);
|
||||
void log_file_write(const char* msg) {
|
||||
log_write("%s", msg);
|
||||
}
|
||||
|
||||
int ftp_vfs_read(struct FtpVfsFile* f, void* buf, size_t size) {
|
||||
Result rc;
|
||||
|
||||
#if VFS_NX_BUFFER_IO
|
||||
if (f->buf_off == f->buf_size) {
|
||||
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";
|
||||
void log_file_fwrite(const char* fmt, ...) {
|
||||
std::va_list v{};
|
||||
va_start(v, fmt);
|
||||
log_write_arg(fmt, v);
|
||||
va_end(v);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -21,7 +21,7 @@ std::string get_internal(const char* str, size_t len) {
|
||||
}
|
||||
|
||||
// add default entry
|
||||
g_tr_cache.emplace(kkey, kkey);
|
||||
const auto it = g_tr_cache.emplace(kkey, kkey).first;
|
||||
|
||||
if (!json || !root) {
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ bool init(long index) {
|
||||
case 10: setLanguage = SetLanguage_PT; break; // "Portuguese"
|
||||
case 11: setLanguage = SetLanguage_RU; break; // "Russian"
|
||||
case 12: lang_name = "se"; break; // "Swedish"
|
||||
case 13: lang_name = "vi"; break; // "Vietnamese"
|
||||
}
|
||||
|
||||
switch (setLanguage) {
|
||||
@@ -91,6 +92,7 @@ bool init(long index) {
|
||||
case SetLanguage_PT: lang_name = "pt"; break;
|
||||
case SetLanguage_RU: lang_name = "ru"; break;
|
||||
case SetLanguage_ZHTW: lang_name = "zh"; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
const fs::FsPath sdmc_path = "/config/sphaira/i18n/" + lang_name + ".json";
|
||||
|
||||
@@ -4,14 +4,19 @@
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wunused-function"
|
||||
#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_STATIC
|
||||
#define STBI_WRITE_NO_STDIO
|
||||
#include "stb_image_write.h"
|
||||
#include <stb_image_write.h>
|
||||
#define STB_IMAGE_RESIZE_IMPLEMENTATION
|
||||
#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
|
||||
|
||||
|
||||
@@ -14,6 +14,16 @@ std::FILE* file{};
|
||||
int nxlink_socket{};
|
||||
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
|
||||
|
||||
auto log_file_init() -> bool {
|
||||
@@ -60,13 +70,17 @@ void log_write(const char* s, ...) {
|
||||
|
||||
std::va_list v{};
|
||||
va_start(v, s);
|
||||
if (file) {
|
||||
std::vfprintf(file, s, v);
|
||||
std::fflush(file);
|
||||
}
|
||||
if (nxlink_socket) {
|
||||
std::vprintf(s, v);
|
||||
}
|
||||
log_write_arg_internal(s, 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
|
||||
|
||||
@@ -63,6 +63,8 @@ void userAppInit(void) {
|
||||
diagAbortWithResult(rc);
|
||||
if (R_FAILED(rc = hidsysInitialize()))
|
||||
diagAbortWithResult(rc);
|
||||
if (R_FAILED(rc = ncmInitialize()))
|
||||
diagAbortWithResult(rc);
|
||||
|
||||
log_nxlink_init();
|
||||
}
|
||||
@@ -70,6 +72,7 @@ void userAppInit(void) {
|
||||
void userAppExit(void) {
|
||||
log_nxlink_exit();
|
||||
|
||||
ncmExit();
|
||||
hidsysExit();
|
||||
setExit();
|
||||
accountExit();
|
||||
|
||||
@@ -124,7 +124,7 @@ auto nro_scan_internal(const fs::FsPath& path, std::vector<NroEntry>& nros, bool
|
||||
if (e.type == FsDirEntryType_Dir) {
|
||||
// assert(!root && "dir should only be scanned on non-root!");
|
||||
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
|
||||
NroEntry entry;
|
||||
@@ -133,12 +133,12 @@ auto nro_scan_internal(const fs::FsPath& path, std::vector<NroEntry>& nros, bool
|
||||
nros.emplace_back(entry);
|
||||
} else {
|
||||
// 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);
|
||||
}
|
||||
} else if (e.type == FsDirEntryType_File && std::string_view{e.name}.ends_with(".nro")) {
|
||||
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;
|
||||
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();
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -301,7 +301,7 @@ void loop(void* args) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log_write("got name: %s\n", name);
|
||||
log_write("got name: %s\n", name.s);
|
||||
|
||||
u32 filesize{};
|
||||
if (!recvall(connfd, &filesize, sizeof(filesize))) {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "app.hpp"
|
||||
#include "ui/progress_box.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
namespace sphaira {
|
||||
namespace {
|
||||
@@ -191,6 +192,40 @@ struct NpdmMeta {
|
||||
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 {
|
||||
char title_name[0x10]{"Application"};
|
||||
char product_code[0x10]{};
|
||||
@@ -578,17 +613,56 @@ auto romfs_build(const FileEntries& entries, u64 *out_size) -> std::vector<u8> {
|
||||
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
|
||||
void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
|
||||
NpdmMeta meta{};
|
||||
NpdmAci0 aci0{};
|
||||
NpdmAcid acid{};
|
||||
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
|
||||
std::memcpy(npdm.data() + 0x20, &patch.title_name, sizeof(patch.title_name));
|
||||
std::memcpy(npdm.data() + 0x30, &patch.product_code, sizeof(patch.product_code));
|
||||
std::memcpy(npdm.data() + meta.aci0_offset + 0x10, &patch.tid, sizeof(patch.tid));
|
||||
std::memcpy(npdm.data() + meta.acid_offset + 0x210, &patch.tid, sizeof(patch.tid));
|
||||
std::memcpy(npdm.data() + meta.acid_offset + 0x218, &patch.tid, sizeof(patch.tid));
|
||||
std::memcpy(meta.title_name, &patch.title_name, sizeof(meta.title_name));
|
||||
std::memcpy(meta.product_code, &patch.product_code, sizeof(patch.product_code));
|
||||
aci0.program_id = patch.tid;
|
||||
acid.program_id_min = 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,7,1)) {
|
||||
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) {
|
||||
|
||||
115
sphaira/source/ui/bubbles.cpp
Normal file
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);
|
||||
}
|
||||
|
||||
auto ErrorBox::OnLayoutChange() -> void {
|
||||
|
||||
}
|
||||
|
||||
auto ErrorBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
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 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, 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, 325, 23, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "%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, 415, 20, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::SILVER, "https://github.com/ITotalJustice/sphaira/issues");
|
||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, box, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
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, 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->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->GetColour(ThemeEntryID_TEXT), "%s", m_message.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, theme->GetColour(ThemeEntryID_TEXT_INFO), "https://github.com/ITotalJustice/sphaira/issues");
|
||||
gfx::drawRectOutline(vg, theme, 4.f, box);
|
||||
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
|
||||
|
||||
189
sphaira/source/ui/list.cpp
Normal file
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 <cstring>
|
||||
#include <yyjson.h>
|
||||
#include <nanovg/stb_image.h>
|
||||
#include <stb_image.h>
|
||||
#include <minizip/unzip.h>
|
||||
#include <mbedtls/md5.h>
|
||||
#include <ranges>
|
||||
@@ -65,60 +65,27 @@ auto BuildIconUrl(const Entry& e) -> std::string {
|
||||
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 {
|
||||
char out[0x100];
|
||||
std::snprintf(out, sizeof(out), "%s/packages/%s/screen.png", URL_BASE, e.name.c_str());
|
||||
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 {
|
||||
char out[0x100];
|
||||
std::snprintf(out, sizeof(out), "%s/zips/%s.zip", URL_BASE, e.name.c_str());
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
auto BuildBannerCachePath(const Entry& e) -> fs::FsPath {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -206,17 +173,17 @@ auto LoadAndParseManifest(const Entry& e) -> ManifestEntries {
|
||||
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
|
||||
if (image.image) {
|
||||
log_write("warning, tried to load image: %s when already loaded\n", path);
|
||||
return;
|
||||
// log_write("warning, tried to load image: %s when already loaded\n", path);
|
||||
return true;
|
||||
}
|
||||
auto vg = App::GetVg();
|
||||
|
||||
std::vector<u8> 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 {
|
||||
int channels_in_file;
|
||||
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) {
|
||||
image.state = ImageDownloadState::Failed;
|
||||
log_write("failed to load image from file: %s\n", path);
|
||||
log_write("failed to load image from file: %s\n", path.s);
|
||||
return false;
|
||||
} else {
|
||||
// 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)) {
|
||||
fs::FsStdio fs;
|
||||
EntryLoadImageFile(fs, path, image);
|
||||
return EntryLoadImageFile(fs, path, image);
|
||||
} else {
|
||||
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) {
|
||||
crop = true;
|
||||
nvgSave(vg);
|
||||
nvgScissor(vg, x, y, w, h);
|
||||
nvgIntersectScissor(vg, x, y, w, h);
|
||||
}
|
||||
if (rounded_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);
|
||||
}
|
||||
|
||||
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 str = std::to_string(value);
|
||||
u32 inc = 3;
|
||||
@@ -321,7 +269,6 @@ auto AppDlToStr(u32 value) -> std::string {
|
||||
|
||||
void ReadFromInfoJson(Entry& e) {
|
||||
const auto info_path = BuildInfoCachePath(e);
|
||||
const auto manifest_path = BuildManifestCachePath(e);
|
||||
|
||||
yyjson_read_err 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);
|
||||
// this will handle read only files, ie, hbmenu.nro
|
||||
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 {
|
||||
log_write("deleted file: %s\n", safe_buf);
|
||||
log_write("deleted file: %s\n", safe_buf.s);
|
||||
// todo: delete empty directories!
|
||||
// fs::delete_directory(safe_buf);
|
||||
}
|
||||
@@ -372,9 +319,9 @@ auto UninstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
||||
const auto dir = BuildPackageCachePath(entry);
|
||||
pbox->NewTransfer("Removing "_i18n + 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 {
|
||||
log_write("deleted: %s\n", dir);
|
||||
log_write("deleted: %s\n", dir.s);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -398,17 +345,13 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
||||
log_write("starting download\n");
|
||||
|
||||
const auto url = BuildZipUrl(entry);
|
||||
if (!DownloadFile(url, zip_out, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
|
||||
if (pbox->ShouldExit()) {
|
||||
return false;
|
||||
}
|
||||
pbox->UpdateTransfer(dlnow, dltotal);
|
||||
return true;
|
||||
})) {
|
||||
if (!curl::Api().ToFile(
|
||||
curl::Url{url},
|
||||
curl::Path{zip_out},
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||
).success) {
|
||||
log_write("error with download\n");
|
||||
// push popup error box
|
||||
return false;
|
||||
// return appletEnterFatalSection();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,7 +424,7 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
||||
if (!pbox->ShouldExit()) {
|
||||
auto zfile = unzOpen64(zip_out);
|
||||
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;
|
||||
}
|
||||
ON_SCOPE_EXIT(unzClose(zfile));
|
||||
@@ -524,7 +467,7 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
||||
pbox->NewTransfer(inzip);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -545,23 +488,23 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
||||
}
|
||||
|
||||
// create directories
|
||||
fs.CreateDirectoryRecursivelyWithPath(output, true);
|
||||
fs.CreateDirectoryRecursivelyWithPath(output);
|
||||
|
||||
Result rc;
|
||||
if (R_FAILED(rc = fs.CreateFile(output, info.uncompressed_size, 0, true)) && rc != FsError_PathAlreadyExists) {
|
||||
log_write("failed to create file: %s 0x%04X\n", output, rc);
|
||||
if (R_FAILED(rc = fs.CreateFile(output, info.uncompressed_size, 0)) && rc != FsError_PathAlreadyExists) {
|
||||
log_write("failed to create file: %s 0x%04X\n", output.s, rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile 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;
|
||||
}
|
||||
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", output, rc);
|
||||
log_write("failed to set file size: %s 0x%04X\n", output.s, rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -574,12 +517,12 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
||||
|
||||
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -637,10 +580,10 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
||||
if (!found) {
|
||||
const auto safe_buf = fs::AppendPath("/", old_entry.path);
|
||||
// std::strcat(safe_buf, old_entry.path);
|
||||
if (R_FAILED(fs.DeleteFile(safe_buf, true))) {
|
||||
log_write("failed to delete: %s\n", safe_buf);
|
||||
if (R_FAILED(fs.DeleteFile(safe_buf))) {
|
||||
log_write("failed to delete: %s\n", safe_buf.s);
|
||||
} 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 file = BuildFeedbackCachePath(m_entry);
|
||||
|
||||
DownloadFileAsync(URL_POST_FEEDBACK, file, post, [](std::vector<u8>& data, bool success){
|
||||
if (success) {
|
||||
log_write("got feedback!\n");
|
||||
} else {
|
||||
log_write("failed to send feedback :(");
|
||||
curl::Api().ToAsync(
|
||||
curl::Url{URL_POST_FEEDBACK},
|
||||
curl::Path{file},
|
||||
curl::Fields{post},
|
||||
curl::StopToken{this->GetToken()},
|
||||
curl::OnComplete{[](auto& result){
|
||||
if (result.success) {
|
||||
log_write("got feedback!\n");
|
||||
} else {
|
||||
log_write("failed to send feedback :(");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, true));
|
||||
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](){
|
||||
SetPop();
|
||||
}})
|
||||
);
|
||||
|
||||
// SidebarEntryCallback
|
||||
// if (!m_entries_current.empty() && !GetEntry().url.empty()) {
|
||||
// options->Add(std::make_shared<SidebarEntryCallback>("Show Release Page"))
|
||||
// }
|
||||
|
||||
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_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 url = BuildBannerUrl(m_entry);
|
||||
|
||||
if (fs::FsNativeSd().FileExists(path)) {
|
||||
EntryLoadImageFile(path, m_banner);
|
||||
}
|
||||
m_banner.cached = EntryLoadImageFile(path, m_banner);
|
||||
|
||||
// race condition if we pop the widget before the download completes
|
||||
if (!m_banner.image) {
|
||||
DownloadFileAsync(url, path, "", [this, path](std::vector<u8>& data, bool success){
|
||||
if (success) {
|
||||
EntryLoadImageFile(path, m_banner);
|
||||
curl::Api().ToFileAsync(
|
||||
curl::Url{url},
|
||||
curl::Path{path},
|
||||
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.description);
|
||||
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() {
|
||||
@@ -772,49 +701,35 @@ void EntryMenu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
constexpr Vec4 line_vec(30, 86, 1220, 646);
|
||||
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 grid_vec(icon_vec.x - 50, line_vec.y + 1, line_vec.w, line_vec.h - line_vec.y - 1);
|
||||
|
||||
// nvgSave(vg);
|
||||
// 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));
|
||||
|
||||
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_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;
|
||||
float text_start_y = 218 + line_vec.y;
|
||||
const float text_inc_y = 32;
|
||||
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::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());
|
||||
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());
|
||||
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;
|
||||
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;
|
||||
// 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());
|
||||
// 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);
|
||||
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, "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;
|
||||
|
||||
// for (const auto& option : m_options) {
|
||||
const auto& text_col = theme->elements[ThemeEntryID_TEXT].colour;
|
||||
|
||||
// todo: rewrite this mess and use list
|
||||
constexpr float mm = 0;//20;
|
||||
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;
|
||||
float y = 1.f + text_start_y + (text_inc_y * 3) ;
|
||||
const float h = block.h;
|
||||
@@ -825,15 +740,10 @@ void EntryMenu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
auto text_id = ThemeEntryID_TEXT;
|
||||
if (m_index == i) {
|
||||
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 {
|
||||
// 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::drawRectOutline(vg, theme, 4.f, Vec4{x, y, w, h});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -863,6 +773,7 @@ void EntryMenu::UpdateOptions() {
|
||||
return InstallApp(pbox, m_entry);
|
||||
}, [this](bool success){
|
||||
if (success) {
|
||||
App::Notify("Downloaded "_i18n + m_entry.title);
|
||||
m_entry.status = EntryStatus::Installed;
|
||||
m_menu.SetDirty();
|
||||
UpdateOptions();
|
||||
@@ -875,6 +786,7 @@ void EntryMenu::UpdateOptions() {
|
||||
return UninstallApp(pbox, m_entry);
|
||||
}, [this](bool success){
|
||||
if (success) {
|
||||
App::Notify("Removed "_i18n + m_entry.title);
|
||||
m_entry.status = EntryStatus::Get;
|
||||
m_menu.SetDirty();
|
||||
UpdateOptions();
|
||||
@@ -913,7 +825,7 @@ void EntryMenu::UpdateOptions() {
|
||||
SetIndex(0);
|
||||
}
|
||||
|
||||
void EntryMenu::SetIndex(std::size_t index) {
|
||||
void EntryMenu::SetIndex(s64 index) {
|
||||
m_index = index;
|
||||
const auto option = m_options[m_index];
|
||||
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/screens");
|
||||
|
||||
// m_span = m_entries;
|
||||
|
||||
this->SetActions(
|
||||
std::make_pair(Button::RIGHT, Action{[this](){
|
||||
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](){
|
||||
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);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
if (m_entries_current.empty()) {
|
||||
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())) {
|
||||
if (m_list->ScrollUp(m_index, 3, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::L2, Action{(u8)ActionType::HELD, [this](){
|
||||
if (m_entries.empty()) {
|
||||
return;
|
||||
std::make_pair(Button::R2, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 9, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
|
||||
if (m_index >= 9) {
|
||||
SetIndex(m_index - 9);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
while (m_index < m_start) {
|
||||
// log_write("moved up\n");
|
||||
m_start -= 3;
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::L2, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 9, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
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);
|
||||
|
||||
SidebarEntryArray::Items order_items;
|
||||
order_items.push_back("Decending"_i18n);
|
||||
order_items.push_back("Descending"_i18n);
|
||||
order_items.push_back("Ascending"_i18n);
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Filter"_i18n, filter_items, [this, filter_items](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);
|
||||
}, (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);
|
||||
}, (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);
|
||||
}, (std::size_t)m_order));
|
||||
}, (s64)m_order));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Search"_i18n, [this](){
|
||||
std::string out;
|
||||
@@ -1058,52 +950,13 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
|
||||
);
|
||||
|
||||
m_repo_download_state = ImageDownloadState::Progress;
|
||||
#if 0
|
||||
DownloadMemoryAsync(URL_JSON, [this](std::vector<u8>& data, bool success){
|
||||
if (success) {
|
||||
repo_json = data;
|
||||
repo_json.push_back('\0');
|
||||
m_repo_download_state = ImageDownloadState::Done;
|
||||
if (HasFocus()) {
|
||||
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) {
|
||||
curl::Api().ToFileAsync(
|
||||
curl::Url{URL_JSON},
|
||||
curl::Path{REPO_PATH},
|
||||
curl::Flags{curl::Flag_Cache},
|
||||
curl::StopToken{this->GetToken()},
|
||||
curl::OnComplete{[this](auto& result){
|
||||
if (result.success) {
|
||||
m_repo_download_state = ImageDownloadState::Done;
|
||||
if (HasFocus()) {
|
||||
ScanHomebrew();
|
||||
@@ -1111,16 +964,16 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
|
||||
} else {
|
||||
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_sort = (SortType)ini_getl(INI_SECTION, "sort", m_sort, App::CONFIG_PATH);
|
||||
m_order = (OrderType)ini_getl(INI_SECTION, "order", m_order, App::CONFIG_PATH);
|
||||
|
||||
const Vec4 v{75, 110, 370, 155};
|
||||
const Vec2 pad{10, 10};
|
||||
m_list = std::make_unique<List>(3, 9, m_pos, v, pad);
|
||||
Sort();
|
||||
}
|
||||
|
||||
@@ -1130,112 +983,132 @@ Menu::~Menu() {
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
m_list->OnUpdate(controller, touch, m_entries_current.size(), [this](auto i) {
|
||||
if (m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
SetIndex(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const u64 SCROLL = m_start;
|
||||
const u64 max_entry_display = 9;
|
||||
const u64 nro_total = m_entries_current.size();
|
||||
const u64 cursor_pos = m_index;
|
||||
// max images per frame, in order to not hit io / gpu too hard.
|
||||
const int image_load_max = 2;
|
||||
int image_load_count = 0;
|
||||
|
||||
// only draw scrollbar if needed
|
||||
if (nro_total > max_entry_display) {
|
||||
const auto scrollbar_size = 500.f;
|
||||
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);
|
||||
}
|
||||
m_list->Draw(vg, theme, m_entries_current.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
|
||||
const auto& [x, y, w, h] = v;
|
||||
const auto index = m_entries_current[pos];
|
||||
auto& e = m_entries[index];
|
||||
auto& image = e.image;
|
||||
|
||||
for (u64 i = 0, pos = SCROLL, y = 110, w = 370, h = 155; pos < nro_total && i < max_entry_display; y += h + 10) {
|
||||
for (u64 j = 0, x = 75; j < 3 && pos < nro_total && i < max_entry_display; j++, i++, pos++, x += w + 10) {
|
||||
const auto index = m_entries_current[pos];
|
||||
auto& e = m_entries[index];
|
||||
|
||||
// lazy load image
|
||||
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;
|
||||
// try and load cached image.
|
||||
if (image_load_count < image_load_max && !image.image && !image.tried_cache) {
|
||||
image.tried_cache = true;
|
||||
image.cached = EntryLoadImageFile(BuildIconCachePath(e), image);
|
||||
if (image.cached) {
|
||||
image_load_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
@@ -1264,21 +1137,16 @@ void Menu::OnFocusGained() {
|
||||
if (m_dirty) {
|
||||
m_dirty = false;
|
||||
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();
|
||||
|
||||
for (u32 i = 0; i < m_entries_current.size(); i++) {
|
||||
if (current_entry.name == m_entries[m_entries_current[i]].name) {
|
||||
SetIndex(i);
|
||||
if (i >= 9) {
|
||||
m_start = (i - 9) / 3 * 3 + 3;
|
||||
m_list->SetYoff((((i - 9) + 3) / 3) * m_list->GetMaxY());
|
||||
} else {
|
||||
m_start = 0;
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
log_write("\nnew index: %zu start: %zu\n", m_index, m_start);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1286,10 +1154,10 @@ void Menu::OnFocusGained() {
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::SetIndex(std::size_t index) {
|
||||
void Menu::SetIndex(s64 index) {
|
||||
m_index = 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()));
|
||||
@@ -1392,7 +1260,7 @@ void Menu::Sort() {
|
||||
case SortType_Updated: {
|
||||
if (lhs.updated_num == rhs.updated_num) {
|
||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
||||
} else if (m_order == OrderType_Decending) {
|
||||
} else if (m_order == OrderType_Descending) {
|
||||
return lhs.updated_num > rhs.updated_num;
|
||||
} else {
|
||||
return lhs.updated_num < rhs.updated_num;
|
||||
@@ -1401,7 +1269,7 @@ void Menu::Sort() {
|
||||
case SortType_Downloads: {
|
||||
if (lhs.app_dls == rhs.app_dls) {
|
||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
||||
} else if (m_order == OrderType_Decending) {
|
||||
} else if (m_order == OrderType_Descending) {
|
||||
return lhs.app_dls > rhs.app_dls;
|
||||
} else {
|
||||
return lhs.app_dls < rhs.app_dls;
|
||||
@@ -1410,14 +1278,14 @@ void Menu::Sort() {
|
||||
case SortType_Size: {
|
||||
if (lhs.extracted == rhs.extracted) {
|
||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
||||
} else if (m_order == OrderType_Decending) {
|
||||
} else if (m_order == OrderType_Descending) {
|
||||
return lhs.extracted > rhs.extracted;
|
||||
} else {
|
||||
return lhs.extracted < rhs.extracted;
|
||||
}
|
||||
} break;
|
||||
case SortType_Alphabetical: {
|
||||
if (m_order == OrderType_Decending) {
|
||||
if (m_order == OrderType_Descending) {
|
||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
||||
} else {
|
||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) > 0;
|
||||
@@ -1484,9 +1352,10 @@ void Menu::SetSearch(const std::string& term) {
|
||||
SetFilter(m_filter);
|
||||
SetIndex(m_entry_search_jump_back);
|
||||
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 {
|
||||
m_start = 0;
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
}});
|
||||
|
||||
@@ -1517,11 +1386,12 @@ void Menu::SetAuthor() {
|
||||
} else {
|
||||
SetFilter(m_filter);
|
||||
}
|
||||
|
||||
SetIndex(m_entry_author_jump_back);
|
||||
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 {
|
||||
m_start = 0;
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
}});
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
526
sphaira/source/ui/menus/ghdl.cpp
Normal file
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 {
|
||||
fs::FsPath out{};
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -43,27 +43,23 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
if (m_index < (m_entries.size() - 1)) {
|
||||
if (m_index < (m_entries.size() - 3)) {
|
||||
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;
|
||||
}
|
||||
if (m_list->ScrollDown(m_index, 3, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
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;
|
||||
}
|
||||
if (m_list->ScrollUp(m_index, 3, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::R2, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 9, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::L2, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 9, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::A, Action{"Launch"_i18n, [this](){
|
||||
@@ -87,15 +83,15 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
||||
sort_items.push_back("Size (Star)"_i18n);
|
||||
|
||||
SidebarEntryArray::Items order_items;
|
||||
order_items.push_back("Decending"_i18n);
|
||||
order_items.push_back("Descending"_i18n);
|
||||
order_items.push_back("Ascending"_i18n);
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](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);
|
||||
SortAndFindLastFile();
|
||||
}, 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);
|
||||
SortAndFindLastFile();
|
||||
}, 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() {
|
||||
@@ -157,68 +157,71 @@ 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 u64 SCROLL = m_start;
|
||||
const u64 max_entry_display = 9;
|
||||
const u64 nro_total = m_entries.size();
|
||||
const u64 cursor_pos = m_index;
|
||||
fs::FsNativeSd fs;
|
||||
// max images per frame, in order to not hit io / gpu too hard.
|
||||
const int image_load_max = 2;
|
||||
int image_load_count = 0;
|
||||
|
||||
// only draw scrollbar if needed
|
||||
if (nro_total > max_entry_display) {
|
||||
const auto scrollbar_size = 500.f;
|
||||
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);
|
||||
}
|
||||
m_list->Draw(vg, theme, m_entries.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
|
||||
const auto& [x, y, w, h] = v;
|
||||
auto& e = m_entries[pos];
|
||||
|
||||
for (u64 i = 0, pos = SCROLL, y = 110, w = 370, h = 155; pos < nro_total && i < max_entry_display; y += h + 10) {
|
||||
for (u64 j = 0, x = 75; j < 3 && pos < nro_total && i < max_entry_display; j++, i++, pos++, x += w + 10) {
|
||||
auto& e = m_entries[pos];
|
||||
|
||||
// lazy load image
|
||||
if (!e.image && e.icon.empty() && e.icon_size && e.icon_offset) {
|
||||
e.icon = nro_get_icon(e.path, e.icon_size, e.icon_offset);
|
||||
if (!e.icon.empty()) {
|
||||
e.image = nvgCreateImageMem(vg, 0, e.icon.data(), e.icon.size());
|
||||
// lazy load image
|
||||
if (image_load_count < image_load_max) {
|
||||
if (!e.image && e.icon_size && e.icon_offset) {
|
||||
// NOTE: it seems that images can be any size. SuperTux uses a 1024x1024
|
||||
// ~300Kb image, which takes a few frames to completely load.
|
||||
// really, switch-tools should handle this by resizing the image before
|
||||
// adding it to the nro, as well as validate its a valid jpeg.
|
||||
const auto icon = nro_get_icon(e.path, e.icon_size, e.icon_offset);
|
||||
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() {
|
||||
@@ -228,14 +231,12 @@ void Menu::OnFocusGained() {
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::SetIndex(std::size_t index) {
|
||||
void Menu::SetIndex(s64 index) {
|
||||
m_index = index;
|
||||
if (!m_index) {
|
||||
m_start = 0;
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
|
||||
const auto& e = m_entries[m_index];
|
||||
|
||||
if (IsStarEnabled()) {
|
||||
const auto star_path = GenerateStarPath(m_entries[m_index].path);
|
||||
if (fs::FsNativeSd().FileExists(star_path)) {
|
||||
@@ -266,18 +267,18 @@ void Menu::SetIndex(std::size_t index) {
|
||||
|
||||
void Menu::InstallHomebrew() {
|
||||
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() {
|
||||
TimeStamp ts;
|
||||
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 {
|
||||
std::vector<NroEntry>& entires;
|
||||
Hbini* ini;
|
||||
std::string last_section;
|
||||
Hbini* ini{};
|
||||
std::string last_section{};
|
||||
} ini_user{ m_entries };
|
||||
|
||||
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;
|
||||
for (auto& p : m_entries) {
|
||||
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 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) {
|
||||
case SortType_UpdatedStar:
|
||||
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()) {
|
||||
return false;
|
||||
}
|
||||
[[fallthrough]];
|
||||
case SortType_Updated: {
|
||||
auto lhs_timestamp = lhs.hbini.timestamp;
|
||||
auto rhs_timestamp = rhs.hbini.timestamp;
|
||||
@@ -348,8 +361,8 @@ void Menu::Sort() {
|
||||
}
|
||||
|
||||
if (lhs_timestamp == rhs_timestamp) {
|
||||
return strcasecmp(lhs.GetName(), rhs.GetName()) < 0;
|
||||
} else if (order == OrderType_Decending) {
|
||||
return name_cmp(lhs, rhs);
|
||||
} else if (order == OrderType_Descending) {
|
||||
return lhs_timestamp > rhs_timestamp;
|
||||
} else {
|
||||
return lhs_timestamp < rhs_timestamp;
|
||||
@@ -362,10 +375,11 @@ void Menu::Sort() {
|
||||
} else if (!lhs.has_star.value() && rhs.has_star.value()) {
|
||||
return false;
|
||||
}
|
||||
[[fallthrough]];
|
||||
case SortType_Size: {
|
||||
if (lhs.size == rhs.size) {
|
||||
return strcasecmp(lhs.GetName(), rhs.GetName()) < 0;
|
||||
} else if (order == OrderType_Decending) {
|
||||
return name_cmp(lhs, rhs);
|
||||
} else if (order == OrderType_Descending) {
|
||||
return lhs.size > rhs.size;
|
||||
} else {
|
||||
return lhs.size < rhs.size;
|
||||
@@ -378,12 +392,9 @@ void Menu::Sort() {
|
||||
} else if (!lhs.has_star.value() && rhs.has_star.value()) {
|
||||
return false;
|
||||
}
|
||||
[[fallthrough]];
|
||||
case SortType_Alphabetical: {
|
||||
if (order == OrderType_Decending) {
|
||||
return strcasecmp(lhs.GetName(), rhs.GetName()) < 0;
|
||||
} else {
|
||||
return strcasecmp(lhs.GetName(), rhs.GetName()) > 0;
|
||||
}
|
||||
return name_cmp(lhs, rhs);
|
||||
} break;
|
||||
}
|
||||
|
||||
@@ -409,9 +420,9 @@ void Menu::SortAndFindLastFile() {
|
||||
if (index >= 0) {
|
||||
// guesstimate where the position is
|
||||
if (index >= 9) {
|
||||
m_start = (index - 9) / 3 * 3 + 3;
|
||||
m_list->SetYoff((((index - 9) + 3) / 3) * m_list->GetMaxY());
|
||||
} else {
|
||||
m_start = 0;
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
SetIndex(index);
|
||||
}
|
||||
|
||||
@@ -139,44 +139,44 @@ Menu::Menu() : MenuBase{"Irs"_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);
|
||||
m_index = index;
|
||||
UpdateConfig(&m_config);
|
||||
}, 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));
|
||||
|
||||
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;
|
||||
updateColourArray();
|
||||
}, 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;
|
||||
UpdateConfig(&m_config);
|
||||
}, 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;
|
||||
UpdateConfig(&m_config);
|
||||
}, 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;
|
||||
UpdateConfig(&m_config);
|
||||
}, 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.trimming_format = index;
|
||||
UpdateConfig(&m_config);
|
||||
}, m_config.orig_format));
|
||||
|
||||
if (hosversionAtLeast(4,0,0)) {
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Trimming Format"_i18n, format_str, [this](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
|
||||
if (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);
|
||||
w = m_irs_width * scale;
|
||||
h = m_irs_height * scale;
|
||||
cx = (m_pos.x + m_pos.w / 2.0) - w / 2.0;
|
||||
cy = (m_pos.y + m_pos.h / 2.0) - h / 2.0;
|
||||
cx = (m_pos.x + m_pos.w / 2.F) - w / 2.F;
|
||||
cy = (m_pos.y + m_pos.h / 2.F) - h / 2.F;
|
||||
angle = 0;
|
||||
} break;
|
||||
case Rotation_90: {
|
||||
@@ -268,8 +268,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
const auto scale = std::min(scale_x, scale_y);
|
||||
w = m_irs_width * scale;
|
||||
h = m_irs_height * scale;
|
||||
cx = (m_pos.x + m_pos.w / 2.0) + h / 2.0;
|
||||
cy = (m_pos.y + m_pos.h / 2.0) - w / 2.0;
|
||||
cx = (m_pos.x + m_pos.w / 2.F) + h / 2.F;
|
||||
cy = (m_pos.y + m_pos.h / 2.F) - w / 2.F;
|
||||
angle = 90;
|
||||
} break;
|
||||
case Rotation_180: {
|
||||
@@ -278,8 +278,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
const auto scale = std::min(scale_x, scale_y);
|
||||
w = m_irs_width * scale;
|
||||
h = m_irs_height * scale;
|
||||
cx = (m_pos.x + m_pos.w / 2.0) + w / 2.0;
|
||||
cy = (m_pos.y + m_pos.h / 2.0) + h / 2.0;
|
||||
cx = (m_pos.x + m_pos.w / 2.F) + w / 2.F;
|
||||
cy = (m_pos.y + m_pos.h / 2.F) + h / 2.F;
|
||||
angle = 180;
|
||||
} break;
|
||||
case Rotation_270: {
|
||||
@@ -288,8 +288,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
const auto scale = std::min(scale_x, scale_y);
|
||||
w = m_irs_width * scale;
|
||||
h = m_irs_height * scale;
|
||||
cx = (m_pos.x + m_pos.w / 2.0) - h / 2.0;
|
||||
cy = (m_pos.y + m_pos.h / 2.0) + w / 2.0;
|
||||
cx = (m_pos.x + m_pos.w / 2.F) - h / 2.F;
|
||||
cy = (m_pos.y + m_pos.h / 2.F) + w / 2.F;
|
||||
angle = 270;
|
||||
} break;
|
||||
}
|
||||
@@ -382,7 +382,7 @@ void Menu::LoadDefaultConfig() {
|
||||
irsGetClusteringProcessorDefaultConfig(&m_clustering_config);
|
||||
irsGetIrLedProcessorDefaultConfig(&m_led_config);
|
||||
|
||||
m_tera_config;
|
||||
m_tera_config = {};
|
||||
m_adaptive_config = {};
|
||||
m_hand_config = {};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "ui/menus/main_menu.hpp"
|
||||
#include "ui/menus/irs_menu.hpp"
|
||||
#include "ui/menus/themezer.hpp"
|
||||
#include "ui/menus/ghdl.hpp"
|
||||
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/popup_list.hpp"
|
||||
@@ -22,6 +23,9 @@
|
||||
namespace sphaira::ui::menu::main {
|
||||
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 {
|
||||
static fs::FsPath zip_out{"/switch/sphaira/cache/update.zip"};
|
||||
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);
|
||||
log_write("starting download: %s\n", url.c_str());
|
||||
|
||||
DownloadClearCache(url);
|
||||
if (!DownloadFile(url, zip_out, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
|
||||
if (pbox->ShouldExit()) {
|
||||
return false;
|
||||
}
|
||||
pbox->UpdateTransfer(dlnow, dltotal);
|
||||
return true;
|
||||
})) {
|
||||
if (!curl::Api().ToFile(
|
||||
curl::Url{url},
|
||||
curl::Path{zip_out},
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||
).success) {
|
||||
log_write("error with download\n");
|
||||
// push popup error box
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
|
||||
if (!pbox->ShouldExit()) {
|
||||
auto zfile = unzOpen64(zip_out);
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
if (std::strstr(file_path, "sphaira.nro")) {
|
||||
file_path = App::GetExePath();
|
||||
}
|
||||
|
||||
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, rc);
|
||||
log_write("failed to create folder: %s 0x%04X\n", file_path.s, rc);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
Result rc;
|
||||
if (R_FAILED(rc = fs.CreateFile(file_path, info.uncompressed_size, 0)) && rc != FsError_PathAlreadyExists) {
|
||||
log_write("failed to create file: %s 0x%04X\n", file_path, rc);
|
||||
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, rc);
|
||||
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, rc);
|
||||
log_write("failed to set file size: %s 0x%04X\n", file_path.s, rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<char> buf(chunk_size);
|
||||
u64 offset{};
|
||||
s64 offset{};
|
||||
while (offset < info.uncompressed_size) {
|
||||
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
|
||||
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))) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -142,63 +146,70 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
|
||||
} // namespace
|
||||
|
||||
MainMenu::MainMenu() {
|
||||
DownloadMemoryAsync("https://api.github.com/repos/ITotalJustice/sphaira/releases/latest", "", [this](std::vector<u8>& data, bool success){
|
||||
m_update_state = UpdateState::Error;
|
||||
ON_SCOPE_EXIT( log_write("update status: %u\n", (u8)m_update_state) );
|
||||
curl::Api().ToFileAsync(
|
||||
curl::Url{GITHUB_URL},
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
if (!result.success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto json = yyjson_read((const char*)data.data(), data.size(), 0);
|
||||
R_UNLESS(json, false);
|
||||
ON_SCOPE_EXIT(yyjson_doc_free(json));
|
||||
auto json = yyjson_read_file(CACHE_PATH, YYJSON_READ_NOFLAG, nullptr, nullptr);
|
||||
R_UNLESS(json, false);
|
||||
ON_SCOPE_EXIT(yyjson_doc_free(json));
|
||||
|
||||
auto root = yyjson_doc_get_root(json);
|
||||
R_UNLESS(root, false);
|
||||
auto root = yyjson_doc_get_root(json);
|
||||
R_UNLESS(root, false);
|
||||
|
||||
auto tag_key = yyjson_obj_get(root, "tag_name");
|
||||
R_UNLESS(tag_key, false);
|
||||
auto tag_key = yyjson_obj_get(root, "tag_name");
|
||||
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;
|
||||
}
|
||||
|
||||
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(
|
||||
std::make_pair(Button::START, Action{App::Exit}),
|
||||
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("Russian"_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](){
|
||||
SidebarEntryArray::Items theme_items{};
|
||||
const auto theme_meta = App::GetThemeMetaList();
|
||||
@@ -232,7 +242,7 @@ MainMenu::MainMenu() {
|
||||
auto options = std::make_shared<Sidebar>("Theme Options"_i18n, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Select Theme"_i18n, theme_items, [this, theme_items](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::GetThemeIndex()));
|
||||
|
||||
@@ -270,11 +280,8 @@ MainMenu::MainMenu() {
|
||||
m_update_state = UpdateState::None;
|
||||
App::Notify("Updated to "_i18n + m_update_version);
|
||||
App::Push(std::make_shared<OptionBox>(
|
||||
"Restart Sphaira?"_i18n,
|
||||
"Back"_i18n, "Restart"_i18n, 1, [this](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
App::ExitRestart();
|
||||
}
|
||||
"Press OK to restart Sphaira"_i18n, "OK"_i18n, [](auto){
|
||||
App::ExitRestart();
|
||||
}
|
||||
));
|
||||
} else {
|
||||
@@ -285,9 +292,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);
|
||||
}, (std::size_t)App::GetLanguage()));
|
||||
}, (s64)App::GetLanguage()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Misc"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Misc Options"_i18n, Sidebar::Side::LEFT);
|
||||
@@ -297,6 +304,10 @@ MainMenu::MainMenu() {
|
||||
App::Push(std::make_shared<menu::themezer::Menu>());
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("GitHub"_i18n, [](){
|
||||
App::Push(std::make_shared<menu::gh::Menu>());
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Irs"_i18n, [](){
|
||||
App::Push(std::make_shared<menu::irs::Menu>());
|
||||
}));
|
||||
@@ -316,6 +327,11 @@ MainMenu::MainMenu() {
|
||||
install_items.push_back("System memory"_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){
|
||||
App::SetLogEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
@@ -328,13 +344,17 @@ MainMenu::MainMenu() {
|
||||
App::SetInstallEnable(enable);
|
||||
}, "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);
|
||||
}, (std::size_t)App::GetInstallSdEnable()));
|
||||
}, (s64)App::GetInstallSdEnable()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [this](bool& enable){
|
||||
App::SetInstallPrompt(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [this](s64& index_out){
|
||||
App::SetTextScrollSpeed(index_out);
|
||||
}, (s64)App::GetTextScrollSpeed()));
|
||||
}));
|
||||
}})
|
||||
);
|
||||
@@ -344,6 +364,8 @@ MainMenu::MainMenu() {
|
||||
m_app_store_menu = std::make_shared<appstore::Menu>(m_homebrew_menu->GetHomebrewList());
|
||||
m_current_menu = m_homebrew_menu;
|
||||
|
||||
AddOnLRPress();
|
||||
|
||||
for (auto [button, action] : m_actions) {
|
||||
m_current_menu->SetAction(button, action);
|
||||
}
|
||||
@@ -363,11 +385,11 @@ void MainMenu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
|
||||
void MainMenu::OnFocusGained() {
|
||||
Widget::OnFocusGained();
|
||||
this->SetHidden(false);
|
||||
m_current_menu->OnFocusGained();
|
||||
}
|
||||
|
||||
void MainMenu::OnFocusLost() {
|
||||
Widget::OnFocusLost();
|
||||
m_current_menu->OnFocusLost();
|
||||
}
|
||||
|
||||
@@ -376,17 +398,11 @@ void MainMenu::OnLRPress(std::shared_ptr<MenuBase> menu, Button b) {
|
||||
if (m_current_menu == m_homebrew_menu) {
|
||||
m_current_menu = menu;
|
||||
RemoveAction(b);
|
||||
if (b == Button::L) {
|
||||
AddOnRPress();
|
||||
} else {
|
||||
AddOnLPress();
|
||||
}
|
||||
} else {
|
||||
m_current_menu = m_homebrew_menu;
|
||||
AddOnRPress();
|
||||
AddOnLPress();
|
||||
}
|
||||
|
||||
AddOnLRPress();
|
||||
m_current_menu->OnFocusGained();
|
||||
|
||||
for (auto [button, action] : m_actions) {
|
||||
@@ -394,18 +410,20 @@ void MainMenu::OnLRPress(std::shared_ptr<MenuBase> menu, Button b) {
|
||||
}
|
||||
}
|
||||
|
||||
void MainMenu::AddOnLPress() {
|
||||
const auto label = m_current_menu == m_homebrew_menu ? "Files" : "Apps";
|
||||
SetAction(Button::L, Action{i18n::get(label), [this]{
|
||||
OnLRPress(m_filebrowser_menu, Button::L);
|
||||
}});
|
||||
}
|
||||
void MainMenu::AddOnLRPress() {
|
||||
if (m_current_menu != m_filebrowser_menu) {
|
||||
const auto label = m_current_menu == m_homebrew_menu ? "Files" : "Apps";
|
||||
SetAction(Button::L, Action{i18n::get(label), [this]{
|
||||
OnLRPress(m_filebrowser_menu, Button::L);
|
||||
}});
|
||||
}
|
||||
|
||||
void MainMenu::AddOnRPress() {
|
||||
const auto label = m_current_menu == m_homebrew_menu ? "Store" : "Apps";
|
||||
SetAction(Button::R, Action{i18n::get(label), [this]{
|
||||
OnLRPress(m_app_store_menu, Button::R);
|
||||
}});
|
||||
if (m_current_menu != m_app_store_menu) {
|
||||
const auto label = m_current_menu == m_homebrew_menu ? "Store" : "Apps";
|
||||
SetAction(Button::R, Action{i18n::get(label), [this]{
|
||||
OnLRPress(m_app_store_menu, Button::R);
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::main
|
||||
|
||||
@@ -9,8 +9,8 @@ namespace sphaira::ui::menu {
|
||||
MenuBase::MenuBase(std::string title) : m_title{title} {
|
||||
// this->SetParent(this);
|
||||
this->SetPos(30, 87, 1220 - 30, 646 - 87);
|
||||
m_applet_type = appletGetAppletType();
|
||||
SetAction(Button::START, Action{App::Exit});
|
||||
UpdateVars();
|
||||
}
|
||||
|
||||
MenuBase::~MenuBase() {
|
||||
@@ -18,30 +18,17 @@ MenuBase::~MenuBase() {
|
||||
|
||||
void MenuBase::Update(Controller* controller, TouchInfo* touch) {
|
||||
Widget::Update(controller, touch);
|
||||
|
||||
// update every second.
|
||||
if (m_poll_timestamp.GetSeconds() >= 1) {
|
||||
UpdateVars();
|
||||
}
|
||||
}
|
||||
|
||||
void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
|
||||
DrawElement(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, ThemeEntryID_BACKGROUND);
|
||||
Widget::Draw(vg, theme);
|
||||
|
||||
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 font_size = 22;
|
||||
const float spacing = 30;
|
||||
@@ -54,34 +41,32 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
|
||||
#define draw(...) \
|
||||
gfx::textBounds(vg, 0, 0, bounds, __VA_ARGS__); \
|
||||
start_x -= bounds[2] - bounds[0]; \
|
||||
gfx::drawTextArgs(vg, start_x, start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->elements[ThemeEntryID_TEXT].colour, __VA_ARGS__); \
|
||||
gfx::drawTextArgs(vg, start_x, start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT), __VA_ARGS__); \
|
||||
start_x -= spacing;
|
||||
|
||||
// draw("version %s", APP_VERSION);
|
||||
draw("%u\uFE6A", battery_percetange);
|
||||
draw("%02u:%02u:%02u", tm.tm_hour, tm.tm_min, tm.tm_sec);
|
||||
if (ip) {
|
||||
draw("%u.%u.%u.%u", ip&0xFF, (ip>>8)&0xFF, (ip>>16)&0xFF, (ip>>24)&0xFF);
|
||||
draw("%u\uFE6A", m_battery_percetange);
|
||||
draw("%02u:%02u:%02u", m_tm.tm_hour, m_tm.tm_min, m_tm.tm_sec);
|
||||
if (m_ip) {
|
||||
draw("%u.%u.%u.%u", m_ip&0xFF, (m_ip>>8)&0xFF, (m_ip>>16)&0xFF, (m_ip>>24)&0xFF);
|
||||
} else {
|
||||
draw(("No Internet"_i18n).c_str());
|
||||
}
|
||||
if (m_applet_type == AppletType_LibraryApplet || m_applet_type == AppletType_SystemApplet) {
|
||||
if (!App::IsApplication()) {
|
||||
draw(("[Applet Mode]"_i18n).c_str());
|
||||
}
|
||||
|
||||
#undef draw
|
||||
|
||||
gfx::drawRect(vg, 30.f, 86.f, 1220.f, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
gfx::drawRect(vg, 30.f, 646.0f, 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->GetColour(ThemeEntryID_LINE));
|
||||
|
||||
nvgFontSize(vg, 28);
|
||||
gfx::textBounds(vg, 0, 0, bounds, m_title.c_str());
|
||||
gfx::drawTextArgs(vg, 80, start_y, 28.f, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->elements[ThemeEntryID_TEXT].colour, 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, start_y, 28.f, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT), m_title.c_str());
|
||||
gfx::drawTextArgs(vg, 80 + (bounds[2] - bounds[0]) + 10, start_y, 16, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), m_title_sub_heading.c_str());
|
||||
|
||||
// gfx::drawTextArgs(vg, 80, 65, 28.f, NVG_ALIGN_LEFT, theme->elements[ThemeEntryID_TEXT].colour, m_title.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());
|
||||
gfx::drawTextArgs(vg, 80, 685.f, 18, NVG_ALIGN_LEFT, theme->GetColour(ThemeEntryID_TEXT), "%s", m_sub_heading.c_str());
|
||||
}
|
||||
|
||||
void MenuBase::SetTitle(std::string title) {
|
||||
@@ -96,4 +81,24 @@ void MenuBase::SetSubHeading(std::string sub_heading) {
|
||||
m_sub_heading = sub_heading;
|
||||
}
|
||||
|
||||
void MenuBase::UpdateVars() {
|
||||
m_tm = {};
|
||||
m_poll_timestamp = {};
|
||||
m_battery_percetange = {};
|
||||
m_charger_type = {};
|
||||
m_type = {};
|
||||
m_status = {};
|
||||
m_strength = {};
|
||||
m_ip = {};
|
||||
|
||||
const auto t = time(NULL);
|
||||
localtime_r(&t, &m_tm);
|
||||
psmGetBatteryChargePercentage(&m_battery_percetange);
|
||||
psmGetChargerType(&m_charger_type);
|
||||
nifmGetInternetConnectionStatus(&m_type, &m_strength, &m_status);
|
||||
nifmGetCurrentIpAddress(&m_ip);
|
||||
|
||||
m_poll_timestamp.Update();
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#include "i18n.hpp"
|
||||
|
||||
#include <minIni.h>
|
||||
#include <nanovg/stb_image.h>
|
||||
#include <stb_image.h>
|
||||
#include <cstring>
|
||||
#include <minizip/unzip.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"]}
|
||||
|
||||
// i know, this is cursed
|
||||
// todo: send actual POST request rather than GET.
|
||||
auto apiBuildUrlListInternal(const Config& e, bool is_pack) -> std::string {
|
||||
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" };
|
||||
|
||||
std::string cmd;
|
||||
@@ -65,7 +67,8 @@ auto apiBuildUrlListInternal(const Config& e, bool is_pack) -> std::string {
|
||||
|
||||
if (is_pack) {
|
||||
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 {
|
||||
cmd = "themeList";
|
||||
p0 += ",$target:String";
|
||||
@@ -99,47 +102,33 @@ auto apiBuildUrlDownloadInternal(const std::string& id, bool is_pack) -> std::st
|
||||
// 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 {
|
||||
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 {
|
||||
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 {
|
||||
fs::FsPath path;
|
||||
std::snprintf(path, sizeof(path), "%s/%s_thumb.jpg", CACHE_PATH, e.id.c_str());
|
||||
return path;
|
||||
}
|
||||
|
||||
auto loadThemeImage(ThemeEntry& e) -> void {
|
||||
auto loadThemeImage(ThemeEntry& e) -> bool {
|
||||
auto& image = e.preview.lazy_image;
|
||||
|
||||
// already have the image
|
||||
if (e.preview.lazy_image.image) {
|
||||
// log_write("warning, tried to load image: %s when already loaded\n", path.c_str());
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
auto vg = App::GetVg();
|
||||
|
||||
@@ -148,43 +137,23 @@ auto loadThemeImage(ThemeEntry& e) -> void {
|
||||
|
||||
const auto path = apiBuildIconCache(e);
|
||||
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 {
|
||||
int channels_in_file;
|
||||
auto buf = stbi_load_from_memory(image_buf.data(), image_buf.size(), &image.w, &image.h, &channels_in_file, 4);
|
||||
if (buf) {
|
||||
ON_SCOPE_EXIT(stbi_image_free(buf));
|
||||
std::memcpy(image.first_pixel, buf, sizeof(image.first_pixel));
|
||||
image.image = nvgCreateImageRGBA(vg, image.w, image.h, 0, buf);
|
||||
}
|
||||
}
|
||||
|
||||
if (!image.image) {
|
||||
image.state = ImageDownloadState::Failed;
|
||||
log_write("failed to load image from file: %s\n", path);
|
||||
log_write("failed to load image from file: %s\n", path.s);
|
||||
return false;
|
||||
} else {
|
||||
// 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 false;
|
||||
}
|
||||
|
||||
void from_json(yyjson_val* json, Creator& e) {
|
||||
@@ -197,35 +166,18 @@ void from_json(yyjson_val* json, Creator& e) {
|
||||
void from_json(yyjson_val* json, Details& e) {
|
||||
JSON_OBJ_ITR(
|
||||
JSON_SET_STR(name);
|
||||
JSON_SET_STR(description);
|
||||
);
|
||||
}
|
||||
|
||||
void from_json(yyjson_val* json, Preview& e) {
|
||||
JSON_OBJ_ITR(
|
||||
JSON_SET_STR(original);
|
||||
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) {
|
||||
JSON_OBJ_ITR(
|
||||
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);
|
||||
);
|
||||
}
|
||||
@@ -235,10 +187,6 @@ void from_json(yyjson_val* json, PackListEntry& e) {
|
||||
JSON_SET_STR(id);
|
||||
JSON_SET_OBJ(creator);
|
||||
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);
|
||||
);
|
||||
}
|
||||
@@ -254,7 +202,6 @@ void from_json(yyjson_val* json, Pagination& e) {
|
||||
|
||||
void from_json(const std::vector<u8>& data, DownloadPack& e) {
|
||||
JSON_INIT_VEC(data, "data");
|
||||
// JSON_GET_OBJ("downloadTheme");
|
||||
JSON_GET_OBJ("downloadPack");
|
||||
JSON_OBJ_ITR(
|
||||
JSON_SET_STR(filename);
|
||||
@@ -263,8 +210,8 @@ void from_json(const std::vector<u8>& data, DownloadPack& e) {
|
||||
);
|
||||
}
|
||||
|
||||
void from_json(const std::vector<u8>& data, PackList& e) {
|
||||
JSON_INIT_VEC(data, "data");
|
||||
void from_json(const fs::FsPath& path, PackList& e) {
|
||||
JSON_INIT_VEC_FILE(path, "data", nullptr);
|
||||
JSON_OBJ_ITR(
|
||||
JSON_SET_ARR_OBJ(packList);
|
||||
JSON_SET_OBJ(pagination);
|
||||
@@ -272,7 +219,7 @@ void from_json(const std::vector<u8>& data, PackList& e) {
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
fs::FsNativeSd fs;
|
||||
@@ -287,22 +234,17 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
|
||||
|
||||
const auto url = apiBuildUrlDownloadPack(entry);
|
||||
log_write("using url: %s\n", url.c_str());
|
||||
DownloadClearCache(url);
|
||||
const auto data = DownloadMemory(url, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
|
||||
if (pbox->ShouldExit()) {
|
||||
return false;
|
||||
}
|
||||
pbox->UpdateTransfer(dlnow, dltotal);
|
||||
return true;
|
||||
});
|
||||
const auto result = curl::Api().ToMemory(
|
||||
curl::Url{url},
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||
);
|
||||
|
||||
if (data.empty()) {
|
||||
if (!result.success || result.data.empty()) {
|
||||
log_write("error with download: %s\n", url.c_str());
|
||||
// push popup error box
|
||||
return false;
|
||||
}
|
||||
|
||||
from_json(data, download_pack);
|
||||
from_json(result.data, download_pack);
|
||||
}
|
||||
|
||||
// 2. download the zip
|
||||
@@ -310,16 +252,11 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
|
||||
pbox->NewTransfer("Downloading "_i18n + entry.details.name);
|
||||
log_write("starting download: %s\n", download_pack.url.c_str());
|
||||
|
||||
DownloadClearCache(download_pack.url);
|
||||
if (!DownloadFile(download_pack.url, zip_out, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
|
||||
if (pbox->ShouldExit()) {
|
||||
return false;
|
||||
}
|
||||
pbox->UpdateTransfer(dlnow, dltotal);
|
||||
return true;
|
||||
})) {
|
||||
if (!curl::Api().ToFile(
|
||||
curl::Url{download_pack.url},
|
||||
curl::Path{zip_out},
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()}).success) {
|
||||
log_write("error with download\n");
|
||||
// push popup error box
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -328,14 +265,14 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
|
||||
|
||||
// create directories
|
||||
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);
|
||||
|
||||
// 3. extract the zip
|
||||
if (!pbox->ShouldExit()) {
|
||||
auto zfile = unzOpen64(zip_out);
|
||||
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;
|
||||
}
|
||||
ON_SCOPE_EXIT(unzClose(zfile));
|
||||
@@ -370,24 +307,24 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
|
||||
|
||||
Result rc;
|
||||
if (R_FAILED(rc = fs.CreateFile(file_path, info.uncompressed_size, 0)) && rc != FsError_PathAlreadyExists) {
|
||||
log_write("failed to create file: %s 0x%04X\n", file_path, rc);
|
||||
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, rc);
|
||||
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, rc);
|
||||
log_write("failed to set file size: %s 0x%04X\n", file_path.s, rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<char> buf(chunk_size);
|
||||
u64 offset{};
|
||||
s64 offset{};
|
||||
while (offset < info.uncompressed_size) {
|
||||
if (pbox->ShouldExit()) {
|
||||
return false;
|
||||
@@ -400,7 +337,7 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -423,6 +360,8 @@ LazyImage::~LazyImage() {
|
||||
}
|
||||
|
||||
Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
fs::FsNativeSd().CreateDirectoryRecursively(CACHE_PATH);
|
||||
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this]{
|
||||
SetPop();
|
||||
}});
|
||||
@@ -445,7 +384,25 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
}}),
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
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);
|
||||
}
|
||||
}}),
|
||||
@@ -461,12 +418,10 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
|
||||
App::Push(std::make_shared<ProgressBox>("Installing "_i18n + entry.details.name, [this, &entry](auto pbox){
|
||||
return InstallTheme(pbox, entry);
|
||||
}, [this](bool success){
|
||||
// if (success) {
|
||||
// m_entry.status = EntryStatus::Installed;
|
||||
// m_menu.SetDirty();
|
||||
// UpdateOptions();
|
||||
// }
|
||||
}, [this, &entry](bool success){
|
||||
if (success) {
|
||||
App::Notify("Downloaded "_i18n + entry.details.name);
|
||||
}
|
||||
}, 2));
|
||||
}
|
||||
}
|
||||
@@ -492,14 +447,14 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
InvalidateAllPages();
|
||||
}, "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) {
|
||||
m_sort.Set(index_out);
|
||||
InvalidateAllPages();
|
||||
}
|
||||
}, 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) {
|
||||
m_order.Set(index_out);
|
||||
InvalidateAllPages();
|
||||
@@ -526,16 +481,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](){
|
||||
m_page_index++;
|
||||
if (m_page_index >= m_page_index_max) {
|
||||
@@ -552,6 +497,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_pages.resize(1);
|
||||
PackListDownload();
|
||||
@@ -563,13 +512,31 @@ Menu::~Menu() {
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* 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) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -577,98 +544,105 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
|
||||
switch (page.m_ready) {
|
||||
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;
|
||||
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;
|
||||
case PageLoadState::Done:
|
||||
break;
|
||||
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;
|
||||
}
|
||||
|
||||
const u64 SCROLL = m_start;
|
||||
const u64 max_entry_display = 9;
|
||||
const u64 nro_total = page.m_packList.size();// m_entries_current.size();
|
||||
const u64 cursor_pos = m_index;
|
||||
// max images per frame, in order to not hit io / gpu too hard.
|
||||
const int image_load_max = 2;
|
||||
int image_load_count = 0;
|
||||
|
||||
// only draw scrollbar if needed
|
||||
if (nro_total > max_entry_display) {
|
||||
const auto scrollbar_size = 500.f;
|
||||
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);
|
||||
}
|
||||
m_list->Draw(vg, theme, page.m_packList.size(), [this, &page, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
|
||||
const auto& [x, y, w, h] = v;
|
||||
auto& e = page.m_packList[pos];
|
||||
|
||||
nvgSave(vg);
|
||||
nvgScissor(vg, 30, 87, 1220 - 30, 646 - 87); // clip
|
||||
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);
|
||||
}
|
||||
|
||||
for (u64 i = 0, pos = SCROLL, y = 110, w = 350, h = 250; pos < nro_total && i < max_entry_display; y += h + 10) {
|
||||
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];
|
||||
const float xoff = (350 - 320) / 2;
|
||||
|
||||
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);
|
||||
}
|
||||
// lazy load image
|
||||
if (e.themes.size()) {
|
||||
auto& theme = e.themes[0];
|
||||
auto& image = e.themes[0].preview.lazy_image;
|
||||
|
||||
const float xoff = (350 - 320) / 2;
|
||||
const float yoff = (350 - 320) / 2;
|
||||
|
||||
// lazy load image
|
||||
if (e.themes.size()) {
|
||||
auto& theme = e.themes[0];
|
||||
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);
|
||||
// try and load cached image.
|
||||
if (image_load_count < image_load_max && !image.image && !image.tried_cache) {
|
||||
image.tried_cache = true;
|
||||
image.cached = loadThemeImage(theme);
|
||||
if (image.cached) {
|
||||
image_load_count++;
|
||||
}
|
||||
}
|
||||
|
||||
gfx::drawTextArgs(vg, x + xoff, y + 180 + 20, 18, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s", e.details.name.c_str());
|
||||
gfx::drawTextArgs(vg, x + xoff, y + 180 + 55, 18, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s", e.creator.display_name.c_str());
|
||||
}
|
||||
}
|
||||
if (!image.image || image.cached) {
|
||||
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() {
|
||||
@@ -691,7 +665,7 @@ void Menu::PackListDownload() {
|
||||
SetSubHeading(subheading);
|
||||
|
||||
m_index = 0;
|
||||
m_start = 0;
|
||||
m_list->SetYoff(0);
|
||||
|
||||
// already downloaded
|
||||
if (m_pages[m_page_index].m_ready != PageLoadState::None) {
|
||||
@@ -706,39 +680,43 @@ void Menu::PackListDownload() {
|
||||
config.order_index = m_order.Get();
|
||||
config.nsfw = m_nsfw.Get();
|
||||
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("\nthemeList_url: %s\n\n", themeList_url.c_str());
|
||||
|
||||
DownloadClearCache(packList_url);
|
||||
DownloadMemoryAsync(packList_url, "", [this, page_index](std::vector<u8>& data, bool success){
|
||||
log_write("got themezer data\n");
|
||||
if (!success) {
|
||||
curl::Api().ToFileAsync(
|
||||
curl::Url{packList_url},
|
||||
curl::Path{packlist_path},
|
||||
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];
|
||||
page.m_ready = PageLoadState::Error;
|
||||
log_write("failed to get themezer data...\n");
|
||||
return;
|
||||
|
||||
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: %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
|
||||
|
||||
@@ -11,10 +11,6 @@ NotifEntry::NotifEntry(std::string text, Side side)
|
||||
, m_side{side} {
|
||||
}
|
||||
|
||||
auto NotifEntry::OnLayoutChange() -> void {
|
||||
|
||||
}
|
||||
|
||||
auto NotifEntry::Draw(NVGcontext* vg, Theme* theme, float y) -> bool {
|
||||
m_pos.y = y;
|
||||
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 overlay_col = theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour;
|
||||
auto selected_col = theme->elements[ThemeEntryID_SELECTED].colour;
|
||||
auto text_col = theme->elements[ThemeEntryID_TEXT].colour;
|
||||
auto text_col = theme->GetColour(ThemeEntryID_TEXT);
|
||||
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) {
|
||||
m_bounds_measured = true;
|
||||
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);
|
||||
}
|
||||
|
||||
auto NotifMananger::OnLayoutChange() -> void {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
}
|
||||
|
||||
auto NotifMananger::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace sphaira::ui::gfx {
|
||||
namespace {
|
||||
|
||||
static constexpr std::array buttons = {
|
||||
constexpr std::array buttons = {
|
||||
std::pair{Button::A, "\uE0E0"},
|
||||
std::pair{Button::B, "\uE0E1"},
|
||||
std::pair{Button::X, "\uE0E2"},
|
||||
@@ -33,67 +33,45 @@ static constexpr std::array buttons = {
|
||||
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 ---------------------
|
||||
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);
|
||||
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 {
|
||||
nvgRect(vg, vec.x, vec.y, vec.w, vec.h);
|
||||
nvgRect(vg, v.x, v.y, v.w, v.h);
|
||||
}
|
||||
nvgFillColor(vg, c);
|
||||
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);
|
||||
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 {
|
||||
nvgRect(vg, vec.x, vec.y, vec.w, vec.h);
|
||||
nvgRect(vg, v.x, v.y, v.w, v.h);
|
||||
}
|
||||
nvgFillPaint(vg, p);
|
||||
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;
|
||||
getHighlightAnimation(&gradientX, &gradientY, &color);
|
||||
|
||||
const auto strokeWidth = 5.0;
|
||||
auto v2 = vec;
|
||||
v2.x -= strokeWidth / 2.0;
|
||||
v2.y -= strokeWidth / 2.0;
|
||||
const auto strokeWidth = 5.F;
|
||||
auto v2 = v;
|
||||
v2.x -= strokeWidth / 2.F;
|
||||
v2.y -= strokeWidth / 2.F;
|
||||
v2.w += strokeWidth;
|
||||
v2.h += strokeWidth;
|
||||
const auto corner_radius = 0.5;
|
||||
const auto corner_radius = 0.5F;
|
||||
|
||||
nvgSave(vg);
|
||||
nvgResetScissor(vg);
|
||||
|
||||
// const auto stroke_width = 5.0f;
|
||||
// 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;
|
||||
const auto shadow_width = 2.F;
|
||||
const auto shadow_offset = 10.F;
|
||||
const auto shadow_feather = 10.F;
|
||||
const auto shadow_opacity = 128.F;
|
||||
|
||||
// Shadow
|
||||
NVGpaint shadowPaint = nvgBoxGradient(vg,
|
||||
@@ -110,8 +88,8 @@ inline void drawRectOutlineInternal(NVGcontext* vg, float size, const NVGcolor&
|
||||
nvgFillPaint(vg, shadowPaint);
|
||||
nvgFill(vg);
|
||||
|
||||
const auto color1 = nvgRGB(25, 138, 198);
|
||||
const auto color2 = nvgRGB(137, 241, 242);
|
||||
const auto color1 = theme->GetColour(ThemeEntryID_HIGHLIGHT_1);
|
||||
const auto color2 = theme->GetColour(ThemeEntryID_HIGHLIGHT_2);
|
||||
const auto borderColor = nvgRGBAf(color2.r, color2.g, color2.b, 0.5);
|
||||
const auto transparent = nvgRGBA(0, 0, 0, 0);
|
||||
|
||||
@@ -147,54 +125,23 @@ inline void drawRectOutlineInternal(NVGcontext* vg, float size, const NVGcolor&
|
||||
nvgStrokeWidth(vg, strokeWidth);
|
||||
nvgRoundedRect(vg, v2.x, v2.y, v2.w, v2.h, corner_radius);
|
||||
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) {
|
||||
float gradientX, gradientY, color;
|
||||
getHighlightAnimation(&gradientX, &gradientY, &color);
|
||||
|
||||
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) {
|
||||
void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, const Vec4& v, const NVGcolor& c) {
|
||||
const auto corner_radius = 0.5;
|
||||
drawRectOutlineInternal(vg, theme, size, v);
|
||||
nvgBeginPath(vg);
|
||||
nvgMoveTo(vg, aX, aY);
|
||||
nvgLineTo(vg, bX, bY);
|
||||
nvgLineTo(vg, cX, cY);
|
||||
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, corner_radius);
|
||||
nvgFillColor(vg, c);
|
||||
nvgFill(vg);
|
||||
}
|
||||
|
||||
inline void drawTriangleInternal(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p) {
|
||||
nvgBeginPath(vg);
|
||||
nvgMoveTo(vg, aX, aY);
|
||||
nvgLineTo(vg, bX, bY);
|
||||
nvgLineTo(vg, cX, cY);
|
||||
nvgFillPaint(vg, p);
|
||||
nvgFill(vg);
|
||||
}
|
||||
|
||||
inline void drawTextIntenal(NVGcontext* vg, Vec2 vec, float size, const char* str, const char* end, int align, const NVGcolor& c) {
|
||||
void drawTextIntenal(NVGcontext* vg, const Vec2& v, float size, const char* str, const char* end, int align, const NVGcolor& c) {
|
||||
nvgBeginPath(vg);
|
||||
nvgFontSize(vg, size);
|
||||
nvgTextAlign(vg, align);
|
||||
nvgFillColor(vg, c);
|
||||
nvgText(vg, vec.x, vec.y, str, end);
|
||||
nvgText(vg, v.x, v.y, str, end);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -208,15 +155,6 @@ const char* getButton(const Button want) {
|
||||
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, ...) {
|
||||
std::va_list v;
|
||||
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);
|
||||
}
|
||||
|
||||
static void drawImageInternal(NVGcontext* vg, Vec4 v, int texture, int rounded = 0) {
|
||||
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) {
|
||||
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, 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);
|
||||
}
|
||||
|
||||
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);
|
||||
nvgBeginPath(vg);
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
nvgFontSize(vg, size);
|
||||
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);
|
||||
}
|
||||
|
||||
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, ...) {
|
||||
char buf[0x100];
|
||||
va_list v;
|
||||
@@ -289,222 +205,89 @@ void textBounds(NVGcontext* vg, float x, float y, float *bounds, const char* str
|
||||
// NEW-----------
|
||||
|
||||
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,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);
|
||||
drawRectIntenal(vg, {0.f,0.f,SCREEN_WIDTH,SCREEN_HEIGHT}, nvgRGBA(0, 0, 0, 180), false);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, bool rounded) {
|
||||
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) {
|
||||
drawRectIntenal(vg, {x,y,w,h}, 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, const Vec4& v, const NVGcolor& c, bool rounded) {
|
||||
drawRectIntenal(vg, v, c, 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);
|
||||
}
|
||||
|
||||
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);
|
||||
void drawRect(NVGcontext* vg, const Vec4& v, const NVGpaint& p, bool rounded) {
|
||||
drawRectIntenal(vg, v, p, rounded);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, Vec4 vec, const NVGpaint& p, bool rounded) {
|
||||
drawRectIntenal(vg, vec, p, rounded);
|
||||
void drawRectOutline(NVGcontext* vg, const Theme* theme, float size, float x, float y, float w, float h) {
|
||||
drawRectOutlineInternal(vg, theme, size, {x,y,w,h}, theme->GetColour(ThemeEntryID_SELECTED_BACKGROUND));
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, Vec4 vec, const NVGpaint&& p, bool rounded) {
|
||||
drawRectIntenal(vg, vec, p, rounded);
|
||||
}
|
||||
|
||||
|
||||
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 drawRectOutline(NVGcontext* vg, const Theme* theme, float size, const Vec4& v) {
|
||||
drawRectOutlineInternal(vg, theme, size, v, theme->GetColour(ThemeEntryID_SELECTED_BACKGROUND));
|
||||
}
|
||||
|
||||
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 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) {
|
||||
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) {
|
||||
drawTextIntenal(vg, {x,y}, size, str, end, align, c);
|
||||
void drawText(NVGcontext* vg, const Vec2& v, float size, const char* str, const char* end, int align, const NVGcolor& 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) {
|
||||
drawTextIntenal(vg, vec, size, str, end, align, c);
|
||||
void drawText(NVGcontext* vg, const Vec2& v, float size, const NVGcolor& c, const char* str, int align, const char* end) {
|
||||
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) {
|
||||
drawTextIntenal(vg, vec, size, str, end, align, c);
|
||||
}
|
||||
void drawScrollbar(NVGcontext* vg, const Theme* theme, float x, float y, float h, u32 index_off, u32 count, u32 max_per_page) {
|
||||
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) {
|
||||
drawTextIntenal(vg, vec, size, str, end, align, c);
|
||||
}
|
||||
|
||||
void drawText(NVGcontext* vg, Vec2 vec, float size, const NVGcolor&& c, const char* str, int align, const char* end) {
|
||||
drawTextIntenal(vg, vec, size, str, end, align, c);
|
||||
}
|
||||
|
||||
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;
|
||||
// only draw scrollbar if needed
|
||||
if (entry_total > max_entry_display) {
|
||||
const float sb_h = 1.f / (float)entry_total * h;
|
||||
const float sb_y = SCROLL;
|
||||
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(max_entry_display) - scw * 2, theme->GetColour(ThemeEntryID_SCROLLBAR), false);
|
||||
}
|
||||
}
|
||||
|
||||
// from gc installer
|
||||
void drawDimBackground(NVGcontext* vg) {
|
||||
// drawRect(vg, 0, 0, 1920, 1080, nvgRGBA(20, 20, 20, 225));
|
||||
drawRect(vg, 0, 0, 1920, 1080, nvgRGBA(0, 0, 0, 220));
|
||||
void drawScrollbar(NVGcontext* vg, const Theme* theme, u32 index_off, u32 count, u32 max_per_page) {
|
||||
drawScrollbar(vg, theme, SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200, index_off, count, max_per_page);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -12,10 +12,10 @@ OptionBoxEntry::OptionBoxEntry(const std::string& text, Vec4 pos)
|
||||
|
||||
auto OptionBoxEntry::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
if (m_selected) {
|
||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
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::drawRectOutline(vg, theme, 4.f, m_pos);
|
||||
gfx::drawText(vg, m_text_pos, 26.f, theme->GetColour(ThemeEntryID_TEXT_SELECTED), m_text.c_str(), NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
|
||||
} 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_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_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 {
|
||||
Widget::Update(controller, touch);
|
||||
|
||||
// if (!controller->GotDown(Button::ANY_HORIZONTAL)) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const auto old_index = m_index;
|
||||
|
||||
// if (controller->GotDown(Button::LEFT) && m_index) {
|
||||
// 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 {
|
||||
|
||||
if (touch->is_clicked) {
|
||||
for (s64 i = 0; i < m_entries.size(); i++) {
|
||||
auto& e = m_entries[i];
|
||||
if (touch->in_range(e.GetPos())) {
|
||||
SetIndex(i);
|
||||
FireAction(Button::A);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto OptionBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
const float padding = 15;
|
||||
gfx::dimBackground(vg);
|
||||
gfx::drawRect(vg, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP));
|
||||
|
||||
nvgSave(vg);
|
||||
nvgTextLineHeight(vg, 1.5);
|
||||
gfx::drawTextBox(vg, m_pos.x + padding, m_pos.y + 110.f, 26.f, m_pos.w - padding*2, theme->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);
|
||||
|
||||
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) {
|
||||
p.Draw(vg, theme);
|
||||
}
|
||||
}
|
||||
|
||||
auto OptionBox::Setup(std::size_t index) -> void {
|
||||
m_index = std::min(m_entries.size() - 1, index);
|
||||
auto OptionBox::OnFocusGained() noexcept -> void {
|
||||
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_spacer_line = Vec4{m_pos.x, m_pos.y + 220.f - 2.f, m_pos.w, 2.f};
|
||||
|
||||
SetActions(
|
||||
std::make_pair(Button::LEFT, Action{[this](){
|
||||
if (m_index) {
|
||||
m_entries[m_index].Selected(false);
|
||||
m_index--;
|
||||
m_entries[m_index].Selected(true);
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
SetIndex(m_index - 1);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::RIGHT, Action{[this](){
|
||||
if (m_index < (m_entries.size() - 1)) {
|
||||
m_entries[m_index].Selected(false);
|
||||
m_index++;
|
||||
m_entries[m_index].Selected(true);
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
SetIndex(m_index + 1);
|
||||
}
|
||||
}}),
|
||||
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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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} {
|
||||
|
||||
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);
|
||||
if (it != m_items.cend()) {
|
||||
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) {
|
||||
@@ -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} {
|
||||
|
||||
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);
|
||||
if (it != m_items.cend()) {
|
||||
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);
|
||||
SetIndex(std::distance(m_items.cbegin(), it));
|
||||
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_items{std::move(items)}
|
||||
, m_callback{cb}
|
||||
, m_index{index} {
|
||||
|
||||
m_pos.w = 1280.f;
|
||||
const float a = std::min(405.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;
|
||||
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h);
|
||||
|
||||
m_scrollbar.Setup(Vec4{1220.f, m_line_top, 1.f, m_line_bottom - m_line_top}, m_block.h, m_items.size());
|
||||
|
||||
SetActions(
|
||||
this->SetActions(
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 1, m_items.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 1, m_items.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::A, Action{"Select"_i18n, [this](){
|
||||
if (m_callback) {
|
||||
m_callback(m_index);
|
||||
@@ -76,85 +79,71 @@ PopupList::PopupList(std::string title, Items items, Callback cb, std::size_t in
|
||||
SetPop();
|
||||
}}),
|
||||
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||
if (m_callback) {
|
||||
m_callback(std::nullopt);
|
||||
}
|
||||
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 {
|
||||
Widget::Update(controller, touch);
|
||||
|
||||
if (!controller->GotDown(Button::ANY_VERTICAL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
m_list->OnUpdate(controller, touch, m_items.size(), [this](auto i) {
|
||||
SetIndex(i);
|
||||
FireAction(Button::A);
|
||||
});
|
||||
}
|
||||
|
||||
auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
gfx::dimBackground(vg);
|
||||
gfx::drawRect(vg, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
gfx::drawText(vg, m_pos + m_title_pos, 24.f, theme->elements[ThemeEntryID_TEXT].colour, 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_bottom, m_line_width, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP));
|
||||
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->GetColour(ThemeEntryID_LINE));
|
||||
gfx::drawRect(vg, 30.f, m_line_bottom, m_line_width, 1.f, theme->GetColour(ThemeEntryID_LINE));
|
||||
|
||||
// todo: cleanup
|
||||
const float x = m_block.x;
|
||||
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) {
|
||||
m_list->Draw(vg, theme, m_items.size(), [this](auto* vg, auto* theme, auto v, auto i) {
|
||||
const auto& [x, y, w, h] = v;
|
||||
if (m_index == i) {
|
||||
gfx::drawRect(vg, x - 4.f, y - 4.f, w + 8.f, h + 8.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour);
|
||||
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->elements[ThemeEntryID_TEXT_SELECTED].colour);
|
||||
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||
gfx::drawText(vg, x + m_text_xoffset, y + (h / 2.f), 20.f, m_items[i].c_str(), NULL, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED));
|
||||
} else {
|
||||
gfx::drawRect(vg, x, y, w, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
gfx::drawRect(vg, x, y + h, w, 1.f, 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->elements[ThemeEntryID_TEXT].colour);
|
||||
if (i != m_items.size() - 1) {
|
||||
gfx::drawRect(vg, x, y + h, w, 1.f, theme->GetColour(ThemeEntryID_LINE_SEPARATOR));
|
||||
}
|
||||
gfx::drawText(vg, x + m_text_xoffset, y + (h / 2.f), 20.f, m_items[i].c_str(), NULL, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT));
|
||||
}
|
||||
y += h;
|
||||
if (y > m_line_bottom) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
nvgRestore(vg);
|
||||
});
|
||||
|
||||
m_scrollbar.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
|
||||
|
||||
@@ -31,7 +31,6 @@ ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback,
|
||||
m_pos.h = 430.f;
|
||||
m_pos.x = 255;
|
||||
m_pos.y = 145;
|
||||
145 + 430; // 575, 200, 420
|
||||
|
||||
m_pos.w = 770.f;
|
||||
m_pos.h = 295.f;
|
||||
@@ -52,9 +51,7 @@ ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback,
|
||||
}
|
||||
|
||||
ProgressBox::~ProgressBox() {
|
||||
mutexLock(&m_mutex);
|
||||
m_exit_requested = true;
|
||||
mutexUnlock(&m_mutex);
|
||||
m_stop_source.request_stop();
|
||||
|
||||
if (R_FAILED(threadWaitForExit(&m_thread))) {
|
||||
log_write("failed to join thread\n");
|
||||
@@ -83,7 +80,7 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
mutexUnlock(&m_mutex);
|
||||
|
||||
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.
|
||||
// const Vec4 box = { 255, 145, 770, 430 };
|
||||
@@ -93,19 +90,15 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
|
||||
// shapes.
|
||||
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;
|
||||
gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((float)offset / (float)size) * prog_bar.w, prog_bar.h, gfx::Colour::CYAN);
|
||||
// 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, gfx::Colour::WHITE, "%u%%", percentage);
|
||||
gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((float)offset / (float)size) * prog_bar.w, prog_bar.h, theme->GetColour(ThemeEntryID_PROGRESSBAR));
|
||||
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, center_x, m_pos.y + 60, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::WHITE, 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);
|
||||
gfx::drawTextArgs(vg, center_x, m_pos.y + 60, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), title.c_str());
|
||||
if (!transfer.empty()) {
|
||||
gfx::drawTextArgs(vg, center_x, prog_bar.y - 15 - 20 * 1.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;
|
||||
}
|
||||
|
||||
auto ProgressBox::UpdateTransfer(u64 offset, u64 size) -> ProgressBox& {
|
||||
auto ProgressBox::UpdateTransfer(s64 offset, s64 size) -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
m_size = size;
|
||||
m_offset = offset;
|
||||
@@ -129,16 +122,11 @@ auto ProgressBox::UpdateTransfer(u64 offset, u64 size) -> ProgressBox& {
|
||||
}
|
||||
|
||||
void ProgressBox::RequestExit() {
|
||||
mutexLock(&m_mutex);
|
||||
m_exit_requested = true;
|
||||
mutexUnlock(&m_mutex);
|
||||
m_stop_source.request_stop();
|
||||
}
|
||||
|
||||
auto ProgressBox::ShouldExit() -> bool {
|
||||
mutexLock(&m_mutex);
|
||||
const auto exit_requested = m_exit_requested;
|
||||
mutexUnlock(&m_mutex);
|
||||
return exit_requested;
|
||||
return m_stop_source.stop_requested();
|
||||
}
|
||||
|
||||
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)
|
||||
: m_font_size{font_size}
|
||||
, m_y_off_base{y}
|
||||
, m_y_off{y}
|
||||
, m_clip_y{y_clip}
|
||||
, m_end_w{w}
|
||||
, m_y_off{y}
|
||||
{
|
||||
SetActions(
|
||||
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 in_clip = m_clip_y / m_step - 1;
|
||||
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.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, 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);
|
||||
gfx::drawRect(vg, banner_vec.w, m_y_off_base, 10, scrollbar_size, theme->GetColour(ThemeEntryID_SCROLLBAR_BACKGROUND));
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
struct SidebarHeader : SidebarEntryBase {
|
||||
|
||||
};
|
||||
auto DistanceBetweenY(Vec4 va, Vec4 vb) -> Vec4 {
|
||||
return Vec4{
|
||||
va.x, va.y,
|
||||
va.w, vb.y - va.y
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -25,14 +32,7 @@ SidebarEntryBase::SidebarEntryBase(std::string&& title)
|
||||
auto SidebarEntryBase::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
// draw spacers or highlight box if in focus (selected)
|
||||
if (HasFocus()) {
|
||||
gfx::drawRect(vg, m_pos, nvgRGB(50,50,50));
|
||||
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
|
||||
gfx::drawRectOutline(vg, theme, 4.f, m_pos);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,16 +61,16 @@ auto SidebarEntryBool::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
SidebarEntryBase::Draw(vg, theme);
|
||||
|
||||
// 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 {
|
||||
// }
|
||||
|
||||
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) {
|
||||
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
|
||||
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);
|
||||
|
||||
// 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 {
|
||||
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)}
|
||||
, m_items{std::move(items)}
|
||||
, m_callback{cb}
|
||||
@@ -155,11 +155,59 @@ auto SidebarEntryArray::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
SidebarEntryBase::Draw(vg, theme);
|
||||
|
||||
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);
|
||||
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);
|
||||
// scrolling text
|
||||
// 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)
|
||||
@@ -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_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
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}});
|
||||
// set button positions
|
||||
SetUiButtonPos({m_pos.x + m_pos.w - 60.f, 675});
|
||||
|
||||
m_selected_y = m_base_pos.y;
|
||||
|
||||
if (!m_items.empty()) {
|
||||
// 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();
|
||||
}
|
||||
const Vec4 pos = DistanceBetweenY(m_top_bar, m_bottom_bar);
|
||||
m_list = std::make_unique<List>(1, 6, pos, m_base_pos);
|
||||
m_list->SetScrollBarPos(GetX() + GetW() - 20, m_base_pos.y - 10, pos.h - m_base_pos.y + 48);
|
||||
}
|
||||
|
||||
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 {
|
||||
m_items[m_index]->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()) {
|
||||
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 {
|
||||
gfx::drawRect(vg, m_pos, nvgRGBA(0, 0, 0, 220));
|
||||
gfx::drawText(vg, m_title_pos, m_title_size, theme->elements[ThemeEntryID_TEXT].colour, m_title.c_str());
|
||||
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_SIDEBAR));
|
||||
gfx::drawText(vg, m_title_pos, m_title_size, theme->GetColour(ThemeEntryID_TEXT), m_title.c_str());
|
||||
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_bottom_bar, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
gfx::drawRect(vg, m_top_bar, theme->GetColour(ThemeEntryID_LINE));
|
||||
gfx::drawRect(vg, m_bottom_bar, theme->GetColour(ThemeEntryID_LINE));
|
||||
|
||||
const auto dist = DistanceBetweenY(m_top_bar, m_bottom_bar);
|
||||
nvgSave(vg);
|
||||
nvgScissor(vg, dist.x, dist.y, dist.w, dist.h);
|
||||
Widget::Draw(vg, theme);
|
||||
|
||||
// for (std::size_t i = m_index_offset; i < m_items.size(); ++i) {
|
||||
// m_items[i]->Draw(vg, theme);
|
||||
// }
|
||||
m_list->Draw(vg, theme, m_items.size(), [this](auto* vg, auto* theme, auto v, auto i) {
|
||||
const auto& [x, y, w, h] = v;
|
||||
|
||||
for (auto&p : m_items) {
|
||||
p->Draw(vg, theme);
|
||||
}
|
||||
if (i != m_items.size() - 1) {
|
||||
gfx::drawRect(vg, x, y + h, w, 1.f, theme->GetColour(ThemeEntryID_LINE_SEPARATOR));
|
||||
}
|
||||
|
||||
nvgRestore(vg);
|
||||
|
||||
// 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);
|
||||
m_items[i]->SetY(y);
|
||||
m_items[i]->Draw(vg, theme);
|
||||
});
|
||||
}
|
||||
|
||||
auto Sidebar::OnFocusGained() noexcept -> void {
|
||||
@@ -303,23 +306,51 @@ auto Sidebar::OnFocusLost() noexcept -> void {
|
||||
void Sidebar::Add(std::shared_ptr<SidebarEntryBase> entry) {
|
||||
m_items.emplace_back(entry);
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
#include "ui/widget.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "app.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
auto uiButton::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
// enable to see button region
|
||||
// gfx::drawRect(vg, m_pos, gfx::Colour::RED);
|
||||
|
||||
nvgTextAlign(vg, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP);
|
||||
nvgFillColor(vg, theme->GetColour(ThemeEntryID_TEXT));
|
||||
nvgFontSize(vg, 20);
|
||||
nvgText(vg, m_hint_pos.x, m_hint_pos.y, m_action.m_hint.c_str(), nullptr);
|
||||
nvgFontSize(vg, 26);
|
||||
nvgText(vg, m_button_pos.x, m_button_pos.y, gfx::getButton(m_button), nullptr);
|
||||
}
|
||||
|
||||
void Widget::Update(Controller* controller, TouchInfo* touch) {
|
||||
for (const auto& [button, action] : m_actions) {
|
||||
if ((action.m_type & ActionType::DOWN) && controller->GotDown(button)) {
|
||||
@@ -11,26 +24,34 @@ void Widget::Update(Controller* controller, TouchInfo* touch) {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
}
|
||||
action.Invoke(true);
|
||||
break;
|
||||
}
|
||||
else if ((action.m_type & ActionType::UP) && controller->GotUp(button)) {
|
||||
action.Invoke(false);
|
||||
break;
|
||||
}
|
||||
else if ((action.m_type & ActionType::HELD) && controller->GotHeld(button)) {
|
||||
action.Invoke(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto draw_actions = GetUiButtons();
|
||||
for (auto& e : draw_actions) {
|
||||
if (touch->is_clicked && touch->in_range(e.GetPos())) {
|
||||
log_write("got click: %s\n", e.m_action.m_hint.c_str());
|
||||
FireAction(e.m_button);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Widget::Draw(NVGcontext* vg, Theme* theme) {
|
||||
Actions draw_actions;
|
||||
auto draw_actions = GetUiButtons();
|
||||
|
||||
for (const auto& [button, action] : m_actions) {
|
||||
if (!action.IsHidden()) {
|
||||
draw_actions.emplace(button, action);
|
||||
}
|
||||
for (auto& e : draw_actions) {
|
||||
e.Draw(vg, theme);
|
||||
}
|
||||
|
||||
gfx::drawButtons(vg, draw_actions, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
}
|
||||
|
||||
auto Widget::HasAction(Button button) const -> bool {
|
||||
@@ -47,4 +68,67 @@ void Widget::RemoveAction(Button button) {
|
||||
}
|
||||
}
|
||||
|
||||
auto Widget::FireAction(Button b, u8 type) -> bool {
|
||||
for (const auto& [button, action] : m_actions) {
|
||||
if (button == b && (action.m_type & type)) {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
action.Invoke(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto Widget::GetUiButtons() const -> uiButtons {
|
||||
auto vg = App::GetVg();
|
||||
auto [x, y] = m_button_pos;
|
||||
|
||||
uiButtons draw_actions;
|
||||
draw_actions.reserve(m_actions.size());
|
||||
|
||||
// build array
|
||||
for (const auto& [button, action] : m_actions) {
|
||||
if (action.IsHidden() || action.m_hint.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uiButton ui_button{button, action};
|
||||
|
||||
// swap
|
||||
if (button == Button::R && draw_actions.size() && draw_actions.back().m_button == Button::L) {
|
||||
const auto s = draw_actions.back();
|
||||
draw_actions.back().m_button = button;
|
||||
draw_actions.back().m_action = action;
|
||||
draw_actions.emplace_back(s);
|
||||
} else {
|
||||
draw_actions.emplace_back(ui_button);
|
||||
}
|
||||
}
|
||||
|
||||
float bounds[4]{};
|
||||
for (auto& e : draw_actions) {
|
||||
nvgTextAlign(vg, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP);
|
||||
|
||||
nvgFontSize(vg, 20.f);
|
||||
nvgTextBounds(vg, x, y, e.m_action.m_hint.c_str(), nullptr, bounds);
|
||||
auto len = bounds[2] - bounds[0];
|
||||
e.m_hint_pos = {x, 675, len, 20};
|
||||
|
||||
x -= len + 8.f;
|
||||
nvgFontSize(vg, 26.f);
|
||||
nvgTextBounds(vg, x, y - 7.f, gfx::getButton(e.m_button), nullptr, bounds);
|
||||
len = bounds[2] - bounds[0];
|
||||
e.m_button_pos = {x, 675 - 4.f, len, 26};
|
||||
x -= len + 34.f;
|
||||
|
||||
e.SetPos(e.m_button_pos);
|
||||
e.SetX(e.GetX() - 40);
|
||||
e.SetW(e.m_hint_pos.x - e.m_button_pos.x + len + 25);
|
||||
e.SetY(e.GetY() - 18);
|
||||
e.SetH(26 + 18 * 2);
|
||||
}
|
||||
|
||||
return draw_actions;
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
|
||||
Reference in New Issue
Block a user