38 Commits
0.5.0 ... touch

Author SHA1 Message Date
ITotalJustice
70d2e9873c add touch scrolling, fix scrollbar, fix appstore search
- when fireing an action, the action array may change. so the loop should break early as soon as an action is handled.
  this fixes the appstore search when pressing B.
- scrollbar no longer goes oob. fixes #76

currently, scrolling has no acceleration.
2025-01-06 22:29:25 +00:00
ITotalJustice
705947fefb add touch support to all objects 2025-01-04 20:31:16 +00:00
ITotalJustice
f48f9a527f Merge branch 'master' into touch 2025-01-01 17:57:32 +00:00
ITotalJustice
cf95128f0b allow for github actions to run on all branches 2025-01-01 17:50:08 +00:00
ITotalJustice
6dbf48d73c fix badly formatted string for i18n
fixes #68
2025-01-01 17:42:07 +00:00
ITotalJustice
1614c8e2e4 remove debug code in homebrew menu 2025-01-01 17:38:07 +00:00
ITotalJustice
cdebcad4fe correctly path npdm kc flags on ams 1.7.1 or greater
fixes #67
2025-01-01 17:34:54 +00:00
ITotalJustice
f824187248 initial work on touch support
list of things not done:
- no scrolling
- only some menus
- no widgets
- no buttons
2025-01-01 17:32:58 +00:00
ITotalJustice
54c63d6f3b add support for mounting different fs.
currently, this feature isn't very useful as you cannot copy/move files across different fs.
2024-12-31 23:58:49 +00:00
ITotalJustice
d840a8ddba re-do how protected files work, by default everything is writeable, aside from fs.
the design in now opt-out rather than opt-in.

for fs, it is still opt-in. this is because the risk of a user deciding to delete a file / folder in the filebrowser menu.
this can now be toggled in the the advanced options menu within filebrowser.
2024-12-31 22:52:45 +00:00
ITotalJustice
c3b31d0fdd initial work on support custom fs mount points 2024-12-31 22:23:50 +00:00
ITotalJustice
dd1a6eb25b initial work on patching npdm debug flags, stubbed for now
stubbed as bit(19) doesn't seem to do what i thought it would.

fixes #67
2024-12-31 22:05:04 +00:00
ITotalJustice
271fab66f5 add option to restore hbmenu when disabling "Replace hbmenu on exit".
fixes #66
2024-12-31 18:13:30 +00:00
Yorunokyujitsu
87642e914e Update korean, japanese language. (#65) 2024-12-31 10:17:22 +00:00
ITotalJustice
45aa7c4e62 fix forwarder icon passing in the wrong size, fix github zip detection / extraction, add back url in json
- i was passing the wrong size for icon, so it always failed to load.
- i didnt realise zips can have many content types, so now i search for the name "zip" which works well.
- extracting a zip will fail if the files are all in the root, ie, no folder, and the subfolders do
  not already exist.
- fix misspelling of download.
2024-12-31 10:15:30 +00:00
ITotalJustice
9b1788d1ec i forgot to enable buffered io for ftp... 2024-12-31 05:52:51 +00:00
shadow2560
389a4cfef5 Update french language. (#63)
Signed-off-by: shadow2560 <24191064+shadow2560@users.noreply.github.com>
2024-12-31 05:06:08 +00:00
ITotalJustice
ac06631156 fix building 2024-12-31 04:53:33 +00:00
ITotalJustice
bc39e668eb add pre/post install message options for github json 2024-12-31 04:41:19 +00:00
ITotalJustice
e452615c77 fix etag cache not being returned upon the 2nd request, add jump page support for all menus.
- all menus feature page jumping, using L2/R2 (or DPAD_LEFT/DPAD_RIGHT in list menus)
- successive calls to fetch the etag would fail, this was seen in themezer and github menus.
- add limit the number of icons loaded per frame in homebrew menu.
- display default icon the image is not ready to be loaded / invalid.

fixes #53
2024-12-31 03:57:08 +00:00
ITotalJustice
588eb01379 poll main_menu vars (ip, charge% time) every 1s, rather than every frame. 2024-12-30 21:28:28 +00:00
ITotalJustice
4855a01f1a further simplify download cache by using a single file
this slightly improves lookup time for each cached entry (etag and last-modified)
by setting both entries next to each other, meaning when the one is found, the other
is already loaded in memory.
2024-12-30 21:12:29 +00:00
ITotalJustice
cb7fb0e506 use ftpsrv config and mountpoints, improve ftp performance by using its vfs. 2024-12-30 21:09:32 +00:00
ITotalJustice
cdb38f27a7 simplify etag caching requests 2024-12-30 02:27:52 +00:00
ITotalJustice
7804bbbcbc add support for finding daybreak in non-standard paths (#62) 2024-12-29 11:52:34 +00:00
ITotalJustice
5db5f93af1 fix building 2024-12-29 01:15:37 +00:00
Ny'hrarr
bab4bfce84 Update pt.json (#61) 2024-12-29 01:02:33 +00:00
Yorunokyujitsu
ec06763e50 Fixed some translations of ko.json (#58)
* Fixed some translations of ko.json

* Add new string to lang.json and update ko, ja.

---------

Co-authored-by: Yorunokyujitsu <seonmini1315@gamil.com>
Co-authored-by: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com>
2024-12-29 01:02:04 +00:00
ITotalJustice
5e315bd65f many fixes and performance improvements for network requests (see commit details)
- add etag support
- add last-modified support

with the above 2 changes, this means that all downloads can be cached. when attempting to download a file,
if the file is an image, load from cache. after, the download is processed with the above tags sent. if a 304 code
is received, then the file hasn't changed. otherwise, the new tags are saved and the downloaded file is now used (in the
case of an image, the new image is now loaded over the cached one).

this results in a *huge* speed improvement and overall a huge amount of bandwidth is saved for both the client and server.

- themezer requests now only request the data needed.

this results in a json file that is 4-5x smaller, meaning a much faster download and parsing time.

- loading images is capped to 2 images a frame. this was done to avoid fs being the bottle neck.
  a 9 page listing will take 5 frames. scrolling through lists is more responsive.

- downloads are pushed to the front of the queue as they're added. the point of this is to prioritise
  data that we need now.

- fix potential crash when sorting files based on names as its possible for a file to have the same name
  in the metadata. this fallsback to sorting by path, which is unique.

- add timeout for processing events. this was done in order to not block the main thread for too long.

- github json files have changed from a name + url to a repo + author pair.
- drawing widgets now starts from the last file in the array. as a menu takes up the whole screen, it
 is pointless drawing menu's underneath. this halves gpu usage.
- download url caching has been removed. this was added to fix a race condition when opening /
  closing a widget which starts a download when created. this would result in 2 same files being
  downloaded at the same time. this is no longer an issue and was overhead per download request.
2024-12-29 00:33:31 +00:00
ITotalJustice
2edfe91ad6 re-write download code to support headers, needed for etag support.
etag support will be added later. github supports it and themezer probably does as well.
appstore does not sadly...
2024-12-27 02:28:44 +00:00
ITotalJustice
7005118876 Create FUNDING.yml 2024-12-26 19:23:39 +00:00
DDinghoya
087d44fb40 Update ko.json (#55)
. Fixed some awkward words
. If there is no abbreviation "...", fixed progressive tense to noun.
2024-12-26 18:34:47 +00:00
ITotalJustice
e3722f2591 include name when prompting the user to select which asset to download 2024-12-26 18:29:03 +00:00
ITotalJustice
47855ce7b4 change workflow to only build MinSizeRel 2024-12-26 18:11:53 +00:00
ITotalJustice
ec7caabdbd add GitHub downloader, fix yyjson helper missing break, hide popup list when out of focus 2024-12-26 18:11:03 +00:00
Yorunokyujitsu
adf0a3b2cd Update ko.json and ja.json and add MTP/FTP strings (#54)
Co-authored-by: Yorunokyujitsu <seeonmini1315@gmail.com>
2024-12-26 04:58:17 +00:00
Ny'hrarr
f88e354ae8 Translate missing fields (Portuguese) (#51)
* Update pt.json
2024-12-26 03:29:33 +00:00
cucholix
df3d8d3990 Update es.json (#49)
Further improvements for ver 0.5.0
2024-12-26 03:29:08 +00:00
77 changed files with 3759 additions and 2409 deletions

15
.github/FUNDING.yml vendored Normal file
View 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']

View File

@@ -1,10 +1,6 @@
name: build name: build
on: on: [push, pull_request]
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs: jobs:
build: build:
@@ -12,7 +8,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest] os: [ubuntu-latest]
preset: [Release, RelWithDebInfo, MinSizeRel, Debug] preset: [MinSizeRel]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
container: devkitpro/devkita64:latest container: devkitpro/devkita64:latest
@@ -24,7 +20,7 @@ jobs:
- name: Configure CMake - name: Configure CMake
run: | run: |
cmake --preset ${{ matrix.preset }} cmake --preset ${{ matrix.preset }} -DUSE_VFS_GC=0
- name: Build - name: Build
run: cmake --build --preset ${{ matrix.preset }} --parallel 4 run: cmake --build --preset ${{ matrix.preset }} --parallel 4

View File

@@ -26,7 +26,8 @@ please include:
## ftp ## ftp
ftp can be enabled via the network menu and listens on port 5000, no username or password is required. ftp can be enabled via the network menu. It uses the same config as ftpsrv `/config/ftpsrv/config.ini`. [See here for the full list
of all configs available](https://github.com/ITotalJustice/ftpsrv/blob/master/assets/config.ini.template).
## mtp ## mtp

View File

@@ -0,0 +1,8 @@
{
"url": "https://github.com/ITotalJustice/ftpsrv",
"assets": [
{
"name": "switch"
}
]
}

View File

@@ -0,0 +1,3 @@
{
"url": "https://github.com/ITotalJustice/sphaira"
}

View File

@@ -0,0 +1,3 @@
{
"url": "https://github.com/ITotalJustice/untitled"
}

View File

@@ -62,6 +62,8 @@
"Music": "Musik", "Music": "Musik",
"Network": "Netzwerk", "Network": "Netzwerk",
"Network Options": "Netzwerk-Optionen", "Network Options": "Netzwerk-Optionen",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink", "Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink verbunden", "Nxlink Connected": "Nxlink verbunden",
"Nxlink Upload": "Nxlink Upload", "Nxlink Upload": "Nxlink Upload",
@@ -199,6 +201,10 @@
"Bad Page": "Ungültige Seite", "Bad Page": "Ungültige Seite",
"Download theme?": "Theme herunterladen?", "Download theme?": "Theme herunterladen?",
"GitHub": "",
"Downloading json": "",
"Select asset to download for ": "",
"Installing ": "Installiere ", "Installing ": "Installiere ",
"Uninstalling ": "Deinstalliere ", "Uninstalling ": "Deinstalliere ",
"Deleting ": "Lösche ", "Deleting ": "Lösche ",
@@ -211,6 +217,8 @@
"Copying ": "Kopiere ", "Copying ": "Kopiere ",
"Trying to load ": "Lade ", "Trying to load ": "Lade ",
"Downloading ": "Lade herunter ", "Downloading ": "Lade herunter ",
"Downloaded ": "",
"Removed ": "",
"Checking MD5": "Prüfe MD5", "Checking MD5": "Prüfe MD5",
"Loading...": "Lade...", "Loading...": "Lade...",
"Loading": "Lade", "Loading": "Lade",

View File

@@ -62,6 +62,8 @@
"Music": "Music", "Music": "Music",
"Network": "Network", "Network": "Network",
"Network Options": "Network Options", "Network Options": "Network Options",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink", "Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink Connected", "Nxlink Connected": "Nxlink Connected",
"Nxlink Upload": "Nxlink Upload", "Nxlink Upload": "Nxlink Upload",
@@ -199,6 +201,10 @@
"Bad Page": "Bad Page", "Bad Page": "Bad Page",
"Download theme?": "Download theme?", "Download theme?": "Download theme?",
"GitHub": "GitHub",
"Downloading json": "Downloading json",
"Select asset to download for ": "Select asset to download for ",
"Installing ": "Installing ", "Installing ": "Installing ",
"Uninstalling ": "Uninstalling ", "Uninstalling ": "Uninstalling ",
"Deleting ": "Deleting ", "Deleting ": "Deleting ",
@@ -211,6 +217,8 @@
"Copying ": "Copying ", "Copying ": "Copying ",
"Trying to load ": "Trying to load ", "Trying to load ": "Trying to load ",
"Downloading ": "Downloading ", "Downloading ": "Downloading ",
"Downloaded ": "Downloaded ",
"Removed ": "Removed ",
"Checking MD5": "Checking MD5", "Checking MD5": "Checking MD5",
"Loading...": "Loading...", "Loading...": "Loading...",
"Loading": "Loading", "Loading": "Loading",

View File

@@ -9,23 +9,23 @@
"OK": "OK", "OK": "OK",
"Back": "Atrás", "Back": "Atrás",
"Select": "Seleccionar", "Select": "Seleccionar",
"Open": "Abierto", "Open": "Abrir",
"Launch": "Ejecutar", "Launch": "Ejecutar",
"Info": "Información", "Info": "Información",
"Install": "Instalar", "Install": "Instalar",
"Delete": "Borrar", "Delete": "Borrar",
"Restart": "", "Restart": "Reiniciar",
"Changelog": "Log de Cambios", "Changelog": "Log de cambios",
"Details": "Detalles", "Details": "Detalles",
"Update": "Actualizar", "Update": "Actualizar",
"Remove": "Borrar", "Remove": "Borrar",
"Download": "Descargar", "Download": "Descargar",
"Next Page": "Página siguiente", "Next Page": "Página siguiente",
"Prev Page": "Página anterior", "Prev Page": "Página anterior",
"Unstar": "", "Unstar": "Quitar favorito",
"Star": "", "Star": "Favorito",
"System memory": "", "System memory": "Memoria de sistema",
"microSD card": "", "microSD card": "microSD",
"Yes": "Sí", "Yes": "Sí",
"No": "No", "No": "No",
"Enabled": "Activado", "Enabled": "Activado",
@@ -38,12 +38,12 @@
"Order": "Orden", "Order": "Orden",
"Search": "Buscar", "Search": "Buscar",
"Updated": "Actualizado", "Updated": "Actualizado",
"Updated (Star)": "Actualizado (Star)", "Updated (Star)": "Actualizado (favorito)",
"Downloads": "Descargas", "Downloads": "Descargas",
"Size": "Tamaño", "Size": "Tamaño",
"Size (Star)": "Tamaño (Star)", "Size (Star)": "Tamaño (favorito)",
"Alphabetical": "Alfabético", "Alphabetical": "Alfabético",
"Alphabetical (Star)": "Alfabético (Star)", "Alphabetical (Star)": "Alfabético (favorito)",
"Likes": "Me Gusta", "Likes": "Me Gusta",
"ID": "ID", "ID": "ID",
"Decending": "Descendente", "Decending": "Descendente",
@@ -53,21 +53,23 @@
"Ascending (Up)": "Ascendente (arriba)", "Ascending (Up)": "Ascendente (arriba)",
"Asc": "Ascendente", "Asc": "Ascendente",
"Menu Options": "Opciones de Menú", "Menu Options": "Opciones de menú",
"Header": "Encabezamiento", "Header": "Encabezado",
"Theme": "Tema", "Theme": "Tema",
"Theme Options": "Opciones de Tema", "Theme Options": "Opciones de tema",
"Select Theme": "Seleccionar Tema", "Select Theme": "Seleccionar tema",
"Shuffle": "Barajar", "Shuffle": "Barajar",
"Music": "Música", "Music": "Música",
"Network": "Red", "Network": "Red",
"Network Options": "Opciones de Red", "Network Options": "Opciones de red",
"Nxlink": "Nxlink", "Ftp": "FTP",
"Nxlink Connected": "Nxlink Conectado", "Mtp": "MTP",
"Nxlink Upload": "Nxlink Subida", "Nxlink": "NXlink",
"Nxlink Finished": "Nxlink Finalizado", "Nxlink Connected": "NXlink conectado",
"Switch-Handheld!": "", "Nxlink Upload": "NXlink subida",
"Switch-Docked!": "", "Nxlink Finished": "NXlink finalizado",
"Switch-Handheld!": "¡Switch-Modo-Portátil!",
"Switch-Docked!": "¡Switch-Modo-TV!",
"Language": "Idioma", "Language": "Idioma",
"Auto": "Automático", "Auto": "Automático",
"English": "English", "English": "English",
@@ -82,41 +84,41 @@
"Portuguese": "Português", "Portuguese": "Português",
"Russian": "Русский", "Russian": "Русский",
"Swedish": "Svenska", "Swedish": "Svenska",
"Logging": "Explotación florestal", "Logging": "Registros",
"Replace hbmenu on exit": "Reemplazar hbmenu al salir", "Replace hbmenu on exit": "Reemplazar hbmenu al salir",
"Misc": "Varios", "Misc": "Varios",
"Misc Options": "Opciones varias", "Misc Options": "Opciones varias",
"Web": "Web", "Web": "Web",
"Install forwarders": "", "Install forwarders": "Instalar forwarders",
"Install location": "", "Install location": "Ruta de instalación ",
"Show install warning": "", "Show install warning": "Mostrar precaución de instalación",
"FileBrowser": "Explorador de Archivos", "FileBrowser": "Explorador de archivos",
"%zd files": "%zd files", "%zd files": "%zd files",
"%zd dirs": "%zd dirs", "%zd dirs": "%zd dirs",
"File Options": "Opciones de Tema", "File Options": "Opciones de archivo",
"Show Hidden": "Mostrar Oculto", "Show Hidden": "Mostrar archivos ocultos",
"Folders First": "Carpetas primero", "Folders First": "Carpetas primero",
"Hidden Last": "Oculto último", "Hidden Last": "Ocultos al final",
"Cut": "Cortar", "Cut": "Cortar",
"Copy": "Copiar", "Copy": "Copiar",
"Paste": "Pegar", "Paste": "Pegar",
"Paste ": "Pegar ", "Paste ": "Pegar ",
" file(s)?": " ¿archivo(s)?", " file(s)?": " ¿archivo(s)?",
"Rename": "Renombrar", "Rename": "Renombrar",
"Set New File Name": "Establecer Nuevo Nombre de Archivo", "Set New File Name": "Establecer nuevo nombre de archivo",
"Advanced": "Avanzado", "Advanced": "Avanzado",
"Advanced Options": "Opciones Avanzadas", "Advanced Options": "Opciones avanzadas",
"Create File": "Crear archivo", "Create File": "Crear archivo",
"Set File Name": "Establecer Nombre de Archivo", "Set File Name": "Establecer nombre de archivo",
"Create Folder": "Crear carpeta", "Create Folder": "Crear carpeta",
"Set Folder Name": "Establecer Nombre de Carpeta", "Set Folder Name": "Establecer nombre de carpeta",
"View as text (unfinished)": "Ver como texto (sin terminar)", "View as text (unfinished)": "Ver como texto (sin terminar)",
"Empty...": "Vacío...", "Empty...": "Vacío...",
"Open with DayBreak?": "Abrir con DayBreak", "Open with DayBreak?": "¿Abrir con DayBreak?",
"Launch ": "", "Launch ": "Abrir ",
"Launch option for: ": "Opción de ejecución para: ", "Launch option for: ": "Opción de abrir con: ",
"Select launcher for: ": "", "Select launcher for: ": "Seleccionar abrir con: ",
"Homebrew": "Honebrew", "Homebrew": "Honebrew",
"Homebrew Options": "Opciones de Homebrew", "Homebrew Options": "Opciones de Homebrew",
@@ -129,15 +131,15 @@
"Creating Meta": "Creando Meta", "Creating Meta": "Creando Meta",
"Writing Nca": "Creando NCA", "Writing Nca": "Creando NCA",
"Updating ncm databse": "Actualizando base de datos ncm", "Updating ncm databse": "Actualizando base de datos ncm",
"Pushing application record": "", "Pushing application record": "Registro de aplicación",
"Installed!": "¡Instalado!", "Installed!": "¡Instalado!",
"Failed to install forwarder": "Fallo al instalar forwarder", "Failed to install forwarder": "Fallo al instalar forwarder",
"Unstarred ": "", "Unstarred ": "Quitar Favorito",
"Starred ": "", "Starred ": "Favorito",
"AppStore": "AppStore", "AppStore": "Tienda",
"Filter: %s | Sort: %s | Order: %s": "Filtrar: %s | Clasificar: %s | Orden: %s", "Filter: %s | Sort: %s | Order: %s": "Filtrar: %s | Clasificar: %s | Orden: %s",
"AppStore Options": "Opciones de la AppStore", "AppStore Options": "Opciones de la Tienda",
"All": "Todo", "All": "Todo",
"Games": "Juegos", "Games": "Juegos",
"Emulators": "Emuladores", "Emulators": "Emuladores",
@@ -145,60 +147,64 @@
"Themes": "Temas", "Themes": "Temas",
"Legacy": "Legado", "Legacy": "Legado",
"version: %s": "version: %s", "version: %s": "version: %s",
"updated: %s": "updated: %s", "updated: %s": "actualizado: %s",
"category: %s": "category: %s", "category: %s": "categoría: %s",
"extracted: %.2f MiB": "extracted: %.2f MiB", "extracted: %.2f MiB": "extraído: %.2f MiB",
"app_dls: %s": "app_dls: %s", "app_dls: %s": "app_dls: %s",
"More by Author": "Mostrar mas del Autor", "More by Author": "Mostrar mas del Autor",
"Leave Feedback": "Dejar Mensaje", "Leave Feedback": "Dejar Mensaje",
"Irs": "IRS", "Irs": "IRS",
"Ambient Noise Level: ": "Nivel de Ruido", "Ambient Noise Level: ": "Nivel de Ruido Ambiente",
"Controller": "Control", "Controller": "Control",
"Pad ": "Almohadilla ", "Pad ": "Almohadilla ",
" (Available)": " (Disponible)", " (Available)": " (Disponible)",
" (Unsupported)": "", " (Unsupported)": "(No Compatible)",
" (Unconnected)": " (Desconectado)", " (Unconnected)": " (Desconectado)",
"HandHeld": "Portátil", "HandHeld": "Portátil",
"Rotation": "Rotación", "Rotation": "Rotación",
"0 (Sideways)": "0 (De Lado)", "0 (Sideways)": "0 (De lado)",
"90 (Flat)": "90 (Plano)", "90 (Flat)": "90 (Plano)",
"180 (-Sideways)": "180 (-De Lado)", "180 (-Sideways)": "180 (-De lado)",
"270 (Upside down)": "270 (Al Revés)", "270 (Upside down)": "270 (Al revés)",
"Colour": "Color", "Colour": "Color",
"Grey": "Gris", "Grey": "Gris",
"Ironbow": "Paleta Térmica", "Ironbow": "Paleta térmica",
"Green": "Verde", "Green": "Verde",
"Red": "Rojo", "Red": "Rojo",
"Blue": "Azul", "Blue": "Azul",
"Light Target": "Objetivo de Luz", "Light Target": "Objetivo de luz",
"All leds": "Todos los leds", "All leds": "Todos los leds",
"Bright group": "Grupo brillante", "Bright group": "Grupo brillo",
"Dim group": "Grupo tenue", "Dim group": "Grupo tenue",
"None": "Ninguno", "None": "Ninguno",
"Gain": "Ganancia", "Gain": "Ganancia",
"Negative Image": "Imagen Negativa", "Negative Image": "Imagen negativa",
"Normal image": "Imagen Normal", "Normal image": "Imagen normal",
"Negative image": "Imagen Negativa", "Negative image": "Imagen negativa",
"Format": "Formato", "Format": "Formato",
"320x240": "320×240", "320x240": "320×240",
"160x120": "160×120", "160x120": "160×120",
"80x60": "80×60", "80x60": "80×60",
"40x30": "40×30", "40x30": "40×30",
"20x15": "20×15", "20x15": "20×15",
"Trimming Format": "Formato de Recorte", "Trimming Format": "Formato de recorte",
"External Light Filter": "Filtro de Luz Externa", "External Light Filter": "Filtro de luz externa",
"Load Default": "Cargar Predeterminado", "Load Default": "Cargar predeterminado",
"Themezer": "Themezer", "Themezer": "Themezer",
"Themezer Options": "Opciones de Themezer", "Themezer Options": "Opciones de Themezer",
"Nsfw": "NSFW", "Nsfw": "NSFW",
"Page": "Página", "Page": "Página",
"Page %zu / %zu": "Page %zu / %zu", "Page %zu / %zu": "Pág. %zu / %zu",
"Enter Page Number": "Ingresar Número de Página", "Enter Page Number": "Ingresar Número de Página",
"Bad Page": "Página Errónea", "Bad Page": "Página Errónea",
"Download theme?": "¿Descargar Tema?", "Download theme?": "¿Descargar Tema?",
"GitHub": "",
"Downloading json": "",
"Select asset to download for ": "",
"Installing ": "Instalando ", "Installing ": "Instalando ",
"Uninstalling ": "Desinstalando ", "Uninstalling ": "Desinstalando ",
"Deleting ": "Borrando ", "Deleting ": "Borrando ",
@@ -209,22 +215,24 @@
"Scanning ": "Escaneando ", "Scanning ": "Escaneando ",
"Creating ": "Creando ", "Creating ": "Creando ",
"Copying ": "Copiando ", "Copying ": "Copiando ",
"Trying to load ": "", "Trying to load ": "Intentando cargar",
"Downloading ": "Descargando ", "Downloading ": "Descargando ",
"Checking MD5": "Chqueando MD5", "Downloaded ": "",
"Removed ": "",
"Checking MD5": "Chequeando MD5",
"Loading...": "Cargando...", "Loading...": "Cargando...",
"Loading": "Cargando", "Loading": "Cargando",
"Empty!": "¡Vacío!", "Empty!": "¡Vacío!",
"Not Ready...": "No Listo Aún...", "Not Ready...": "No listo aún...",
"Error loading page!": "¡Error cargando la página!", "Error loading page!": "¡Error cargando la página!",
"Update avaliable: ": "Actualización disponible: ", "Update avaliable: ": "Actualización disponible: ",
"Download update: ": "Descargar actualización: ", "Download update: ": "Descargar actualización: ",
"Updated to ": "", "Updated to ": "Actualizado a ",
"Restart Sphaira?": "", "Restart Sphaira?": "¿Reiniciar Sphaira?",
"Failed to download update": "Fallo al descargar actualización", "Failed to download update": "Fallo al descargar actualización",
"Delete Selected files?": "¿Eliminar archivos Seleccionados?", "Delete Selected files?": "¿Eliminar archivos seleccionados?",
"Completely remove ": "Eliminar completamente", "Completely remove ": "Eliminar completamente",
"Are you sure you want to delete ": "¿Estás seguro que quieres eliminar? ", "Are you sure you want to delete ": "¿Estás seguro que quieres eliminar? ",
"Are you sure you wish to cancel?": "¿Estás seguro que deseas cancelar?", "Are you sure you wish to cancel?": "¿Estás seguro que deseas cancelar?",
"If this message appears repeatedly, please open an issue.": "" "If this message appears repeatedly, please open an issue.": "Si este mensaje aparece repetidamente, por favor abrir un 'issue'."
} }

View File

@@ -62,6 +62,8 @@
"Music": "Musique", "Music": "Musique",
"Network": "Réseau", "Network": "Réseau",
"Network Options": "Options Réseau", "Network Options": "Options Réseau",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink", "Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink Connecté", "Nxlink Connected": "Nxlink Connecté",
"Nxlink Upload": "Nxlink téléversement", "Nxlink Upload": "Nxlink téléversement",
@@ -199,6 +201,10 @@
"Bad Page": "Page inexistante", "Bad Page": "Page inexistante",
"Download theme?": "Télécharger le thème?", "Download theme?": "Télécharger le thème?",
"GitHub": "GitHub",
"Downloading json": "Téléchargement du json",
"Select asset to download for ": "Sélectionner l'asset pour télécharger ",
"Installing ": "Installation ", "Installing ": "Installation ",
"Uninstalling ": "Désinstallation ", "Uninstalling ": "Désinstallation ",
"Deleting ": "Suppression ", "Deleting ": "Suppression ",
@@ -211,6 +217,8 @@
"Copying ": "Copie ", "Copying ": "Copie ",
"Trying to load ": "Tente de charger ", "Trying to load ": "Tente de charger ",
"Downloading ": "Téléchargement ", "Downloading ": "Téléchargement ",
"Downloaded ": "Téléchargé",
"Removed ": "Supprimé ",
"Checking MD5": "Vérification MD5", "Checking MD5": "Vérification MD5",
"Loading...": "Chargement...", "Loading...": "Chargement...",
"Loading": "Chargement", "Loading": "Chargement",

View File

@@ -62,6 +62,8 @@
"Music": "Musica", "Music": "Musica",
"Network": "Rete", "Network": "Rete",
"Network Options": "Opzioni di rete", "Network Options": "Opzioni di rete",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink", "Nxlink": "Nxlink",
"Nxlink Connected": "", "Nxlink Connected": "",
"Nxlink Upload": "", "Nxlink Upload": "",
@@ -199,6 +201,10 @@
"Bad Page": "", "Bad Page": "",
"Download theme?": "", "Download theme?": "",
"GitHub": "",
"Downloading json": "",
"Select asset to download for ": "",
"Installing ": "", "Installing ": "",
"Uninstalling ": "", "Uninstalling ": "",
"Deleting ": "", "Deleting ": "",
@@ -211,6 +217,8 @@
"Copying ": "", "Copying ": "",
"Trying to load ": "", "Trying to load ": "",
"Downloading ": "", "Downloading ": "",
"Downloaded ": "",
"Removed ": "",
"Checking MD5": "", "Checking MD5": "",
"Loading...": "", "Loading...": "",
"Loading": "", "Loading": "",

View File

@@ -62,6 +62,8 @@
"Music": "BGM", "Music": "BGM",
"Network": "ネットワーク", "Network": "ネットワーク",
"Network Options": "ネットワーク設定", "Network Options": "ネットワーク設定",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink", "Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink 接続", "Nxlink Connected": "Nxlink 接続",
"Nxlink Upload": "Nxlink アップロード", "Nxlink Upload": "Nxlink アップロード",
@@ -199,6 +201,10 @@
"Bad Page": "ページが見つかりません", "Bad Page": "ページが見つかりません",
"Download theme?": "テーマをインストールしますか?", "Download theme?": "テーマをインストールしますか?",
"GitHub": "GitHub",
"Downloading json": "JSONからダウンロード",
"Select asset to download for ": "ダウンロードアイテムを選択 ",
"Installing ": "インストール中 ", "Installing ": "インストール中 ",
"Uninstalling ": "アンインストール中 ", "Uninstalling ": "アンインストール中 ",
"Deleting ": "削除中 ", "Deleting ": "削除中 ",
@@ -211,6 +217,8 @@
"Copying ": "コピー中 ", "Copying ": "コピー中 ",
"Trying to load ": "サムネイルを取得中 ", "Trying to load ": "サムネイルを取得中 ",
"Downloading ": "ダウンロード中 ", "Downloading ": "ダウンロード中 ",
"Downloaded ": "ダウンロード完了 ",
"Removed ": "除去完了 ",
"Checking MD5": "MD5を確認中 ", "Checking MD5": "MD5を確認中 ",
"Loading...": "ロード中", "Loading...": "ロード中",
"Loading": "ロード中", "Loading": "ロード中",

View File

@@ -1,6 +1,6 @@
{ {
"[Applet Mode]": "[애플릿 모드]", "[Applet Mode]": "[애플릿 모드]",
"No Internet": "네트워크 연결 없음", "No Internet": "인터넷 연결 없음",
"Files": "파일 탐색기", "Files": "파일 탐색기",
"Apps": "홈브류", "Apps": "홈브류",
"Store": "앱스토어", "Store": "앱스토어",
@@ -32,7 +32,7 @@
"Disabled": "", "Disabled": "",
"Sort By": "정렬", "Sort By": "정렬",
"Sort Options": "정렬 설정", "Sort Options": "정렬 옵션",
"Filter": "필터", "Filter": "필터",
"Sort": "분류", "Sort": "분류",
"Order": "정렬", "Order": "정렬",
@@ -45,7 +45,7 @@
"Alphabetical": "알파벳순", "Alphabetical": "알파벳순",
"Alphabetical (Star)": "알파벳순 (즐겨찾기)", "Alphabetical (Star)": "알파벳순 (즐겨찾기)",
"Likes": "좋아요순", "Likes": "좋아요순",
"ID": "작성자순", "ID": "ID순",
"Decending": "내림차순", "Decending": "내림차순",
"Descending (down)": "내림차순", "Descending (down)": "내림차순",
"Desc": "내림차순", "Desc": "내림차순",
@@ -56,18 +56,20 @@
"Menu Options": "메뉴", "Menu Options": "메뉴",
"Header": "헤더", "Header": "헤더",
"Theme": "테마", "Theme": "테마",
"Theme Options": "테마 설정", "Theme Options": "테마 옵션",
"Select Theme": "테마 선택", "Select Theme": "테마 선택",
"Shuffle": "셔플", "Shuffle": "셔플",
"Music": "BGM", "Music": "BGM",
"Network": "네트워크", "Network": "네트워크",
"Network Options": "네트워크 설정", "Network Options": "네트워크 옵션",
"Ftp": "FTP (무선)",
"Mtp": "MTP (유선)",
"Nxlink": "Nxlink", "Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink 연결됨", "Nxlink Connected": "Nxlink 연결됨",
"Nxlink Upload": "Nxlink 업로드", "Nxlink Upload": "Nxlink 업로드",
"Nxlink Finished": "Nxlink 종료됨", "Nxlink Finished": "Nxlink 종료됨",
"Switch-Handheld!": "휴대모드로 전환되었습니다!", "Switch-Handheld!": "휴대모드로 전환!",
"Switch-Docked!": "독 모드로 전환되었습니다!", "Switch-Docked!": "독 모드로 전환!",
"Language": "언어", "Language": "언어",
"Auto": "자동", "Auto": "자동",
"English": "English", "English": "English",
@@ -87,14 +89,14 @@
"Misc": "기타", "Misc": "기타",
"Misc Options": "기타", "Misc Options": "기타",
"Web": "웹 브라우저", "Web": "웹 브라우저",
"Install forwarders": "바로가기 설치 기능", "Install forwarders": "바로가기 설치",
"Install location": "설치 경로", "Install location": "설치 위치",
"Show install warning": "바로가기 설치 경고 표시", "Show install warning": "설치 경고 표시",
"FileBrowser": "파일 탐색기", "FileBrowser": "파일 탐색기",
"%zd files": "%zd개 파일", "%zd files": "%zd 개 파일",
"%zd dirs": "%zd개 폴더", "%zd dirs": "%zd 개 폴더",
"File Options": "파일 설정", "File Options": "파일 옵션",
"Show Hidden": "숨겨진 항목 표시", "Show Hidden": "숨겨진 항목 표시",
"Folders First": "폴더 우선 정렬", "Folders First": "폴더 우선 정렬",
"Hidden Last": "숨겨진 항목 후순 정렬", "Hidden Last": "숨겨진 항목 후순 정렬",
@@ -102,34 +104,34 @@
"Copy": "복사", "Copy": "복사",
"Paste": "붙여넣기", "Paste": "붙여넣기",
"Paste ": " ", "Paste ": " ",
" file(s)?": "개 항목을 붙여넣으시겠습니까?", " file(s)?": "개 항목을 붙여넣을까요?",
"Rename": "이름 바꾸기", "Rename": "이름 바꾸기",
"Set New File Name": "새 파일명 입력", "Set New File Name": "새 파일명 입력",
"Advanced": "고급", "Advanced": "고급",
"Advanced Options": "고급 설정", "Advanced Options": "고급 옵션",
"Create File": "파일 생성", "Create File": "파일",
"Set File Name": "파일명 입력", "Set File Name": "파일명 입력",
"Create Folder": "새 폴더", "Create Folder": "새 폴더",
"Set Folder Name": "폴더명 입력", "Set Folder Name": "폴더명 입력",
"View as text (unfinished)": "텍스트로 보기 (미완성)", "View as text (unfinished)": "텍스트로 보기 (미완성)",
"Empty...": "비어있습니다...", "Empty...": "비어있...",
"Open with DayBreak?": "DayBreak로 여시겠습니까?", "Open with DayBreak?": "DayBreak로 열까요?",
"Launch ": "실행하시겠습니까 ", "Launch ": "실행할까요 ",
"Launch option for: ": "실행 설정: ", "Launch option for: ": "실행 옵션: ",
"Select launcher for: ": "실행 런처: ", "Select launcher for: ": "실행 런처: ",
"Homebrew": "홈브류", "Homebrew": "홈브류",
"Homebrew Options": "홈브류 설정", "Homebrew Options": "홈브류 옵션",
"Hide Sphaira": "Sphaira 숨기기", "Hide Sphaira": "Sphaira 숨기기",
"Install Forwarder": "바로가기 설치", "Install Forwarder": "바로가기 설치",
"WARNING: Installing forwarders will lead to a ban!": "주의: 시스낸드에서 바로가기 설치시 BAN 위험이 있습니다!", "WARNING: Installing forwarders will lead to a ban!": "경고: 시스낸드에서 바로가기 설치시 위험이 있습니다!",
"Installing Forwarder": "바로가기 설치중...", "Installing Forwarder": "바로가기 설치",
"Creating Program": "프로그램 작성중...", "Creating Program": "프로그램 생성",
"Creating Control": "컨트롤 작성중...", "Creating Control": "컨트롤 생성",
"Creating Meta": "메타 작성중...", "Creating Meta": "메타 생성",
"Writing Nca": "Nca 쓰는중...", "Writing Nca": "Nca 쓰",
"Updating ncm databse": "ncm 데이터베이스 업데이트중...", "Updating ncm databse": "Ncm 데이터베이스 업데이트",
"Pushing application record": "응용 프로그램 기록 푸시중...", "Pushing application record": "응용 프로그램 기록 푸",
"Installed!": "설치 완료!", "Installed!": "설치 완료!",
"Failed to install forwarder": "바로가기 설치 실패", "Failed to install forwarder": "바로가기 설치 실패",
"Unstarred ": "즐겨찾기 해제: ", "Unstarred ": "즐겨찾기 해제: ",
@@ -137,40 +139,40 @@
"AppStore": "앱스토어", "AppStore": "앱스토어",
"Filter: %s | Sort: %s | Order: %s": "필터: %s | 분류: %s | 정렬: %s", "Filter: %s | Sort: %s | Order: %s": "필터: %s | 분류: %s | 정렬: %s",
"AppStore Options": "앱스토어 설정", "AppStore Options": "앱스토어 옵션",
"All": "전체", "All": "모두",
"Games": "게임", "Games": "게임",
"Emulators": "에뮬레이터", "Emulators": "에뮬레이터",
"Tools": "도구", "Tools": "도구",
"Themes": "테마", "Themes": "테마",
"Legacy": "레거시", "Legacy": "레거시",
"version: %s": "버전: %s", "version: %s": "버전: %s",
"updated: %s": "갱신일: %s", "updated: %s": "업데이트: %s",
"category: %s": "카테고리: %s", "category: %s": "카테고리: %s",
"extracted: %.2f MiB": "용량: %.2f MiB", "extracted: %.2f MiB": "용량: %.2f MiB",
"app_dls: %s": "다운로드 횟수: %s", "app_dls: %s": "다운로드 횟수: %s",
"More by Author": "개발자의 다른 앱 더 보기", "More by Author": "개발자의 다른 앱 더 보기",
"Leave Feedback": "피드백 남기기", "Leave Feedback": "피드백 남기기",
"Irs": "Joy-Con IR 카메라", "Irs": "조이콘 적외선 카메라",
"Ambient Noise Level: ": "노이즈 레벨: ", "Ambient Noise Level: ": "주변 노이즈 레벨: ",
"Controller": "컨트롤러", "Controller": "컨트롤러",
"Pad ": "조이콘 ", "Pad ": "조이콘 ",
" (Available)": " (사용 가능)", " (Available)": " (사용 가능)",
" (Unsupported)": " (지원 안됨)", " (Unsupported)": " (지원 안됨)",
" (Unconnected)": " (연결 없음)", " (Unconnected)": " (연결 없음)",
"HandHeld": "- 본체 연결", "HandHeld": "본체 연결",
"Rotation": "화면 회전", "Rotation": "화면 회전",
"0 (Sideways)": "0 (좌회전)", "0 (Sideways)": "반시계방향 90° 회전",
"90 (Flat)": "90 (정방향)", "90 (Flat)": "정방향",
"180 (-Sideways)": "180 (우회전)", "180 (-Sideways)": "시계방향 90° 회전",
"270 (Upside down)": "270 (역전)", "270 (Upside down)": "상하반전",
"Colour": "컬러 팔레트", "Colour": "색상",
"Grey": "그레이", "Grey": "회색",
"Ironbow": "아이언보우", "Ironbow": "아이언보우",
"Green": "그린", "Green": "초록색",
"Red": "레드", "Red": "빨간색",
"Blue": "블루", "Blue": "파란색",
"Light Target": "반사 표적", "Light Target": "반사 표적",
"All leds": "모든 LED 켜기", "All leds": "모든 LED 켜기",
"Bright group": "Bright LED 켜기", "Bright group": "Bright LED 켜기",
@@ -191,40 +193,46 @@
"Load Default": "기본값으로 설정", "Load Default": "기본값으로 설정",
"Themezer": "Themezer", "Themezer": "Themezer",
"Themezer Options": "Themezer 설정", "Themezer Options": "Themezer 옵션",
"Nsfw": "선정성 테마", "Nsfw": "선정성 테마",
"Page": "페이지", "Page": "페이지",
"Page %zu / %zu": "페이지 %zu / %zu", "Page %zu / %zu": "페이지 %zu / %zu",
"Enter Page Number": "페이지 번호 입력", "Enter Page Number": "페이지 번호 입력",
"Bad Page": "잘못된 페이지", "Bad Page": "잘못된 페이지",
"Download theme?": "테마를 내려받으시겠습니까?", "Download theme?": "테마를 다운로드할까요?",
"Installing ": "설치중... ", "GitHub": "GitHub",
"Uninstalling ": "설치 제거중... ", "Downloading json": "JSON에서 다운로드",
"Deleting ": "삭제중... ", "Select asset to download for ": "다운로드 아이템 선택 ",
"Deleting": "삭제중...",
"Pasting ": "붙여넣는중... ", "Installing ": "설치 ",
"Pasting": "붙여넣는중...", "Uninstalling ": "설치 제거 ",
"Removing ": "제거중... ", "Deleting ": "제 ",
"Scanning ": "스캔중... ", "Deleting": "삭제",
"Creating ": "작성중... ", "Pasting ": "붙여넣기 ",
"Copying ": "복사중... ", "Pasting": "붙여넣기",
"Removing ": "제거 ",
"Scanning ": "스캔 ",
"Creating ": "생성 ",
"Copying ": "복사 ",
"Trying to load ": "썸네일 받아오는 중... ", "Trying to load ": "썸네일 받아오는 중... ",
"Downloading ": "다운로드중... ", "Downloading ": "다운로드 ",
"Checking MD5": "MD5 확인중... ", "Downloaded ": "다운로드 완료: ",
"Removed ": "제거 됨: ",
"Checking MD5": "MD5 확인",
"Loading...": "로딩 중...", "Loading...": "로딩 중...",
"Loading": "로딩 중...", "Loading": "로딩 중...",
"Empty!": "찾을 수 없습니다!", "Empty!": "찾을 수 없습니다!",
"Not Ready...": "준비되지 않았습니다...", "Not Ready...": "준비되지 않...",
"Error loading page!": "페이지 로딩 오류!", "Error loading page!": "페이지 로딩 오류!",
"Update avaliable: ": "업데이트 가능: ", "Update avaliable: ": "업데이트 가능: ",
"Download update: ": "업데이트 다운로드: ", "Download update: ": "업데이트 다운로드: ",
"Updated to ": "업데이트: ", "Updated to ": "업데이트: ",
"Restart Sphaira?": "Sphaira를 재시작 하시겠습니까?", "Restart Sphaira?": "Sphaira를 재시작할까요?",
"Failed to download update": "업데이트 다운로드 실패!", "Failed to download update": "업데이트 다운로드 실패",
"Delete Selected files?": "정말 삭제하시겠습니까?", "Delete Selected files?": "선택한 파일을 삭제할까요?",
"Completely remove ": "제거하시겠습니까 ", "Completely remove ": "정말 삭제할까요 ",
"Are you sure you want to delete ": "정말 삭제하시겠습니까 ", "Are you sure you want to delete ": "정말 삭제할까요 ",
"Are you sure you wish to cancel?": "정말 취소하시겠습니까?", "Are you sure you wish to cancel?": "정말 취소할까요?",
"If this message appears repeatedly, please open an issue.": "해당 메시지가 반복해서 나타나는 경우, 이슈를 열어주세요." "If this message appears repeatedly, please open an issue.": "해당 메시지가 반복해서 나타나는 경우, 이슈를 등록하세요."
} }

View File

@@ -62,6 +62,8 @@
"Music": "Muziek", "Music": "Muziek",
"Network": "Netwerk", "Network": "Netwerk",
"Network Options": "Netwerkopties", "Network Options": "Netwerkopties",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink", "Nxlink": "Nxlink",
"Nxlink Connected": "", "Nxlink Connected": "",
"Nxlink Upload": "", "Nxlink Upload": "",
@@ -199,6 +201,10 @@
"Bad Page": "", "Bad Page": "",
"Download theme?": "", "Download theme?": "",
"GitHub": "",
"Downloading json": "",
"Select asset to download for ": "",
"Installing ": "", "Installing ": "",
"Uninstalling ": "", "Uninstalling ": "",
"Deleting ": "", "Deleting ": "",
@@ -211,6 +217,8 @@
"Copying ": "", "Copying ": "",
"Trying to load ": "", "Trying to load ": "",
"Downloading ": "", "Downloading ": "",
"Downloaded ": "",
"Removed ": "",
"Checking MD5": "", "Checking MD5": "",
"Loading...": "", "Loading...": "",
"Loading": "", "Loading": "",

View File

@@ -1,31 +1,31 @@
{ {
"[Applet Mode]": "[Modo Applet]", "[Applet Mode]": "[Modo Applet]",
"No Internet": "Sem Internet", "No Internet": "Sem Internet",
"Files": "", "Files": "Arquivos",
"Apps": "", "Apps": "Aplicativos",
"Store": "", "Store": "Loja",
"Menu": "Menu", "Menu": "Menu",
"Options": "Opções", "Options": "Opções",
"OK": "", "OK": "OK",
"Back": "Voltar", "Back": "Voltar",
"Select": "", "Select": "Selecionar",
"Open": "Abrir", "Open": "Abrir",
"Launch": "Iniciar", "Launch": "Iniciar",
"Info": "Informações", "Info": "Informações",
"Install": "Instalar", "Install": "Instalar",
"Delete": "Excluir", "Delete": "Excluir",
"Restart": "", "Restart": "Reiniciar",
"Changelog": "", "Changelog": "Changelog",
"Details": "", "Details": "Detalhes",
"Update": "", "Update": "Atualizar",
"Remove": "", "Remove": "Remover",
"Download": "Download", "Download": "Download",
"Next Page": "Próxima página", "Next Page": "Próxima página",
"Prev Page": "Página anterior", "Prev Page": "Página anterior",
"Unstar": "", "Unstar": "Desfavoritar",
"Star": "", "Star": "Favoritar",
"System memory": "", "System memory": "Memória do console",
"microSD card": "", "microSD card": "Cartão microSD",
"Yes": "Sim", "Yes": "Sim",
"No": "Não", "No": "Não",
"Enabled": "Habilitado", "Enabled": "Habilitado",
@@ -38,20 +38,20 @@
"Order": "Ordem", "Order": "Ordem",
"Search": "Procurar", "Search": "Procurar",
"Updated": "Atualizado", "Updated": "Atualizado",
"Updated (Star)": "", "Updated (Star)": "Atualizado (Favoritos)",
"Downloads": "Downloads", "Downloads": "Downloads",
"Size": "Tamanho", "Size": "Tamanho",
"Size (Star)": "", "Size (Star)": "Tamanho (Favoritos)",
"Alphabetical": "Alfabético", "Alphabetical": "Alfabético",
"Alphabetical (Star)": "", "Alphabetical (Star)": "Alfabético (Favoritos)",
"Likes": "", "Likes": "Curtidas",
"ID": "", "ID": "ID",
"Decending": "Decrescente", "Decending": "Decrescente",
"Descending (down)": "Decrescente", "Descending (down)": "Decrescente (Baixo)",
"Desc": "Decrescente", "Desc": "Decr.",
"Ascending": "Ascendente", "Ascending": "Ascendente",
"Ascending (Up)": "Ascendente", "Ascending (Up)": "Ascendente (Cima)",
"Asc": "Ascendente", "Asc": "Asc.",
"Menu Options": "Opções do menu", "Menu Options": "Opções do menu",
"Header": "Cabeçalho", "Header": "Cabeçalho",
@@ -62,14 +62,16 @@
"Music": "Música", "Music": "Música",
"Network": "Rede", "Network": "Rede",
"Network Options": "Opções de rede", "Network Options": "Opções de rede",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink", "Nxlink": "Nxlink",
"Nxlink Connected": "", "Nxlink Connected": "Nxlink conectado",
"Nxlink Upload": "", "Nxlink Upload": "Envio Nxlink",
"Nxlink Finished": "", "Nxlink Finished": "Nxlink finalizado",
"Switch-Handheld!": "", "Switch-Handheld!": "Switch-Portátil",
"Switch-Docked!": "", "Switch-Docked!": "Switch-Docado",
"Language": "Idioma", "Language": "Idioma",
"Auto": "", "Auto": "Automático",
"English": "English", "English": "English",
"Japanese": "日本語", "Japanese": "日本語",
"French": "Français", "French": "Français",
@@ -83,59 +85,59 @@
"Russian": "Русский", "Russian": "Русский",
"Swedish": "Svenska", "Swedish": "Svenska",
"Logging": "Logging", "Logging": "Logging",
"Replace hbmenu on exit": "Substitua hbmenu ao sair", "Replace hbmenu on exit": "Substituir hbmenu ao sair",
"Misc": "Diversos", "Misc": "Diversos",
"Misc Options": "Opções diversas", "Misc Options": "Opções diversas",
"Web": "Rede", "Web": "Navegador web",
"Install forwarders": "", "Install forwarders": "Instalar forwarder",
"Install location": "", "Install location": "Local de instalação",
"Show install warning": "", "Show install warning": "Mostrar aviso de instalação",
"FileBrowser": "Navegador de arquivos", "FileBrowser": "Navegador de arquivos",
"%zd files": "%zd files", "%zd files": "%zd arquivo(s)",
"%zd dirs": "%zd dirs", "%zd dirs": "%zd diretório(s)",
"File Options": "Opções de arquivo", "File Options": "Opções de arquivo",
"Show Hidden": "Mostrar oculto", "Show Hidden": "Mostrar ocultos",
"Folders First": "Pastas primeiro", "Folders First": "Pastas primeiro",
"Hidden Last": "Oculto por último", "Hidden Last": "Ocultos por último",
"Cut": "Cortar", "Cut": "Cortar",
"Copy": "Copiar", "Copy": "Copiar",
"Paste": "", "Paste": "Colar",
"Paste ": "", "Paste ": "Colar",
" file(s)?": "", " file(s)?": " arquivo(s)?",
"Rename": "Renomear", "Rename": "Renomear",
"Set New File Name": "", "Set New File Name": "Definir novo nome do arquivo",
"Advanced": "Avançado", "Advanced": "Avançado",
"Advanced Options": "Criar arquivo", "Advanced Options": "Opções avançadas",
"Create File": "Criar arquivo", "Create File": "Criar arquivo",
"Set File Name": "", "Set File Name": "Definir nome do arquivo",
"Create Folder": "Criar pasta", "Create Folder": "Criar pasta",
"Set Folder Name": "", "Set Folder Name": "Definir novo nome da pasta",
"View as text (unfinished)": "Ver como texto (inacabado)", "View as text (unfinished)": "Ver como texto (inacabado)",
"Empty...": "", "Empty...": "Vazio...",
"Open with DayBreak?": "", "Open with DayBreak?": "Abrir com DayBreak?",
"Launch ": "", "Launch ": "Iniciar",
"Launch option for: ": "", "Launch option for: ": "Opções de inicialização para: ",
"Select launcher for: ": "", "Select launcher for: ": "Selecionar launcher para: ",
"Homebrew": "Homebrew", "Homebrew": "Homebrew",
"Homebrew Options": "Opções do Homebrew", "Homebrew Options": "Opções do Homebrew",
"Hide Sphaira": "Esconder Sphaira", "Hide Sphaira": "Esconder Sphaira",
"Install Forwarder": "Instalar forwarder", "Install Forwarder": "Instalar forwarder",
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Isso pode resultar em um banimento!", "WARNING: Installing forwarders will lead to a ban!": "AVISO: Isso pode resultar em um banimento!",
"Installing Forwarder": "", "Installing Forwarder": "Instalando forwarder",
"Creating Program": "", "Creating Program": "Criando Program",
"Creating Control": "", "Creating Control": "Criando Control",
"Creating Meta": "", "Creating Meta": "Criando Meta",
"Writing Nca": "", "Writing Nca": "Escrevendo NCA",
"Updating ncm databse": "", "Updating ncm databse": "Atualizando base de dados NCM",
"Pushing application record": "", "Pushing application record": "Aplicando registro do aplicativo",
"Installed!": "", "Installed!": "Instalado!",
"Failed to install forwarder": "", "Failed to install forwarder": "Falha ao instalar forwarder",
"Unstarred ": "", "Unstarred ": "Desfavoritado ",
"Starred ": "", "Starred ": "Favoritado ",
"AppStore": "", "AppStore": "AppStore",
"Filter: %s | Sort: %s | Order: %s": "Filtro: %s | Organizar: %s | Ordem: %s", "Filter: %s | Sort: %s | Order: %s": "Filtro: %s | Organizar: %s | Ordem: %s",
"AppStore Options": "Opções da AppStore", "AppStore Options": "Opções da AppStore",
"All": "Todos", "All": "Todos",
@@ -144,20 +146,20 @@
"Tools": "Ferramentas", "Tools": "Ferramentas",
"Themes": "Temas", "Themes": "Temas",
"Legacy": "Legado", "Legacy": "Legado",
"version: %s": "version: %s", "version: %s": "versão: %s",
"updated: %s": "updated: %s", "updated: %s": "atualizado: %s",
"category: %s": "category: %s", "category: %s": "categoria: %s",
"extracted: %.2f MiB": "extracted: %.2f MiB", "extracted: %.2f MiB": "tam. extraído: %.2f MiB",
"app_dls: %s": "app_dls: %s", "app_dls: %s": "downloads: %s",
"More by Author": "", "More by Author": "Mais do autor",
"Leave Feedback": "", "Leave Feedback": "Deixar um feedback",
"Irs": "Irs", "Irs": "Irs",
"Ambient Noise Level: ": "", "Ambient Noise Level: ": "Nível de ruído ambiente",
"Controller": "Controle", "Controller": "Controle",
"Pad ": "Pad ", "Pad ": "Pad ",
" (Available)": " (Disponível)", " (Available)": " (Disponível)",
" (Unsupported)": "", " (Unsupported)": "(Não suportado)",
" (Unconnected)": " (Desconectado)", " (Unconnected)": " (Desconectado)",
"HandHeld": "Portátil", "HandHeld": "Portátil",
"Rotation": "Rotação", "Rotation": "Rotação",
@@ -180,51 +182,57 @@
"Negative Image": "Imagem negativa", "Negative Image": "Imagem negativa",
"Normal image": "Imagem normal", "Normal image": "Imagem normal",
"Negative image": "Imagem negativa", "Negative image": "Imagem negativa",
"Format": "Formatar", "Format": "Formato",
"320x240": "320×240", "320x240": "320×240",
"160x120": "160×120", "160x120": "160×120",
"80x60": "80×60", "80x60": "80×60",
"40x30": "40×30", "40x30": "40×30",
"20x15": "20×15", "20x15": "20×15",
"Trimming Format": "Formato de corte", "Trimming Format": "Formato de corte",
"External Light Filter": "Filtro de luz externo", "External Light Filter": "Filtro de luz externa",
"Load Default": "Carregar padrão", "Load Default": "Carregar padrão",
"Themezer": "Themezer", "Themezer": "Themezer",
"Themezer Options": "", "Themezer Options": "Opções do Themezer",
"Nsfw": "", "Nsfw": "NSFW",
"Page": "", "Page": "Página",
"Page %zu / %zu": "Page %zu / %zu", "Page %zu / %zu": "Page %zu / %zu",
"Enter Page Number": "", "Enter Page Number": "Digite o número da página",
"Bad Page": "", "Bad Page": "Página inválida",
"Download theme?": "", "Download theme?": "Baixar tema?",
"Installing ": "", "GitHub": "",
"Uninstalling ": "", "Downloading json": "",
"Deleting ": "", "Select asset to download for ": "",
"Deleting": "",
"Pasting ": "", "Installing ": "Instalando ",
"Pasting": "", "Uninstalling ": "Desinstalando ",
"Removing ": "", "Deleting ": "Deletando ",
"Scanning ": "", "Deleting": "Deletando ",
"Creating ": "", "Pasting ": "Colando ",
"Copying ": "", "Pasting": "Colando ",
"Trying to load ": "", "Removing ": "Removendo ",
"Downloading ": "", "Scanning ": "Analisando ",
"Checking MD5": "", "Creating ": "Criando ",
"Loading...": "", "Copying ": "Copiando ",
"Loading": "", "Trying to load ": "Tentando carregar ",
"Empty!": "", "Downloading ": "Baixando ",
"Not Ready...": "", "Downloaded ": "",
"Error loading page!": "", "Removed ": "",
"Update avaliable: ": "", "Checking MD5": "Checando MD5",
"Download update: ": "", "Loading...": "Carregando...",
"Updated to ": "", "Loading": "Carregando",
"Restart Sphaira?": "", "Empty!": "Vazio!",
"Failed to download update": "", "Not Ready...": "Não está pronto...",
"Delete Selected files?": "", "Error loading page!": "Erro ao carregar página!",
"Completely remove ": "", "Update avaliable: ": "Atualização disponível: ",
"Are you sure you want to delete ": "Excluir ", "Download update: ": "Baixar autalização: ",
"Are you sure you wish to cancel?": "", "Updated to ": "Atualizado para ",
"If this message appears repeatedly, please open an issue.": "" "Restart Sphaira?": "Reiniciar Sphaira?",
"Failed to download update": "Falha ao baixar a atualização",
"Delete Selected files?": "Deletar arquivos selecionados?",
"Completely remove ": "Remover completamente ",
"Are you sure you want to delete ": "Você tem certeza que quer deletar ",
"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."
} }

View File

@@ -62,6 +62,8 @@
"Music": "Музыка", "Music": "Музыка",
"Network": "Сеть", "Network": "Сеть",
"Network Options": "Параметры сети", "Network Options": "Параметры сети",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink", "Nxlink": "Nxlink",
"Nxlink Connected": "", "Nxlink Connected": "",
"Nxlink Upload": "", "Nxlink Upload": "",
@@ -199,6 +201,10 @@
"Bad Page": "", "Bad Page": "",
"Download theme?": "", "Download theme?": "",
"GitHub": "",
"Downloading json": "",
"Select asset to download for ": "",
"Installing ": "", "Installing ": "",
"Uninstalling ": "", "Uninstalling ": "",
"Deleting ": "", "Deleting ": "",
@@ -211,6 +217,8 @@
"Copying ": "", "Copying ": "",
"Trying to load ": "", "Trying to load ": "",
"Downloading ": "", "Downloading ": "",
"Downloaded ": "",
"Removed ": "",
"Checking MD5": "", "Checking MD5": "",
"Loading...": "", "Loading...": "",
"Loading": "", "Loading": "",

View File

@@ -62,6 +62,8 @@
"Music": "Musik", "Music": "Musik",
"Network": "Nätverk", "Network": "Nätverk",
"Network Options": "Nätverksalternativ", "Network Options": "Nätverksalternativ",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink", "Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink ansluten", "Nxlink Connected": "Nxlink ansluten",
"Nxlink Upload": "Nxlink uppladdning", "Nxlink Upload": "Nxlink uppladdning",
@@ -199,6 +201,10 @@
"Bad Page": "Ogiltig sida", "Bad Page": "Ogiltig sida",
"Download theme?": "Ladda ner tema?", "Download theme?": "Ladda ner tema?",
"GitHub": "",
"Downloading json": "",
"Select asset to download for ": "",
"Installing ": "Installerar ", "Installing ": "Installerar ",
"Uninstalling ": "Avinstallerar ", "Uninstalling ": "Avinstallerar ",
"Deleting ": "Raderar ", "Deleting ": "Raderar ",
@@ -211,6 +217,8 @@
"Copying ": "Kopierar ", "Copying ": "Kopierar ",
"Trying to load ": "", "Trying to load ": "",
"Downloading ": "Laddar ner ", "Downloading ": "Laddar ner ",
"Downloaded ": "",
"Removed ": "",
"Checking MD5": "Kontrollerar MD5", "Checking MD5": "Kontrollerar MD5",
"Loading...": "Laddar...", "Loading...": "Laddar...",
"Loading": "Laddar", "Loading": "Laddar",

View File

@@ -62,6 +62,8 @@
"Music": "音乐", "Music": "音乐",
"Network": "网络", "Network": "网络",
"Network Options": "网络选项", "Network Options": "网络选项",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink", "Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink 已连接", "Nxlink Connected": "Nxlink 已连接",
"Nxlink Upload": "Nxlink 上传中", "Nxlink Upload": "Nxlink 上传中",
@@ -199,6 +201,10 @@
"Bad Page": "错误的页面", "Bad Page": "错误的页面",
"Download theme?": "下载该主题?", "Download theme?": "下载该主题?",
"GitHub": "",
"Downloading json": "",
"Select asset to download for ": "",
"Installing ": "正在安装 ", "Installing ": "正在安装 ",
"Uninstalling ": "正在卸载 ", "Uninstalling ": "正在卸载 ",
"Deleting ": "正在删除 ", "Deleting ": "正在删除 ",
@@ -211,6 +217,8 @@
"Copying ": "正在复制 ", "Copying ": "正在复制 ",
"Trying to load ": "", "Trying to load ": "",
"Downloading ": "正在下载 ", "Downloading ": "正在下载 ",
"Downloaded ": "",
"Removed ": "",
"Checking MD5": "正在校验 MD5", "Checking MD5": "正在校验 MD5",
"Loading...": "加载中...", "Loading...": "加载中...",
"Loading": "加载中", "Loading": "加载中",

View File

@@ -45,18 +45,18 @@ add_executable(sphaira
source/ui/menus/main_menu.cpp source/ui/menus/main_menu.cpp
source/ui/menus/menu_base.cpp source/ui/menus/menu_base.cpp
source/ui/menus/themezer.cpp source/ui/menus/themezer.cpp
source/ui/menus/ghdl.cpp
source/ui/error_box.cpp source/ui/error_box.cpp
source/ui/notification.cpp source/ui/notification.cpp
source/ui/nvg_util.cpp source/ui/nvg_util.cpp
source/ui/option_box.cpp source/ui/option_box.cpp
source/ui/option_list.cpp
source/ui/popup_list.cpp source/ui/popup_list.cpp
source/ui/progress_box.cpp source/ui/progress_box.cpp
source/ui/scrollable_text.cpp source/ui/scrollable_text.cpp
source/ui/scrollbar.cpp
source/ui/sidebar.cpp source/ui/sidebar.cpp
source/ui/widget.cpp source/ui/widget.cpp
source/ui/list.cpp
source/app.cpp source/app.cpp
source/download.cpp source/download.cpp
@@ -85,7 +85,7 @@ set(FETCHCONTENT_QUIET FALSE)
FetchContent_Declare(ftpsrv FetchContent_Declare(ftpsrv
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
GIT_TAG 8d5a14e GIT_TAG 1.2.1
) )
FetchContent_Declare(libhaze FetchContent_Declare(libhaze
@@ -113,12 +113,12 @@ FetchContent_Declare(yyjson
GIT_TAG 0.10.0 GIT_TAG 0.10.0
) )
FetchContent_Declare(minIni-sphaira FetchContent_Declare(minIni
GIT_REPOSITORY https://github.com/ITotalJustice/minIni-nx.git GIT_REPOSITORY https://github.com/ITotalJustice/minIni-nx.git
GIT_TAG 63ec295 GIT_TAG 63ec295
) )
set(MININI_LIB_NAME minIni-sphaira) set(MININI_LIB_NAME minIni)
set(MININI_USE_STDIO ON) set(MININI_USE_STDIO ON)
set(MININI_USE_NX ON) set(MININI_USE_NX ON)
set(MININI_USE_FLOAT OFF) set(MININI_USE_FLOAT OFF)
@@ -139,30 +139,68 @@ set(NANOVG_STBI_STATIC OFF)
set(NANOVG_STBTT_STATIC ON) set(NANOVG_STBTT_STATIC ON)
set(YYJSON_DISABLE_READER OFF) set(YYJSON_DISABLE_READER OFF)
set(YYJSON_DISABLE_WRITER ON) set(YYJSON_DISABLE_WRITER OFF)
set(YYJSON_DISABLE_UTILS ON) set(YYJSON_DISABLE_UTILS ON)
set(YYJSON_DISABLE_FAST_FP_CONV ON) set(YYJSON_DISABLE_FAST_FP_CONV ON)
set(YYJSON_DISABLE_NON_STANDARD ON) set(YYJSON_DISABLE_NON_STANDARD ON)
set(YYJSON_DISABLE_UTF8_VALIDATION ON) set(YYJSON_DISABLE_UTF8_VALIDATION ON)
set(YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS OFF) set(YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS OFF)
set(FTPSRV_LIB_BUILD TRUE)
set(FTPSRV_LIB_SOCK_UNISTD TRUE)
set(FTPSRV_LIB_VFS_CUSTOM ${CMAKE_CURRENT_SOURCE_DIR}/include/ftpsrv_helper.hpp)
set(FTPSRV_LIB_PATH_SIZE 0x301)
set(FTPSRV_LIB_SESSIONS 32)
set(FTPSRV_LIB_BUF_SIZE 1024*64)
FetchContent_MakeAvailable( FetchContent_MakeAvailable(
ftpsrv # ftpsrv
libhaze libhaze
libpulsar libpulsar
nanovg nanovg
stb stb
minIni-sphaira minIni
yyjson yyjson
) )
FetchContent_GetProperties(ftpsrv)
if (NOT ftpsrv_POPULATED)
FetchContent_Populate(ftpsrv)
endif()
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}>
VFS_NX_BUFFER_IO=$<BOOL:TRUE>
)
add_subdirectory(${ftpsrv_SOURCE_DIR} binary_dir)
add_library(ftpsrv_helper
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs_nx.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_none.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_root.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_fs.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_save.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_storage.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/utils.c
)
target_link_libraries(ftpsrv_helper PUBLIC ftpsrv)
target_include_directories(ftpsrv_helper PUBLIC ${ftpsrv_SOURCE_DIR}/src/platform)
if (USE_VFS_GC)
target_sources(ftpsrv_helper PRIVATE
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_gc.c
)
endif()
# todo: upstream cmake # todo: upstream cmake
add_library(libhaze add_library(libhaze
${libhaze_SOURCE_DIR}/source/async_usb_server.cpp ${libhaze_SOURCE_DIR}/source/async_usb_server.cpp
@@ -240,10 +278,10 @@ set_target_properties(sphaira PROPERTIES
) )
target_link_libraries(sphaira PRIVATE target_link_libraries(sphaira PRIVATE
ftpsrv ftpsrv_helper
libhaze libhaze
libpulsar libpulsar
minIni-sphaira minIni
nanovg nanovg
stb stb
yyjson yyjson

View File

@@ -33,7 +33,9 @@ enum class LaunchType {
Forwader_Sphaira, Forwader_Sphaira,
}; };
// todo: why is this global???
void DrawElement(float x, float y, float w, float h, ThemeEntryID id); void DrawElement(float x, float y, float w, float h, ThemeEntryID id);
void DrawElement(const Vec4&, ThemeEntryID id);
class App { class App {
public: public:
@@ -45,6 +47,8 @@ public:
static void ExitRestart(); static void ExitRestart();
static auto GetVg() -> NVGcontext*; static auto GetVg() -> NVGcontext*;
static void Push(std::shared_ptr<ui::Widget>); static void Push(std::shared_ptr<ui::Widget>);
// pops all widgets above a menu
static void PopToMenu();
// this is thread safe // this is thread safe
static void Notify(std::string text, ui::NotifEntry::Side side = ui::NotifEntry::Side::RIGHT); static void Notify(std::string text, ui::NotifEntry::Side side = ui::NotifEntry::Side::RIGHT);
@@ -54,8 +58,10 @@ public:
static void NotifyFlashLed(); static void NotifyFlashLed();
static auto GetThemeMetaList() -> std::span<ThemeMeta>; static auto GetThemeMetaList() -> std::span<ThemeMeta>;
static void SetTheme(u64 theme_index); static void SetTheme(s64 theme_index);
static auto GetThemeIndex() -> u64; static auto GetThemeIndex() -> s64;
static auto GetDefaultImage(int* w = nullptr, int* h = nullptr) -> int;
// returns argv[0] // returns argv[0]
static auto GetExePath() -> fs::FsPath; static auto GetExePath() -> fs::FsPath;
@@ -119,6 +125,7 @@ public:
u64 m_start_timestamp{}; u64 m_start_timestamp{};
u64 m_prev_timestamp{}; u64 m_prev_timestamp{};
fs::FsPath m_prev_last_launch{}; fs::FsPath m_prev_last_launch{};
int m_default_image{};
bool m_is_launched_via_sphaira_forwader{}; bool m_is_launched_via_sphaira_forwader{};
@@ -138,7 +145,7 @@ public:
Theme m_theme{}; Theme m_theme{};
fs::FsPath theme_path{}; fs::FsPath theme_path{};
std::size_t m_theme_index{}; s64 m_theme_index{};
bool m_quit{}; bool m_quit{};

View File

@@ -1,41 +1,194 @@
#pragma once #pragma once
#include "fs.hpp"
#include <vector> #include <vector>
#include <string> #include <string>
#include <functional> #include <functional>
#include <unordered_map>
#include <algorithm>
#include <switch.h> #include <switch.h>
namespace sphaira { namespace sphaira::curl {
using DownloadCallback = std::function<void(std::vector<u8>& data, bool success)>; enum {
using ProgressCallback = std::function<bool(u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow)>; Flag_None = 0,
// requests to download send etag in the header.
// the received etag is then saved on success.
// this api is only available on downloading to file.
Flag_Cache = 1 << 0,
};
enum class DownloadPriority { enum class Priority {
Normal, // gets pushed to the back of the queue Normal, // gets pushed to the back of the queue
High, // gets pushed to the front of the queue High, // gets pushed to the front of the queue
}; };
struct DownloadEventData { struct Api;
DownloadCallback callback; struct ApiResult;
std::vector<u8> data;
bool result; using Path = fs::FsPath;
using OnComplete = std::function<void(ApiResult& result)>;
using OnProgress = std::function<bool(u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow)>;
struct Url {
Url() = default;
Url(const std::string& str) : m_str{str} {}
std::string m_str;
}; };
auto DownloadInit() -> bool; struct Fields {
void DownloadExit(); Fields() = default;
Fields(const std::string& str) : m_str{str} {}
std::string m_str;
};
struct Header {
Header() = default;
Header(std::initializer_list<std::pair<const std::string, std::string>> p) : m_map{p} {}
std::unordered_map<std::string, std::string> m_map;
auto Find(const std::string& key) const {
return std::find_if(m_map.cbegin(), m_map.cend(), [&key](auto& e) {
return !strcasecmp(key.c_str(), e.first.c_str());
});
}
};
struct Flags {
Flags() = default;
Flags(u32 flags) : m_flags{flags} {}
u32 m_flags{Flag_None};
};
struct ApiResult {
bool success;
long code;
Header header; // returned headers in request
std::vector<u8> data; // empty if downloaded a file
fs::FsPath path; // empty if downloaded memory
};
struct DownloadEventData {
OnComplete callback;
ApiResult result;
};
auto Init() -> bool;
void Exit();
// sync functions // sync functions
auto DownloadMemory(const std::string& url, const std::string& post, ProgressCallback pcallback = nullptr) -> std::vector<u8>; auto ToMemory(const Api& e) -> ApiResult;
auto DownloadFile(const std::string& url, const std::string& out, const std::string& post, ProgressCallback pcallback = nullptr) -> bool; auto ToFile(const Api& e) -> ApiResult;
// async functions // async functions
// starts the downloads in a new thread, pushes an event when complete auto ToMemoryAsync(const Api& e) -> bool;
// then, the callback will be called on the main thread. auto ToFileAsync(const Api& e) -> bool;
// auto DownloadMemoryAsync(const std::string& url, DownloadCallback callback, DownloadPriority prio = DownloadPriority::Normal) -> bool;
// auto DownloadFileAsync(const std::string& url, const std::string& out, DownloadCallback callback, DownloadPriority prio = DownloadPriority::Normal) -> bool;
auto DownloadMemoryAsync(const std::string& url, const std::string& post, DownloadCallback callback, ProgressCallback pcallback = nullptr, DownloadPriority prio = DownloadPriority::Normal) -> bool; struct Api {
auto DownloadFileAsync(const std::string& url, const std::string& out, const std::string& post, DownloadCallback callback, ProgressCallback pcallback = nullptr, DownloadPriority prio = DownloadPriority::Normal) -> bool; 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");
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");
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");
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");
Api::set_option(std::forward<Ts>(ts)...);
return curl::ToFileAsync(*this);
}
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;
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;
}
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)...);
}
};
} // namespace sphaira::curl

View File

@@ -26,7 +26,7 @@ using EventData = std::variant<
ExitEventData, ExitEventData,
HazeCallbackData, HazeCallbackData,
NxlinkCallbackData, NxlinkCallbackData,
DownloadEventData curl::DownloadEventData
>; >;
// returns number of events // returns number of events

View File

@@ -171,39 +171,39 @@ static_assert(FsPath::TestFrom(FsPath{"abc"}));
FsPath AppendPath(const fs::FsPath& root_path, const fs::FsPath& file_path); FsPath AppendPath(const fs::FsPath& root_path, const fs::FsPath& file_path);
Result CreateFile(FsFileSystem* fs, const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false); Result CreateFile(FsFileSystem* fs, const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = true);
Result CreateDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false); Result CreateDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false); Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false); Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
Result DeleteFile(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false); Result DeleteFile(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
Result DeleteDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false); Result DeleteDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
Result DeleteDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false); Result DeleteDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
Result RenameFile(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only = false); Result RenameFile(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only = true);
Result RenameDirectory(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only = false); Result RenameDirectory(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only = true);
Result GetEntryType(FsFileSystem* fs, const FsPath& path, FsDirEntryType* out); Result GetEntryType(FsFileSystem* fs, const FsPath& path, FsDirEntryType* out);
Result GetFileTimeStampRaw(FsFileSystem* fs, const FsPath& path, FsTimeStampRaw *out); Result GetFileTimeStampRaw(FsFileSystem* fs, const FsPath& path, FsTimeStampRaw *out);
bool FileExists(FsFileSystem* fs, const FsPath& path); bool FileExists(FsFileSystem* fs, const FsPath& path);
bool DirExists(FsFileSystem* fs, const FsPath& path); bool DirExists(FsFileSystem* fs, const FsPath& path);
Result read_entire_file(FsFileSystem* fs, const FsPath& path, std::vector<u8>& out); Result read_entire_file(FsFileSystem* fs, const FsPath& path, std::vector<u8>& out);
Result write_entire_file(FsFileSystem* fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = false); Result write_entire_file(FsFileSystem* fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = true);
Result copy_entire_file(FsFileSystem* fs, const FsPath& dst, const FsPath& src, bool ignore_read_only = false); Result copy_entire_file(FsFileSystem* fs, const FsPath& dst, const FsPath& src, bool ignore_read_only = true);
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false); Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = true);
Result CreateDirectory(const FsPath& path, bool ignore_read_only = false); Result CreateDirectory(const FsPath& path, bool ignore_read_only = true);
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = false); Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = true);
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = false); Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = true);
Result DeleteFile(const FsPath& path, bool ignore_read_only = false); Result DeleteFile(const FsPath& path, bool ignore_read_only = true);
Result DeleteDirectory(const FsPath& path, bool ignore_read_only = false); Result DeleteDirectory(const FsPath& path, bool ignore_read_only = true);
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = false); Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = true);
Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = false); Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = true);
Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = false); Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = true);
Result GetEntryType(const FsPath& path, FsDirEntryType* out); Result GetEntryType(const FsPath& path, FsDirEntryType* out);
Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out); Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out);
bool FileExists(const FsPath& path); bool FileExists(const FsPath& path);
bool DirExists(const FsPath& path); bool DirExists(const FsPath& path);
Result read_entire_file(const FsPath& path, std::vector<u8>& out); Result read_entire_file(const FsPath& path, std::vector<u8>& out);
Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = false); Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = true);
Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only = false); Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only = true);
struct Fs { struct Fs {
static constexpr inline u32 FsModule = 505; static constexpr inline u32 FsModule = 505;
@@ -222,51 +222,64 @@ struct Fs {
static constexpr inline Result ResultUnknownStdioError = MAKERESULT(FsModule, 13); static constexpr inline Result ResultUnknownStdioError = MAKERESULT(FsModule, 13);
static constexpr inline Result ResultReadOnly = MAKERESULT(FsModule, 14); static constexpr inline Result ResultReadOnly = MAKERESULT(FsModule, 14);
virtual Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false) = 0; Fs(bool ignore_read_only = true) : m_ignore_read_only{ignore_read_only} {}
virtual Result CreateDirectory(const FsPath& path, bool ignore_read_only = false) = 0; virtual ~Fs() = default;
virtual Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) = 0;
virtual Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = false) = 0; virtual Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0) = 0;
virtual Result DeleteFile(const FsPath& path, bool ignore_read_only = false) = 0; virtual Result CreateDirectory(const FsPath& path) = 0;
virtual Result DeleteDirectory(const FsPath& path, bool ignore_read_only = false) = 0; virtual Result CreateDirectoryRecursively(const FsPath& path) = 0;
virtual Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) = 0; virtual Result CreateDirectoryRecursivelyWithPath(const FsPath& path) = 0;
virtual Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) = 0; virtual Result DeleteFile(const FsPath& path) = 0;
virtual Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) = 0; virtual Result DeleteDirectory(const FsPath& path) = 0;
virtual Result DeleteDirectoryRecursively(const FsPath& path) = 0;
virtual Result RenameFile(const FsPath& src, const FsPath& dst) = 0;
virtual Result RenameDirectory(const FsPath& src, const FsPath& dst) = 0;
virtual Result GetEntryType(const FsPath& path, FsDirEntryType* out) = 0; virtual Result GetEntryType(const FsPath& path, FsDirEntryType* out) = 0;
virtual Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out) = 0; virtual Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out) = 0;
virtual bool FileExists(const FsPath& path) = 0; virtual bool FileExists(const FsPath& path) = 0;
virtual bool DirExists(const FsPath& path) = 0; virtual bool DirExists(const FsPath& path) = 0;
virtual Result read_entire_file(const FsPath& path, std::vector<u8>& out) = 0; virtual Result read_entire_file(const FsPath& path, std::vector<u8>& out) = 0;
virtual Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = false) = 0; virtual Result write_entire_file(const FsPath& path, const std::vector<u8>& in) = 0;
virtual Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only = false) = 0; virtual Result copy_entire_file(const FsPath& dst, const FsPath& src) = 0;
void SetIgnoreReadOnly(bool enable) {
m_ignore_read_only = enable;
}
protected:
bool m_ignore_read_only;
}; };
struct FsStdio : Fs { struct FsStdio : Fs {
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false) override { FsStdio(bool ignore_read_only = true) : Fs{ignore_read_only} {}
return fs::CreateFile(path, size, option, ignore_read_only); virtual ~FsStdio() = default;
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0) override {
return fs::CreateFile(path, size, option, m_ignore_read_only);
} }
Result CreateDirectory(const FsPath& path, bool ignore_read_only = false) override { Result CreateDirectory(const FsPath& path) override {
return fs::CreateDirectory(path, ignore_read_only); return fs::CreateDirectory(path, m_ignore_read_only);
} }
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) override { Result CreateDirectoryRecursively(const FsPath& path) override {
return fs::CreateDirectoryRecursively(path, ignore_read_only); return fs::CreateDirectoryRecursively(path, m_ignore_read_only);
} }
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = false) override { Result CreateDirectoryRecursivelyWithPath(const FsPath& path) override {
return fs::CreateDirectoryRecursivelyWithPath(path, ignore_read_only); return fs::CreateDirectoryRecursivelyWithPath(path, m_ignore_read_only);
} }
Result DeleteFile(const FsPath& path, bool ignore_read_only = false) override { Result DeleteFile(const FsPath& path) override {
return fs::DeleteFile(path, ignore_read_only); return fs::DeleteFile(path, m_ignore_read_only);
} }
Result DeleteDirectory(const FsPath& path, bool ignore_read_only = false) override { Result DeleteDirectory(const FsPath& path) override {
return fs::DeleteDirectory(path, ignore_read_only); return fs::DeleteDirectory(path, m_ignore_read_only);
} }
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) override { Result DeleteDirectoryRecursively(const FsPath& path) override {
return fs::DeleteDirectoryRecursively(path, ignore_read_only); return fs::DeleteDirectoryRecursively(path, m_ignore_read_only);
} }
Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) override { Result RenameFile(const FsPath& src, const FsPath& dst) override {
return fs::RenameFile(src, dst, ignore_read_only); return fs::RenameFile(src, dst, m_ignore_read_only);
} }
Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) override { Result RenameDirectory(const FsPath& src, const FsPath& dst) override {
return fs::RenameDirectory(src, dst, ignore_read_only); return fs::RenameDirectory(src, dst, m_ignore_read_only);
} }
Result GetEntryType(const FsPath& path, FsDirEntryType* out) override { Result GetEntryType(const FsPath& path, FsDirEntryType* out) override {
return fs::GetEntryType(path, out); return fs::GetEntryType(path, out);
@@ -283,17 +296,17 @@ struct FsStdio : Fs {
Result read_entire_file(const FsPath& path, std::vector<u8>& out) override { Result read_entire_file(const FsPath& path, std::vector<u8>& out) override {
return fs::read_entire_file(path, out); return fs::read_entire_file(path, out);
} }
Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = false) override { Result write_entire_file(const FsPath& path, const std::vector<u8>& in) override {
return fs::write_entire_file(path, in, ignore_read_only); return fs::write_entire_file(path, in, m_ignore_read_only);
} }
Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only = false) override { Result copy_entire_file(const FsPath& dst, const FsPath& src) override {
return fs::copy_entire_file(dst, src, ignore_read_only); return fs::copy_entire_file(dst, src, m_ignore_read_only);
} }
}; };
struct FsNative : Fs { struct FsNative : Fs {
FsNative() = default; explicit FsNative(bool ignore_read_only = true) : Fs{ignore_read_only} {}
FsNative(FsFileSystem* fs, bool own) : m_fs{*fs}, m_own{own} {} explicit FsNative(FsFileSystem* fs, bool own, bool ignore_read_only = true) : Fs{ignore_read_only}, m_fs{*fs}, m_own{own} {}
virtual ~FsNative() { virtual ~FsNative() {
if (m_own) { if (m_own) {
@@ -355,32 +368,32 @@ struct FsNative : Fs {
return m_open_result; return m_open_result;
} }
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false) override { Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0) override {
return fs::CreateFile(&m_fs, path, size, option, ignore_read_only); return fs::CreateFile(&m_fs, path, size, option, m_ignore_read_only);
} }
Result CreateDirectory(const FsPath& path, bool ignore_read_only = false) override { Result CreateDirectory(const FsPath& path) override {
return fs::CreateDirectory(&m_fs, path, ignore_read_only); return fs::CreateDirectory(&m_fs, path, m_ignore_read_only);
} }
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) override { Result CreateDirectoryRecursively(const FsPath& path) override {
return fs::CreateDirectoryRecursively(&m_fs, path, ignore_read_only); return fs::CreateDirectoryRecursively(&m_fs, path, m_ignore_read_only);
} }
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = false) override { Result CreateDirectoryRecursivelyWithPath(const FsPath& path) override {
return fs::CreateDirectoryRecursivelyWithPath(&m_fs, path, ignore_read_only); return fs::CreateDirectoryRecursivelyWithPath(&m_fs, path, m_ignore_read_only);
} }
Result DeleteFile(const FsPath& path, bool ignore_read_only = false) override { Result DeleteFile(const FsPath& path) override {
return fs::DeleteFile(&m_fs, path, ignore_read_only); return fs::DeleteFile(&m_fs, path, m_ignore_read_only);
} }
Result DeleteDirectory(const FsPath& path, bool ignore_read_only = false) override { Result DeleteDirectory(const FsPath& path) override {
return fs::DeleteDirectory(&m_fs, path, ignore_read_only); return fs::DeleteDirectory(&m_fs, path, m_ignore_read_only);
} }
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) override { Result DeleteDirectoryRecursively(const FsPath& path) override {
return fs::DeleteDirectoryRecursively(&m_fs, path, ignore_read_only); return fs::DeleteDirectoryRecursively(&m_fs, path, m_ignore_read_only);
} }
Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) override { Result RenameFile(const FsPath& src, const FsPath& dst) override {
return fs::RenameFile(&m_fs, src, dst, ignore_read_only); return fs::RenameFile(&m_fs, src, dst, m_ignore_read_only);
} }
Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) override { Result RenameDirectory(const FsPath& src, const FsPath& dst) override {
return fs::RenameDirectory(&m_fs, src, dst, ignore_read_only); return fs::RenameDirectory(&m_fs, src, dst, m_ignore_read_only);
} }
Result GetEntryType(const FsPath& path, FsDirEntryType* out) override { Result GetEntryType(const FsPath& path, FsDirEntryType* out) override {
return fs::GetEntryType(&m_fs, path, out); return fs::GetEntryType(&m_fs, path, out);
@@ -397,11 +410,11 @@ struct FsNative : Fs {
Result read_entire_file(const FsPath& path, std::vector<u8>& out) override { Result read_entire_file(const FsPath& path, std::vector<u8>& out) override {
return fs::read_entire_file(&m_fs, path, out); return fs::read_entire_file(&m_fs, path, out);
} }
Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = false) override { Result write_entire_file(const FsPath& path, const std::vector<u8>& in) override {
return fs::write_entire_file(&m_fs, path, in, ignore_read_only); return fs::write_entire_file(&m_fs, path, in, m_ignore_read_only);
} }
Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only = false) override { Result copy_entire_file(const FsPath& dst, const FsPath& src) override {
return fs::copy_entire_file(&m_fs, dst, src, ignore_read_only); return fs::copy_entire_file(&m_fs, dst, src, m_ignore_read_only);
} }
FsFileSystem m_fs{}; FsFileSystem m_fs{};
@@ -417,43 +430,28 @@ struct FsNativeSd final : FsNative {
}; };
#else #else
struct FsNativeSd final : FsNative { struct FsNativeSd final : FsNative {
FsNativeSd() : FsNative{fsdevGetDeviceFileSystem("sdmc:"), false} { FsNativeSd(bool ignore_read_only = true) : FsNative{fsdevGetDeviceFileSystem("sdmc:"), false, ignore_read_only} {
m_open_result = 0; m_open_result = 0;
} }
}; };
#endif #endif
struct FsNativeBis final : FsNative { struct FsNativeBis final : FsNative {
FsNativeBis(FsBisPartitionId id, const FsPath& string) { FsNativeBis(FsBisPartitionId id, const FsPath& string, bool ignore_read_only = true) : FsNative{ignore_read_only} {
m_open_result = fsOpenBisFileSystem(&m_fs, id, string); m_open_result = fsOpenBisFileSystem(&m_fs, id, string);
} }
}; };
struct FsNativeImage final : FsNative { struct FsNativeImage final : FsNative {
FsNativeImage(FsImageDirectoryId id) { FsNativeImage(FsImageDirectoryId id, bool ignore_read_only = true) : FsNative{ignore_read_only} {
m_open_result = fsOpenImageDirectoryFileSystem(&m_fs, id); m_open_result = fsOpenImageDirectoryFileSystem(&m_fs, id);
} }
}; };
struct FsNativeContentStorage final : FsNative { struct FsNativeContentStorage final : FsNative {
FsNativeContentStorage(FsContentStorageId id) { FsNativeContentStorage(FsContentStorageId id, bool ignore_read_only = true) : FsNative{ignore_read_only} {
m_open_result = fsOpenContentStorageFileSystem(&m_fs, id); m_open_result = fsOpenContentStorageFileSystem(&m_fs, id);
} }
}; };
// auto file_exists(const FsPath& path) -> bool;
// auto create_file(const FsPath& path, u64 size = 0) -> Result;
// auto delete_file(const FsPath& path) -> Result;
// auto create_directory(const FsPath& path) -> Result;
// auto create_directory_recursively(const FsPath& path) -> Result;
// auto delete_directory(const FsPath& path) -> Result;
// auto delete_directory_recursively(const FsPath& path) -> Result;
// auto rename_file(const FsPath& src, const FsPath& dst) -> Result;
// auto rename_directory(const FsPath& src, const FsPath& dst) -> Result;
// auto read_entire_file(const FsPath& path, std::vector<u8>& out) -> Result;
// auto write_entire_file(const FsPath& path, const std::vector<u8>& in) -> Result;
// // single threaded one shot copy, only use for very small files!
// auto copy_entire_file(const FsPath& dst, const FsPath& src) -> Result;
} // namespace fs } // namespace fs

View File

@@ -1,33 +1,8 @@
#pragma once #pragma once
#include <switch.h>
struct FtpVfsFile {
FsFile fd;
s64 off;
s64 buf_off;
s64 buf_size;
bool is_write;
bool is_valid;
u8 buf[1024 * 1024 * 1];
};
struct FtpVfsDir {
FsDir dir;
bool is_valid;
};
struct FtpVfsDirEntry {
FsDirectoryEntry buf;
};
#ifdef __cplusplus
namespace sphaira::ftpsrv { namespace sphaira::ftpsrv {
bool Init(); bool Init();
void Exit(); void Exit();
} // namespace sphaira::ftpsrv } // namespace sphaira::ftpsrv
#endif // __cplusplus

View File

@@ -2,12 +2,15 @@
#define sphaira_USE_LOG 1 #define sphaira_USE_LOG 1
#include <cstdarg>
#if sphaira_USE_LOG #if sphaira_USE_LOG
auto log_file_init() -> bool; auto log_file_init() -> bool;
auto log_nxlink_init() -> bool; auto log_nxlink_init() -> bool;
void log_file_exit(); void log_file_exit();
void log_nxlink_exit(); void log_nxlink_exit();
void log_write(const char* s, ...) __attribute__ ((format (printf, 1, 2))); void log_write(const char* s, ...) __attribute__ ((format (printf, 1, 2)));
void log_write_arg(const char* s, std::va_list& v);
#else #else
inline auto log_file_init() -> bool { inline auto log_file_init() -> bool {
return true; return true;

View File

@@ -19,7 +19,6 @@ struct NroEntry {
s64 size{}; s64 size{};
NacpStruct nacp{}; NacpStruct nacp{};
std::vector<u8> icon{};
u64 icon_size{}; u64 icon_size{};
u64 icon_offset{}; u64 icon_offset{};
@@ -76,4 +75,10 @@ auto nro_add_arg_file(std::string arg) -> std::string;
// strips sdmc: // strips sdmc:
auto nro_normalise_path(const std::string& p) -> std::string; auto nro_normalise_path(const std::string& p) -> std::string;
// helpers to find nro entry, will be made methods soon once i convert vector into a struct.
auto nro_find(std::span<const NroEntry> array, std::string_view name, std::string_view author, const fs::FsPath& path) -> std::optional<NroEntry>;
auto nro_find_name(std::span<const NroEntry> array, std::string_view name) -> std::optional<NroEntry>;
auto nro_find_author(std::span<const NroEntry> array, std::string_view author) -> std::optional<NroEntry>;
auto nro_find_path(std::span<const NroEntry> array, const fs::FsPath& path) -> std::optional<NroEntry>;
} // namespace sphaira } // namespace sphaira

View File

@@ -10,7 +10,6 @@ public:
ErrorBox(Result code, const std::string& message); ErrorBox(Result code, const std::string& message);
auto Update(Controller* controller, TouchInfo* touch) -> void override; auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto OnLayoutChange() -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> void override; auto Draw(NVGcontext* vg, Theme* theme) -> void override;
private: private:

View 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

View File

@@ -2,6 +2,7 @@
#include "ui/menus/menu_base.hpp" #include "ui/menus/menu_base.hpp"
#include "ui/scrollable_text.hpp" #include "ui/scrollable_text.hpp"
#include "ui/list.hpp"
#include "nro.hpp" #include "nro.hpp"
#include "fs.hpp" #include "fs.hpp"
#include <span> #include <span>
@@ -27,6 +28,8 @@ struct LazyImage {
~LazyImage(); ~LazyImage();
int image{}; int image{};
int w{}, h{}; int w{}, h{};
bool tried_cache{};
bool cached{};
ImageDownloadState state{ImageDownloadState::None}; ImageDownloadState state{ImageDownloadState::None};
u8 first_pixel[4]{}; u8 first_pixel[4]{};
}; };
@@ -75,7 +78,7 @@ struct EntryMenu final : MenuBase {
// void OnFocusGained() override; // void OnFocusGained() override;
void ShowChangelogAction(); void ShowChangelogAction();
void SetIndex(std::size_t index); void SetIndex(s64 index);
void UpdateOptions(); void UpdateOptions();
@@ -95,10 +98,10 @@ private:
const LazyImage& m_default_icon; const LazyImage& m_default_icon;
Menu& m_menu; Menu& m_menu;
std::size_t m_index{}; // where i am in the array s64 m_index{}; // where i am in the array
std::vector<Option> m_options; std::vector<Option> m_options;
LazyImage m_banner; LazyImage m_banner;
std::vector<LazyImage> m_screens; std::unique_ptr<List> m_list;
std::shared_ptr<ScrollableText> m_details; std::shared_ptr<ScrollableText> m_details;
std::shared_ptr<ScrollableText> m_changelog; std::shared_ptr<ScrollableText> m_changelog;
@@ -147,7 +150,7 @@ struct FeedbackMenu final : MenuBase {
void Draw(NVGcontext* vg, Theme* theme) override; void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override; void OnFocusGained() override;
void SetIndex(std::size_t index); void SetIndex(s64 index);
void ScanHomebrew(); void ScanHomebrew();
void Sort(); void Sort();
@@ -155,8 +158,7 @@ private:
const std::vector<Entry>& m_package_entries; const std::vector<Entry>& m_package_entries;
LazyImage& m_default_image; LazyImage& m_default_image;
std::vector<FeedbackEntry> m_entries; std::vector<FeedbackEntry> m_entries;
std::size_t m_start{}; s64 m_index{}; // where i am in the array
std::size_t m_index{}; // where i am in the array
ImageDownloadState m_repo_download_state{ImageDownloadState::None}; ImageDownloadState m_repo_download_state{ImageDownloadState::None};
}; };
@@ -168,7 +170,7 @@ struct Menu final : MenuBase {
void Draw(NVGcontext* vg, Theme* theme) override; void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override; void OnFocusGained() override;
void SetIndex(std::size_t index); void SetIndex(s64 index);
void ScanHomebrew(); void ScanHomebrew();
void Sort(); void Sort();
@@ -199,19 +201,19 @@ private:
SortType m_sort{SortType::SortType_Updated}; SortType m_sort{SortType::SortType_Updated};
OrderType m_order{OrderType::OrderType_Decending}; OrderType m_order{OrderType::OrderType_Decending};
std::size_t m_start{}; s64 m_index{}; // where i am in the array
std::size_t m_index{}; // where i am in the array
LazyImage m_default_image; LazyImage m_default_image;
LazyImage m_update; LazyImage m_update;
LazyImage m_get; LazyImage m_get;
LazyImage m_local; LazyImage m_local;
LazyImage m_installed; LazyImage m_installed;
ImageDownloadState m_repo_download_state{ImageDownloadState::None}; ImageDownloadState m_repo_download_state{ImageDownloadState::None};
std::unique_ptr<List> m_list;
std::string m_search_term; std::string m_search_term;
std::string m_author_term; std::string m_author_term;
u64 m_entry_search_jump_back{}; s64 m_entry_search_jump_back{};
u64 m_entry_author_jump_back{}; s64 m_entry_author_jump_back{};
bool m_is_search{}; bool m_is_search{};
bool m_is_author{}; bool m_is_author{};
bool m_dirty{}; // if set, does a sort bool m_dirty{}; // if set, does a sort

View File

@@ -23,8 +23,8 @@ private:
std::unique_ptr<ScrollableText> m_scroll_text; std::unique_ptr<ScrollableText> m_scroll_text;
std::size_t m_start{}; s64 m_start{};
std::size_t m_index{}; // where i am in the array s64 m_index{}; // where i am in the array
}; };
} // namespace sphaira::ui::menu::fileview } // namespace sphaira::ui::menu::fileview

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "ui/menus/menu_base.hpp" #include "ui/menus/menu_base.hpp"
#include "ui/list.hpp"
#include "nro.hpp" #include "nro.hpp"
#include "fs.hpp" #include "fs.hpp"
#include "option.hpp" #include "option.hpp"
@@ -9,6 +10,12 @@
namespace sphaira::ui::menu::filebrowser { namespace sphaira::ui::menu::filebrowser {
enum class FsType {
Sd,
ImageNand,
ImageSd,
};
enum class SelectedType { enum class SelectedType {
None, None,
Copy, Copy,
@@ -83,13 +90,23 @@ struct FileAssocEntry {
std::vector<std::string> ext; // list of ext std::vector<std::string> ext; // list of ext
std::vector<std::string> database; // list of systems std::vector<std::string> database; // list of systems
}; };
struct LastFile { struct LastFile {
fs::FsPath name; fs::FsPath name;
u64 index; s64 index;
u64 offset; float offset;
u64 entries_count; s64 entries_count;
}; };
struct FsDirCollection {
fs::FsPath path;
fs::FsPath parent_name;
std::vector<FsDirectoryEntry> files;
std::vector<FsDirectoryEntry> dirs;
};
using FsDirCollections = std::vector<FsDirCollection>;
struct Menu final : MenuBase { struct Menu final : MenuBase {
Menu(const std::vector<NroEntry>& nro_entries); Menu(const std::vector<NroEntry>& nro_entries);
~Menu(); ~Menu();
@@ -103,7 +120,7 @@ struct Menu final : MenuBase {
} }
private: private:
void SetIndex(std::size_t index); void SetIndex(s64 index);
void InstallForwarder(); void InstallForwarder();
auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result; auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result;
@@ -115,7 +132,7 @@ private:
return GetNewPath(m_path, entry.name); return GetNewPath(m_path, entry.name);
} }
auto GetNewPath(u64 index) const -> fs::FsPath { auto GetNewPath(s64 index) const -> fs::FsPath {
return GetNewPath(m_path, GetEntry(index).name); return GetNewPath(m_path, GetEntry(index).name);
} }
@@ -203,11 +220,19 @@ private:
void OnDeleteCallback(); void OnDeleteCallback();
void OnPasteCallback(); void OnPasteCallback();
void OnRenameCallback(); void OnRenameCallback();
auto CheckIfUpdateFolder() -> Result;
auto get_collection(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollection& out, bool inc_file, bool inc_dir, bool inc_size) -> Result;
auto get_collections(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out) -> Result;
void SetFs(const fs::FsPath& new_path, u32 new_type);
private: private:
static constexpr inline const char* INI_SECTION = "filebrowser"; static constexpr inline const char* INI_SECTION = "filebrowser";
const std::vector<NroEntry>& m_nro_entries; const std::vector<NroEntry>& m_nro_entries;
std::unique_ptr<fs::FsNative> m_fs;
FsType m_fs_type;
fs::FsPath m_path; fs::FsPath m_path;
std::vector<FileEntry> m_entries; std::vector<FileEntry> m_entries;
std::vector<u32> m_entries_index; // files not including hidden std::vector<u32> m_entries_index; // files not including hidden
@@ -215,6 +240,9 @@ private:
std::vector<u32> m_entries_index_search; // files found via search std::vector<u32> m_entries_index_search; // files found via search
std::span<u32> m_entries_current; std::span<u32> m_entries_current;
std::unique_ptr<List> m_list;
std::optional<fs::FsPath> m_daybreak_path;
// search options // search options
// show files [X] // show files [X]
// show folders [X] // show folders [X]
@@ -229,9 +257,8 @@ private:
// if it does, the index becomes that file. // if it does, the index becomes that file.
std::vector<LastFile> m_previous_highlighted_file; std::vector<LastFile> m_previous_highlighted_file;
fs::FsPath m_selected_path; fs::FsPath m_selected_path;
std::size_t m_index{}; s64 m_index{};
std::size_t m_index_offset{}; s64 m_selected_count{};
std::size_t m_selected_count{};
SelectedType m_selected_type{SelectedType::None}; SelectedType m_selected_type{SelectedType::None};
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Alphabetical}; option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Alphabetical};
@@ -239,10 +266,8 @@ private:
option::OptionBool m_show_hidden{INI_SECTION, "show_hidden", false}; option::OptionBool m_show_hidden{INI_SECTION, "show_hidden", false};
option::OptionBool m_folders_first{INI_SECTION, "folders_first", true}; option::OptionBool m_folders_first{INI_SECTION, "folders_first", true};
option::OptionBool m_hidden_last{INI_SECTION, "hidden_last", false}; option::OptionBool m_hidden_last{INI_SECTION, "hidden_last", false};
option::OptionBool m_ignore_read_only{INI_SECTION, "ignore_read_only", false};
option::OptionBool m_search_show_files{INI_SECTION, "search_show_files", true}; option::OptionLong m_mount{INI_SECTION, "mount", 0};
option::OptionBool m_search_show_folders{INI_SECTION, "search_show_folders", true};
option::OptionBool m_search_recursive{INI_SECTION, "search_recursive", false};
bool m_loaded_assoc_entries{}; bool m_loaded_assoc_entries{};
bool m_is_update_folder{}; bool m_is_update_folder{};

View File

@@ -0,0 +1,75 @@
#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{};
s64 m_index_offset{};
std::unique_ptr<List> m_list;
};
} // namespace sphaira::ui::menu::gh

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "ui/menus/menu_base.hpp" #include "ui/menus/menu_base.hpp"
#include "ui/list.hpp"
#include "nro.hpp" #include "nro.hpp"
#include "fs.hpp" #include "fs.hpp"
#include "option.hpp" #include "option.hpp"
@@ -29,7 +30,7 @@ struct Menu final : MenuBase {
void Draw(NVGcontext* vg, Theme* theme) override; void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override; void OnFocusGained() override;
void SetIndex(std::size_t index); void SetIndex(s64 index);
void InstallHomebrew(); void InstallHomebrew();
void ScanHomebrew(); void ScanHomebrew();
void Sort(); void Sort();
@@ -50,8 +51,8 @@ private:
static constexpr inline const char* INI_SECTION = "homebrew"; static constexpr inline const char* INI_SECTION = "homebrew";
std::vector<NroEntry> m_entries; std::vector<NroEntry> m_entries;
std::size_t m_start{}; s64 m_index{}; // where i am in the array
std::size_t m_index{}; // where i am in the array std::unique_ptr<List> m_list;
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_AlphabeticalStar}; option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_AlphabeticalStar};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Decending}; option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Decending};

View File

@@ -61,7 +61,7 @@ private:
Rotation m_rotation{Rotation_90}; Rotation m_rotation{Rotation_90};
Colour m_colour{Colour_Grey}; Colour m_colour{Colour_Grey};
int m_image{}; int m_image{};
std::size_t m_index{}; s64 m_index{};
}; };
} // namespace sphaira::ui::menu::irs } // namespace sphaira::ui::menu::irs

View File

@@ -28,10 +28,13 @@ struct MainMenu final : Widget {
void OnFocusGained() override; void OnFocusGained() override;
void OnFocusLost() override; void OnFocusLost() override;
auto IsMenu() const -> bool override {
return true;
}
private: private:
void OnLRPress(std::shared_ptr<MenuBase> menu, Button b); void OnLRPress(std::shared_ptr<MenuBase> menu, Button b);
void AddOnLPress(); void AddOnLRPress();
void AddOnRPress();
private: private:
std::shared_ptr<homebrew::Menu> m_homebrew_menu{}; std::shared_ptr<homebrew::Menu> m_homebrew_menu{};

View File

@@ -12,15 +12,31 @@ struct MenuBase : Widget {
virtual void Update(Controller* controller, TouchInfo* touch); virtual void Update(Controller* controller, TouchInfo* touch);
virtual void Draw(NVGcontext* vg, Theme* theme); virtual void Draw(NVGcontext* vg, Theme* theme);
auto IsMenu() const -> bool override {
return true;
}
void SetTitle(std::string title); void SetTitle(std::string title);
void SetTitleSubHeading(std::string sub_heading); void SetTitleSubHeading(std::string sub_heading);
void SetSubHeading(std::string sub_heading); void SetSubHeading(std::string sub_heading);
private:
void UpdateVars();
private: private:
std::string m_title; std::string m_title;
std::string m_title_sub_heading; std::string m_title_sub_heading;
std::string m_sub_heading; std::string m_sub_heading;
AppletType m_applet_type;
struct tm m_tm{};
TimeStamp m_poll_timestamp{};
u32 m_battery_percetange{};
PsmChargerType m_charger_type{};
NifmInternetConnectionType m_type{};
NifmInternetConnectionStatus m_status{};
u32 m_strength{};
u32 m_ip{};
}; };
} // namespace sphaira::ui::menu } // namespace sphaira::ui::menu

View File

@@ -2,6 +2,7 @@
#include "ui/menus/menu_base.hpp" #include "ui/menus/menu_base.hpp"
#include "ui/scrollable_text.hpp" #include "ui/scrollable_text.hpp"
#include "ui/list.hpp"
#include "option.hpp" #include "option.hpp"
#include <span> #include <span>
@@ -15,28 +16,14 @@ enum class ImageDownloadState {
}; };
struct LazyImage { struct LazyImage {
LazyImage() = default;
~LazyImage(); ~LazyImage();
int image{}; int image{};
int w{}, h{}; int w{}, h{};
bool tried_cache{};
bool cached{};
ImageDownloadState state{ImageDownloadState::None}; ImageDownloadState state{ImageDownloadState::None};
u8 first_pixel[4]{};
}; };
// "mutation setLike($type: String!, $id: String!, $value: Boolean!) {\n setLike(type: $type, id: $id, value: $value)\n}\n"
// https://api.themezer.net/?query=query($nsfw:Boolean,$target:String,$page:Int,$limit:Int,$sort:String,$order:String,$query:String){themeList(nsfw:$nsfw,target:$target,page:$page,limit:$limit,sort:$sort,order:$order,query:$query){id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}}&variables={"nsfw":false,"target":null,"page":1,"limit":10,"sort":"updated","order":"desc","query":null}
// https://api.themezer.net/?query=query($nsfw:Boolean,$page:Int,$limit:Int,$sort:String,$order:String,$query:String){packList(nsfw:$nsfw,page:$page,limit:$limit,sort:$sort,order:$order,query:$query){id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count,themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}}}&variables={"nsfw":false,"page":1,"limit":10,"sort":"updated","order":"desc","query":null}
// https://api.themezer.net/?query=query($id:String!){pack(id:$id){id,creator{display_name},details{name,description},last_updated,categories,dl_count,like_count,themes{id,details{name},layout{id,details{name}},categories,target,preview{original,thumb},last_updated,dl_count,like_count}}}&variables={"id":"16d"}
// https://api.themezer.net/?query=query{nxinstaller(id:"t9a6"){themes{filename,url,mimetype}}}
// https://api.themezer.net/?query=query{downloadTheme(id:"t9a6"){filename,url,mimetype}}
// https://api.themezer.net/?query=query{downloadPack(id:"t9a6"){filename,url,mimetype}}
// {"data":{"setLike":true}}
// https://api.themezer.net/?query=mutation{setLike(type:"packs",id:"5",value:true){data{setLike}}}
// https://api.themezer.net/?query=mutation($type:String!,$id:String!,$value:Boolean!){setLike(type:$type,id:$id,value:$value){data{setLike}}}&variables={"type":"packs","id":"5","value":true}
enum MenuState { enum MenuState {
MenuState_Normal, MenuState_Normal,
MenuState_Search, MenuState_Search,
@@ -55,6 +42,11 @@ enum class PageLoadState {
Error, Error,
}; };
// all commented out entries are those that we don't query for.
// this saves time not only processing the json, but also the download
// of said json.
// by reducing the fields to only what we need, the size is 4-5x smaller.
struct Creator { struct Creator {
std::string id; std::string id;
std::string display_name; std::string display_name;
@@ -62,11 +54,11 @@ struct Creator {
struct Details { struct Details {
std::string name; std::string name;
std::string description; // std::string description;
}; };
struct Preview { struct Preview {
std::string original; // std::string original;
std::string thumb; std::string thumb;
LazyImage lazy_image; LazyImage lazy_image;
}; };
@@ -81,13 +73,13 @@ using DownloadTheme = DownloadPack;
struct ThemeEntry { struct ThemeEntry {
std::string id; std::string id;
Creator creator; // Creator creator;
Details details; // Details details;
std::string last_updated; // std::string last_updated;
u64 dl_count; // u64 dl_count;
u64 like_count; // u64 like_count;
std::vector<std::string> categories; // std::vector<std::string> categories;
std::string target; // std::string target;
Preview preview; Preview preview;
}; };
@@ -106,10 +98,10 @@ struct PackListEntry {
std::string id; std::string id;
Creator creator; Creator creator;
Details details; Details details;
std::string last_updated; // std::string last_updated;
std::vector<std::string> categories; // std::vector<std::string> categories;
u64 dl_count; // u64 dl_count;
u64 like_count; // u64 like_count;
std::vector<ThemeEntry> themes; std::vector<ThemeEntry> themes;
}; };
@@ -173,8 +165,11 @@ struct Menu final : MenuBase {
void Draw(NVGcontext* vg, Theme* theme) override; void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override; void OnFocusGained() override;
void SetIndex(std::size_t index) { void SetIndex(s64 index) {
m_index = index; m_index = index;
if (!m_index) {
m_list->SetYoff(0);
}
} }
// void SetSearch(const std::string& term); // void SetSearch(const std::string& term);
@@ -189,13 +184,13 @@ private:
static constexpr inline u32 MAX_ON_PAGE = 16; // same as website static constexpr inline u32 MAX_ON_PAGE = 16; // same as website
std::vector<PageEntry> m_pages; std::vector<PageEntry> m_pages;
std::size_t m_page_index{}; s64 m_page_index{};
std::size_t m_page_index_max{1}; s64 m_page_index_max{1};
std::string m_search{}; std::string m_search{};
std::size_t m_start{}; s64 m_index{}; // where i am in the array
std::size_t m_index{}; // where i am in the array std::unique_ptr<List> m_list;
// options // options
option::OptionLong m_sort{INI_SECTION, "sort", 0}; option::OptionLong m_sort{INI_SECTION, "sort", 0};

View File

@@ -19,7 +19,6 @@ public:
auto IsDone() const noexcept { return m_count == 0; } auto IsDone() const noexcept { return m_count == 0; }
private: private:
void OnLayoutChange() override;
void Draw(NVGcontext* vg, Theme* theme) override; void Draw(NVGcontext* vg, Theme* theme) override;
private: private:
@@ -34,7 +33,6 @@ public:
NotifMananger() = default; NotifMananger() = default;
~NotifMananger() = default; ~NotifMananger() = default;
void OnLayoutChange() override;
void Draw(NVGcontext* vg, Theme* theme) override; void Draw(NVGcontext* vg, Theme* theme) override;
void Push(const NotifEntry& entry); void Push(const NotifEntry& entry);

View File

@@ -1,7 +1,7 @@
#pragma once #pragma once
#include "nanovg.h" #include "nanovg.h"
#include "ui/widget.hpp" #include "ui/types.hpp"
namespace sphaira::ui::gfx { namespace sphaira::ui::gfx {
@@ -81,10 +81,11 @@ void textBounds(NVGcontext*, float x, float y, float *bounds, const char* str, .
// void textBounds(NVGcontext*, float *bounds, const char* str); // void textBounds(NVGcontext*, float *bounds, const char* str);
auto getButton(Button button) -> const char*; auto getButton(Button button) -> const char*;
void drawButton(NVGcontext* vg, float x, float y, float size, Button button); void drawScrollbar(NVGcontext* vg, Theme* theme, u32 index_off, u32 count, u32 max_per_page);
void drawButtons(NVGcontext* vg, const Widget::Actions& actions, const NVGcolor& c, float start_x = 1220.f); void drawScrollbar(NVGcontext* vg, Theme* theme, float x, float y, float h, u32 index_off, u32 count, u32 max_per_page);
void drawDimBackground(NVGcontext* vg); void drawScrollbar2(NVGcontext* vg, Theme* theme, float x, float y, float h, s64 index_off, s64 count, s64 row, s64 page);
void drawScrollbar2(NVGcontext* vg, Theme* theme, s64 index_off, s64 count, s64 row, s64 page);
void updateHighlightAnimation(); void updateHighlightAnimation();
void getHighlightAnimation(float* gradientX, float* gradientY, float* color); void getHighlightAnimation(float* gradientX, float* gradientY, float* color);

View File

@@ -9,8 +9,6 @@ public:
Object() = default; Object() = default;
virtual ~Object() = default; virtual ~Object() = default;
// virtual auto OnLayoutChange() -> void = 0;
virtual auto OnLayoutChange() -> void {};
virtual auto Draw(NVGcontext* vg, Theme* theme) -> void = 0; virtual auto Draw(NVGcontext* vg, Theme* theme) -> void = 0;
auto GetPos() const noexcept { auto GetPos() const noexcept {

View File

@@ -12,7 +12,6 @@ public:
OptionBoxEntry(const std::string& text, Vec4 pos); OptionBoxEntry(const std::string& text, Vec4 pos);
auto Update(Controller* controller, TouchInfo* touch) -> void override {} auto Update(Controller* controller, TouchInfo* touch) -> void override {}
auto OnLayoutChange() -> void override {}
auto Draw(NVGcontext* vg, Theme* theme) -> void override; auto Draw(NVGcontext* vg, Theme* theme) -> void override;
auto Selected(bool enable) -> void; auto Selected(bool enable) -> void;
@@ -28,23 +27,25 @@ private:
// todo: support upto 4 options. // todo: support upto 4 options.
class OptionBox final : public Widget { class OptionBox final : public Widget {
public: public:
using Callback = std::function<void(std::optional<std::size_t> index)>; using Callback = std::function<void(std::optional<s64> index)>;
using Option = std::string; using Option = std::string;
using Options = std::vector<Option>; using Options = std::vector<Option>;
public: public:
OptionBox(const std::string& message, const Option& a, Callback cb); // confirm OptionBox(const std::string& message, const Option& a, Callback cb = [](auto){}); // confirm
OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb); // yesno OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb); // yesno
OptionBox(const std::string& message, const Option& a, const Option& b, std::size_t index, Callback cb); // yesno OptionBox(const std::string& message, const Option& a, const Option& b, s64 index, Callback cb); // yesno
OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, Callback cb); // tri OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, Callback cb); // tri
OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, std::size_t index, Callback cb); // tri OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, s64 index, Callback cb); // tri
auto Update(Controller* controller, TouchInfo* touch) -> void override; auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto OnLayoutChange() -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> void override; auto Draw(NVGcontext* vg, Theme* theme) -> void override;
auto OnFocusGained() noexcept -> void override;
auto OnFocusLost() noexcept -> void override;
private: private:
auto Setup(std::size_t index) -> void; // common setup values auto Setup(s64 index) -> void; // common setup values
void SetIndex(s64 index);
private: private:
std::string m_message; std::string m_message;
@@ -52,7 +53,7 @@ private:
Vec4 m_spacer_line{}; Vec4 m_spacer_line{};
std::size_t m_index{}; s64 m_index{};
std::vector<OptionBoxEntry> m_entries; std::vector<OptionBoxEntry> m_entries;
}; };

View File

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

View File

@@ -1,7 +1,7 @@
#pragma once #pragma once
#include "ui/widget.hpp" #include "ui/widget.hpp"
#include "ui/scrollbar.hpp" #include "ui/list.hpp"
#include <optional> #include <optional>
namespace sphaira::ui { namespace sphaira::ui {
@@ -9,18 +9,22 @@ namespace sphaira::ui {
class PopupList final : public Widget { class PopupList final : public Widget {
public: public:
using Items = std::vector<std::string>; using Items = std::vector<std::string>;
using Callback = std::function<void(std::optional<std::size_t>)>; using Callback = std::function<void(std::optional<s64>)>;
public: public:
explicit PopupList(std::string title, Items items, Callback cb, std::size_t index = 0); explicit PopupList(std::string title, Items items, Callback cb, s64 index = 0);
PopupList(std::string title, Items items, Callback cb, std::string index); PopupList(std::string title, Items items, Callback cb, std::string index);
PopupList(std::string title, Items items, std::string& index_str_ref, std::size_t& index); PopupList(std::string title, Items items, std::string& index_str_ref, s64& index);
PopupList(std::string title, Items items, std::string& index_ref); PopupList(std::string title, Items items, std::string& index_ref);
PopupList(std::string title, Items items, std::size_t& index_ref); PopupList(std::string title, Items items, s64& index_ref);
auto Update(Controller* controller, TouchInfo* touch) -> void override; auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto OnLayoutChange() -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> void override; auto Draw(NVGcontext* vg, Theme* theme) -> void override;
auto OnFocusGained() noexcept -> void override;
auto OnFocusLost() noexcept -> void override;
private:
void SetIndex(s64 index);
private: private:
static constexpr Vec2 m_title_pos{70.f, 28.f}; static constexpr Vec2 m_title_pos{70.f, 28.f};
@@ -31,17 +35,14 @@ private:
std::string m_title; std::string m_title;
Items m_items; Items m_items;
Callback m_callback; Callback m_callback;
std::size_t m_index; // index in list array s64 m_index; // index in list array
std::size_t m_index_offset{}; // drawing from array start s64 m_index_offset{}; // drawing from array start
// std::size_t& index_ref; std::unique_ptr<List> m_list;
// std::string& index_str_ref;
float m_selected_y{};
float m_yoff{}; float m_yoff{};
float m_line_top{}; float m_line_top{};
float m_line_bottom{}; float m_line_bottom{};
ScrollBar m_scrollbar;
}; };
} // namespace sphaira::ui } // namespace sphaira::ui

View File

@@ -22,7 +22,7 @@ struct ProgressBox final : Widget {
auto Draw(NVGcontext* vg, Theme* theme) -> void override; auto Draw(NVGcontext* vg, Theme* theme) -> void override;
auto NewTransfer(const std::string& transfer) -> ProgressBox&; auto NewTransfer(const std::string& transfer) -> ProgressBox&;
auto UpdateTransfer(u64 offset, u64 size) -> ProgressBox&; auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
void RequestExit(); void RequestExit();
auto ShouldExit() -> bool; auto ShouldExit() -> bool;
@@ -30,6 +30,16 @@ struct ProgressBox final : Widget {
auto CopyFile(const fs::FsPath& src, const fs::FsPath& dst) -> Result; auto CopyFile(const fs::FsPath& src, const fs::FsPath& dst) -> Result;
void Yield(); void Yield();
auto OnDownloadProgressCallback() {
return [this](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
if (this->ShouldExit()) {
return false;
}
this->UpdateTransfer(dlnow, dltotal);
return true;
};
}
public: public:
struct ThreadData { struct ThreadData {
ProgressBox* pbox; ProgressBox* pbox;
@@ -45,8 +55,8 @@ private:
ProgressBoxDoneCallback m_done{}; ProgressBoxDoneCallback m_done{};
std::string m_title{}; std::string m_title{};
std::string m_transfer{}; std::string m_transfer{};
u64 m_size{}; s64 m_size{};
u64 m_offset{}; s64 m_offset{};
bool m_exit_requested{}; bool m_exit_requested{};
}; };

View File

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

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "ui/widget.hpp" #include "ui/widget.hpp"
#include "ui/list.hpp"
#include <memory> #include <memory>
namespace sphaira::ui { namespace sphaira::ui {
@@ -9,7 +10,6 @@ class SidebarEntryBase : public Widget {
public: public:
SidebarEntryBase(std::string&& title); SidebarEntryBase(std::string&& title);
virtual auto Draw(NVGcontext* vg, Theme* theme) -> void override; virtual auto Draw(NVGcontext* vg, Theme* theme) -> void override;
virtual auto OnLayoutChange() -> void override {}
protected: protected:
std::string m_title; std::string m_title;
@@ -24,9 +24,9 @@ public:
SidebarEntryBool(std::string title, bool option, Callback cb, std::string true_str = "On", std::string false_str = "Off"); SidebarEntryBool(std::string title, bool option, Callback cb, std::string true_str = "On", std::string false_str = "Off");
SidebarEntryBool(std::string title, bool& option, std::string true_str = "On", std::string false_str = "Off"); SidebarEntryBool(std::string title, bool& option, std::string true_str = "On", std::string false_str = "Off");
private:
auto Draw(NVGcontext* vg, Theme* theme) -> void override; auto Draw(NVGcontext* vg, Theme* theme) -> void override;
private:
bool m_option; bool m_option;
Callback m_callback; Callback m_callback;
std::string m_true_str; std::string m_true_str;
@@ -50,10 +50,10 @@ class SidebarEntryArray final : public SidebarEntryBase {
public: public:
using Items = std::vector<std::string>; using Items = std::vector<std::string>;
using ListCallback = std::function<void()>; using ListCallback = std::function<void()>;
using Callback = std::function<void(std::size_t& index)>; using Callback = std::function<void(s64& index)>;
public: public:
explicit SidebarEntryArray(std::string title, Items items, Callback cb, std::size_t index = 0); explicit SidebarEntryArray(std::string title, Items items, Callback cb, s64 index = 0);
SidebarEntryArray(std::string title, Items items, Callback cb, std::string index); SidebarEntryArray(std::string title, Items items, Callback cb, std::string index);
SidebarEntryArray(std::string title, Items items, std::string& index); SidebarEntryArray(std::string title, Items items, std::string& index);
@@ -63,7 +63,7 @@ private:
Items m_items; Items m_items;
ListCallback m_list_callback; ListCallback m_list_callback;
Callback m_callback; Callback m_callback;
std::size_t m_index; s64 m_index;
}; };
template <typename T> template <typename T>
@@ -101,33 +101,31 @@ public:
Sidebar(std::string title, std::string sub, Side side); Sidebar(std::string title, std::string sub, Side side);
auto Update(Controller* controller, TouchInfo* touch) -> void override; auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto OnLayoutChange() -> void override {}
auto Draw(NVGcontext* vg, Theme* theme) -> void override; auto Draw(NVGcontext* vg, Theme* theme) -> void override;
auto OnFocusGained() noexcept -> void override; auto OnFocusGained() noexcept -> void override;
auto OnFocusLost() noexcept -> void override; auto OnFocusLost() noexcept -> void override;
void Add(std::shared_ptr<SidebarEntryBase> entry); void Add(std::shared_ptr<SidebarEntryBase> entry);
void AddSpacer();
void AddHeader(std::string name);
private: private:
void SetIndex(std::size_t index); void SetIndex(s64 index);
void SetupButtons();
private: private:
std::string m_title; std::string m_title;
std::string m_sub; std::string m_sub;
Side m_side; Side m_side;
Items m_items; Items m_items;
std::size_t m_index{}; s64 m_index{};
std::size_t m_index_offset{}; s64 m_index_offset{};
std::unique_ptr<List> m_list;
Vec4 m_top_bar{}; Vec4 m_top_bar{};
Vec4 m_bottom_bar{}; Vec4 m_bottom_bar{};
Vec2 m_title_pos{}; Vec2 m_title_pos{};
Vec4 m_base_pos{}; Vec4 m_base_pos{};
float m_selected_y{};
static constexpr float m_title_size{28.f}; static constexpr float m_title_size{28.f};
// static constexpr Vec2 box_size{380.f, 70.f}; // static constexpr Vec2 box_size{380.f, 70.f};
static constexpr Vec2 m_box_size{400.f, 70.f}; static constexpr Vec2 m_box_size{400.f, 70.f};

View File

@@ -114,15 +114,34 @@ struct [[nodiscard]] Vec4 {
struct TimeStamp { struct TimeStamp {
TimeStamp() { TimeStamp() {
Update();
}
void Update() {
start = armGetSystemTick(); start = armGetSystemTick();
} }
auto GetNs() -> u64 { auto GetNs() const -> u64 {
const auto end_ticks = armGetSystemTick(); const auto end_ticks = armGetSystemTick();
return armTicksToNs(end_ticks) - armTicksToNs(start); return armTicksToNs(end_ticks) - armTicksToNs(start);
} }
auto GetSeconds() -> double { auto GetMs() const -> u64 {
const auto ns = GetNs();
return ns/1000/1000;
}
auto GetSeconds() const -> u64 {
const auto ns = GetNs();
return ns/1000/1000/1000;
}
auto GetMsD() const -> double {
const double ns = GetNs();
return ns/1000.0/1000.0;
}
auto GetSecondsD() const -> double {
const double ns = GetNs(); const double ns = GetNs();
return ns/1000.0/1000.0/1000.0; return ns/1000.0/1000.0/1000.0;
} }
@@ -177,39 +196,31 @@ struct Theme {
fs::FsPath path; fs::FsPath path;
PLSR_BFSTM music; PLSR_BFSTM music;
ElementEntry elements[ThemeEntryID_MAX]; 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);
}; };
enum class TouchState { // enum class TouchGesture {
Start, // set when touch has started // None,
Touching, // set when touch is held longer than 1 frame // Tap,
Stop, // set after touch is released // Scroll,
None, // set when there is no touch // };
};
struct TouchInfo { struct TouchInfo {
s32 initial_x; HidTouchState initial;
s32 initial_y; HidTouchState cur;
s32 cur_x; auto in_range(const Vec4& v) const -> bool {
s32 cur_y; return cur.x >= v.x && cur.x <= v.x + v.w && cur.y >= v.y && cur.y <= v.y + v.h;
}
s32 prev_x; auto in_range(s32 x, s32 y, s32 w, s32 h) const -> bool {
s32 prev_y; return in_range(Vec4(x, y, w, h));
}
u32 finger_id;
bool is_touching; bool is_touching;
bool is_tap; bool is_tap;
bool is_scroll;
bool is_clicked;
bool is_end;
}; };
enum class Button : u64 { enum class Button : u64 {
@@ -340,7 +351,7 @@ struct Controller {
m_kup = 0; m_kup = 0;
} }
void UpdateButtonHeld(HidNpadButton buttons) { void UpdateButtonHeld(u64 buttons) {
if (m_kdown & buttons) { if (m_kdown & buttons) {
m_step = 50; m_step = 50;
m_counter = 0; m_counter = 0;
@@ -348,7 +359,7 @@ struct Controller {
m_counter += m_step; m_counter += m_step;
if (m_counter >= m_MAX) { if (m_counter >= m_MAX) {
m_kdown |= buttons; m_kdown |= m_kheld & buttons;
m_counter = 0; m_counter = 0;
m_step = std::min(m_step + 50, m_MAX_STEP); m_step = std::min(m_step + 50, m_MAX_STEP);
} }

View File

@@ -8,7 +8,20 @@
namespace sphaira::ui { namespace sphaira::ui {
struct uiButton final : Object {
uiButton(Button button, Action action) : m_button{button}, m_action{action} {}
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
Button m_button;
Action m_action;
Vec4 m_button_pos{};
Vec4 m_hint_pos{};
};
struct Widget : public Object { struct Widget : public Object {
using Actions = std::map<Button, Action>;
using uiButtons = std::vector<uiButton>;
virtual ~Widget() = default; virtual ~Widget() = default;
virtual void Update(Controller* controller, TouchInfo* touch); virtual void Update(Controller* controller, TouchInfo* touch);
@@ -26,6 +39,10 @@ struct Widget : public Object {
return m_focus; return m_focus;
} }
virtual auto IsMenu() const -> bool {
return false;
}
auto HasAction(Button button) const -> bool; auto HasAction(Button button) const -> bool;
void SetAction(Button button, Action action); void SetAction(Button button, Action action);
void SetActions(std::same_as<std::pair<Button, Action>> auto ...args) { void SetActions(std::same_as<std::pair<Button, Action>> auto ...args) {
@@ -45,6 +62,8 @@ struct Widget : public Object {
m_actions.clear(); m_actions.clear();
} }
auto FireAction(Button button, u8 type = ActionType::DOWN) -> bool;
void SetPop(bool pop = true) { void SetPop(bool pop = true) {
m_pop = pop; m_pop = pop;
} }
@@ -53,9 +72,14 @@ struct Widget : public Object {
return m_pop; return m_pop;
} }
using Actions = std::map<Button, Action>; auto SetUiButtonPos(Vec2 pos) {
// using Actions = std::unordered_map<Button, Action>; m_button_pos = pos;
}
auto GetUiButtons() const -> uiButtons;
Actions m_actions; Actions m_actions;
Vec2 m_button_pos{1220, 675};
bool m_focus{false}; bool m_focus{false};
bool m_pop{false}; bool m_pop{false};
}; };

View File

@@ -39,13 +39,14 @@ constexpr auto cexprHash(const char *str, std::size_t v = 0) noexcept -> std::si
JSON_SKIP_IF_NULL_PTR(str); \ JSON_SKIP_IF_NULL_PTR(str); \
e.name = str; \ e.name = str; \
} \ } \
} } break
#define JSON_SET_OBJ(name) case cexprHash(#name): { \ #define JSON_SET_OBJ(name) case cexprHash(#name): { \
if (yyjson_is_obj(val)) { \ if (yyjson_is_obj(val)) { \
from_json(val, e.name); \ from_json(val, e.name); \
} \ } \
} } break
#define JSON_SET_UINT(name) JSON_SET_TYPE(name, uint) #define JSON_SET_UINT(name) JSON_SET_TYPE(name, uint)
#define JSON_SET_STR(name) JSON_SET_TYPE(name, str) #define JSON_SET_STR(name) JSON_SET_TYPE(name, str)
#define JSON_SET_BOOL(name) JSON_SET_TYPE(name, bool) #define JSON_SET_BOOL(name) JSON_SET_TYPE(name, bool)
@@ -72,7 +73,7 @@ constexpr auto cexprHash(const char *str, std::size_t v = 0) noexcept -> std::si
JSON_SET_ARR_TYPE(name, type); \ JSON_SET_ARR_TYPE(name, type); \
} \ } \
} \ } \
} } break
#define JSON_SET_ARR_OBJ2(name, member) case cexprHash(#name): { \ #define JSON_SET_ARR_OBJ2(name, member) case cexprHash(#name): { \
if (yyjson_is_arr(val)) { \ if (yyjson_is_arr(val)) { \
@@ -87,7 +88,7 @@ constexpr auto cexprHash(const char *str, std::size_t v = 0) noexcept -> std::si
from_json(hit, member[idx]); \ from_json(hit, member[idx]); \
} \ } \
} \ } \
} } break
#define JSON_SET_ARR_OBJ(name) JSON_SET_ARR_OBJ2(name, e.name) #define JSON_SET_ARR_OBJ(name) JSON_SET_ARR_OBJ2(name, e.name)

View File

@@ -1,5 +1,6 @@
#include "ui/menus/main_menu.hpp" #include "ui/menus/main_menu.hpp"
#include "ui/error_box.hpp" #include "ui/error_box.hpp"
#include "ui/option_box.hpp"
#include "app.hpp" #include "app.hpp"
#include "log.hpp" #include "log.hpp"
@@ -228,9 +229,22 @@ void App::Loop() {
ui::gfx::updateHighlightAnimation(); ui::gfx::updateHighlightAnimation();
auto events = evman::popall(); // fire all events in in a 3ms timeslice
// while (auto e = evman::pop()) { TimeStamp ts_event;
for (auto& e : events) { const u64 event_timeout = 3;
// limit events to a max per frame in order to not block for too long.
while (true) {
if (ts_event.GetMs() >= event_timeout) {
log_write("event loop timed-out\n");
break;
}
auto event = evman::pop();
if (!event.has_value()) {
break;
}
std::visit([this](auto&& arg){ std::visit([this](auto&& arg){
using T = std::decay_t<decltype(arg)>; using T = std::decay_t<decltype(arg)>;
if constexpr(std::is_same_v<T, evman::LaunchNroEventData>) { if constexpr(std::is_same_v<T, evman::LaunchNroEventData>) {
@@ -276,13 +290,13 @@ void App::Loop() {
App::Notify("Nxlink Finished"_i18n); App::Notify("Nxlink Finished"_i18n);
break; break;
} }
} else if constexpr(std::is_same_v<T, DownloadEventData>) { } else if constexpr(std::is_same_v<T, curl::DownloadEventData>) {
log_write("[DownloadEventData] got event\n"); log_write("[DownloadEventData] got event\n");
arg.callback(arg.data, arg.result); arg.callback(arg.result);
} else { } else {
static_assert(false, "non-exhaustive visitor!"); static_assert(false, "non-exhaustive visitor!");
} }
}, e); }, event.value());
} }
u32 w{},h{}; u32 w{},h{};
@@ -325,6 +339,16 @@ auto App::Push(std::shared_ptr<ui::Widget> widget) -> void {
log_write("did it\n"); log_write("did it\n");
} }
auto App::PopToMenu() -> void {
for (auto it = g_app->m_widgets.rbegin(); it != g_app->m_widgets.rend(); it++) {
const auto& p = *it;
if (p->IsMenu()) {
break;
}
p->SetPop();
}
}
void App::Notify(std::string text, ui::NotifEntry::Side side) { void App::Notify(std::string text, ui::NotifEntry::Side side) {
g_app->m_notif_manager.Push({text, side}); g_app->m_notif_manager.Push({text, side});
} }
@@ -367,15 +391,19 @@ auto App::GetThemeMetaList() -> std::span<ThemeMeta> {
return g_app->m_theme_meta_entries; return g_app->m_theme_meta_entries;
} }
void App::SetTheme(u64 theme_index) { void App::SetTheme(s64 theme_index) {
g_app->LoadTheme(g_app->m_theme_meta_entries[theme_index].ini_path.c_str()); g_app->LoadTheme(g_app->m_theme_meta_entries[theme_index].ini_path.c_str());
g_app->m_theme_index = theme_index; g_app->m_theme_index = theme_index;
} }
auto App::GetThemeIndex() -> u64 { auto App::GetThemeIndex() -> s64 {
return g_app->m_theme_index; return g_app->m_theme_index;
} }
auto App::GetDefaultImage(int* w, int* h) -> int {
return g_app->m_default_image;
}
auto App::GetExePath() -> fs::FsPath { auto App::GetExePath() -> fs::FsPath {
return g_app->m_app_path; return g_app->m_app_path;
} }
@@ -451,7 +479,104 @@ void App::SetLogEnable(bool enable) {
} }
void App::SetReplaceHbmenuEnable(bool enable) { void App::SetReplaceHbmenuEnable(bool enable) {
if (App::GetReplaceHbmenuEnable() != enable) {
g_app->m_replace_hbmenu.Set(enable); g_app->m_replace_hbmenu.Set(enable);
if (!enable) {
// check we have already replaced hbmenu with sphaira
NacpStruct hbmenu_nacp;
if (R_FAILED(nro_get_nacp("/hbmenu.nro", hbmenu_nacp))) {
return;
}
if (std::strcmp(hbmenu_nacp.lang[0].name, "sphaira")) {
return;
}
// ask user if they want to restore hbmenu
App::Push(std::make_shared<ui::OptionBox>(
"Restore hbmenu?"_i18n,
"Back"_i18n, "Restore"_i18n, 1, [hbmenu_nacp](auto op_index){
if (!op_index || *op_index == 0) {
return;
}
NacpStruct actual_hbmenu_nacp;
if (R_FAILED(nro_get_nacp("/switch/hbmenu.nro", actual_hbmenu_nacp))) {
App::Push(std::make_shared<ui::OptionBox>(
"Failed to find /switch/hbmenu.nro\n"
"Use the Appstore to re-install hbmenu"_i18n,
"OK"_i18n
));
return;
}
// NOTE: do NOT use rename anywhere here as it's possible
// to have a race condition with another app that opens hbmenu as a file
// in between the delete + rename.
// this would require a sys-module to open hbmenu.nro, such as an ftp server.
// a copy means that it opens the file handle, if successfull, then
// the full read/write will succeed.
fs::FsNativeSd fs;
NacpStruct sphaira_nacp;
fs::FsPath sphaira_path = "/switch/sphaira/sphaira.nro";
Result rc;
// first, try and backup sphaira, its not super important if this fails.
rc = nro_get_nacp(sphaira_path, sphaira_nacp);
if (R_FAILED(rc) || std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
sphaira_path = "/switch/sphaira.nro";
rc = nro_get_nacp(sphaira_path, sphaira_nacp);
}
if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
if (std::strcmp(sphaira_nacp.display_version, hbmenu_nacp.display_version) < 0) {
if (R_FAILED(rc = fs.copy_entire_file(sphaira_path, "/hbmenu.nro"))) {
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path, rc, R_MODULE(rc), R_DESCRIPTION(rc));
} else {
log_write("success with updating hbmenu!\n");
}
}
} else {
// sphaira doesn't yet exist, create a new file.
sphaira_path = "/switch/sphaira/sphaira.nro";
fs.CreateDirectoryRecursively("/switch/sphaira/");
fs.copy_entire_file(sphaira_path, "/hbmenu.nro");
}
// this should never fail, if it does, well then the sd card is fucked.
if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", "/switch/hbmenu.nro"))) {
// try and restore sphaira in a last ditch effort.
if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", sphaira_path))) {
App::Push(std::make_shared<ui::ErrorBox>(rc,
"Failed to restore hbmenu, please re-download hbmenu"_i18n
));
} else {
App::Push(std::make_shared<ui::OptionBox>(
"Failed to restore hbmenu, using sphaira instead"_i18n,
"OK"_i18n
));
}
return;
}
// don't need this any more.
fs.DeleteFile("/switch/hbmenu.nro");
// if we were hbmenu, exit now (as romfs is gone).
if (IsHbmenu()) {
App::Push(std::make_shared<ui::OptionBox>(
"Restored hbmenu, closing sphaira"_i18n,
"OK"_i18n, [](auto) {
App::Exit();
}
));
} else {
App::Notify("Restored hbmenu"_i18n);
}
}
));
}
}
} }
void App::SetInstallEnable(bool enable) { void App::SetInstallEnable(bool enable) {
@@ -576,58 +701,40 @@ void App::ExitRestart() {
void App::Poll() { void App::Poll() {
m_controller.Reset(); m_controller.Reset();
HidTouchScreenState state{};
hidGetTouchScreenStates(&state, 1);
m_touch_info.is_clicked = false;
if (state.count == 1 && !m_touch_info.is_touching) {
m_touch_info.initial = m_touch_info.cur = state.touches[0];
m_touch_info.is_touching = true;
m_touch_info.is_tap = true;
} else if (state.count >= 1 && m_touch_info.is_touching) {
m_touch_info.cur = state.touches[0];
if (m_touch_info.is_tap &&
(std::abs((s32)m_touch_info.initial.x - (s32)m_touch_info.cur.x) > 20 ||
std::abs((s32)m_touch_info.initial.y - (s32)m_touch_info.cur.y) > 20)) {
m_touch_info.is_tap = false;
m_touch_info.is_scroll = true;
}
} else if (m_touch_info.is_touching) {
m_touch_info.is_touching = false;
m_touch_info.is_scroll = false;
if (m_touch_info.is_tap) {
m_touch_info.is_clicked = true;
} else {
m_touch_info.is_end = true;
}
}
// todo: better implement this to match hos
if (!m_touch_info.is_touching && !m_touch_info.is_clicked) {
padUpdate(&m_pad); padUpdate(&m_pad);
m_controller.m_kdown = padGetButtonsDown(&m_pad); m_controller.m_kdown = padGetButtonsDown(&m_pad);
m_controller.m_kheld = padGetButtons(&m_pad); m_controller.m_kheld = padGetButtons(&m_pad);
m_controller.m_kup = padGetButtonsUp(&m_pad); m_controller.m_kup = padGetButtonsUp(&m_pad);
m_controller.UpdateButtonHeld(static_cast<u64>(Button::ANY_DIRECTION));
// dpad
m_controller.UpdateButtonHeld(HidNpadButton_Left);
m_controller.UpdateButtonHeld(HidNpadButton_Right);
m_controller.UpdateButtonHeld(HidNpadButton_Down);
m_controller.UpdateButtonHeld(HidNpadButton_Up);
// ls
m_controller.UpdateButtonHeld(HidNpadButton_StickLLeft);
m_controller.UpdateButtonHeld(HidNpadButton_StickLRight);
m_controller.UpdateButtonHeld(HidNpadButton_StickLDown);
m_controller.UpdateButtonHeld(HidNpadButton_StickLUp);
// rs
m_controller.UpdateButtonHeld(HidNpadButton_StickRLeft);
m_controller.UpdateButtonHeld(HidNpadButton_StickRRight);
m_controller.UpdateButtonHeld(HidNpadButton_StickRDown);
m_controller.UpdateButtonHeld(HidNpadButton_StickRUp);
HidTouchScreenState touch_state{};
hidGetTouchScreenStates(&touch_state, 1);
if (touch_state.count == 1 && !m_touch_info.is_touching) {
m_touch_info.initial_x = m_touch_info.prev_x = m_touch_info.cur_x = touch_state.touches[0].x;
m_touch_info.initial_y = m_touch_info.prev_y = m_touch_info.cur_y = touch_state.touches[0].y;
m_touch_info.finger_id = touch_state.touches[0].finger_id;
m_touch_info.is_touching = true;
m_touch_info.is_tap = true;
// PlaySoundEffect(SoundEffect_Limit);
} else if (touch_state.count >= 1 && m_touch_info.is_touching && m_touch_info.finger_id == touch_state.touches[0].finger_id) {
m_touch_info.prev_x = m_touch_info.cur_x;
m_touch_info.prev_y = m_touch_info.cur_y;
m_touch_info.cur_x = touch_state.touches[0].x;
m_touch_info.cur_y = touch_state.touches[0].y;
if (m_touch_info.is_tap &&
(std::abs(m_touch_info.initial_x - m_touch_info.cur_x) > 20 ||
std::abs(m_touch_info.initial_y - m_touch_info.cur_y) > 20)) {
m_touch_info.is_tap = false;
}
} else if (m_touch_info.is_touching) {
m_touch_info.is_touching = false;
// check if we clicked on anything, if so, handle it
if (m_touch_info.is_tap) {
// todo:
}
} }
} }
@@ -663,11 +770,30 @@ void App::Draw() {
nvgBeginFrame(this->vg, s_width, s_height, 1.f); nvgBeginFrame(this->vg, s_width, s_height, 1.f);
nvgScale(vg, m_scale.x, m_scale.y); nvgScale(vg, m_scale.x, m_scale.y);
// NOTE: widgets should never pop themselves from drawing! // find the last menu in the list, start drawing from there
for (auto& p : m_widgets) { auto menu_it = m_widgets.rend();
for (auto it = m_widgets.rbegin(); it != m_widgets.rend(); it++) {
const auto& p = *it;
if (!p->IsHidden() && p->IsMenu()) {
menu_it = it;
break;
}
}
// reverse itr so loop backwards to go forwarders.
if (menu_it != m_widgets.rend()) {
for (auto it = menu_it; ; it--) {
const auto& p = *it;
// draw everything not hidden on top of the menu.
if (!p->IsHidden()) { if (!p->IsHidden()) {
p->Draw(vg, &m_theme); p->Draw(vg, &m_theme);
} }
if (it == m_widgets.rbegin()) {
break;
}
}
} }
m_notif_manager.Draw(vg, &m_theme); m_notif_manager.Draw(vg, &m_theme);
@@ -682,17 +808,21 @@ auto App::GetVg() -> NVGcontext* {
} }
void DrawElement(float x, float y, float w, float h, ThemeEntryID id) { void DrawElement(float x, float y, float w, float h, ThemeEntryID id) {
DrawElement({x, y, w, h}, id);
}
void DrawElement(const Vec4& v, ThemeEntryID id) {
const auto& e = g_app->m_theme.elements[id]; const auto& e = g_app->m_theme.elements[id];
switch (e.type) { switch (e.type) {
case ElementType::None: { case ElementType::None: {
} break; } break;
case ElementType::Texture: { case ElementType::Texture: {
const auto paint = nvgImagePattern(g_app->vg, x, y, w, h, 0, e.texture, 1.f); const auto paint = nvgImagePattern(g_app->vg, v.x, v.y, v.w, v.h, 0, e.texture, 1.f);
ui::gfx::drawRect(g_app->vg, x, y, w, h, paint); ui::gfx::drawRect(g_app->vg, v, paint);
} break; } break;
case ElementType::Colour: { case ElementType::Colour: {
ui::gfx::drawRect(g_app->vg, x, y, w, h, e.colour); ui::gfx::drawRect(g_app->vg, v, e.colour);
} break; } break;
} }
} }
@@ -845,6 +975,10 @@ void App::ScanThemes(const std::string& path) {
continue; continue;
} }
if (d->d_type != DT_REG) {
continue;
}
const std::string name = d->d_name; const std::string name = d->d_name;
if (!name.ends_with(".ini")) { if (!name.ends_with(".ini")) {
continue; continue;
@@ -917,6 +1051,8 @@ App::App(const char* argv0) {
fs::FsNativeSd fs; fs::FsNativeSd fs;
fs.CreateDirectoryRecursively("/config/sphaira/assoc"); fs.CreateDirectoryRecursively("/config/sphaira/assoc");
fs.CreateDirectoryRecursively("/config/sphaira/themes"); fs.CreateDirectoryRecursively("/config/sphaira/themes");
fs.CreateDirectoryRecursively("/config/sphaira/github");
fs.CreateDirectoryRecursively("/config/sphaira/i18n");
if (App::GetLogEnable()) { if (App::GetLogEnable()) {
log_file_init(); log_file_init();
@@ -935,7 +1071,7 @@ App::App(const char* argv0) {
nxlinkInitialize(nxlink_callback); nxlinkInitialize(nxlink_callback);
} }
DownloadInit(); curl::Init();
// Create the deko3d device // Create the deko3d device
this->device = dk::DeviceMaker{} this->device = dk::DeviceMaker{}
@@ -1069,6 +1205,15 @@ App::App(const char* argv0) {
log_write("sd_total_space: %zd\n", sd_total_space); log_write("sd_total_space: %zd\n", sd_total_space);
} }
// load default image
if (R_SUCCEEDED(romfsInit())) {
ON_SCOPE_EXIT(romfsExit());
const auto image = ImageLoadFromFile("romfs:/default.png");
if (!image.data.empty()) {
m_default_image = nvgCreateImageRGBA(vg, image.w, image.h, 0, image.data.data());
}
}
App::Push(std::make_shared<ui::menu::main::MainMenu>()); App::Push(std::make_shared<ui::menu::main::MainMenu>());
log_write("finished app constructor\n"); log_write("finished app constructor\n");
} }
@@ -1090,11 +1235,12 @@ App::~App() {
log_write("starting to exit\n"); log_write("starting to exit\n");
i18n::exit(); i18n::exit();
DownloadExit(); curl::Exit();
// this has to be called before any cleanup to ensure the lifetime of // this has to be called before any cleanup to ensure the lifetime of
// nvg is still active as some widgets may need to free images. // nvg is still active as some widgets may need to free images.
m_widgets.clear(); m_widgets.clear();
nvgDeleteImage(vg, m_default_image);
appletUnhook(&m_appletHookCookie); appletUnhook(&m_appletHookCookie);
@@ -1121,29 +1267,34 @@ App::~App() {
// backup hbmenu if it is not sphaira // backup hbmenu if it is not sphaira
if (App::GetReplaceHbmenuEnable() && !IsHbmenu()) { if (App::GetReplaceHbmenuEnable() && !IsHbmenu()) {
NacpStruct nacp; NacpStruct hbmenu_nacp;
fs::FsNativeSd fs; fs::FsNativeSd fs;
if (R_SUCCEEDED(nro_get_nacp("/hbmenu.nro", nacp)) && std::strcmp(nacp.lang[0].name, "sphaira")) { Result rc;
if (R_SUCCEEDED(rc = nro_get_nacp("/hbmenu.nro", hbmenu_nacp)) && std::strcmp(hbmenu_nacp.lang[0].name, "sphaira")) {
log_write("backing up hbmenu.nro\n"); log_write("backing up hbmenu.nro\n");
if (R_FAILED(fs.copy_entire_file("/switch/hbmenu.nro", "/hbmenu.nro", true))) { if (R_FAILED(rc = fs.copy_entire_file("/switch/hbmenu.nro", "/hbmenu.nro"))) {
log_write("failed to backup hbmenu.nro\n"); log_write("failed to backup hbmenu.nro\n");
} }
} else { } else {
log_write("not backing up\n"); log_write("not backing up\n");
} }
Result rc; if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", GetExePath()))) {
if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", GetExePath(), true))) {
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", GetExePath(), rc, R_MODULE(rc), R_DESCRIPTION(rc)); log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", GetExePath(), rc, R_MODULE(rc), R_DESCRIPTION(rc));
} else { } else {
log_write("success with copying over root file!\n"); log_write("success with copying over root file!\n");
} }
} else if (IsHbmenu()) { } else if (IsHbmenu()) {
// check we have a version that's newer than current. // check we have a version that's newer than current.
NacpStruct hbmenu_nacp;
fs::FsNativeSd fs; fs::FsNativeSd fs;
Result rc;
// ensure that are still sphaira
if (R_SUCCEEDED(rc = nro_get_nacp("/hbmenu.nro", hbmenu_nacp)) && !std::strcmp(hbmenu_nacp.lang[0].name, "sphaira")) {
NacpStruct sphaira_nacp; NacpStruct sphaira_nacp;
fs::FsPath sphaira_path = "/switch/sphaira/sphaira.nro"; fs::FsPath sphaira_path = "/switch/sphaira/sphaira.nro";
Result rc;
rc = nro_get_nacp(sphaira_path, sphaira_nacp); rc = nro_get_nacp(sphaira_path, sphaira_nacp);
if (R_FAILED(rc) || std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) { if (R_FAILED(rc) || std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
@@ -1153,14 +1304,17 @@ App::~App() {
// found sphaira, now lets get compare version // found sphaira, now lets get compare version
if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) { if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
if (std::strcmp(APP_VERSION, sphaira_nacp.display_version) < 0) { if (std::strcmp(hbmenu_nacp.display_version, sphaira_nacp.display_version) < 0) {
if (R_FAILED(rc = fs.copy_entire_file(GetExePath(), sphaira_path, true))) { if (R_FAILED(rc = fs.copy_entire_file(GetExePath(), sphaira_path))) {
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path, rc, R_MODULE(rc), R_DESCRIPTION(rc)); log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path, rc, R_MODULE(rc), R_DESCRIPTION(rc));
} else { } else {
log_write("success with updating hbmenu!\n"); log_write("success with updating hbmenu!\n");
} }
} }
} }
} else {
log_write("no longer hbmenu!\n");
}
} }
if (App::GetMtpEnable()) { if (App::GetMtpEnable()) {

View File

@@ -10,8 +10,9 @@
#include <deque> #include <deque>
#include <mutex> #include <mutex>
#include <curl/curl.h> #include <curl/curl.h>
#include <yyjson.h>
namespace sphaira { namespace sphaira::curl {
namespace { namespace {
#define CURL_EASY_SETOPT_LOG(handle, opt, v) \ #define CURL_EASY_SETOPT_LOG(handle, opt, v) \
@@ -24,9 +25,6 @@ namespace {
log_write("curl_share_setopt(%s, %s) msg: %s\n", #opt, #v, curl_share_strerror(r)); \ log_write("curl_share_setopt(%s, %s) msg: %s\n", #opt, #v, curl_share_strerror(r)); \
} \ } \
void DownloadThread(void* p);
void DownloadThreadQueue(void* p);
#define USE_THREAD_QUEUE 1 #define USE_THREAD_QUEUE 1
constexpr auto API_AGENT = "ITotalJustice"; constexpr auto API_AGENT = "ITotalJustice";
constexpr u64 CHUNK_SIZE = 1024*1024; constexpr u64 CHUNK_SIZE = 1024*1024;
@@ -38,51 +36,204 @@ std::atomic_bool g_running{};
CURLSH* g_curl_share{}; CURLSH* g_curl_share{};
Mutex g_mutex_share[CURL_LOCK_DATA_LAST]{}; Mutex g_mutex_share[CURL_LOCK_DATA_LAST]{};
struct UrlCache {
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);
return true;
} else {
if (force) {
return true;
} else {
return false;
}
}
}
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);
}
}
std::vector<std::string> cache;
Mutex mutex{};
};
struct DataStruct { struct DataStruct {
std::vector<u8> data; std::vector<u8> data;
u64 offset{}; s64 offset{};
FsFileSystem fs{};
FsFile f{}; FsFile f{};
s64 file_offset{}; 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 {
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());
}
}
}
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"};
Mutex m_mutex{};
yyjson_mut_doc* m_json{};
yyjson_mut_val* m_root{};
std::unordered_map<std::string, Value> m_cache{};
};
struct ThreadEntry { struct ThreadEntry {
auto Create() -> Result { auto Create() -> Result {
m_curl = curl_easy_init(); m_curl = curl_easy_init();
R_UNLESS(m_curl != nullptr, 0x1); R_UNLESS(m_curl != nullptr, 0x1);
ueventCreate(&m_uevent, true); ueventCreate(&m_uevent, true);
R_TRY(threadCreate(&m_thread, DownloadThread, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE)); R_TRY(threadCreate(&m_thread, ThreadFunc, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
R_TRY(threadStart(&m_thread)); R_TRY(threadStart(&m_thread));
R_SUCCEED(); R_SUCCEED();
} }
@@ -101,7 +252,7 @@ struct ThreadEntry {
return m_in_progress == true; return m_in_progress == true;
} }
auto Setup(DownloadCallback callback, ProgressCallback pcallback, std::string url, std::string file, std::string post) -> bool { auto Setup(const Api& api) -> bool {
assert(m_in_progress == false && "Setting up thread while active"); assert(m_in_progress == false && "Setting up thread while active");
mutexLock(&m_mutex); mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
@@ -109,35 +260,25 @@ struct ThreadEntry {
if (m_in_progress) { if (m_in_progress) {
return false; return false;
} }
m_url = url; m_api = api;
m_file = file;
m_post = post;
m_callback = callback;
m_pcallback = pcallback;
m_in_progress = true; m_in_progress = true;
// log_write("started download :)\n"); // log_write("started download :)\n");
ueventSignal(&m_uevent); ueventSignal(&m_uevent);
return true; return true;
} }
static void ThreadFunc(void* p);
CURL* m_curl{}; CURL* m_curl{};
Thread m_thread{}; Thread m_thread{};
std::string m_url{}; Api m_api{};
std::string m_file{}; // if empty, downloads to buffer
std::string m_post{}; // if empty, downloads to buffer
DownloadCallback m_callback{};
ProgressCallback m_pcallback{};
std::atomic_bool m_in_progress{}; std::atomic_bool m_in_progress{};
Mutex m_mutex{}; Mutex m_mutex{};
UEvent m_uevent{}; UEvent m_uevent{};
}; };
struct ThreadQueueEntry { struct ThreadQueueEntry {
std::string url; Api api;
std::string file;
std::string post;
DownloadCallback callback;
ProgressCallback pcallback;
bool m_delete{}; bool m_delete{};
}; };
@@ -149,7 +290,7 @@ struct ThreadQueue {
auto Create() -> Result { auto Create() -> Result {
ueventCreate(&m_uevent, true); ueventCreate(&m_uevent, true);
R_TRY(threadCreate(&m_thread, DownloadThreadQueue, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE)); R_TRY(threadCreate(&m_thread, ThreadFunc, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
R_TRY(threadStart(&m_thread)); R_TRY(threadStart(&m_thread));
R_SUCCEED(); R_SUCCEED();
} }
@@ -160,22 +301,18 @@ struct ThreadQueue {
threadClose(&m_thread); threadClose(&m_thread);
} }
auto Add(DownloadPriority prio, DownloadCallback callback, ProgressCallback pcallback, std::string url, std::string file, std::string post) -> bool { auto Add(const Api& api) -> bool {
mutexLock(&m_mutex); mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
ThreadQueueEntry entry{}; ThreadQueueEntry entry{};
entry.url = url; entry.api = api;
entry.file = file;
entry.post = post;
entry.callback = callback;
entry.pcallback = pcallback;
switch (prio) { switch (api.m_prio) {
case DownloadPriority::Normal: case Priority::Normal:
m_entries.emplace_back(entry); m_entries.emplace_back(entry);
break; break;
case DownloadPriority::High: case Priority::High:
m_entries.emplace_front(entry); m_entries.emplace_front(entry);
break; break;
} }
@@ -183,11 +320,13 @@ struct ThreadQueue {
ueventSignal(&m_uevent); ueventSignal(&m_uevent);
return true; return true;
} }
static void ThreadFunc(void* p);
}; };
ThreadEntry g_threads[MAX_THREADS]{}; ThreadEntry g_threads[MAX_THREADS]{};
ThreadQueue g_thread_queue; ThreadQueue g_thread_queue;
UrlCache g_url_cache; Cache g_cache;
void GetDownloadTempPath(fs::FsPath& buf) { void GetDownloadTempPath(fs::FsPath& buf) {
static Mutex mutex{}; static Mutex mutex{};
@@ -216,7 +355,7 @@ auto ProgressCallbackFunc2(void *clientp, curl_off_t dltotal, curl_off_t dlnow,
} }
// log_write("pcall called %u %u %u %u\n", dltotal, dlnow, ultotal, ulnow); // log_write("pcall called %u %u %u %u\n", dltotal, dlnow, ultotal, ulnow);
auto callback = *static_cast<ProgressCallback*>(clientp); auto callback = *static_cast<OnProgress*>(clientp);
if (!callback(dltotal, dlnow, ultotal, ulnow)) { if (!callback(dltotal, dlnow, ultotal, ulnow)) {
return 1; return 1;
} }
@@ -283,36 +422,60 @@ auto WriteFileCallback(void *contents, size_t size, size_t num_files, void *user
return realsize; return realsize;
} }
auto DownloadInternal(CURL* curl, DataStruct& chunk, ProgressCallback pcallback, const std::string& url, const std::string& file, const std::string& post) -> bool { auto header_callback(char* b, size_t size, size_t nitems, void* userdata) -> size_t {
fs::FsPath safe_buf; auto header = static_cast<Header*>(userdata);
fs::FsPath tmp_buf; const auto numbytes = size * nitems;
const bool has_file = !file.empty() && file != "";
const bool has_post = !post.empty() && post != "";
ON_SCOPE_EXIT(if (has_file) { fsFsClose(&chunk.fs); } ); if (b && numbytes) {
const auto dilem = (const char*)memchr(b, ':', numbytes);
if (has_file) { if (dilem) {
std::strcpy(safe_buf, file.c_str()); const int key_len = dilem - b;
GetDownloadTempPath(tmp_buf); const int value_len = numbytes - key_len - 4; // "\r\n"
R_TRY_RESULT(fsOpenSdCardFileSystem(&chunk.fs), false); if (key_len > 0 && value_len > 0) {
const std::string key(b, key_len);
fs::CreateDirectoryRecursivelyWithPath(&chunk.fs, tmp_buf); const std::string value(dilem + 2, value_len);
header->m_map.insert_or_assign(key, value);
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 (R_FAILED(fsFsOpenFile(&chunk.fs, tmp_buf, FsOpenMode_Write|FsOpenMode_Append, &chunk.f))) { return numbytes;
}
auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
fs::FsPath tmp_buf;
const bool has_file = !e.m_path.empty() && e.m_path != "";
const bool has_post = !e.m_fields.m_str.empty() && e.m_fields.m_str != "";
DataStruct chunk;
Header header_in = e.m_header;
Header header_out;
fs::FsNativeSd fs;
if (has_file) {
GetDownloadTempPath(tmp_buf);
fs.CreateDirectoryRecursivelyWithPath(tmp_buf);
if (auto rc = fs.CreateFile(tmp_buf, 0, 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
log_write("failed to create file: %s\n", tmp_buf);
return {};
}
if (R_FAILED(fs.OpenFile(tmp_buf, FsOpenMode_Write|FsOpenMode_Append, &chunk.f))) {
log_write("failed to open file: %s\n", tmp_buf); log_write("failed to open file: %s\n", tmp_buf);
return false; return {};
}
if (e.m_flags.m_flags & Flag_Cache) {
g_cache.get(e.m_path, header_in);
} }
} }
// reserve the first chunk // reserve the first chunk
chunk.data.reserve(CHUNK_SIZE); chunk.data.reserve(CHUNK_SIZE);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, url.c_str()); curl_easy_reset(curl);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, e.m_url.m_str.c_str());
CURL_EASY_SETOPT_LOG(curl, CURLOPT_USERAGENT, "TotalJustice"); CURL_EASY_SETOPT_LOG(curl, CURLOPT_USERAGENT, "TotalJustice");
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FOLLOWLOCATION, 1L); CURL_EASY_SETOPT_LOG(curl, CURLOPT_FOLLOWLOCATION, 1L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYPEER, 0L); CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYPEER, 0L);
@@ -320,15 +483,42 @@ auto DownloadInternal(CURL* curl, DataStruct& chunk, ProgressCallback pcallback,
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FAILONERROR, 1L); CURL_EASY_SETOPT_LOG(curl, CURLOPT_FAILONERROR, 1L);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SHARE, g_curl_share); CURL_EASY_SETOPT_LOG(curl, CURLOPT_SHARE, g_curl_share);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_BUFFERSIZE, 1024*512); CURL_EASY_SETOPT_LOG(curl, CURLOPT_BUFFERSIZE, 1024*512);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERFUNCTION, header_callback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERDATA, &header_out);
if (has_post) { if (has_post) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_POSTFIELDS, post.c_str()); CURL_EASY_SETOPT_LOG(curl, CURLOPT_POSTFIELDS, e.m_fields.m_str.c_str());
log_write("setting post field: %s\n", post.c_str()); log_write("setting post field: %s\n", e.m_fields.m_str.c_str());
}
struct curl_slist* list = NULL;
ON_SCOPE_EXIT(if (list) { curl_slist_free_all(list); } );
for (const auto& [key, value] : header_in.m_map) {
if (value.empty()) {
continue;
}
// create header key value pair.
const auto header_str = key + ": " + value;
// try to append header chunk.
auto temp = curl_slist_append(list, header_str.c_str());
if (temp) {
log_write("adding header: %s\n", header_str.c_str());
list = temp;
} else {
log_write("failed to append header\n");
}
}
if (list) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HTTPHEADER, list);
} }
// progress calls. // progress calls.
if (pcallback) { if (e.m_on_progress) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &pcallback); CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &e.m_on_progress);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc2); CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc2);
} else { } else {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc1); CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc1);
@@ -343,21 +533,34 @@ auto DownloadInternal(CURL* curl, DataStruct& chunk, ProgressCallback pcallback,
const auto res = curl_easy_perform(curl); const auto res = curl_easy_perform(curl);
bool success = res == CURLE_OK; bool success = res == CURLE_OK;
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
if (has_file) { if (has_file) {
ON_SCOPE_EXIT( fs.DeleteFile(tmp_buf) );
if (res == CURLE_OK && chunk.offset) { if (res == CURLE_OK && chunk.offset) {
fsFileWrite(&chunk.f, chunk.file_offset, chunk.data.data(), chunk.offset, FsWriteOption_None); fsFileWrite(&chunk.f, chunk.file_offset, chunk.data.data(), chunk.offset, FsWriteOption_None);
} }
fsFileClose(&chunk.f); fsFileClose(&chunk.f);
if (res != CURLE_OK) {
fsFsDeleteFile(&chunk.fs, tmp_buf); if (res == CURLE_OK) {
if (http_code == 304) {
log_write("cached download: %s\n", e.m_url.m_str.c_str());
} else { } else {
fsFsDeleteFile(&chunk.fs, safe_buf); log_write("un-cached download: %s code: %u\n", e.m_url.m_str.c_str(), http_code);
fs::CreateDirectoryRecursivelyWithPath(&chunk.fs, safe_buf); if (e.m_flags.m_flags & Flag_Cache) {
if (R_FAILED(fsFsRenameFile(&chunk.fs, tmp_buf, safe_buf))) { g_cache.set(e.m_path, header_out);
fsFsDeleteFile(&chunk.fs, tmp_buf); }
fs.DeleteFile(e.m_path);
fs.CreateDirectoryRecursivelyWithPath(e.m_path);
if (R_FAILED(fs.RenameFile(tmp_buf, e.m_path))) {
success = false; success = false;
} }
} }
}
chunk.data.clear();
} else { } else {
// empty data if we failed // empty data if we failed
if (res != CURLE_OK) { if (res != CURLE_OK) {
@@ -365,21 +568,29 @@ auto DownloadInternal(CURL* curl, DataStruct& chunk, ProgressCallback pcallback,
} }
} }
log_write("Downloaded %s %s\n", url.c_str(), curl_easy_strerror(res)); log_write("Downloaded %s %s\n", e.m_url.m_str.c_str(), curl_easy_strerror(res));
return success; return {success, http_code, header_out, chunk.data, e.m_path};
} }
auto DownloadInternal(DataStruct& chunk, ProgressCallback pcallback, const std::string& url, const std::string& file, const std::string& post) -> bool { auto DownloadInternal(const Api& e) -> ApiResult {
auto curl = curl_easy_init(); auto curl = curl_easy_init();
if (!curl) { if (!curl) {
log_write("curl init failed\n"); log_write("curl init failed\n");
return false; return {};
} }
ON_SCOPE_EXIT(curl_easy_cleanup(curl)); ON_SCOPE_EXIT(curl_easy_cleanup(curl));
return DownloadInternal(curl, chunk, pcallback, url, file, post); return DownloadInternal(curl, e);
} }
void DownloadThread(void* p) { void my_lock(CURL *handle, curl_lock_data data, curl_lock_access laccess, void *useptr) {
mutexLock(&g_mutex_share[data]);
}
void my_unlock(CURL *handle, curl_lock_data data, void *useptr) {
mutexUnlock(&g_mutex_share[data]);
}
void ThreadEntry::ThreadFunc(void* p) {
auto data = static_cast<ThreadEntry*>(p); auto data = static_cast<ThreadEntry*>(p);
while (g_running) { while (g_running) {
auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX); auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX);
@@ -391,11 +602,10 @@ void DownloadThread(void* p) {
continue; continue;
} }
DataStruct chunk;
#if 1 #if 1
const auto result = DownloadInternal(data->m_curl, chunk, data->m_pcallback, data->m_url, data->m_file, data->m_post); const auto result = DownloadInternal(data->m_curl, data->m_api);
if (g_running) { if (g_running) {
DownloadEventData event_data{data->m_callback, std::move(chunk.data), result}; const DownloadEventData event_data{data->m_api.m_on_complete, result};
evman::push(std::move(event_data), false); evman::push(std::move(event_data), false);
} else { } else {
break; break;
@@ -411,7 +621,7 @@ void DownloadThread(void* p) {
log_write("exited download thread\n"); log_write("exited download thread\n");
} }
void DownloadThreadQueue(void* p) { void ThreadQueue::ThreadFunc(void* p) {
auto data = static_cast<ThreadQueue*>(p); auto data = static_cast<ThreadQueue*>(p);
while (g_running) { while (g_running) {
auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX); auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX);
@@ -444,7 +654,7 @@ void DownloadThreadQueue(void* p) {
} }
if (!thread.InProgress()) { if (!thread.InProgress()) {
thread.Setup(entry.callback, entry.pcallback, entry.url, entry.file, entry.post); thread.Setup(entry.api);
// log_write("[dl queue] starting download\n"); // log_write("[dl queue] starting download\n");
// mark entry for deletion // mark entry for deletion
entry.m_delete = true; entry.m_delete = true;
@@ -454,10 +664,6 @@ void DownloadThreadQueue(void* p) {
} }
} }
if (!g_running) {
return;
}
if (!keep_going) { if (!keep_going) {
break; break;
} }
@@ -467,29 +673,14 @@ void DownloadThreadQueue(void* p) {
for (u32 i = 0; i < pop_count; i++) { for (u32 i = 0; i < pop_count; i++) {
data->m_entries.pop_front(); data->m_entries.pop_front();
} }
// if (delete_any) {
// data->m_entries.clear();
// data->m_entries.
// data->m_entries.erase(std::remove_if(data->m_entries.begin(), data->m_entries.end(), [](auto& a) {
// return a.m_delete;
// }));
// }
} }
log_write("exited download thread queue\n"); log_write("exited download thread queue\n");
} }
void my_lock(CURL *handle, curl_lock_data data, curl_lock_access laccess, void *useptr) {
mutexLock(&g_mutex_share[data]);
}
void my_unlock(CURL *handle, curl_lock_data data, void *useptr) {
mutexUnlock(&g_mutex_share[data]);
}
} // namespace } // namespace
auto DownloadInit() -> bool { auto Init() -> bool {
if (CURLE_OK != curl_global_init(CURL_GLOBAL_DEFAULT)) { if (CURLE_OK != curl_global_init(CURL_GLOBAL_DEFAULT)) {
return false; return false;
} }
@@ -518,10 +709,15 @@ auto DownloadInit() -> bool {
} }
log_write("finished creating threads\n"); log_write("finished creating threads\n");
if (!g_cache.init()) {
log_write("failed to init json cache\n");
}
return true; return true;
} }
void DownloadExit() { void Exit() {
g_running = false; g_running = false;
g_thread_queue.Close(); g_thread_queue.Close();
@@ -536,35 +732,26 @@ void DownloadExit() {
} }
curl_global_cleanup(); curl_global_cleanup();
g_cache.exit();
} }
auto DownloadMemory(const std::string& url, const std::string& post, ProgressCallback pcallback) -> std::vector<u8> { auto ToMemory(const Api& e) -> ApiResult {
if (g_url_cache.AddToCache(url)) { if (!e.m_path.empty()) {
DataStruct chunk{};
if (DownloadInternal(chunk, pcallback, url, "", post)) {
return chunk.data;
}
}
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;
}
}
return false;
} }
auto DownloadMemoryAsync(const std::string& url, const std::string& post, DownloadCallback callback, ProgressCallback pcallback, DownloadPriority prio) -> bool { auto ToFile(const Api& e) -> ApiResult {
if (e.m_path.empty()) {
return {};
}
return DownloadInternal(e);
}
auto ToMemoryAsync(const Api& api) -> bool {
#if USE_THREAD_QUEUE #if USE_THREAD_QUEUE
if (g_url_cache.AddToCache(url)) { return g_thread_queue.Add(api);
return g_thread_queue.Add(prio, callback, pcallback, url, "", post);
} else {
return false;
}
#else #else
// mutexLock(&g_thread_queue.m_mutex); // mutexLock(&g_thread_queue.m_mutex);
// ON_SCOPE_EXIT(mutexUnlock(&g_thread_queue.m_mutex)); // ON_SCOPE_EXIT(mutexUnlock(&g_thread_queue.m_mutex));
@@ -580,13 +767,9 @@ auto DownloadMemoryAsync(const std::string& url, const std::string& post, Downlo
#endif #endif
} }
auto DownloadFileAsync(const std::string& url, const std::string& out, const std::string& post, DownloadCallback callback, ProgressCallback pcallback, DownloadPriority prio) -> bool { auto ToFileAsync(const Api& e) -> bool {
#if USE_THREAD_QUEUE #if USE_THREAD_QUEUE
if (g_url_cache.AddToCache(url)) { return g_thread_queue.Add(e);
return g_thread_queue.Add(prio, callback, pcallback, url, out, post);
} else {
return false;
}
#else #else
// mutexLock(&g_thread_queue.m_mutex); // mutexLock(&g_thread_queue.m_mutex);
// ON_SCOPE_EXIT(mutexUnlock(&g_thread_queue.m_mutex)); // ON_SCOPE_EXIT(mutexUnlock(&g_thread_queue.m_mutex));
@@ -602,9 +785,4 @@ auto DownloadFileAsync(const std::string& url, const std::string& out, const std
#endif #endif
} }
void DownloadClearCache(const std::string& url) { } // namespace sphaira::curl
g_url_cache.AddToCache(url);
g_url_cache.RemoveFromCache(url);
}
} // namespace sphaira

View File

@@ -248,10 +248,10 @@ Result read_entire_file(FsFileSystem* _fs, const FsPath& path, std::vector<u8>&
Result write_entire_file(FsFileSystem* _fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only) { Result write_entire_file(FsFileSystem* _fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly); R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
FsNative fs{_fs, false}; FsNative fs{_fs, false, ignore_read_only};
R_TRY(fs.GetFsOpenResult()); R_TRY(fs.GetFsOpenResult());
if (auto rc = fs.CreateFile(path, in.size(), 0, ignore_read_only); R_FAILED(rc) && rc != FsError_PathAlreadyExists) { if (auto rc = fs.CreateFile(path, in.size(), 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
return rc; return rc;
} }

View File

@@ -1,24 +1,25 @@
#include "ftpsrv_helper.hpp" #include "ftpsrv_helper.hpp"
#include <ftpsrv.h>
#include <ftpsrv_vfs.h>
#include "app.hpp" #include "app.hpp"
#include "fs.hpp" #include "fs.hpp"
#include "log.hpp" #include "log.hpp"
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <mutex> #include <mutex>
#include <algorithm> #include <algorithm>
#include <minIni.h>
#include <ftpsrv.h>
#include <ftpsrv_vfs.h>
#include <nx/vfs_nx.h>
#include <nx/utils.h>
namespace { namespace {
const char* INI_PATH = "/config/ftpsrv/config.ini";
FtpSrvConfig g_ftpsrv_config = {0}; FtpSrvConfig g_ftpsrv_config = {0};
volatile bool g_should_exit = false; volatile bool g_should_exit = false;
bool g_is_running{false}; bool g_is_running{false};
Thread g_thread; Thread g_thread;
std::mutex g_mutex{}; std::mutex g_mutex{};
FsFileSystem* g_fs;
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) { void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
sphaira::App::NotifyFlashLed(); sphaira::App::NotifyFlashLed();
@@ -28,32 +29,6 @@ void ftp_progress_callback(void) {
sphaira::App::NotifyFlashLed(); sphaira::App::NotifyFlashLed();
} }
int vfs_fs_set_errno(Result rc) {
switch (rc) {
case FsError_TargetLocked: errno = EBUSY; break;
case FsError_PathNotFound: errno = ENOENT; break;
case FsError_PathAlreadyExists: errno = EEXIST; break;
case FsError_UsableSpaceNotEnoughMmcCalibration: errno = ENOSPC; break;
case FsError_UsableSpaceNotEnoughMmcSafe: errno = ENOSPC; break;
case FsError_UsableSpaceNotEnoughMmcUser: errno = ENOSPC; break;
case FsError_UsableSpaceNotEnoughMmcSystem: errno = ENOSPC; break;
case FsError_UsableSpaceNotEnoughSdCard: errno = ENOSPC; break;
case FsError_OutOfRange: errno = ESPIPE; break;
case FsError_TooLongPath: errno = ENAMETOOLONG; break;
case FsError_UnsupportedWriteForReadOnlyFileSystem: errno = EROFS; break;
default: errno = EIO; break;
}
return -1;
}
Result flush_buffered_write(struct FtpVfsFile* f) {
Result rc;
if (R_SUCCEEDED(rc = fsFileSetSize(&f->fd, f->off + f->buf_off))) {
rc = fsFileWrite(&f->fd, f->off, f->buf, f->buf_off, FsWriteOption_None);
}
return rc;
}
void loop(void* arg) { void loop(void* arg) {
while (!g_should_exit) { while (!g_should_exit) {
ftpsrv_init(&g_ftpsrv_config); ftpsrv_init(&g_ftpsrv_config);
@@ -77,16 +52,51 @@ bool Init() {
return false; return false;
} }
g_fs = fsdevGetDeviceFileSystem("sdmc"); if (R_FAILED(fsdev_wrapMountSdmc())) {
return false;
}
g_ftpsrv_config.log_callback = ftp_log_callback; g_ftpsrv_config.log_callback = ftp_log_callback;
g_ftpsrv_config.progress_callback = ftp_progress_callback; g_ftpsrv_config.progress_callback = ftp_progress_callback;
g_ftpsrv_config.anon = 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; g_ftpsrv_config.anon = true;
g_ftpsrv_config.timeout = 15; }
g_ftpsrv_config.port = 5000;
vfs_nx_init(mount_devices, save_writable, mount_bis);
Result rc; Result rc;
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*64, 0x2C, 2))) { if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*16, 0x2C, 2))) {
log_write("failed to create nxlink thread: 0x%X\n", rc); log_write("failed to create nxlink thread: 0x%X\n", rc);
return false; return false;
} }
@@ -108,232 +118,24 @@ void Exit() {
g_should_exit = true; g_should_exit = true;
threadWaitForExit(&g_thread); threadWaitForExit(&g_thread);
threadClose(&g_thread); threadClose(&g_thread);
vfs_nx_exit();
fsdev_wrapUnmountAll();
} }
} // namespace sphaira::ftpsrv } // namespace sphaira::ftpsrv
extern "C" { extern "C" {
#define VFS_NX_BUFFER_IO 1 void log_file_write(const char* msg) {
log_write("%s", msg);
int ftp_vfs_open(struct FtpVfsFile* f, const char* path, enum FtpVfsOpenMode mode) {
u32 open_mode;
if (mode == FtpVfsOpenMode_READ) {
open_mode = FsOpenMode_Read;
f->is_write = false;
} else {
fsFsCreateFile(g_fs, path, 0, 0);
open_mode = FsOpenMode_Write | FsOpenMode_Append;
#if !VFS_NX_BUFFER_IO
open_mode |= FsOpenMode_Append;
#endif
f->is_write = true;
} }
Result rc; void log_file_fwrite(const char* fmt, ...) {
if (R_FAILED(rc = fsFsOpenFile(g_fs, path, open_mode, &f->fd))) { std::va_list v{};
return vfs_fs_set_errno(rc); va_start(v, fmt);
} log_write_arg(fmt, v);
va_end(v);
f->off = f->buf_off = f->buf_size = 0;
if (mode == FtpVfsOpenMode_WRITE) {
if (R_FAILED(rc = fsFileSetSize(&f->fd, 0))) {
goto fail_close;
}
} else if (mode == FtpVfsOpenMode_APPEND) {
if (R_FAILED(rc = fsFileGetSize(&f->fd, &f->off))) {
goto fail_close;
}
}
f->is_valid = true;
return 0;
fail_close:
fsFileClose(&f->fd);
return vfs_fs_set_errno(rc);
}
int ftp_vfs_read(struct FtpVfsFile* f, void* buf, size_t size) {
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";
} }
} // extern "C" } // extern "C"

View File

@@ -21,7 +21,7 @@ std::string get_internal(const char* str, size_t len) {
} }
// add default entry // add default entry
g_tr_cache.emplace(kkey, kkey); const auto it = g_tr_cache.emplace(kkey, kkey).first;
if (!json || !root) { if (!json || !root) {
log_write("no json or root\n"); log_write("no json or root\n");
@@ -43,7 +43,7 @@ std::string get_internal(const char* str, size_t len) {
// update entry in cache // update entry in cache
const std::string ret = {val, val_len}; const std::string ret = {val, val_len};
g_tr_cache.insert_or_assign(kkey, ret); g_tr_cache.insert_or_assign(it, kkey, ret);
return ret; return ret;
} }

View File

@@ -14,6 +14,16 @@ std::FILE* file{};
int nxlink_socket{}; int nxlink_socket{};
std::mutex mutex{}; std::mutex mutex{};
void log_write_arg_internal(const char* s, std::va_list& v) {
if (file) {
std::vfprintf(file, s, v);
std::fflush(file);
}
if (nxlink_socket) {
std::vprintf(s, v);
}
}
} // namespace } // namespace
auto log_file_init() -> bool { auto log_file_init() -> bool {
@@ -60,13 +70,17 @@ void log_write(const char* s, ...) {
std::va_list v{}; std::va_list v{};
va_start(v, s); va_start(v, s);
if (file) { log_write_arg_internal(s, v);
std::vfprintf(file, s, v);
std::fflush(file);
}
if (nxlink_socket) {
std::vprintf(s, v);
}
va_end(v); va_end(v);
} }
void log_write_arg(const char* s, std::va_list& v) {
std::scoped_lock lock{mutex};
if (!file && !nxlink_socket) {
return;
}
log_write_arg_internal(s, v);
}
#endif #endif

View File

@@ -63,6 +63,8 @@ void userAppInit(void) {
diagAbortWithResult(rc); diagAbortWithResult(rc);
if (R_FAILED(rc = hidsysInitialize())) if (R_FAILED(rc = hidsysInitialize()))
diagAbortWithResult(rc); diagAbortWithResult(rc);
if (R_FAILED(rc = ncmInitialize()))
diagAbortWithResult(rc);
log_nxlink_init(); log_nxlink_init();
} }
@@ -70,6 +72,7 @@ void userAppInit(void) {
void userAppExit(void) { void userAppExit(void) {
log_nxlink_exit(); log_nxlink_exit();
ncmExit();
hidsysExit(); hidsysExit();
setExit(); setExit();
accountExit(); accountExit();

View File

@@ -309,4 +309,37 @@ auto nro_normalise_path(const std::string& p) -> std::string {
return p; return p;
} }
auto nro_find(std::span<const NroEntry> array, std::string_view name, std::string_view author, const fs::FsPath& path) -> std::optional<NroEntry> {
const auto it = std::find_if(array.cbegin(), array.cend(), [name, author, path](auto& e){
if (!name.empty() && !author.empty() && !path.empty()) {
return e.GetName() == name && e.GetAuthor() == author && e.path == path;
} else if (!name.empty()) {
return e.GetName() == name;
} else if (!author.empty()) {
return e.GetAuthor() == author;
} else if (!path.empty()) {
return e.path == path;
}
return false;
});
if (it == array.cend()) {
return std::nullopt;
}
return *it;
}
auto nro_find_name(std::span<const NroEntry> array, std::string_view name) -> std::optional<NroEntry> {
return nro_find(array, name, {}, {});
}
auto nro_find_author(std::span<const NroEntry> array, std::string_view author) -> std::optional<NroEntry> {
return nro_find(array, {}, author, {});
}
auto nro_find_path(std::span<const NroEntry> array, const fs::FsPath& path) -> std::optional<NroEntry> {
return nro_find(array, {}, {}, path);
}
} // namespace sphaira } // namespace sphaira

View File

@@ -12,6 +12,7 @@
#include "app.hpp" #include "app.hpp"
#include "ui/progress_box.hpp" #include "ui/progress_box.hpp"
#include "i18n.hpp" #include "i18n.hpp"
#include "log.hpp"
namespace sphaira { namespace sphaira {
namespace { namespace {
@@ -191,6 +192,40 @@ struct NpdmMeta {
u32 acid_size; u32 acid_size;
}; };
struct NpdmAcid {
u8 rsa_sig[0x100];
u8 rsa_pub[0x100];
u32 magic; // "ACID"
u32 size;
u8 version;
u8 _0x209[0x1];
u8 _0x20A[0x2];
u32 flags;
u64 program_id_min;
u64 program_id_max;
u32 fac_offset;
u32 fac_size;
u32 sac_offset;
u32 sac_size;
u32 kac_offset;
u32 kac_size;
u8 _0x238[0x8];
};
struct NpdmAci0 {
u32 magic; // "ACI0"
u8 _0x4[0xC];
u64 program_id;
u8 _0x18[0x8];
u32 fac_offset;
u32 fac_size;
u32 sac_offset;
u32 sac_size;
u32 kac_offset;
u32 kac_size;
u8 _0x38[0x8];
};
struct NpdmPatch { struct NpdmPatch {
char title_name[0x10]{"Application"}; char title_name[0x10]{"Application"};
char product_code[0x10]{}; char product_code[0x10]{};
@@ -578,17 +613,56 @@ auto romfs_build(const FileEntries& entries, u64 *out_size) -> std::vector<u8> {
return buf.buf; return buf.buf;
} }
auto npdm_patch_kc(std::vector<u8>& npdm, u32 off, u32 size, u32 bitmask, u32 value) -> bool {
const u32 pattern = BIT(bitmask) - 1;
const u32 mask = BIT(bitmask) | pattern;
for (u32 i = 0; i < size; i += 4) {
u32 cup;
std::memcpy(&cup, npdm.data() + off + i, sizeof(cup));
if ((cup & mask) == pattern) {
cup = value | pattern;
std::memcpy(npdm.data() + off + i, &cup, sizeof(cup));
return true;
}
}
return false;
}
// todo: manually build npdm // todo: manually build npdm
void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) { void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
NpdmMeta meta{}; NpdmMeta meta{};
NpdmAci0 aci0{};
NpdmAcid acid{};
std::memcpy(&meta, npdm.data(), sizeof(meta)); std::memcpy(&meta, npdm.data(), sizeof(meta));
std::memcpy(&aci0, npdm.data() + meta.aci0_offset, sizeof(aci0));
std::memcpy(&acid, npdm.data() + meta.acid_offset, sizeof(acid));
// apply patch // apply patch
std::memcpy(npdm.data() + 0x20, &patch.title_name, sizeof(patch.title_name)); std::memcpy(meta.title_name, &patch.title_name, sizeof(meta.title_name));
std::memcpy(npdm.data() + 0x30, &patch.product_code, sizeof(patch.product_code)); std::memcpy(meta.product_code, &patch.product_code, sizeof(patch.product_code));
std::memcpy(npdm.data() + meta.aci0_offset + 0x10, &patch.tid, sizeof(patch.tid)); aci0.program_id = patch.tid;
std::memcpy(npdm.data() + meta.acid_offset + 0x210, &patch.tid, sizeof(patch.tid)); acid.program_id_min = patch.tid;
std::memcpy(npdm.data() + meta.acid_offset + 0x218, &patch.tid, sizeof(patch.tid)); acid.program_id_max = patch.tid;
// patch debug flags based on ams version
// SEE: https://github.com/ITotalJustice/sphaira/issues/67
u64 ver{};
splInitialize();
ON_SCOPE_EXIT(splExit());
const auto SplConfigItem_ExosphereVersion = (SplConfigItem)65000;
splGetConfig(SplConfigItem_ExosphereVersion, &ver);
ver >>= 40;
if (ver >= MAKEHOSVERSION(1,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) { void patch_nacp(NacpStruct& nacp, const NcapPatch& patch) {

View File

@@ -1134,10 +1134,6 @@ auto ErrorBox::Update(Controller* controller, TouchInfo* touch) -> void {
Widget::Update(controller, touch); Widget::Update(controller, touch);
} }
auto ErrorBox::OnLayoutChange() -> void {
}
auto ErrorBox::Draw(NVGcontext* vg, Theme* theme) -> void { auto ErrorBox::Draw(NVGcontext* vg, Theme* theme) -> void {
gfx::dimBackground(vg); gfx::dimBackground(vg);
gfx::drawRect(vg, m_pos, theme->elements[ThemeEntryID_SELECTED].colour); gfx::drawRect(vg, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);

189
sphaira/source/ui/list.cpp Normal file
View 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

View File

@@ -206,17 +206,17 @@ auto LoadAndParseManifest(const Entry& e) -> ManifestEntries {
return ParseManifest(std::span{(const char*)data.data(), data.size()}); return ParseManifest(std::span{(const char*)data.data(), data.size()});
} }
void EntryLoadImageFile(fs::Fs& fs, const fs::FsPath& path, LazyImage& image) { auto EntryLoadImageFile(fs::Fs& fs, const fs::FsPath& path, LazyImage& image) -> bool {
// already have the image // already have the image
if (image.image) { if (image.image) {
log_write("warning, tried to load image: %s when already loaded\n", path); // log_write("warning, tried to load image: %s when already loaded\n", path);
return; return true;
} }
auto vg = App::GetVg(); auto vg = App::GetVg();
std::vector<u8> image_buf; std::vector<u8> image_buf;
if (R_FAILED(fs.read_entire_file(path, image_buf))) { if (R_FAILED(fs.read_entire_file(path, image_buf))) {
image.state = ImageDownloadState::Failed; log_write("failed to load image from file: %s\n", path.s);
} else { } else {
int channels_in_file; int channels_in_file;
auto buf = stbi_load_from_memory(image_buf.data(), image_buf.size(), &image.w, &image.h, &channels_in_file, 4); auto buf = stbi_load_from_memory(image_buf.data(), image_buf.size(), &image.w, &image.h, &channels_in_file, 4);
@@ -228,20 +228,21 @@ void EntryLoadImageFile(fs::Fs& fs, const fs::FsPath& path, LazyImage& image) {
} }
if (!image.image) { if (!image.image) {
image.state = ImageDownloadState::Failed; log_write("failed to load image from file: %s\n", path.s);
log_write("failed to load image from file: %s\n", path); return false;
} else { } else {
// log_write("loaded image from file: %s\n", path); // log_write("loaded image from file: %s\n", path);
return true;
} }
} }
void EntryLoadImageFile(const fs::FsPath& path, LazyImage& image) { auto EntryLoadImageFile(const fs::FsPath& path, LazyImage& image) -> bool {
if (!strncasecmp("romfs:/", path, 7)) { if (!strncasecmp("romfs:/", path, 7)) {
fs::FsStdio fs; fs::FsStdio fs;
EntryLoadImageFile(fs, path, image); return EntryLoadImageFile(fs, path, image);
} else { } else {
fs::FsNativeSd fs; fs::FsNativeSd fs;
EntryLoadImageFile(fs, path, image); return EntryLoadImageFile(fs, path, image);
} }
} }
@@ -270,45 +271,17 @@ void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, float x, f
rounded_image = false; rounded_image = false;
gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded); gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded);
} }
if (iw > w || ih > h) {
crop = true;
nvgSave(vg);
nvgScissor(vg, x, y, w, h);
}
if (rounded_image) { if (rounded_image) {
gfx::drawImageRounded(vg, ix, iy, iw, ih, i.image); gfx::drawImageRounded(vg, ix, iy, iw, ih, i.image);
} else { } else {
gfx::drawImage(vg, ix, iy, iw, ih, i.image); gfx::drawImage(vg, ix, iy, iw, ih, i.image);
} }
if (crop) {
nvgRestore(vg);
}
} }
void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, Vec4 vec, bool rounded = true, float scale = 1.0) { void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, Vec4 vec, bool rounded = true, float scale = 1.0) {
DrawIcon(vg, l, d, vec.x, vec.y, vec.w, vec.h, rounded, scale); DrawIcon(vg, l, d, vec.x, vec.y, vec.w, vec.h, rounded, scale);
} }
auto ScrollHelperDown(u64& index, u64& start, u64 step, u64 max, u64 size) -> bool {
if (size && index < (size - 1)) {
if (index < (size - step)) {
index = index + step;
App::PlaySoundEffect(SoundEffect_Scroll);
} else {
index = size - 1;
App::PlaySoundEffect(SoundEffect_Scroll);
}
if (index - start >= max) {
log_write("moved down\n");
start += step;
}
return true;
}
return false;
}
auto AppDlToStr(u32 value) -> std::string { auto AppDlToStr(u32 value) -> std::string {
auto str = std::to_string(value); auto str = std::to_string(value);
u32 inc = 3; u32 inc = 3;
@@ -398,17 +371,13 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
log_write("starting download\n"); log_write("starting download\n");
const auto url = BuildZipUrl(entry); const auto url = BuildZipUrl(entry);
if (!DownloadFile(url, zip_out, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){ if (!curl::Api().ToFile(
if (pbox->ShouldExit()) { curl::Url{url},
return false; curl::Path{zip_out},
} curl::OnProgress{pbox->OnDownloadProgressCallback()}
pbox->UpdateTransfer(dlnow, dltotal); ).success) {
return true;
})) {
log_write("error with download\n"); log_write("error with download\n");
// push popup error box
return false; return false;
// return appletEnterFatalSection();
} }
} }
@@ -545,10 +514,10 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
} }
// create directories // create directories
fs.CreateDirectoryRecursivelyWithPath(output, true); fs.CreateDirectoryRecursivelyWithPath(output);
Result rc; Result rc;
if (R_FAILED(rc = fs.CreateFile(output, info.uncompressed_size, 0, true)) && rc != FsError_PathAlreadyExists) { if (R_FAILED(rc = fs.CreateFile(output, info.uncompressed_size, 0)) && rc != FsError_PathAlreadyExists) {
log_write("failed to create file: %s 0x%04X\n", output, rc); log_write("failed to create file: %s 0x%04X\n", output, rc);
return false; return false;
} }
@@ -637,7 +606,7 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
if (!found) { if (!found) {
const auto safe_buf = fs::AppendPath("/", old_entry.path); const auto safe_buf = fs::AppendPath("/", old_entry.path);
// std::strcat(safe_buf, old_entry.path); // std::strcat(safe_buf, old_entry.path);
if (R_FAILED(fs.DeleteFile(safe_buf, true))) { if (R_FAILED(fs.DeleteFile(safe_buf))) {
log_write("failed to delete: %s\n", safe_buf); log_write("failed to delete: %s\n", safe_buf);
} else { } else {
log_write("deleted file: %s\n", safe_buf); log_write("deleted file: %s\n", safe_buf);
@@ -682,36 +651,29 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
const auto post = "name=" "switch_user" "&package=" + m_entry.name + "&message=" + out; const auto post = "name=" "switch_user" "&package=" + m_entry.name + "&message=" + out;
const auto file = BuildFeedbackCachePath(m_entry); const auto file = BuildFeedbackCachePath(m_entry);
DownloadFileAsync(URL_POST_FEEDBACK, file, post, [](std::vector<u8>& data, bool success){ curl::Api().ToAsync(
if (success) { curl::Url{URL_POST_FEEDBACK},
curl::Path{file},
curl::Fields{post},
curl::OnComplete{[](auto& result){
if (result.success) {
log_write("got feedback!\n"); log_write("got feedback!\n");
} else { } else {
log_write("failed to send feedback :("); log_write("failed to send feedback :(");
} }
}
}); });
} }
}, true)); }, true));
App::Push(sidebar); App::Push(sidebar);
}}), }}),
// std::make_pair(Button::A, Action{m_entry.status == EntryStatus::Update ? "Update" : "Install", [this](){
// App::Push(std::make_shared<ProgressBox>("App Install", [this](auto pbox){
// InstallApp(pbox, m_entry);
// }, 2));
// }}),
std::make_pair(Button::B, Action{"Back"_i18n, [this](){ std::make_pair(Button::B, Action{"Back"_i18n, [this](){
SetPop(); SetPop();
}}) }})
); );
// SidebarEntryCallback
// if (!m_entries_current.empty() && !GetEntry().url.empty()) {
// options->Add(std::make_shared<SidebarEntryCallback>("Show Release Page"))
// }
SetTitleSubHeading("by " + m_entry.author); SetTitleSubHeading("by " + m_entry.author);
// const char* very_long = "total@fedora:~/dev/switch/sphaira$ nxlink build/MinSizeRel/*.nro total@fedora:~/dev/switch/sphaira$ nxlink build/MinSizeRel/*.nro total@fedora:~/dev/switch/sphaira$ nxlink build/MinSizeRel/*.nro total@fedora:~/dev/switch/sphaira$ nxlink build/MinSizeRel/*.nro";
m_details = std::make_shared<ScrollableText>(m_entry.details, 0, 374, 250, 768, 18); m_details = std::make_shared<ScrollableText>(m_entry.details, 0, 374, 250, 768, 18);
m_changelog = std::make_shared<ScrollableText>(m_entry.changelog, 0, 374, 250, 768, 18); m_changelog = std::make_shared<ScrollableText>(m_entry.changelog, 0, 374, 250, 768, 18);
@@ -720,41 +682,32 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
const auto path = BuildBannerCachePath(m_entry); const auto path = BuildBannerCachePath(m_entry);
const auto url = BuildBannerUrl(m_entry); const auto url = BuildBannerUrl(m_entry);
m_banner.cached = EntryLoadImageFile(path, m_banner);
if (fs::FsNativeSd().FileExists(path)) {
EntryLoadImageFile(path, m_banner);
}
// race condition if we pop the widget before the download completes // race condition if we pop the widget before the download completes
if (!m_banner.image) { curl::Api().ToFileAsync(
DownloadFileAsync(url, path, "", [this, path](std::vector<u8>& data, bool success){ curl::Url{url},
if (success) { curl::Path{path},
curl::Flags{curl::Flag_Cache},
curl::OnComplete{[this, path](auto& result){
if (result.success) {
if (result.code == 304) {
m_banner.cached = false;
} else {
EntryLoadImageFile(path, m_banner); EntryLoadImageFile(path, m_banner);
} }
}, nullptr, DownloadPriority::High);
}
// ignore screen shots, most apps don't have any sadly.
#if 0
m_screens.resize(m_entry.screens);
for (u32 i = 0; i < m_screens.size(); i++) {
path = BuildScreensCachePath(m_entry.name, i);
url = BuildScreensUrl(m_entry.name, i);
if (fs::file_exists(path.c_str())) {
EntryLoadImageFile(path, m_screens[i]);
} else {
DownloadFileAsync(url.c_str(), path.c_str(), [this, i, path](std::vector<u8>& data, bool success){
EntryLoadImageFile(path, m_screens[i]);
}, nullptr, DownloadPriority::High);
} }
} }
#endif });
SetSubHeading(m_entry.binary); SetSubHeading(m_entry.binary);
SetSubHeading(m_entry.description); SetSubHeading(m_entry.description);
UpdateOptions(); UpdateOptions();
// todo: see Draw()
// const Vec4 v{75, 110, 370, 155};
// const Vec2 pad{10, 10};
// m_list = std::make_unique<List>(3, 3, v, pad);
} }
EntryMenu::~EntryMenu() { EntryMenu::~EntryMenu() {
@@ -780,30 +733,17 @@ void EntryMenu::Draw(NVGcontext* vg, Theme* theme) {
DrawIcon(vg, m_banner, m_entry.image.image ? m_entry.image : m_default_icon, banner_vec, false); DrawIcon(vg, m_banner, m_entry.image.image ? m_entry.image : m_default_icon, banner_vec, false);
DrawIcon(vg, m_entry.image, m_default_icon, icon_vec); DrawIcon(vg, m_entry.image, m_default_icon, icon_vec);
// gfx::drawImage(vg, icon_vec, m_entry.image.image);
constexpr float text_start_x = icon_vec.x;// - 10; constexpr float text_start_x = icon_vec.x;// - 10;
float text_start_y = 218 + line_vec.y; float text_start_y = 218 + line_vec.y;
const float text_inc_y = 32; const float text_inc_y = 32;
const float font_size = 20; const float font_size = 20;
// gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "%s", m_entry.name.c_str());
// gfx::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->elements[ThemeEntryID_TEXT].colour, "version: %s"_i18n.c_str(), m_entry.version.c_str());
text_start_y += text_inc_y; text_start_y += text_inc_y;
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "updated: %s"_i18n.c_str(), m_entry.updated.c_str()); gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "updated: %s"_i18n.c_str(), m_entry.updated.c_str());
text_start_y += text_inc_y; text_start_y += text_inc_y;
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "category: %s"_i18n.c_str(), m_entry.category.c_str()); gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "category: %s"_i18n.c_str(), m_entry.category.c_str());
text_start_y += text_inc_y; text_start_y += text_inc_y;
// gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "license: %s", m_entry.license.c_str());
// 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->elements[ThemeEntryID_TEXT].colour, "extracted: %.2f MiB"_i18n.c_str(), (double)m_entry.extracted / 1024.0);
text_start_y += text_inc_y; text_start_y += text_inc_y;
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "app_dls: %s"_i18n.c_str(), AppDlToStr(m_entry.app_dls).c_str()); gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "app_dls: %s"_i18n.c_str(), AppDlToStr(m_entry.app_dls).c_str());
@@ -812,6 +752,7 @@ void EntryMenu::Draw(NVGcontext* vg, Theme* theme) {
// for (const auto& option : m_options) { // for (const auto& option : m_options) {
const auto& text_col = theme->elements[ThemeEntryID_TEXT].colour; const auto& text_col = theme->elements[ThemeEntryID_TEXT].colour;
// todo: rewrite this mess and use list
constexpr float mm = 0;//20; constexpr float mm = 0;//20;
constexpr Vec4 block{968.f + mm, 110.f, 256.f - mm*2, 60.f}; constexpr Vec4 block{968.f + mm, 110.f, 256.f - mm*2, 60.f};
constexpr float text_xoffset{15.f}; constexpr float text_xoffset{15.f};
@@ -863,6 +804,7 @@ void EntryMenu::UpdateOptions() {
return InstallApp(pbox, m_entry); return InstallApp(pbox, m_entry);
}, [this](bool success){ }, [this](bool success){
if (success) { if (success) {
App::Notify("Downloaded "_i18n + m_entry.title);
m_entry.status = EntryStatus::Installed; m_entry.status = EntryStatus::Installed;
m_menu.SetDirty(); m_menu.SetDirty();
UpdateOptions(); UpdateOptions();
@@ -875,6 +817,7 @@ void EntryMenu::UpdateOptions() {
return UninstallApp(pbox, m_entry); return UninstallApp(pbox, m_entry);
}, [this](bool success){ }, [this](bool success){
if (success) { if (success) {
App::Notify("Removed "_i18n + m_entry.title);
m_entry.status = EntryStatus::Get; m_entry.status = EntryStatus::Get;
m_menu.SetDirty(); m_menu.SetDirty();
UpdateOptions(); UpdateOptions();
@@ -913,7 +856,7 @@ void EntryMenu::UpdateOptions() {
SetIndex(0); SetIndex(0);
} }
void EntryMenu::SetIndex(std::size_t index) { void EntryMenu::SetIndex(s64 index) {
m_index = index; m_index = index;
const auto option = m_options[m_index]; const auto option = m_options[m_index];
if (option.confirm_text.empty()) { if (option.confirm_text.empty()) {
@@ -941,8 +884,6 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/banners"); fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/banners");
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/screens"); fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/screens");
// m_span = m_entries;
this->SetActions( this->SetActions(
std::make_pair(Button::RIGHT, Action{[this](){ std::make_pair(Button::RIGHT, Action{[this](){
if (m_entries_current.empty()) { if (m_entries_current.empty()) {
@@ -967,41 +908,23 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
} }
}}), }}),
std::make_pair(Button::DOWN, Action{[this](){ std::make_pair(Button::DOWN, Action{[this](){
if (ScrollHelperDown(m_index, m_start, 3, 9, m_entries_current.size())) { if (m_list->ScrollDown(m_index, 3, m_entries_current.size())) {
SetIndex(m_index); SetIndex(m_index);
} }
}}), }}),
std::make_pair(Button::UP, Action{[this](){ std::make_pair(Button::UP, Action{[this](){
if (m_entries_current.empty()) { if (m_list->ScrollUp(m_index, 3, m_entries_current.size())) {
return;
}
if (m_index >= 3) {
SetIndex(m_index - 3);
App::PlaySoundEffect(SoundEffect_Scroll);
if (m_index < m_start ) {
// log_write("moved up\n");
m_start -= 3;
}
}
}}),
std::make_pair(Button::R2, Action{(u8)ActionType::HELD, [this](){
if (ScrollHelperDown(m_index, m_start, 9, 9, m_entries_current.size())) {
SetIndex(m_index); SetIndex(m_index);
} }
}}), }}),
std::make_pair(Button::L2, Action{(u8)ActionType::HELD, [this](){ std::make_pair(Button::R2, Action{[this](){
if (m_entries.empty()) { if (m_list->ScrollDown(m_index, 9, m_entries_current.size())) {
return; SetIndex(m_index);
}
if (m_index >= 9) {
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](){ std::make_pair(Button::A, Action{"Info"_i18n, [this](){
@@ -1035,17 +958,17 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
order_items.push_back("Decending"_i18n); order_items.push_back("Decending"_i18n);
order_items.push_back("Ascending"_i18n); order_items.push_back("Ascending"_i18n);
options->Add(std::make_shared<SidebarEntryArray>("Filter"_i18n, filter_items, [this, filter_items](std::size_t& index_out){ options->Add(std::make_shared<SidebarEntryArray>("Filter"_i18n, filter_items, [this, filter_items](s64& index_out){
SetFilter((Filter)index_out); SetFilter((Filter)index_out);
}, (std::size_t)m_filter)); }, (s64)m_filter));
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](std::size_t& index_out){ options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
SetSort((SortType)index_out); SetSort((SortType)index_out);
}, (std::size_t)m_sort)); }, (s64)m_sort));
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](std::size_t& index_out){ options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](s64& index_out){
SetOrder((OrderType)index_out); SetOrder((OrderType)index_out);
}, (std::size_t)m_order)); }, (s64)m_order));
options->Add(std::make_shared<SidebarEntryCallback>("Search"_i18n, [this](){ options->Add(std::make_shared<SidebarEntryCallback>("Search"_i18n, [this](){
std::string out; std::string out;
@@ -1058,11 +981,12 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
); );
m_repo_download_state = ImageDownloadState::Progress; m_repo_download_state = ImageDownloadState::Progress;
#if 0 curl::Api().ToFileAsync(
DownloadMemoryAsync(URL_JSON, [this](std::vector<u8>& data, bool success){ curl::Url{URL_JSON},
if (success) { curl::Path{REPO_PATH},
repo_json = data; curl::Flags{curl::Flag_Cache},
repo_json.push_back('\0'); curl::OnComplete{[this](auto& result){
if (result.success) {
m_repo_download_state = ImageDownloadState::Done; m_repo_download_state = ImageDownloadState::Done;
if (HasFocus()) { if (HasFocus()) {
ScanHomebrew(); ScanHomebrew();
@@ -1070,57 +994,16 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
} else { } else {
m_repo_download_state = ImageDownloadState::Failed; m_repo_download_state = ImageDownloadState::Failed;
} }
});
#else
FsTimeStampRaw time_stamp{};
u64 current_time{};
bool download_file = false;
if (R_SUCCEEDED(fs.GetFsOpenResult())) {
fs.GetFileTimeStampRaw(REPO_PATH, &time_stamp);
timeGetCurrentTime(TimeType_Default, &current_time);
}
// this fails if we don't have the file or on fw < 3.0.0
if (!time_stamp.is_valid) {
download_file = true;
} else {
// check the date, if older than 1hour, then fetch new file
// this relaxes the spam to their server, don't want to fetch repo
// every time the user opens the app!
const auto time_file = time_stamp.created;
const auto time_cur = current_time;
const auto day = 60 * 60;
if (time_file > time_cur || time_cur - time_file >= day) {
log_write("repo.json expired, downloading new! time_file: %zu time_cur: %zu\n", time_file, time_cur);
download_file = true;
} else {
log_write("repo.json not expired! time_file: %zu time_cur: %zu\n", time_file, time_cur);
}
}
// todo: remove me soon
// download_file = true;
if (download_file) {
DownloadFileAsync(URL_JSON, REPO_PATH, "", [this](std::vector<u8>& data, bool success){
if (success) {
m_repo_download_state = ImageDownloadState::Done;
if (HasFocus()) {
ScanHomebrew();
}
} 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_filter = (Filter)ini_getl(INI_SECTION, "filter", m_filter, App::CONFIG_PATH);
m_sort = (SortType)ini_getl(INI_SECTION, "sort", m_sort, App::CONFIG_PATH); m_sort = (SortType)ini_getl(INI_SECTION, "sort", m_sort, App::CONFIG_PATH);
m_order = (OrderType)ini_getl(INI_SECTION, "order", m_order, App::CONFIG_PATH); m_order = (OrderType)ini_getl(INI_SECTION, "order", m_order, App::CONFIG_PATH);
const Vec4 v{75, 110, 370, 155};
const Vec2 pad{10, 10};
m_list = std::make_unique<List>(3, 9, m_pos, v, pad);
Sort(); Sort();
} }
@@ -1130,6 +1013,14 @@ Menu::~Menu() {
void Menu::Update(Controller* controller, TouchInfo* touch) { void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch); MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_entries_current.size(), [this](auto i) {
if (m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
SetIndex(i);
}
});
} }
void Menu::Draw(NVGcontext* vg, Theme* theme) { void Menu::Draw(NVGcontext* vg, Theme* theme) {
@@ -1145,50 +1036,62 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
return; return;
} }
const u64 SCROLL = m_start; // max images per frame, in order to not hit io / gpu too hard.
const u64 max_entry_display = 9; const int image_load_max = 2;
const u64 nro_total = m_entries_current.size(); int image_load_count = 0;
const u64 cursor_pos = m_index;
// only draw scrollbar if needed m_list->Draw(vg, theme, m_entries_current.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
if (nro_total > max_entry_display) { const auto& [x, y, w, h] = v;
const auto scrollbar_size = 500.f;
const auto sb_h = 3.f / (float)nro_total * scrollbar_size;
const auto sb_y = SCROLL / 3.f;
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * 2) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
}
for (u64 i = 0, pos = SCROLL, y = 110, w = 370, h = 155; pos < nro_total && i < max_entry_display; y += h + 10) {
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]; const auto index = m_entries_current[pos];
auto& e = m_entries[index]; auto& e = m_entries[index];
auto& image = e.image;
// 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 // lazy load image
if (!e.image.image) { if (!image.image || image.cached) {
switch (e.image.state) { switch (image.state) {
case ImageDownloadState::None: { case ImageDownloadState::None: {
const auto path = BuildIconCachePath(e); const auto path = BuildIconCachePath(e);
if (fs::FsNativeSd().FileExists(path)) {
EntryLoadImageFile(path, e.image);
} else {
const auto url = BuildIconUrl(e); const auto url = BuildIconUrl(e);
e.image.state = ImageDownloadState::Progress; image.state = ImageDownloadState::Progress;
DownloadFileAsync(url, path, "", [this, index](std::vector<u8>& data, bool success) { curl::Api().ToFileAsync(
if (success) { curl::Url{url},
m_entries[index].image.state = ImageDownloadState::Done; curl::Path{path},
curl::Flags{curl::Flag_Cache},
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 { } else {
m_entries[index].image.state = ImageDownloadState::Failed; image.state = ImageDownloadState::Failed;
log_write("failed to download image\n"); log_write("failed to download image\n");
} }
}, nullptr, DownloadPriority::High);
} }
});
} break; } break;
case ImageDownloadState::Progress: { case ImageDownloadState::Progress: {
} break; } break;
case ImageDownloadState::Done: { case ImageDownloadState::Done: {
EntryLoadImageFile(BuildIconCachePath(e), e.image); 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; } break;
case ImageDownloadState::Failed: { case ImageDownloadState::Failed: {
} break; } break;
@@ -1196,7 +1099,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
} }
auto text_id = ThemeEntryID_TEXT; auto text_id = ThemeEntryID_TEXT;
if (pos == cursor_pos) { if (pos == m_index) {
text_id = ThemeEntryID_TEXT_SELECTED; text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour); gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
} else { } else {
@@ -1207,10 +1110,10 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
// const float image_size = 256 / image_scale; // const float image_size = 256 / image_scale;
// const float image_size_h = 150 / 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); 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); // gfx::drawImage(vg, x + 20, y + 20, image_size, image_size_h, image.image ? image.image : m_default_image);
nvgSave(vg); nvgSave(vg);
nvgScissor(vg, x, y, w - 30.f, h); // clip nvgIntersectScissor(vg, v.x, v.y, w - 30.f, h); // clip
{ {
const float font_size = 18; 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 + 45, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.title.c_str());
@@ -1234,8 +1137,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image); gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image);
break; break;
} }
} });
}
} }
void Menu::OnFocusGained() { void Menu::OnFocusGained() {
@@ -1264,21 +1166,16 @@ void Menu::OnFocusGained() {
if (m_dirty) { if (m_dirty) {
m_dirty = false; m_dirty = false;
const auto& current_entry = m_entries[m_entries_current[m_index]]; const auto& current_entry = m_entries[m_entries_current[m_index]];
// m_start = 0;
// m_index = 0;
log_write("\nold index: %zu start: %zu\n", m_index, m_start);
// old index: 19 start: 12
Sort(); Sort();
for (u32 i = 0; i < m_entries_current.size(); i++) { for (u32 i = 0; i < m_entries_current.size(); i++) {
if (current_entry.name == m_entries[m_entries_current[i]].name) { if (current_entry.name == m_entries[m_entries_current[i]].name) {
SetIndex(i); SetIndex(i);
if (i >= 9) { if (i >= 9) {
m_start = (i - 9) / 3 * 3 + 3; m_list->SetYoff((((i - 9) + 3) / 3) * m_list->GetMaxY());
} else { } else {
m_start = 0; m_list->SetYoff(0);
} }
log_write("\nnew index: %zu start: %zu\n", m_index, m_start);
break; break;
} }
} }
@@ -1286,10 +1183,10 @@ void Menu::OnFocusGained() {
} }
} }
void Menu::SetIndex(std::size_t index) { void Menu::SetIndex(s64 index) {
m_index = index; m_index = index;
if (!m_index) { if (!m_index) {
m_start = 0; m_list->SetYoff(0);
} }
this->SetSubHeading(std::to_string(m_index + 1) + " / " + std::to_string(m_entries_current.size())); this->SetSubHeading(std::to_string(m_index + 1) + " / " + std::to_string(m_entries_current.size()));
@@ -1484,9 +1381,10 @@ void Menu::SetSearch(const std::string& term) {
SetFilter(m_filter); SetFilter(m_filter);
SetIndex(m_entry_search_jump_back); SetIndex(m_entry_search_jump_back);
if (m_entry_search_jump_back >= 9) { if (m_entry_search_jump_back >= 9) {
m_start = (m_entry_search_jump_back - 9) / 3 * 3 + 3; m_list->SetYoff(0);
m_list->SetYoff((((m_entry_search_jump_back - 9) + 3) / 3) * m_list->GetMaxY());
} else { } else {
m_start = 0; m_list->SetYoff(0);
} }
}}); }});
@@ -1517,11 +1415,12 @@ void Menu::SetAuthor() {
} else { } else {
SetFilter(m_filter); SetFilter(m_filter);
} }
SetIndex(m_entry_author_jump_back); SetIndex(m_entry_author_jump_back);
if (m_entry_author_jump_back >= 9) { if (m_entry_author_jump_back >= 9) {
m_start = (m_entry_author_jump_back - 9) / 3 * 3 + 3; m_list->SetYoff((((m_entry_author_jump_back - 9) + 3) / 3) * m_list->GetMaxY());
} else { } else {
m_start = 0; m_list->SetYoff(0);
} }
}}); }});

View File

@@ -36,20 +36,11 @@
namespace sphaira::ui::menu::filebrowser { namespace sphaira::ui::menu::filebrowser {
namespace { namespace {
struct FsDirCollection {
fs::FsPath path;
fs::FsPath parent_name;
std::vector<FsDirectoryEntry> files;
std::vector<FsDirectoryEntry> dirs;
};
struct ExtDbEntry { struct ExtDbEntry {
std::string_view db_name; std::string_view db_name;
std::span<const std::string_view> ext; std::span<const std::string_view> ext;
}; };
using FsDirCollections = std::vector<FsDirCollection>;
constexpr std::string_view AUDIO_EXTENSIONS[] = { constexpr std::string_view AUDIO_EXTENSIONS[] = {
"mp3", "ogg", "flac", "wav", "aac" "ac3", "aif", "asf", "bfwav", "mp3", "ogg", "flac", "wav", "aac" "ac3", "aif", "asf", "bfwav",
"bfsar", "bfstm", "bfsar", "bfstm",
@@ -107,16 +98,6 @@ constexpr RomDatabaseEntry PATHS[]{
constexpr fs::FsPath DAYBREAK_PATH{"/switch/daybreak.nro"}; constexpr fs::FsPath DAYBREAK_PATH{"/switch/daybreak.nro"};
constexpr const char* SORT_STR[] = {
"Size",
"Alphabetical",
};
constexpr const char* ORDER_STR[] = {
"Desc",
"Asc",
};
auto IsExtension(std::string_view ext, std::span<const std::string_view> list) -> bool { auto IsExtension(std::string_view ext, std::span<const std::string_view> list) -> bool {
for (auto e : list) { for (auto e : list) {
if (e.length() == ext.length() && !strncasecmp(ext.data(), e.data(), ext.length())) { if (e.length() == ext.length() && !strncasecmp(ext.data(), e.data(), ext.length())) {
@@ -173,7 +154,7 @@ auto GetRomDatabaseFromPath(std::string_view path) -> int {
} }
// //
auto GetRomIcon(ProgressBox* pbox, std::string filename, std::string extension, int db_idx, const NroEntry& nro) { auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, std::string extension, int db_idx, const NroEntry& nro) {
// if no db entries, use nro icon // if no db entries, use nro icon
if (db_idx < 0) { if (db_idx < 0) {
log_write("using nro image\n"); log_write("using nro image\n");
@@ -225,24 +206,21 @@ auto GetRomIcon(ProgressBox* pbox, std::string filename, std::string extension,
if (!pbox->ShouldExit()) { if (!pbox->ShouldExit()) {
pbox->NewTransfer("Trying to load "_i18n + ra_thumbnail_path); pbox->NewTransfer("Trying to load "_i18n + ra_thumbnail_path);
std::vector<u8> image_file; std::vector<u8> image_file;
if (R_SUCCEEDED(fs::FsNativeSd().read_entire_file(ra_thumbnail_path.c_str(), image_file))) { if (R_SUCCEEDED(fs->read_entire_file(ra_thumbnail_path.c_str(), image_file))) {
return image_file; return image_file;
} }
} }
// try and download icon // try and download icon
if (!pbox->ShouldExit()) { if (!pbox->ShouldExit()) {
// auto png_image = DownloadMemory(ra_thumbnail_url.c_str());
pbox->NewTransfer("Downloading "_i18n + gh_thumbnail_url); pbox->NewTransfer("Downloading "_i18n + gh_thumbnail_url);
auto png_image = DownloadMemory(gh_thumbnail_url, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){ const auto result = curl::Api().ToMemory(
if (pbox->ShouldExit()) { curl::Url{gh_thumbnail_url},
return false; curl::OnProgress{pbox->OnDownloadProgressCallback()}
} );
pbox->UpdateTransfer(dlnow, dltotal);
return true; if (result.success && !result.data.empty()) {
}); return result.data;
if (!png_image.empty()) {
return png_image;
} }
} }
@@ -251,93 +229,6 @@ auto GetRomIcon(ProgressBox* pbox, std::string filename, std::string extension,
return nro_get_icon(nro.path, nro.icon_size, nro.icon_offset); return nro_get_icon(nro.path, nro.icon_size, nro.icon_offset);
} }
// returns 0 if true
auto CheckIfUpdateFolder(const fs::FsPath& path, std::span<FileEntry> entries) -> Result {
fs::FsNativeSd fs;
R_TRY(fs.GetFsOpenResult());
// check that we have daybreak installed
R_UNLESS(fs.FileExists(DAYBREAK_PATH), FsError_FileNotFound);
FsDir d;
R_TRY(fs.OpenDirectory(path, FsDirOpenMode_ReadDirs, &d));
ON_SCOPE_EXIT(fs.DirClose(&d));
s64 count;
R_TRY(fs.DirGetEntryCount(&d, &count));
// check that we are at the bottom level
R_UNLESS(count == 0, 0x1);
// check that we have enough ncas and not too many
R_UNLESS(entries.size() > 150 && entries.size() < 300, 0x1);
// check that all entries end in .nca
const auto nca_ext = std::string_view{".nca"};
for (auto& e : entries) {
const auto ext = std::strrchr(e.name, '.');
R_UNLESS(ext && ext == nca_ext, 0x1);
}
R_SUCCEED();
}
auto get_collection(fs::FsNative& fs, const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollection& out, bool inc_file, bool inc_dir, bool inc_size) -> Result {
out.path = path;
out.parent_name = parent_name;
const auto fetch = [&path, &fs](std::vector<FsDirectoryEntry>& out, u32 flags) -> Result {
FsDir d;
R_TRY(fs.OpenDirectory(path, flags, &d));
ON_SCOPE_EXIT(fs.DirClose(&d));
s64 count;
R_TRY(fs.DirGetEntryCount(&d, &count));
out.resize(count);
return fs.DirRead(&d, &count, out.size(), out.data());
};
if (inc_file) {
u32 flags = FsDirOpenMode_ReadFiles;
if (!inc_size) {
flags |= FsDirOpenMode_NoFileSize;
}
R_TRY(fetch(out.files, flags));
}
if (inc_dir) {
R_TRY(fetch(out.dirs, FsDirOpenMode_ReadDirs));
}
R_SUCCEED();
}
auto get_collections(fs::FsNative& fs, const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out) -> Result {
// get a list of all the files / dirs
FsDirCollection collection;
R_TRY(get_collection(fs, path, parent_name, collection, true, true, false));
log_write("got collection: %s parent_name: %s files: %zu dirs: %zu\n", path, parent_name, collection.files.size(), collection.dirs.size());
out.emplace_back(collection);
// for (size_t i = 0; i < collection.dirs.size(); i++) {
for (const auto&p : collection.dirs) {
// use heap as to not explode the stack
const auto new_path = std::make_unique<fs::FsPath>(Menu::GetNewPath(path, p.name));
const auto new_parent_name = std::make_unique<fs::FsPath>(Menu::GetNewPath(parent_name, p.name));
log_write("trying to get nested collection: %s parent_name: %s\n", *new_path, *new_parent_name);
R_TRY(get_collections(fs, *new_path, *new_parent_name, out));
}
R_SUCCEED();
}
auto get_collections(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out) -> Result {
fs::FsNativeSd fs;
R_TRY(fs.GetFsOpenResult());
return get_collections(fs, path, parent_name, out);
}
} // namespace } // namespace
Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i18n}, m_nro_entries{nro_entries} { Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i18n}, m_nro_entries{nro_entries} {
@@ -355,23 +246,23 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
} }
}}), }}),
std::make_pair(Button::DOWN, Action{[this](){ std::make_pair(Button::DOWN, Action{[this](){
if (m_index < (m_entries_current.size() - 1)) { if (m_list->ScrollDown(m_index, 1, m_entries_current.size())) {
SetIndex(m_index + 1); SetIndex(m_index);
App::PlaySoundEffect(SoundEffect_Scroll);
if (m_index - m_index_offset >= 8) {
log_write("moved down\n");
m_index_offset++;
}
} }
}}), }}),
std::make_pair(Button::UP, Action{[this](){ std::make_pair(Button::UP, Action{[this](){
if (m_index != 0) { if (m_list->ScrollUp(m_index, 1, m_entries_current.size())) {
SetIndex(m_index - 1); SetIndex(m_index);
App::PlaySoundEffect(SoundEffect_Scroll);
if (m_index < m_index_offset ) {
log_write("moved up\n");
m_index_offset--;
} }
}}),
std::make_pair(Button::DPAD_RIGHT, Action{[this](){
if (m_list->ScrollDown(m_index, 8, m_entries_current.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::DPAD_LEFT, Action{[this](){
if (m_list->ScrollUp(m_index, 8, m_entries_current.size())) {
SetIndex(m_index);
} }
}}), }}),
std::make_pair(Button::A, Action{"Open"_i18n, [this](){ std::make_pair(Button::A, Action{"Open"_i18n, [this](){
@@ -379,12 +270,12 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
return; return;
} }
if (m_is_update_folder) { if (m_fs_type == FsType::Sd && m_is_update_folder && m_daybreak_path.has_value()) {
App::Push(std::make_shared<OptionBox>("Open with DayBreak?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){ App::Push(std::make_shared<OptionBox>("Open with DayBreak?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){
if (op_index && *op_index) { if (op_index && *op_index) {
// daybreak uses native fs so do not use nro_add_arg_file // daybreak uses native fs so do not use nro_add_arg_file
// otherwise it'll fail to open the folder... // otherwise it'll fail to open the folder...
nro_launch(DAYBREAK_PATH, nro_add_arg(m_path)); nro_launch(m_daybreak_path.value(), nro_add_arg(m_path));
} }
})); }));
return; return;
@@ -394,7 +285,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
if (entry.type == FsDirEntryType_Dir) { if (entry.type == FsDirEntryType_Dir) {
Scan(GetNewPathCurrent()); Scan(GetNewPathCurrent());
} else { } else if (m_fs_type == FsType::Sd) {
// special case for nro // special case for nro
if (entry.GetExtension() == "nro") { if (entry.GetExtension() == "nro") {
App::Push(std::make_shared<OptionBox>("Launch "_i18n + entry.GetName() + '?', App::Push(std::make_shared<OptionBox>("Launch "_i18n + entry.GetName() + '?',
@@ -464,12 +355,12 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
order_items.push_back("Decending"_i18n); order_items.push_back("Decending"_i18n);
order_items.push_back("Ascending"_i18n); order_items.push_back("Ascending"_i18n);
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](std::size_t& index_out){ options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this](s64& index_out){
m_sort.Set(index_out); m_sort.Set(index_out);
SortAndFindLastFile(); SortAndFindLastFile();
}, m_sort.Get())); }, m_sort.Get()));
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](std::size_t& index_out){ options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this](s64& index_out){
m_order.Set(index_out); m_order.Set(index_out);
SortAndFindLastFile(); SortAndFindLastFile();
}, m_order.Get())); }, m_order.Get()));
@@ -517,12 +408,13 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
App::Push(std::make_shared<OptionBox>( App::Push(std::make_shared<OptionBox>(
"Delete Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){ "Delete Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){
if (op_index && *op_index) { if (op_index && *op_index) {
App::PopToMenu();
OnDeleteCallback(); OnDeleteCallback();
} }
} }
)); ));
log_write("pushed delete\n"); log_write("pushed delete\n");
}, true)); }));
} }
if (!m_selected_files.empty() && (m_selected_type == SelectedType::Cut || m_selected_type == SelectedType::Copy)) { if (!m_selected_files.empty() && (m_selected_type == SelectedType::Cut || m_selected_type == SelectedType::Copy)) {
@@ -531,10 +423,11 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
App::Push(std::make_shared<OptionBox>( App::Push(std::make_shared<OptionBox>(
buf, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){ buf, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){
if (op_index && *op_index) { if (op_index && *op_index) {
App::PopToMenu();
OnPasteCallback(); OnPasteCallback();
} }
})); }));
}, true)); }));
} }
// can't rename more than 1 file // can't rename more than 1 file
@@ -544,14 +437,16 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
const auto& entry = GetEntry(); const auto& entry = GetEntry();
const auto name = entry.GetName(); const auto name = entry.GetName();
if (R_SUCCEEDED(swkbd::ShowText(out, "Set New File Name"_i18n.c_str(), name.c_str())) && !out.empty() && out != name) { if (R_SUCCEEDED(swkbd::ShowText(out, "Set New File Name"_i18n.c_str(), name.c_str())) && !out.empty() && out != name) {
App::PopToMenu();
const auto src_path = GetNewPath(entry); const auto src_path = GetNewPath(entry);
const auto dst_path = GetNewPath(m_path, out); const auto dst_path = GetNewPath(m_path, out);
Result rc; Result rc;
if (entry.IsFile()) { if (entry.IsFile()) {
rc = fs::FsNativeSd().RenameFile(src_path, dst_path); rc = m_fs->RenameFile(src_path, dst_path);
} else { } else {
rc = fs::FsNativeSd().RenameDirectory(src_path, dst_path); rc = m_fs->RenameDirectory(src_path, dst_path);
} }
if (R_SUCCEEDED(rc)) { if (R_SUCCEEDED(rc)) {
@@ -562,7 +457,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
App::Push(std::make_shared<ErrorBox>(rc, msg)); App::Push(std::make_shared<ErrorBox>(rc, msg));
} }
} }
}, true)); }));
} }
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [this](){ options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [this](){
@@ -572,6 +467,8 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
options->Add(std::make_shared<SidebarEntryCallback>("Create File"_i18n, [this](){ options->Add(std::make_shared<SidebarEntryCallback>("Create File"_i18n, [this](){
std::string out; std::string out;
if (R_SUCCEEDED(swkbd::ShowText(out, "Set File Name"_i18n.c_str())) && !out.empty()) { if (R_SUCCEEDED(swkbd::ShowText(out, "Set File Name"_i18n.c_str())) && !out.empty()) {
App::PopToMenu();
fs::FsPath full_path; fs::FsPath full_path;
if (out[0] == '/') { if (out[0] == '/') {
full_path = out; full_path = out;
@@ -579,20 +476,21 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
full_path = fs::AppendPath(m_path, out); full_path = fs::AppendPath(m_path, out);
} }
fs::FsNativeSd fs; m_fs->CreateDirectoryRecursivelyWithPath(full_path);
fs.CreateDirectoryRecursivelyWithPath(full_path); if (R_SUCCEEDED(m_fs->CreateFile(full_path, 0, 0))) {
if (R_SUCCEEDED(fs.CreateFile(full_path, 0, 0))) {
log_write("created file: %s\n", full_path); log_write("created file: %s\n", full_path);
Scan(m_path); Scan(m_path);
} else { } else {
log_write("failed to create file: %s\n", full_path); log_write("failed to create file: %s\n", full_path);
} }
} }
}, true)); }));
options->Add(std::make_shared<SidebarEntryCallback>("Create Folder"_i18n, [this](){ options->Add(std::make_shared<SidebarEntryCallback>("Create Folder"_i18n, [this](){
std::string out; std::string out;
if (R_SUCCEEDED(swkbd::ShowText(out, "Set Folder Name"_i18n.c_str())) && !out.empty()) { if (R_SUCCEEDED(swkbd::ShowText(out, "Set Folder Name"_i18n.c_str())) && !out.empty()) {
App::PopToMenu();
fs::FsPath full_path; fs::FsPath full_path;
if (out[0] == '/') { if (out[0] == '/') {
full_path = out; full_path = out;
@@ -600,22 +498,22 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
full_path = fs::AppendPath(m_path, out); full_path = fs::AppendPath(m_path, out);
} }
if (R_SUCCEEDED(fs::FsNativeSd().CreateDirectoryRecursively(full_path))) { if (R_SUCCEEDED(m_fs->CreateDirectoryRecursively(full_path))) {
log_write("created dir: %s\n", full_path); log_write("created dir: %s\n", full_path);
Scan(m_path); Scan(m_path);
} else { } else {
log_write("failed to create dir: %s\n", full_path); log_write("failed to create dir: %s\n", full_path);
} }
} }
}, true)); }));
if (m_entries_current.size() && !m_selected_count && GetEntry().IsFile() && GetEntry().file_size < 1024*64) { if (m_fs_type == FsType::Sd && m_entries_current.size() && !m_selected_count && GetEntry().IsFile() && GetEntry().file_size < 1024*64) {
options->Add(std::make_shared<SidebarEntryCallback>("View as text (unfinished)"_i18n, [this](){ options->Add(std::make_shared<SidebarEntryCallback>("View as text (unfinished)"_i18n, [this](){
App::Push(std::make_shared<fileview::Menu>(GetNewPathCurrent())); App::Push(std::make_shared<fileview::Menu>(GetNewPathCurrent()));
}, true)); }));
} }
if (m_entries_current.size()) { if (m_fs_type == FsType::Sd && m_entries_current.size()) {
if (App::GetInstallEnable() && HasTypeInSelectedEntries(FsDirEntryType_File) && !m_selected_count && (GetEntry().GetExtension() == "nro" || !FindFileAssocFor().empty())) { if (App::GetInstallEnable() && HasTypeInSelectedEntries(FsDirEntryType_File) && !m_selected_count && (GetEntry().GetExtension() == "nro" || !FindFileAssocFor().empty())) {
options->Add(std::make_shared<SidebarEntryCallback>("Install Forwarder"_i18n, [this](){; options->Add(std::make_shared<SidebarEntryCallback>("Install Forwarder"_i18n, [this](){;
if (App::GetInstallPrompt()) { if (App::GetInstallPrompt()) {
@@ -633,13 +531,32 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
})); }));
} }
} }
options->Add(std::make_shared<SidebarEntryBool>("Ignore read only"_i18n, m_ignore_read_only.Get(), [this](bool& v_out){
m_ignore_read_only.Set(v_out);
m_fs->SetIgnoreReadOnly(v_out);
}, "Yes"_i18n, "No"_i18n));
SidebarEntryArray::Items mount_items;
mount_items.push_back("Sd"_i18n);
mount_items.push_back("Image System memory"_i18n);
mount_items.push_back("Image microSD card"_i18n);
options->Add(std::make_shared<SidebarEntryArray>("Mount"_i18n, mount_items, [this](s64& index_out){
App::PopToMenu();
m_mount.Set(index_out);
SetFs("/", index_out);
}, m_mount.Get()));
})); }));
}}) }})
); );
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);
fs::FsPath buf; fs::FsPath buf;
ini_gets("paths", "last_path", "/", buf, sizeof(buf), App::CONFIG_PATH); ini_gets("paths", "last_path", "/", buf, sizeof(buf), App::CONFIG_PATH);
m_path = buf; SetFs(buf, m_mount.Get());
} }
Menu::~Menu() { Menu::~Menu() {
@@ -648,6 +565,14 @@ Menu::~Menu() {
void Menu::Update(Controller* controller, TouchInfo* touch) { void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch); MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_entries_current.size(), [this](auto i) {
if (m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
SetIndex(i);
}
});
} }
void Menu::Draw(NVGcontext* vg, Theme* theme) { void Menu::Draw(NVGcontext* vg, Theme* theme) {
@@ -660,43 +585,17 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
return; return;
} }
const u64 SCROLL = m_index_offset;
constexpr u64 max_entry_display = 8;
const u64 entry_total = m_entries_current.size();
// only draw scrollbar if needed
if (entry_total > max_entry_display) {
const auto scrollbar_size = 500.f;
const auto sb_h = 1.f / (float)entry_total * scrollbar_size;
const auto sb_y = SCROLL;
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, gfx::getColour(gfx::Colour::BLACK));
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * (max_entry_display - 1)) - 4, gfx::getColour(gfx::Colour::SILVER));
}
// constexpr Vec4 line_top{30.f, 86.f, 1220.f, 1.f};
// constexpr Vec4 line_bottom{30.f, 646.f, 1220.f, 1.f};
// constexpr Vec4 block{280.f, 110.f, 720.f, 60.f};
constexpr Vec4 block{75.f, 110.f, 1220.f-45.f*2, 60.f};
constexpr float text_xoffset{15.f}; constexpr float text_xoffset{15.f};
// todo: cleanup m_list->Draw(vg, theme, m_entries_current.size(), [this, text_col](auto* vg, auto* theme, auto v, auto i) {
const float x = block.x; const auto& [x, y, w, h] = v;
float y = GetY() + 1.f + 42.f;
const float h = block.h;
const float w = block.w;
nvgSave(vg);
nvgScissor(vg, GetX(), GetY(), GetW(), GetH());
for (std::size_t i = m_index_offset; i < m_entries_current.size(); i++) {
auto& e = GetEntry(i); auto& e = GetEntry(i);
if (e.IsDir()) { if (e.IsDir()) {
if (e.file_count == -1 && e.dir_count == -1) { if (e.file_count == -1 && e.dir_count == -1) {
const auto full_path = GetNewPath(e); const auto full_path = GetNewPath(e);
fs::FsNativeSd fs; m_fs->DirGetEntryCount(full_path, FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize, &e.file_count);
fs.DirGetEntryCount(full_path, FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize, &e.file_count); m_fs->DirGetEntryCount(full_path, FsDirOpenMode_ReadDirs | FsDirOpenMode_NoFileSize, &e.dir_count);
fs.DirGetEntryCount(full_path, FsDirOpenMode_ReadDirs | FsDirOpenMode_NoFileSize, &e.dir_count);
} }
} else if (!e.checked_extension) { } else if (!e.checked_extension) {
e.checked_extension = true; e.checked_extension = true;
@@ -715,7 +614,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
text_id = ThemeEntryID_TEXT_SELECTED; text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour); gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
} else { } else {
if (i == m_index_offset) { if (i == m_index) {
gfx::drawRect(vg, x, y, w, 1.f, text_col); gfx::drawRect(vg, x, y, w, 1.f, text_col);
} }
gfx::drawRect(vg, x, y + h, w, 1.f, text_col); gfx::drawRect(vg, x, y + h, w, 1.f, text_col);
@@ -742,8 +641,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
} }
nvgSave(vg); nvgSave(vg);
const auto txt_clip = std::min(GetY() + GetH(), y + h) - y; nvgIntersectScissor(vg, x + text_xoffset+65, y, w-(x+text_xoffset+65+50), h);
nvgScissor(vg, x + text_xoffset+65, y, w-(x+text_xoffset+65+50), txt_clip);
gfx::drawText(vg, x + text_xoffset+65, y + (h / 2.f), 20.f, e.name, NULL, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->elements[text_id].colour); gfx::drawText(vg, x + text_xoffset+65, y + (h / 2.f), 20.f, e.name, NULL, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->elements[text_id].colour);
nvgRestore(vg); nvgRestore(vg);
@@ -752,12 +650,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->elements[text_id].colour, "%zd dirs"_i18n.c_str(), e.dir_count); gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->elements[text_id].colour, "%zd dirs"_i18n.c_str(), e.dir_count);
} else { } else {
if (!e.time_stamp.is_valid) { if (!e.time_stamp.is_valid) {
fs::FsNativeSd fs;
const auto path = GetNewPath(e); const auto path = GetNewPath(e);
m_fs->GetFileTimeStampRaw(path, &e.time_stamp);
if (R_SUCCEEDED(fs.GetFsOpenResult())) {
fs.GetFileTimeStampRaw(path, &e.time_stamp);
}
} }
const auto t = (time_t)(e.time_stamp.modified); const auto t = (time_t)(e.time_stamp.modified);
struct tm tm{}; struct tm tm{};
@@ -769,14 +663,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->elements[text_id].colour, "%.2f MiB", (double)e.file_size / 1024.0 / 1024.0); gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->elements[text_id].colour, "%.2f MiB", (double)e.file_size / 1024.0 / 1024.0);
} }
} }
});
y += h;
if (!InYBounds(y)) {
break;
}
}
nvgRestore(vg);
} }
void Menu::OnFocusGained() { void Menu::OnFocusGained() {
@@ -796,10 +683,10 @@ void Menu::OnFocusGained() {
} }
} }
void Menu::SetIndex(std::size_t index) { void Menu::SetIndex(s64 index) {
m_index = index; m_index = index;
if (!m_index) { if (!m_index) {
m_index_offset = 0; m_list->SetYoff();
} }
if (!m_entries_current.empty() && !GetEntry().checked_internal_extension && GetEntry().extension == "zip") { if (!m_entries_current.empty() && !GetEntry().checked_internal_extension && GetEntry().extension == "zip") {
@@ -880,7 +767,7 @@ void Menu::InstallForwarder() {
config.name = nro.nacp.lang[0].name + std::string{" | "} + file_name; config.name = nro.nacp.lang[0].name + std::string{" | "} + file_name;
// config.name = file_name; // config.name = file_name;
config.nacp = nro.nacp; config.nacp = nro.nacp;
config.icon = GetRomIcon(pbox, file_name, extension, db_idx, nro); config.icon = GetRomIcon(m_fs.get(), pbox, file_name, extension, db_idx, nro);
return R_SUCCEEDED(App::Install(pbox, config)); return R_SUCCEEDED(App::Install(pbox, config));
})); }));
@@ -894,42 +781,31 @@ void Menu::InstallForwarder() {
auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result { auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
log_write("new scan path: %s\n", new_path); log_write("new scan path: %s\n", new_path);
if (!is_walk_up && !m_path.empty() && !m_entries_current.empty()) { if (!is_walk_up && !m_path.empty() && !m_entries_current.empty()) {
const LastFile f{GetEntry().name, m_index, m_index_offset, m_entries_current.size()}; const LastFile f(GetEntry().name, m_index, m_list->GetYoff(), m_entries_current.size());
m_previous_highlighted_file.emplace_back(f); m_previous_highlighted_file.emplace_back(f);
} }
log_write("\nold index: %zu start: %zu\n", m_index, m_index_offset);
m_path = new_path; m_path = new_path;
m_entries.clear(); m_entries.clear();
m_index = 0; m_index = 0;
m_index_offset = 0; m_list->SetYoff(0);
SetTitleSubHeading(m_path); SetTitleSubHeading(m_path);
if (m_selected_type == SelectedType::None) { if (m_selected_type == SelectedType::None) {
ResetSelection(); ResetSelection();
} }
// fs::FsNativeSd fs;
// R_TRY(fs.GetFsOpenResult());
// FsDirCollection collection;
// R_TRY(get_collection(fs, new_path, "", collection, true, true, false));
fs::FsNativeSd fs;
R_TRY(fs.GetFsOpenResult());
FsDir d; FsDir d;
R_TRY(fsFsOpenDirectory(&fs.m_fs, new_path, FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize, &d)); R_TRY(m_fs->OpenDirectory(new_path, FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize, &d));
ON_SCOPE_EXIT(fsDirClose(&d)); ON_SCOPE_EXIT(fsDirClose(&d));
s64 count; s64 count;
R_TRY(fs.DirGetEntryCount(&d, &count)); R_TRY(m_fs->DirGetEntryCount(&d, &count));
// we won't run out of memory here (tm) // we won't run out of memory here (tm)
std::vector<FsDirectoryEntry> dir_entries(count); std::vector<FsDirectoryEntry> dir_entries(count);
R_TRY(fs.DirRead(&d, &count, dir_entries.size(), dir_entries.data())); R_TRY(m_fs->DirRead(&d, &count, dir_entries.size(), dir_entries.data()));
// size may of changed // size may of changed
dir_entries.resize(count); dir_entries.resize(count);
@@ -959,7 +835,7 @@ auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
Sort(); Sort();
// quick check to see if this is an update folder // quick check to see if this is an update folder
m_is_update_folder = R_SUCCEEDED(CheckIfUpdateFolder(new_path, m_entries)); m_is_update_folder = R_SUCCEEDED(CheckIfUpdateFolder());
SetIndex(0); SetIndex(0);
@@ -1029,13 +905,16 @@ void Menu::LoadAssocEntriesPath(const fs::FsPath& path) {
return; return;
} }
ON_SCOPE_EXIT(closedir(dir)); ON_SCOPE_EXIT(closedir(dir));
fs::FsNativeSd fs;
while (auto d = readdir(dir)) { while (auto d = readdir(dir)) {
if (d->d_name[0] == '.') { if (d->d_name[0] == '.') {
continue; continue;
} }
if (d->d_type != DT_REG) {
continue;
}
const auto ext = std::strrchr(d->d_name, '.'); const auto ext = std::strrchr(d->d_name, '.');
if (!ext || strcasecmp(ext, ".ini")) { if (!ext || strcasecmp(ext, ".ini")) {
continue; continue;
@@ -1081,7 +960,7 @@ void Menu::LoadAssocEntriesPath(const fs::FsPath& path) {
// if path isn't empty, check if the file exists // if path isn't empty, check if the file exists
bool file_exists{}; bool file_exists{};
if (!assoc.path.empty()) { if (!assoc.path.empty()) {
file_exists = fs.FileExists(assoc.path); file_exists = m_fs->FileExists(assoc.path);
} else { } else {
const auto nro_name = assoc.name + ".nro"; const auto nro_name = assoc.name + ".nro";
for (const auto& nro : m_nro_entries) { for (const auto& nro : m_nro_entries) {
@@ -1187,7 +1066,7 @@ void Menu::Sort() {
void Menu::SortAndFindLastFile() { void Menu::SortAndFindLastFile() {
std::optional<LastFile> last_file; std::optional<LastFile> last_file;
if (!m_path.empty() && !m_entries_current.empty()) { if (!m_path.empty() && !m_entries_current.empty()) {
last_file = LastFile{GetEntry().name, m_index, m_index_offset, m_entries_current.size()}; last_file = LastFile(GetEntry().name, m_index, m_list->GetYoff(), m_entries_current.size());
} }
Sort(); Sort();
@@ -1208,21 +1087,21 @@ void Menu::SetIndexFromLastFile(const LastFile& last_file) {
} }
} }
if (index >= 0) { if (index >= 0) {
if ((u64)index == last_file.index && m_entries_current.size() == last_file.entries_count) { if (index == last_file.index && m_entries_current.size() == last_file.entries_count) {
m_index_offset = last_file.offset; m_list->SetYoff(last_file.offset);
log_write("index is the same as last time\n"); log_write("index is the same as last time\n");
} else { } else {
// file position changed! // file position changed!
log_write("file position changed\n"); log_write("file position changed\n");
// guesstimate where the position is // guesstimate where the position is
if (index >= 8) { if (index >= 8) {
m_index_offset = (index - 8) + 1; m_list->SetYoff(((index - 8) + 1) * m_list->GetMaxY());
} else { } else {
m_index_offset = 0; m_list->SetYoff(0);
} }
} }
SetIndex(index); SetIndex(index);
log_write("\nnew index: %zu start: %zu mod: %zu\n", m_index, m_index_offset, index % 8); log_write("\nnew index: %zu %zu mod: %zu\n", m_index, index % 8);
} }
} }
@@ -1236,19 +1115,18 @@ void Menu::OnDeleteCallback() {
// check if we only have 1 file / folder // check if we only have 1 file / folder
if (m_selected_files.size() == 1) { if (m_selected_files.size() == 1) {
fs::FsNativeSd fs;
const auto& entry = m_selected_files[0]; const auto& entry = m_selected_files[0];
const auto full_path = GetNewPath(m_selected_path, entry.name); const auto full_path = GetNewPath(m_selected_path, entry.name);
if (entry.IsDir()) { if (entry.IsDir()) {
s64 count{}; s64 count{};
fs.DirGetEntryCount(full_path, FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize, &count); m_fs->DirGetEntryCount(full_path, FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize, &count);
if (!count) { if (!count) {
fs.DeleteDirectory(full_path); m_fs->DeleteDirectory(full_path);
use_progress_box = false; use_progress_box = false;
} }
} else { } else {
fs.DeleteFile(full_path); m_fs->DeleteFile(full_path);
use_progress_box = false; use_progress_box = false;
} }
} }
@@ -1259,7 +1137,6 @@ void Menu::OnDeleteCallback() {
log_write("did delete\n"); log_write("did delete\n");
} else { } else {
App::Push(std::make_shared<ProgressBox>("Deleting"_i18n, [this](auto pbox){ App::Push(std::make_shared<ProgressBox>("Deleting"_i18n, [this](auto pbox){
fs::FsNativeSd fs;
FsDirCollections collections; FsDirCollections collections;
// build list of dirs / files // build list of dirs / files
@@ -1292,10 +1169,10 @@ void Menu::OnDeleteCallback() {
pbox->NewTransfer("Deleting "_i18n + full_path); pbox->NewTransfer("Deleting "_i18n + full_path);
if (p.type == FsDirEntryType_Dir) { if (p.type == FsDirEntryType_Dir) {
log_write("deleting dir: %s\n", full_path); log_write("deleting dir: %s\n", full_path);
fs.DeleteDirectory(full_path); m_fs->DeleteDirectory(full_path);
} else { } else {
log_write("deleting file: %s\n", full_path); log_write("deleting file: %s\n", full_path);
fs.DeleteFile(full_path); m_fs->DeleteFile(full_path);
} }
} }
return true; return true;
@@ -1320,10 +1197,10 @@ void Menu::OnDeleteCallback() {
if (p.IsDir()) { if (p.IsDir()) {
log_write("deleting dir: %s\n", full_path); log_write("deleting dir: %s\n", full_path);
fs.DeleteDirectory(full_path); m_fs->DeleteDirectory(full_path);
} else { } else {
log_write("deleting file: %s\n", full_path); log_write("deleting file: %s\n", full_path);
fs.DeleteFile(full_path); m_fs->DeleteFile(full_path);
} }
} }
@@ -1341,14 +1218,13 @@ void Menu::OnPasteCallback() {
// check if we only have 1 file / folder and is cut (rename) // check if we only have 1 file / folder and is cut (rename)
if (m_selected_files.size() == 1 && m_selected_type == SelectedType::Cut) { if (m_selected_files.size() == 1 && m_selected_type == SelectedType::Cut) {
fs::FsNativeSd fs;
const auto& entry = m_selected_files[0]; const auto& entry = m_selected_files[0];
const auto full_path = GetNewPath(m_selected_path, entry.name); const auto full_path = GetNewPath(m_selected_path, entry.name);
if (entry.IsDir()) { if (entry.IsDir()) {
fs.RenameDirectory(full_path, GetNewPath(entry)); m_fs->RenameDirectory(full_path, GetNewPath(entry));
} else { } else {
fs.RenameFile(full_path, GetNewPath(entry)); m_fs->RenameFile(full_path, GetNewPath(entry));
} }
ResetSelection(); ResetSelection();
@@ -1356,7 +1232,6 @@ void Menu::OnPasteCallback() {
log_write("did paste\n"); log_write("did paste\n");
} else { } else {
App::Push(std::make_shared<ProgressBox>("Pasting"_i18n, [this](auto pbox){ App::Push(std::make_shared<ProgressBox>("Pasting"_i18n, [this](auto pbox){
fs::FsNativeSd fs;
if (m_selected_type == SelectedType::Cut) { if (m_selected_type == SelectedType::Cut) {
for (const auto& p : m_selected_files) { for (const auto& p : m_selected_files) {
@@ -1371,9 +1246,9 @@ void Menu::OnPasteCallback() {
pbox->NewTransfer("Pasting "_i18n + src_path); pbox->NewTransfer("Pasting "_i18n + src_path);
if (p.IsDir()) { if (p.IsDir()) {
fs.RenameDirectory(src_path, dst_path); m_fs->RenameDirectory(src_path, dst_path);
} else { } else {
fs.RenameFile(src_path, dst_path); m_fs->RenameFile(src_path, dst_path);
} }
} }
} else { } else {
@@ -1407,7 +1282,7 @@ void Menu::OnPasteCallback() {
if (p.IsDir()) { if (p.IsDir()) {
pbox->NewTransfer("Creating "_i18n + dst_path); pbox->NewTransfer("Creating "_i18n + dst_path);
fs.CreateDirectory(dst_path); m_fs->CreateDirectory(dst_path);
} else { } else {
pbox->NewTransfer("Copying "_i18n + src_path); pbox->NewTransfer("Copying "_i18n + src_path);
R_TRY_RESULT(pbox->CopyFile(src_path, dst_path), false); R_TRY_RESULT(pbox->CopyFile(src_path, dst_path), false);
@@ -1429,7 +1304,7 @@ void Menu::OnPasteCallback() {
log_write("creating: %s to %s\n", src_path, dst_path); log_write("creating: %s to %s\n", src_path, dst_path);
pbox->NewTransfer("Creating "_i18n + dst_path); pbox->NewTransfer("Creating "_i18n + dst_path);
fs.CreateDirectory(dst_path); m_fs->CreateDirectory(dst_path);
} }
for (const auto& p : c.files) { for (const auto& p : c.files) {
@@ -1461,6 +1336,147 @@ void Menu::OnRenameCallback() {
} }
auto Menu::CheckIfUpdateFolder() -> Result {
R_UNLESS(m_fs_type == FsType::Sd, FsError_InvalidMountName);
// check if we have already tried to find daybreak
if (m_daybreak_path.has_value() && m_daybreak_path.value().empty()) {
return FsError_FileNotFound;
}
// check that we have daybreak installed
if (!m_daybreak_path.has_value()) {
auto daybreak_path = DAYBREAK_PATH;
if (!m_fs->FileExists(DAYBREAK_PATH)) {
if (auto e = nro_find(m_nro_entries, "Daybreak", "Atmosphere-NX", {}); e.has_value()) {
daybreak_path = e.value().path;
} else {
log_write("failed to find daybreak\n");
m_daybreak_path = "";
return FsError_FileNotFound;
}
}
m_daybreak_path = daybreak_path;
log_write("found daybreak in: %s\n", m_daybreak_path.value().s);
}
FsDir d;
R_TRY(m_fs->OpenDirectory(m_path, FsDirOpenMode_ReadDirs, &d));
ON_SCOPE_EXIT(m_fs->DirClose(&d));
s64 count;
R_TRY(m_fs->DirGetEntryCount(&d, &count));
// check that we are at the bottom level
R_UNLESS(count == 0, 0x1);
// check that we have enough ncas and not too many
R_UNLESS(m_entries.size() > 150 && m_entries.size() < 300, 0x1);
// check that all entries end in .nca
const auto nca_ext = std::string_view{".nca"};
for (auto& e : m_entries) {
const auto ext = std::strrchr(e.name, '.');
R_UNLESS(ext && ext == nca_ext, 0x1);
}
R_SUCCEED();
}
auto Menu::get_collection(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollection& out, bool inc_file, bool inc_dir, bool inc_size) -> Result {
out.path = path;
out.parent_name = parent_name;
const auto fetch = [this, &path](std::vector<FsDirectoryEntry>& out, u32 flags) -> Result {
FsDir d;
R_TRY(m_fs->OpenDirectory(path, flags, &d));
ON_SCOPE_EXIT(m_fs->DirClose(&d));
s64 count;
R_TRY(m_fs->DirGetEntryCount(&d, &count));
out.resize(count);
return m_fs->DirRead(&d, &count, out.size(), out.data());
};
if (inc_file) {
u32 flags = FsDirOpenMode_ReadFiles;
if (!inc_size) {
flags |= FsDirOpenMode_NoFileSize;
}
R_TRY(fetch(out.files, flags));
}
if (inc_dir) {
R_TRY(fetch(out.dirs, FsDirOpenMode_ReadDirs));
}
R_SUCCEED();
}
auto Menu::get_collections(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out) -> Result {
// get a list of all the files / dirs
FsDirCollection collection;
R_TRY(get_collection(path, parent_name, collection, true, true, false));
log_write("got collection: %s parent_name: %s files: %zu dirs: %zu\n", path, parent_name, collection.files.size(), collection.dirs.size());
out.emplace_back(collection);
// for (size_t i = 0; i < collection.dirs.size(); i++) {
for (const auto&p : collection.dirs) {
// use heap as to not explode the stack
const auto new_path = std::make_unique<fs::FsPath>(Menu::GetNewPath(path, p.name));
const auto new_parent_name = std::make_unique<fs::FsPath>(Menu::GetNewPath(parent_name, p.name));
log_write("trying to get nested collection: %s parent_name: %s\n", *new_path, *new_parent_name);
R_TRY(get_collections(*new_path, *new_parent_name, out));
}
R_SUCCEED();
}
void Menu::SetFs(const fs::FsPath& new_path, u32 _new_type) {
const auto new_type = static_cast<FsType>(_new_type);
if (m_fs && new_type == m_fs_type) {
return;
}
// m_fs.reset();
m_path = new_path;
m_entries.clear();
m_entries_index.clear();
m_entries_index_hidden.clear();
m_entries_index_search.clear();
m_entries_current = {};
m_previous_highlighted_file.clear();
m_selected_path.clear();
m_selected_count = 0;
m_selected_type = SelectedType::None;
switch (new_type) {
default: case FsType::Sd:
m_fs = std::make_unique<fs::FsNativeSd>(m_ignore_read_only.Get());
m_fs_type = FsType::Sd;
log_write("doing fs: %u\n", _new_type);
break;
case FsType::ImageNand:
m_fs = std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Nand);
m_fs_type = FsType::ImageNand;
log_write("doing image nand\n");
break;
case FsType::ImageSd:
m_fs = std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Sd);
m_fs_type = FsType::ImageSd;
log_write("doing image sd\n");
break;
}
if (HasFocus()) {
if (m_path.empty()) {
Scan("/");
} else {
Scan(m_path);
}
}
}
} // namespace sphaira::ui::menu::filebrowser } // namespace sphaira::ui::menu::filebrowser
// options // options

View File

@@ -0,0 +1,533 @@
#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);
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, 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, 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, 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);
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);
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->elements[ThemeEntryID_TEXT].colour;
if (m_entries.empty()) {
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, text_col, "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, 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);
}
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->elements[text_id].colour, "%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->elements[text_id].colour, "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_index_offset = 0;
}
if (m_index > m_index_offset && m_index - m_index_offset >= 7) {
m_index_offset = m_index - 7;
}
SetTitleSubHeading(m_entries[m_index].json_path);
UpdateSubheading();
}
void Menu::Scan() {
m_entries.clear();
m_index = 0;
m_index_offset = 0;
// 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

View File

@@ -43,27 +43,23 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
} }
}}), }}),
std::make_pair(Button::DOWN, Action{[this](){ std::make_pair(Button::DOWN, Action{[this](){
if (m_index < (m_entries.size() - 1)) { if (m_list->ScrollDown(m_index, 3, m_entries.size())) {
if (m_index < (m_entries.size() - 3)) { SetIndex(m_index);
SetIndex(m_index + 3);
} else {
SetIndex(m_entries.size() - 1);
}
App::PlaySoundEffect(SoundEffect_Scroll);
if (m_index - m_start >= 9) {
log_write("moved down\n");
m_start += 3;
}
} }
}}), }}),
std::make_pair(Button::UP, Action{[this](){ std::make_pair(Button::UP, Action{[this](){
if (m_index >= 3) { if (m_list->ScrollUp(m_index, 3, m_entries.size())) {
SetIndex(m_index - 3); SetIndex(m_index);
App::PlaySoundEffect(SoundEffect_Scroll);
if (m_index < m_start ) {
// log_write("moved up\n");
m_start -= 3;
} }
}}),
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](){ std::make_pair(Button::A, Action{"Launch"_i18n, [this](){
@@ -90,12 +86,12 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
order_items.push_back("Decending"_i18n); order_items.push_back("Decending"_i18n);
order_items.push_back("Ascending"_i18n); order_items.push_back("Ascending"_i18n);
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](std::size_t& index_out){ options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
m_sort.Set(index_out); m_sort.Set(index_out);
SortAndFindLastFile(); SortAndFindLastFile();
}, m_sort.Get())); }, m_sort.Get()));
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](std::size_t& index_out){ options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](s64& index_out){
m_order.Set(index_out); m_order.Set(index_out);
SortAndFindLastFile(); SortAndFindLastFile();
}, m_order.Get())); }, m_order.Get()));
@@ -145,6 +141,10 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
} }
}}) }})
); );
const Vec4 v{75, 110, 370, 155};
const Vec2 pad{10, 10};
m_list = std::make_unique<List>(3, 9, m_pos, v, pad);
} }
Menu::~Menu() { Menu::~Menu() {
@@ -157,56 +157,60 @@ Menu::~Menu() {
void Menu::Update(Controller* controller, TouchInfo* touch) { void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch); MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_entries.size(), [this](auto i) {
if (m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
SetIndex(i);
}
});
} }
void Menu::Draw(NVGcontext* vg, Theme* theme) { void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme); MenuBase::Draw(vg, theme);
const u64 SCROLL = m_start; // max images per frame, in order to not hit io / gpu too hard.
const u64 max_entry_display = 9; const int image_load_max = 2;
const u64 nro_total = m_entries.size(); int image_load_count = 0;
const u64 cursor_pos = m_index;
fs::FsNativeSd fs;
// only draw scrollbar if needed m_list->Draw(vg, theme, m_entries.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
if (nro_total > max_entry_display) { const auto& [x, y, w, h] = v;
const auto scrollbar_size = 500.f;
const auto sb_h = 3.f / (float)nro_total * scrollbar_size;
const auto sb_y = SCROLL / 3.f;
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * 2) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
}
for (u64 i = 0, pos = SCROLL, y = 110, w = 370, h = 155; pos < nro_total && i < max_entry_display; y += h + 10) {
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]; auto& e = m_entries[pos];
// lazy load image // lazy load image
if (!e.image && e.icon.empty() && e.icon_size && e.icon_offset) { if (image_load_count < image_load_max) {
e.icon = nro_get_icon(e.path, e.icon_size, e.icon_offset); if (!e.image && e.icon_size && e.icon_offset) {
if (!e.icon.empty()) { // NOTE: it seems that images can be any size. SuperTux uses a 1024x1024
e.image = nvgCreateImageMem(vg, 0, e.icon.data(), e.icon.size()); // ~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; auto text_id = ThemeEntryID_TEXT;
if (pos == cursor_pos) { if (pos == m_index) {
text_id = ThemeEntryID_TEXT_SELECTED; text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour); gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, v, theme->elements[ThemeEntryID_SELECTED].colour);
} else { } else {
DrawElement(x, y, w, h, ThemeEntryID_GRID); DrawElement(v, ThemeEntryID_GRID);
} }
const float image_size = 115; const float image_size = 115;
gfx::drawImageRounded(vg, x + 20, y + 20, image_size, image_size, e.image); gfx::drawImageRounded(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage());
nvgSave(vg); nvgSave(vg);
nvgScissor(vg, x, y, w - 30.f, h); // clip nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip
{ {
bool has_star = false; bool has_star = false;
if (IsStarEnabled()) { if (IsStarEnabled()) {
if (!e.has_star.has_value()) { if (!e.has_star.has_value()) {
e.has_star = fs.FileExists(GenerateStarPath(e.path)); e.has_star = fs::FsNativeSd().FileExists(GenerateStarPath(e.path));
} }
has_star = e.has_star.value(); has_star = e.has_star.value();
} }
@@ -217,8 +221,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetDisplayVersion()); gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetDisplayVersion());
} }
nvgRestore(vg); nvgRestore(vg);
} });
}
} }
void Menu::OnFocusGained() { void Menu::OnFocusGained() {
@@ -228,10 +231,10 @@ void Menu::OnFocusGained() {
} }
} }
void Menu::SetIndex(std::size_t index) { void Menu::SetIndex(s64 index) {
m_index = index; m_index = index;
if (!m_index) { if (!m_index) {
m_start = 0; m_list->SetYoff(0);
} }
const auto& e = m_entries[m_index]; const auto& e = m_entries[m_index];
@@ -266,13 +269,13 @@ void Menu::SetIndex(std::size_t index) {
void Menu::InstallHomebrew() { void Menu::InstallHomebrew() {
const auto& nro = m_entries[m_index]; const auto& nro = m_entries[m_index];
InstallHomebrew(nro.path, nro.nacp, nro.icon); InstallHomebrew(nro.path, nro.nacp, nro_get_icon(nro.path, nro.icon_size, nro.icon_offset));
} }
void Menu::ScanHomebrew() { void Menu::ScanHomebrew() {
TimeStamp ts; TimeStamp ts;
nro_scan("/switch", m_entries, m_hide_sphaira.Get()); nro_scan("/switch", m_entries, m_hide_sphaira.Get());
log_write("nros found: %zu time_taken: %.2f\n", m_entries.size(), ts.GetSeconds()); log_write("nros found: %zu time_taken: %.2f\n", m_entries.size(), ts.GetSecondsD());
struct IniUser { struct IniUser {
std::vector<NroEntry>& entires; std::vector<NroEntry>& entires;
@@ -330,6 +333,22 @@ void Menu::Sort() {
const auto order = m_order.Get(); const auto order = m_order.Get();
const auto sorter = [this, sort, order](const NroEntry& lhs, const NroEntry& rhs) -> bool { const auto sorter = [this, sort, order](const NroEntry& lhs, const NroEntry& rhs) -> bool {
const auto name_cmp = [order](const NroEntry& lhs, const NroEntry& rhs) -> bool {
auto r = strcasecmp(lhs.GetName(), rhs.GetName());
if (!r) {
auto r = strcasecmp(lhs.GetAuthor(), rhs.GetAuthor());
if (!r) {
auto r = strcasecmp(lhs.path, rhs.path);
}
}
if (order == OrderType_Decending) {
return r < 0;
} else {
return r > 0;
}
};
switch (sort) { switch (sort) {
case SortType_UpdatedStar: case SortType_UpdatedStar:
if (lhs.has_star.value() && !rhs.has_star.value()) { if (lhs.has_star.value() && !rhs.has_star.value()) {
@@ -348,7 +367,7 @@ void Menu::Sort() {
} }
if (lhs_timestamp == rhs_timestamp) { if (lhs_timestamp == rhs_timestamp) {
return strcasecmp(lhs.GetName(), rhs.GetName()) < 0; return name_cmp(lhs, rhs);
} else if (order == OrderType_Decending) { } else if (order == OrderType_Decending) {
return lhs_timestamp > rhs_timestamp; return lhs_timestamp > rhs_timestamp;
} else { } else {
@@ -364,7 +383,7 @@ void Menu::Sort() {
} }
case SortType_Size: { case SortType_Size: {
if (lhs.size == rhs.size) { if (lhs.size == rhs.size) {
return strcasecmp(lhs.GetName(), rhs.GetName()) < 0; return name_cmp(lhs, rhs);
} else if (order == OrderType_Decending) { } else if (order == OrderType_Decending) {
return lhs.size > rhs.size; return lhs.size > rhs.size;
} else { } else {
@@ -379,11 +398,7 @@ void Menu::Sort() {
return false; return false;
} }
case SortType_Alphabetical: { case SortType_Alphabetical: {
if (order == OrderType_Decending) { return name_cmp(lhs, rhs);
return strcasecmp(lhs.GetName(), rhs.GetName()) < 0;
} else {
return strcasecmp(lhs.GetName(), rhs.GetName()) > 0;
}
} break; } break;
} }
@@ -409,9 +424,9 @@ void Menu::SortAndFindLastFile() {
if (index >= 0) { if (index >= 0) {
// guesstimate where the position is // guesstimate where the position is
if (index >= 9) { if (index >= 9) {
m_start = (index - 9) / 3 * 3 + 3; m_list->SetYoff((((index - 9) + 3) / 3) * m_list->GetMaxY());
} else { } else {
m_start = 0; m_list->SetYoff(0);
} }
SetIndex(index); SetIndex(index);
} }

View File

@@ -139,44 +139,44 @@ Menu::Menu() : MenuBase{"Irs"_i18n} {
format_str.emplace_back("20x15"_i18n); format_str.emplace_back("20x15"_i18n);
} }
options->Add(std::make_shared<SidebarEntryArray>("Controller"_i18n, controller_str, [this](std::size_t& index){ options->Add(std::make_shared<SidebarEntryArray>("Controller"_i18n, controller_str, [this](s64& index){
irsStopImageProcessor(m_entries[m_index].m_handle); irsStopImageProcessor(m_entries[m_index].m_handle);
m_index = index; m_index = index;
UpdateConfig(&m_config); UpdateConfig(&m_config);
}, m_index)); }, m_index));
options->Add(std::make_shared<SidebarEntryArray>("Rotation"_i18n, rotation_str, [this](std::size_t& index){ options->Add(std::make_shared<SidebarEntryArray>("Rotation"_i18n, rotation_str, [this](s64& index){
m_rotation = (Rotation)index; m_rotation = (Rotation)index;
}, m_rotation)); }, m_rotation));
options->Add(std::make_shared<SidebarEntryArray>("Colour"_i18n, colour_str, [this](std::size_t& index){ options->Add(std::make_shared<SidebarEntryArray>("Colour"_i18n, colour_str, [this](s64& index){
m_colour = (Colour)index; m_colour = (Colour)index;
updateColourArray(); updateColourArray();
}, m_colour)); }, m_colour));
options->Add(std::make_shared<SidebarEntryArray>("Light Target"_i18n, light_target_str, [this](std::size_t& index){ options->Add(std::make_shared<SidebarEntryArray>("Light Target"_i18n, light_target_str, [this](s64& index){
m_config.light_target = index; m_config.light_target = index;
UpdateConfig(&m_config); UpdateConfig(&m_config);
}, m_config.light_target)); }, m_config.light_target));
options->Add(std::make_shared<SidebarEntryArray>("Gain"_i18n, gain_str, [this](std::size_t& index){ options->Add(std::make_shared<SidebarEntryArray>("Gain"_i18n, gain_str, [this](s64& index){
m_config.gain = GAIN_MIN + index; m_config.gain = GAIN_MIN + index;
UpdateConfig(&m_config); UpdateConfig(&m_config);
}, m_config.gain - GAIN_MIN)); }, m_config.gain - GAIN_MIN));
options->Add(std::make_shared<SidebarEntryArray>("Negative Image"_i18n, is_negative_image_used_str, [this](std::size_t& index){ options->Add(std::make_shared<SidebarEntryArray>("Negative Image"_i18n, is_negative_image_used_str, [this](s64& index){
m_config.is_negative_image_used = index; m_config.is_negative_image_used = index;
UpdateConfig(&m_config); UpdateConfig(&m_config);
}, m_config.is_negative_image_used)); }, m_config.is_negative_image_used));
options->Add(std::make_shared<SidebarEntryArray>("Format"_i18n, format_str, [this](std::size_t& index){ options->Add(std::make_shared<SidebarEntryArray>("Format"_i18n, format_str, [this](s64& index){
m_config.orig_format = index; m_config.orig_format = index;
m_config.trimming_format = index; m_config.trimming_format = index;
UpdateConfig(&m_config); UpdateConfig(&m_config);
}, m_config.orig_format)); }, m_config.orig_format));
if (hosversionAtLeast(4,0,0)) { if (hosversionAtLeast(4,0,0)) {
options->Add(std::make_shared<SidebarEntryArray>("Trimming Format"_i18n, format_str, [this](std::size_t& index){ options->Add(std::make_shared<SidebarEntryArray>("Trimming Format"_i18n, format_str, [this](s64& index){
// you cannot set trim a larger region than the source // you cannot set trim a larger region than the source
if (index < m_config.orig_format) { if (index < m_config.orig_format) {
index = m_config.orig_format; index = m_config.orig_format;

View File

@@ -1,6 +1,7 @@
#include "ui/menus/main_menu.hpp" #include "ui/menus/main_menu.hpp"
#include "ui/menus/irs_menu.hpp" #include "ui/menus/irs_menu.hpp"
#include "ui/menus/themezer.hpp" #include "ui/menus/themezer.hpp"
#include "ui/menus/ghdl.hpp"
#include "ui/sidebar.hpp" #include "ui/sidebar.hpp"
#include "ui/popup_list.hpp" #include "ui/popup_list.hpp"
@@ -22,6 +23,9 @@
namespace sphaira::ui::menu::main { namespace sphaira::ui::menu::main {
namespace { namespace {
constexpr const char* GITHUB_URL{"https://api.github.com/repos/ITotalJustice/sphaira/releases/latest"};
constexpr fs::FsPath CACHE_PATH{"/switch/sphaira/cache/sphaira_latest.json"};
auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string version) -> bool { auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string version) -> bool {
static fs::FsPath zip_out{"/switch/sphaira/cache/update.zip"}; static fs::FsPath zip_out{"/switch/sphaira/cache/update.zip"};
constexpr auto chunk_size = 1024 * 512; // 512KiB constexpr auto chunk_size = 1024 * 512; // 512KiB
@@ -34,16 +38,12 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
pbox->NewTransfer("Downloading "_i18n + version); pbox->NewTransfer("Downloading "_i18n + version);
log_write("starting download: %s\n", url.c_str()); log_write("starting download: %s\n", url.c_str());
DownloadClearCache(url); if (!curl::Api().ToFile(
if (!DownloadFile(url, zip_out, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){ curl::Url{url},
if (pbox->ShouldExit()) { curl::Path{zip_out},
return false; curl::OnProgress{pbox->OnDownloadProgressCallback()}
} ).success) {
pbox->UpdateTransfer(dlnow, dltotal);
return true;
})) {
log_write("error with download\n"); log_write("error with download\n");
// push popup error box
return false; return false;
} }
} }
@@ -115,7 +115,7 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
} }
std::vector<char> buf(chunk_size); std::vector<char> buf(chunk_size);
u64 offset{}; s64 offset{};
while (offset < info.uncompressed_size) { while (offset < info.uncompressed_size) {
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size()); const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
if (bytes_read <= 0) { if (bytes_read <= 0) {
@@ -142,15 +142,23 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
} // namespace } // namespace
MainMenu::MainMenu() { MainMenu::MainMenu() {
DownloadMemoryAsync("https://api.github.com/repos/ITotalJustice/sphaira/releases/latest", "", [this](std::vector<u8>& data, bool success){ curl::Api().ToFileAsync(
curl::Url{GITHUB_URL},
curl::Path{CACHE_PATH},
curl::Flags{curl::Flag_Cache},
curl::Header{
{ "Accept", "application/vnd.github+json" },
},
curl::OnComplete{[this](auto& result){
log_write("inside github download\n");
m_update_state = UpdateState::Error; m_update_state = UpdateState::Error;
ON_SCOPE_EXIT( log_write("update status: %u\n", (u8)m_update_state) ); ON_SCOPE_EXIT( log_write("update status: %u\n", (u8)m_update_state) );
if (!success) { if (!result.success) {
return false; return false;
} }
auto json = yyjson_read((const char*)data.data(), data.size(), 0); auto json = yyjson_read_file(CACHE_PATH, YYJSON_READ_NOFLAG, nullptr, nullptr);
R_UNLESS(json, false); R_UNLESS(json, false);
ON_SCOPE_EXIT(yyjson_doc_free(json)); ON_SCOPE_EXIT(yyjson_doc_free(json));
@@ -194,11 +202,9 @@ MainMenu::MainMenu() {
App::Notify("Update avaliable: "_i18n + m_update_version); App::Notify("Update avaliable: "_i18n + m_update_version);
return true; return true;
}
}); });
AddOnLPress();
AddOnRPress();
this->SetActions( this->SetActions(
std::make_pair(Button::START, Action{App::Exit}), std::make_pair(Button::START, Action{App::Exit}),
std::make_pair(Button::Y, Action{"Menu"_i18n, [this](){ std::make_pair(Button::Y, Action{"Menu"_i18n, [this](){
@@ -220,8 +226,6 @@ MainMenu::MainMenu() {
language_items.push_back("Russian"_i18n); language_items.push_back("Russian"_i18n);
language_items.push_back("Swedish"_i18n); language_items.push_back("Swedish"_i18n);
options->AddHeader("Header"_i18n);
options->AddSpacer();
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [this](){ options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [this](){
SidebarEntryArray::Items theme_items{}; SidebarEntryArray::Items theme_items{};
const auto theme_meta = App::GetThemeMetaList(); const auto theme_meta = App::GetThemeMetaList();
@@ -232,7 +236,7 @@ MainMenu::MainMenu() {
auto options = std::make_shared<Sidebar>("Theme Options"_i18n, Sidebar::Side::LEFT); auto options = std::make_shared<Sidebar>("Theme Options"_i18n, Sidebar::Side::LEFT);
ON_SCOPE_EXIT(App::Push(options)); ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<SidebarEntryArray>("Select Theme"_i18n, theme_items, [this, theme_items](std::size_t& index_out){ options->Add(std::make_shared<SidebarEntryArray>("Select Theme"_i18n, theme_items, [this, theme_items](s64& index_out){
App::SetTheme(index_out); App::SetTheme(index_out);
}, App::GetThemeIndex())); }, App::GetThemeIndex()));
@@ -285,9 +289,9 @@ MainMenu::MainMenu() {
} }
})); }));
options->Add(std::make_shared<SidebarEntryArray>("Language"_i18n, language_items, [this](std::size_t& index_out){ options->Add(std::make_shared<SidebarEntryArray>("Language"_i18n, language_items, [this](s64& index_out){
App::SetLanguage(index_out); App::SetLanguage(index_out);
}, (std::size_t)App::GetLanguage())); }, (s64)App::GetLanguage()));
options->Add(std::make_shared<SidebarEntryCallback>("Misc"_i18n, [this](){ options->Add(std::make_shared<SidebarEntryCallback>("Misc"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Misc Options"_i18n, Sidebar::Side::LEFT); auto options = std::make_shared<Sidebar>("Misc Options"_i18n, Sidebar::Side::LEFT);
@@ -297,6 +301,10 @@ MainMenu::MainMenu() {
App::Push(std::make_shared<menu::themezer::Menu>()); App::Push(std::make_shared<menu::themezer::Menu>());
})); }));
options->Add(std::make_shared<SidebarEntryCallback>("GitHub"_i18n, [](){
App::Push(std::make_shared<menu::gh::Menu>());
}));
options->Add(std::make_shared<SidebarEntryCallback>("Irs"_i18n, [](){ options->Add(std::make_shared<SidebarEntryCallback>("Irs"_i18n, [](){
App::Push(std::make_shared<menu::irs::Menu>()); App::Push(std::make_shared<menu::irs::Menu>());
})); }));
@@ -328,9 +336,9 @@ MainMenu::MainMenu() {
App::SetInstallEnable(enable); App::SetInstallEnable(enable);
}, "Enabled"_i18n, "Disabled"_i18n)); }, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryArray>("Install location"_i18n, install_items, [this](std::size_t& index_out){ options->Add(std::make_shared<SidebarEntryArray>("Install location"_i18n, install_items, [this](s64& index_out){
App::SetInstallSdEnable(index_out); App::SetInstallSdEnable(index_out);
}, (std::size_t)App::GetInstallSdEnable())); }, (s64)App::GetInstallSdEnable()));
options->Add(std::make_shared<SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [this](bool& enable){ options->Add(std::make_shared<SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [this](bool& enable){
App::SetInstallPrompt(enable); App::SetInstallPrompt(enable);
@@ -344,6 +352,8 @@ MainMenu::MainMenu() {
m_app_store_menu = std::make_shared<appstore::Menu>(m_homebrew_menu->GetHomebrewList()); m_app_store_menu = std::make_shared<appstore::Menu>(m_homebrew_menu->GetHomebrewList());
m_current_menu = m_homebrew_menu; m_current_menu = m_homebrew_menu;
AddOnLRPress();
for (auto [button, action] : m_actions) { for (auto [button, action] : m_actions) {
m_current_menu->SetAction(button, action); m_current_menu->SetAction(button, action);
} }
@@ -363,11 +373,11 @@ void MainMenu::Draw(NVGcontext* vg, Theme* theme) {
void MainMenu::OnFocusGained() { void MainMenu::OnFocusGained() {
Widget::OnFocusGained(); Widget::OnFocusGained();
this->SetHidden(false);
m_current_menu->OnFocusGained(); m_current_menu->OnFocusGained();
} }
void MainMenu::OnFocusLost() { void MainMenu::OnFocusLost() {
Widget::OnFocusLost();
m_current_menu->OnFocusLost(); m_current_menu->OnFocusLost();
} }
@@ -376,17 +386,11 @@ void MainMenu::OnLRPress(std::shared_ptr<MenuBase> menu, Button b) {
if (m_current_menu == m_homebrew_menu) { if (m_current_menu == m_homebrew_menu) {
m_current_menu = menu; m_current_menu = menu;
RemoveAction(b); RemoveAction(b);
if (b == Button::L) {
AddOnRPress();
} else {
AddOnLPress();
}
} else { } else {
m_current_menu = m_homebrew_menu; m_current_menu = m_homebrew_menu;
AddOnRPress();
AddOnLPress();
} }
AddOnLRPress();
m_current_menu->OnFocusGained(); m_current_menu->OnFocusGained();
for (auto [button, action] : m_actions) { for (auto [button, action] : m_actions) {
@@ -394,18 +398,20 @@ void MainMenu::OnLRPress(std::shared_ptr<MenuBase> menu, Button b) {
} }
} }
void MainMenu::AddOnLPress() { void MainMenu::AddOnLRPress() {
if (m_current_menu != m_filebrowser_menu) {
const auto label = m_current_menu == m_homebrew_menu ? "Files" : "Apps"; const auto label = m_current_menu == m_homebrew_menu ? "Files" : "Apps";
SetAction(Button::L, Action{i18n::get(label), [this]{ SetAction(Button::L, Action{i18n::get(label), [this]{
OnLRPress(m_filebrowser_menu, Button::L); OnLRPress(m_filebrowser_menu, Button::L);
}}); }});
} }
void MainMenu::AddOnRPress() { if (m_current_menu != m_app_store_menu) {
const auto label = m_current_menu == m_homebrew_menu ? "Store" : "Apps"; const auto label = m_current_menu == m_homebrew_menu ? "Store" : "Apps";
SetAction(Button::R, Action{i18n::get(label), [this]{ SetAction(Button::R, Action{i18n::get(label), [this]{
OnLRPress(m_app_store_menu, Button::R); OnLRPress(m_app_store_menu, Button::R);
}}); }});
} }
}
} // namespace sphaira::ui::menu::main } // namespace sphaira::ui::menu::main

View File

@@ -9,8 +9,8 @@ namespace sphaira::ui::menu {
MenuBase::MenuBase(std::string title) : m_title{title} { MenuBase::MenuBase(std::string title) : m_title{title} {
// this->SetParent(this); // this->SetParent(this);
this->SetPos(30, 87, 1220 - 30, 646 - 87); this->SetPos(30, 87, 1220 - 30, 646 - 87);
m_applet_type = appletGetAppletType();
SetAction(Button::START, Action{App::Exit}); SetAction(Button::START, Action{App::Exit});
UpdateVars();
} }
MenuBase::~MenuBase() { MenuBase::~MenuBase() {
@@ -18,30 +18,17 @@ MenuBase::~MenuBase() {
void MenuBase::Update(Controller* controller, TouchInfo* touch) { void MenuBase::Update(Controller* controller, TouchInfo* touch) {
Widget::Update(controller, touch); Widget::Update(controller, touch);
// update every second.
if (m_poll_timestamp.GetSeconds() >= 1) {
UpdateVars();
}
} }
void MenuBase::Draw(NVGcontext* vg, Theme* theme) { void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
DrawElement(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, ThemeEntryID_BACKGROUND); DrawElement(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, ThemeEntryID_BACKGROUND);
Widget::Draw(vg, theme); Widget::Draw(vg, theme);
u32 battery_percetange{};
PsmChargerType charger_type{};
NifmInternetConnectionType type{};
NifmInternetConnectionStatus status{};
u32 strength{};
u32 ip{};
const auto t = time(NULL);
struct tm tm{};
localtime_r(&t, &tm);
// todo: app thread poll every 1s and this query the result
psmGetBatteryChargePercentage(&battery_percetange);
psmGetChargerType(&charger_type);
nifmGetInternetConnectionStatus(&type, &strength, &status);
nifmGetCurrentIpAddress(&ip);
const float start_y = 70; const float start_y = 70;
const float font_size = 22; const float font_size = 22;
const float spacing = 30; const float spacing = 30;
@@ -58,14 +45,14 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
start_x -= spacing; start_x -= spacing;
// draw("version %s", APP_VERSION); // draw("version %s", APP_VERSION);
draw("%u\uFE6A", battery_percetange); draw("%u\uFE6A", m_battery_percetange);
draw("%02u:%02u:%02u", tm.tm_hour, tm.tm_min, tm.tm_sec); draw("%02u:%02u:%02u", m_tm.tm_hour, m_tm.tm_min, m_tm.tm_sec);
if (ip) { if (m_ip) {
draw("%u.%u.%u.%u", ip&0xFF, (ip>>8)&0xFF, (ip>>16)&0xFF, (ip>>24)&0xFF); draw("%u.%u.%u.%u", m_ip&0xFF, (m_ip>>8)&0xFF, (m_ip>>16)&0xFF, (m_ip>>24)&0xFF);
} else { } else {
draw(("No Internet"_i18n).c_str()); 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()); draw(("[Applet Mode]"_i18n).c_str());
} }
@@ -96,4 +83,24 @@ void MenuBase::SetSubHeading(std::string sub_heading) {
m_sub_heading = sub_heading; m_sub_heading = sub_heading;
} }
void MenuBase::UpdateVars() {
m_tm = {};
m_poll_timestamp = {};
m_battery_percetange = {};
m_charger_type = {};
m_type = {};
m_status = {};
m_strength = {};
m_ip = {};
const auto t = time(NULL);
localtime_r(&t, &m_tm);
psmGetBatteryChargePercentage(&m_battery_percetange);
psmGetChargerType(&m_charger_type);
nifmGetInternetConnectionStatus(&m_type, &m_strength, &m_status);
nifmGetCurrentIpAddress(&m_ip);
m_poll_timestamp.Update();
}
} // namespace sphaira::ui::menu } // namespace sphaira::ui::menu

View File

@@ -53,9 +53,11 @@ constexpr const char* REQUEST_ORDER[]{
// https://api.themezer.net/?query=query($nsfw:Boolean,$page:Int,$limit:Int,$sort:String,$order:String,$query:String,$creators:[String!]){packList(nsfw:$nsfw,page:$page,limit:$limit,sort:$sort,order:$order,query:$query,creators:$creators){id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count,themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}}}&variables={"nsfw":false,"page":1,"limit":10,"sort":"updated","order":"desc","query":null,"creators":["695065006068334622"]} // https://api.themezer.net/?query=query($nsfw:Boolean,$page:Int,$limit:Int,$sort:String,$order:String,$query:String,$creators:[String!]){packList(nsfw:$nsfw,page:$page,limit:$limit,sort:$sort,order:$order,query:$query,creators:$creators){id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count,themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}}}&variables={"nsfw":false,"page":1,"limit":10,"sort":"updated","order":"desc","query":null,"creators":["695065006068334622"]}
// i know, this is cursed // i know, this is cursed
// todo: send actual POST request rather than GET.
auto apiBuildUrlListInternal(const Config& e, bool is_pack) -> std::string { auto apiBuildUrlListInternal(const Config& e, bool is_pack) -> std::string {
std::string api = "https://api.themezer.net/?query=query"; std::string api = "https://api.themezer.net/?query=query";
std::string fields = "{id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count"; // std::string fields = "{id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count";
std::string fields = "{id,creator{id,display_name},details{name}";
const char* boolarr[2] = { "false", "true" }; const char* boolarr[2] = { "false", "true" };
std::string cmd; std::string cmd;
@@ -65,7 +67,8 @@ auto apiBuildUrlListInternal(const Config& e, bool is_pack) -> std::string {
if (is_pack) { if (is_pack) {
cmd = "packList"; cmd = "packList";
fields += ",themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}"; // fields += ",themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}";
fields += ",themes{id, preview{thumb}}";
} else { } else {
cmd = "themeList"; cmd = "themeList";
p0 += ",$target:String"; p0 += ",$target:String";
@@ -113,11 +116,13 @@ auto apiBuildFilePack(const PackListEntry& e) -> fs::FsPath {
return path; return path;
} }
#if 0
auto apiBuildUrlPack(const PackListEntry& e) -> std::string { auto apiBuildUrlPack(const PackListEntry& e) -> std::string {
char url[2048]; 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()); 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; return url;
} }
#endif
auto apiBuildUrlThemeList(const Config& e) -> std::string { auto apiBuildUrlThemeList(const Config& e) -> std::string {
return apiBuildUrlListInternal(e, false); return apiBuildUrlListInternal(e, false);
@@ -127,19 +132,25 @@ auto apiBuildUrlListPacks(const Config& e) -> std::string {
return apiBuildUrlListInternal(e, true); return apiBuildUrlListInternal(e, true);
} }
auto apiBuildListPacksCache(const Config& e) -> fs::FsPath {
fs::FsPath path;
std::snprintf(path, sizeof(path), "%s/%u_page.json", CACHE_PATH, e.page);
return path;
}
auto apiBuildIconCache(const ThemeEntry& e) -> fs::FsPath { auto apiBuildIconCache(const ThemeEntry& e) -> fs::FsPath {
fs::FsPath path; fs::FsPath path;
std::snprintf(path, sizeof(path), "%s/%s_thumb.jpg", CACHE_PATH, e.id.c_str()); std::snprintf(path, sizeof(path), "%s/%s_thumb.jpg", CACHE_PATH, e.id.c_str());
return path; return path;
} }
auto loadThemeImage(ThemeEntry& e) -> void { auto loadThemeImage(ThemeEntry& e) -> bool {
auto& image = e.preview.lazy_image; auto& image = e.preview.lazy_image;
// already have the image // already have the image
if (e.preview.lazy_image.image) { if (e.preview.lazy_image.image) {
// log_write("warning, tried to load image: %s when already loaded\n", path.c_str()); // log_write("warning, tried to load image: %s when already loaded\n", path.c_str());
return; return true;
} }
auto vg = App::GetVg(); auto vg = App::GetVg();
@@ -148,43 +159,24 @@ auto loadThemeImage(ThemeEntry& e) -> void {
const auto path = apiBuildIconCache(e); const auto path = apiBuildIconCache(e);
if (R_FAILED(fs.read_entire_file(path, image_buf))) { if (R_FAILED(fs.read_entire_file(path, image_buf))) {
e.preview.lazy_image.state = ImageDownloadState::Failed; log_write("failed to load image from file: %s\n", path.s);
} else { } else {
int channels_in_file; int channels_in_file;
auto buf = stbi_load_from_memory(image_buf.data(), image_buf.size(), &image.w, &image.h, &channels_in_file, 4); auto buf = stbi_load_from_memory(image_buf.data(), image_buf.size(), &image.w, &image.h, &channels_in_file, 4);
if (buf) { if (buf) {
ON_SCOPE_EXIT(stbi_image_free(buf)); ON_SCOPE_EXIT(stbi_image_free(buf));
std::memcpy(image.first_pixel, buf, sizeof(image.first_pixel));
image.image = nvgCreateImageRGBA(vg, image.w, image.h, 0, buf); image.image = nvgCreateImageRGBA(vg, image.w, image.h, 0, buf);
} }
} }
if (!image.image) { if (!image.image) {
image.state = ImageDownloadState::Failed; log_write("failed to load image from file: %s\n", path.s);
log_write("failed to load image from file: %s\n", path); log_write("failed to load image from file: %s\n", path);
return false;
} else { } else {
// log_write("loaded image from file: %s\n", path); // log_write("loaded image from file: %s\n", path);
}
}
auto ScrollHelperDown(u64& index, u64& start, u64 step, u64 max, u64 size) -> bool {
if (size && index < (size - 1)) {
if (index < (size - step)) {
index = index + step;
App::PlaySoundEffect(SoundEffect_Scroll);
} else {
index = size - 1;
App::PlaySoundEffect(SoundEffect_Scroll);
}
if (index - start >= max) {
log_write("moved down\n");
start += step;
}
return true; return true;
} }
return false;
} }
void from_json(yyjson_val* json, Creator& e) { void from_json(yyjson_val* json, Creator& e) {
@@ -197,13 +189,13 @@ void from_json(yyjson_val* json, Creator& e) {
void from_json(yyjson_val* json, Details& e) { void from_json(yyjson_val* json, Details& e) {
JSON_OBJ_ITR( JSON_OBJ_ITR(
JSON_SET_STR(name); JSON_SET_STR(name);
JSON_SET_STR(description); // JSON_SET_STR(description);
); );
} }
void from_json(yyjson_val* json, Preview& e) { void from_json(yyjson_val* json, Preview& e) {
JSON_OBJ_ITR( JSON_OBJ_ITR(
JSON_SET_STR(original); // JSON_SET_STR(original);
JSON_SET_STR(thumb); JSON_SET_STR(thumb);
); );
} }
@@ -219,13 +211,13 @@ void from_json(yyjson_val* json, DownloadPack& e) {
void from_json(yyjson_val* json, ThemeEntry& e) { void from_json(yyjson_val* json, ThemeEntry& e) {
JSON_OBJ_ITR( JSON_OBJ_ITR(
JSON_SET_STR(id); JSON_SET_STR(id);
JSON_SET_OBJ(creator); // JSON_SET_OBJ(creator);
JSON_SET_OBJ(details); // JSON_SET_OBJ(details);
JSON_SET_STR(last_updated); // JSON_SET_STR(last_updated);
JSON_SET_UINT(dl_count); // JSON_SET_UINT(dl_count);
JSON_SET_UINT(like_count); // JSON_SET_UINT(like_count);
JSON_SET_ARR_STR(categories); // JSON_SET_ARR_STR(categories);
JSON_SET_STR(target); // JSON_SET_STR(target);
JSON_SET_OBJ(preview); JSON_SET_OBJ(preview);
); );
} }
@@ -235,10 +227,10 @@ void from_json(yyjson_val* json, PackListEntry& e) {
JSON_SET_STR(id); JSON_SET_STR(id);
JSON_SET_OBJ(creator); JSON_SET_OBJ(creator);
JSON_SET_OBJ(details); JSON_SET_OBJ(details);
JSON_SET_STR(last_updated); // JSON_SET_STR(last_updated);
JSON_SET_ARR_STR(categories); // JSON_SET_ARR_STR(categories);
JSON_SET_UINT(dl_count); // JSON_SET_UINT(dl_count);
JSON_SET_UINT(like_count); // JSON_SET_UINT(like_count);
JSON_SET_ARR_OBJ(themes); JSON_SET_ARR_OBJ(themes);
); );
} }
@@ -263,8 +255,8 @@ void from_json(const std::vector<u8>& data, DownloadPack& e) {
); );
} }
void from_json(const std::vector<u8>& data, PackList& e) { void from_json(const fs::FsPath& path, PackList& e) {
JSON_INIT_VEC(data, "data"); JSON_INIT_VEC_FILE(path, "data", nullptr);
JSON_OBJ_ITR( JSON_OBJ_ITR(
JSON_SET_ARR_OBJ(packList); JSON_SET_ARR_OBJ(packList);
JSON_SET_OBJ(pagination); JSON_SET_OBJ(pagination);
@@ -272,7 +264,7 @@ void from_json(const std::vector<u8>& data, PackList& e) {
} }
auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool { auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
static fs::FsPath zip_out{"/switch/sphaira/cache/themezer/temp.zip"}; static const fs::FsPath zip_out{"/switch/sphaira/cache/themezer/temp.zip"};
constexpr auto chunk_size = 1024 * 512; // 512KiB constexpr auto chunk_size = 1024 * 512; // 512KiB
fs::FsNativeSd fs; fs::FsNativeSd fs;
@@ -287,22 +279,17 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
const auto url = apiBuildUrlDownloadPack(entry); const auto url = apiBuildUrlDownloadPack(entry);
log_write("using url: %s\n", url.c_str()); log_write("using url: %s\n", url.c_str());
DownloadClearCache(url); const auto result = curl::Api().ToMemory(
const auto data = DownloadMemory(url, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){ curl::Url{url},
if (pbox->ShouldExit()) { curl::OnProgress{pbox->OnDownloadProgressCallback()}
return false; );
}
pbox->UpdateTransfer(dlnow, dltotal);
return true;
});
if (data.empty()) { if (!result.success || result.data.empty()) {
log_write("error with download: %s\n", url.c_str()); log_write("error with download: %s\n", url.c_str());
// push popup error box
return false; return false;
} }
from_json(data, download_pack); from_json(result.data, download_pack);
} }
// 2. download the zip // 2. download the zip
@@ -310,16 +297,11 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
pbox->NewTransfer("Downloading "_i18n + entry.details.name); pbox->NewTransfer("Downloading "_i18n + entry.details.name);
log_write("starting download: %s\n", download_pack.url.c_str()); log_write("starting download: %s\n", download_pack.url.c_str());
DownloadClearCache(download_pack.url); if (!curl::Api().ToFile(
if (!DownloadFile(download_pack.url, zip_out, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){ curl::Url{download_pack.url},
if (pbox->ShouldExit()) { curl::Path{zip_out},
return false; curl::OnProgress{pbox->OnDownloadProgressCallback()}).success) {
}
pbox->UpdateTransfer(dlnow, dltotal);
return true;
})) {
log_write("error with download\n"); log_write("error with download\n");
// push popup error box
return false; return false;
} }
} }
@@ -387,7 +369,7 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
} }
std::vector<char> buf(chunk_size); std::vector<char> buf(chunk_size);
u64 offset{}; s64 offset{};
while (offset < info.uncompressed_size) { while (offset < info.uncompressed_size) {
if (pbox->ShouldExit()) { if (pbox->ShouldExit()) {
return false; return false;
@@ -423,6 +405,8 @@ LazyImage::~LazyImage() {
} }
Menu::Menu() : MenuBase{"Themezer"_i18n} { Menu::Menu() : MenuBase{"Themezer"_i18n} {
fs::FsNativeSd().CreateDirectoryRecursively(CACHE_PATH);
SetAction(Button::B, Action{"Back"_i18n, [this]{ SetAction(Button::B, Action{"Back"_i18n, [this]{
SetPop(); SetPop();
}}); }});
@@ -445,7 +429,25 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
}}), }}),
std::make_pair(Button::DOWN, Action{[this](){ std::make_pair(Button::DOWN, Action{[this](){
const auto& page = m_pages[m_page_index]; const auto& page = m_pages[m_page_index];
if (ScrollHelperDown(m_index, m_start, 3, 6, page.m_packList.size())) { if (m_list->ScrollDown(m_index, 3, page.m_packList.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
const auto& page = m_pages[m_page_index];
if (m_list->ScrollUp(m_index, 3, page.m_packList.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::R2, Action{[this](){
const auto& page = m_pages[m_page_index];
if (m_list->ScrollDown(m_index, 6, page.m_packList.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::L2, Action{[this](){
const auto& page = m_pages[m_page_index];
if (m_list->ScrollUp(m_index, 6, page.m_packList.size())) {
SetIndex(m_index); SetIndex(m_index);
} }
}}), }}),
@@ -461,12 +463,10 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
App::Push(std::make_shared<ProgressBox>("Installing "_i18n + entry.details.name, [this, &entry](auto pbox){ App::Push(std::make_shared<ProgressBox>("Installing "_i18n + entry.details.name, [this, &entry](auto pbox){
return InstallTheme(pbox, entry); return InstallTheme(pbox, entry);
}, [this](bool success){ }, [this, &entry](bool success){
// if (success) { if (success) {
// m_entry.status = EntryStatus::Installed; App::Notify("Downloaded "_i18n + entry.details.name);
// m_menu.SetDirty(); }
// UpdateOptions();
// }
}, 2)); }, 2));
} }
} }
@@ -492,14 +492,14 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
InvalidateAllPages(); InvalidateAllPages();
}, "Enabled"_i18n, "Disabled"_i18n)); }, "Enabled"_i18n, "Disabled"_i18n));
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](std::size_t& index_out){ options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
if (m_sort.Get() != index_out) { if (m_sort.Get() != index_out) {
m_sort.Set(index_out); m_sort.Set(index_out);
InvalidateAllPages(); InvalidateAllPages();
} }
}, m_sort.Get())); }, m_sort.Get()));
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](std::size_t& index_out){ options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](s64& index_out){
if (m_order.Get() != index_out) { if (m_order.Get() != index_out) {
m_order.Set(index_out); m_order.Set(index_out);
InvalidateAllPages(); InvalidateAllPages();
@@ -526,16 +526,6 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
} }
})); }));
}}), }}),
std::make_pair(Button::UP, Action{[this](){
if (m_index >= 3) {
SetIndex(m_index - 3);
App::PlaySoundEffect(SoundEffect_Scroll);
if (m_index < m_start ) {
// log_write("moved up\n");
m_start -= 3;
}
}
}}),
std::make_pair(Button::R, Action{"Next Page"_i18n, [this](){ std::make_pair(Button::R, Action{"Next Page"_i18n, [this](){
m_page_index++; m_page_index++;
if (m_page_index >= m_page_index_max) { if (m_page_index >= m_page_index_max) {
@@ -555,6 +545,10 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
m_page_index = 0; m_page_index = 0;
m_pages.resize(1); m_pages.resize(1);
PackListDownload(); PackListDownload();
const Vec4 v{75, 110, 350, 250};
const Vec2 pad{10, 10};
m_list = std::make_unique<List>(3, 6, m_pos, v, pad);
} }
Menu::~Menu() { Menu::~Menu() {
@@ -563,6 +557,24 @@ Menu::~Menu() {
void Menu::Update(Controller* controller, TouchInfo* touch) { void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch); MenuBase::Update(controller, touch);
if (m_pages.empty()) {
return;
}
const auto& page = m_pages[m_page_index];
if (page.m_ready != PageLoadState::Done) {
return;
}
m_list->OnUpdate(controller, touch, page.m_packList.size(), [this](auto i) {
if (m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
SetIndex(i);
}
});
} }
void Menu::Draw(NVGcontext* vg, Theme* theme) { void Menu::Draw(NVGcontext* vg, Theme* theme) {
@@ -589,30 +601,16 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
return; return;
} }
const u64 SCROLL = m_start; // max images per frame, in order to not hit io / gpu too hard.
const u64 max_entry_display = 9; const int image_load_max = 2;
const u64 nro_total = page.m_packList.size();// m_entries_current.size(); int image_load_count = 0;
const u64 cursor_pos = m_index;
// only draw scrollbar if needed m_list->Draw(vg, theme, page.m_packList.size(), [this, &page, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
if (nro_total > max_entry_display) { const auto& [x, y, w, h] = v;
const auto scrollbar_size = 500.f; auto& e = page.m_packList[pos];
const auto sb_h = 3.f / (float)(nro_total + 3) * scrollbar_size;
const auto sb_y = SCROLL / 3.f;
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * 2) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
}
nvgSave(vg);
nvgScissor(vg, 30, 87, 1220 - 30, 646 - 87); // clip
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];
auto text_id = ThemeEntryID_TEXT; auto text_id = ThemeEntryID_TEXT;
if (pos == cursor_pos) { if (pos == m_index) {
text_id = ThemeEntryID_TEXT_SELECTED; text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour); gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
} else { } else {
@@ -626,49 +624,70 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
if (e.themes.size()) { if (e.themes.size()) {
auto& theme = e.themes[0]; auto& theme = e.themes[0];
auto& image = e.themes[0].preview.lazy_image; auto& image = e.themes[0].preview.lazy_image;
if (!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++;
}
}
if (!image.image || image.cached) {
switch (image.state) { switch (image.state) {
case ImageDownloadState::None: { case ImageDownloadState::None: {
const auto path = apiBuildIconCache(theme); const auto path = apiBuildIconCache(theme);
log_write("downloading theme!: %s\n", path); log_write("downloading theme!: %s\n", path);
if (fs::FsNativeSd().FileExists(path)) {
loadThemeImage(theme);
} else {
const auto url = theme.preview.thumb; const auto url = theme.preview.thumb;
log_write("downloading url: %s\n", url.c_str()); log_write("downloading url: %s\n", url.c_str());
image.state = ImageDownloadState::Progress; image.state = ImageDownloadState::Progress;
DownloadFileAsync(url, path, "", [this, index, &image](std::vector<u8>& data, bool success) { curl::Api().ToFileAsync(
if (success) { curl::Url{url},
curl::Path{path},
curl::Flags{curl::Flag_Cache},
curl::OnComplete{[this, &image](auto& result) {
if (result.success) {
image.state = ImageDownloadState::Done; image.state = ImageDownloadState::Done;
log_write("downloaded themezer image\n"); // data hasn't changed
if (result.code == 304) {
image.cached = false;
}
} else { } else {
image.state = ImageDownloadState::Failed; image.state = ImageDownloadState::Failed;
log_write("failed to download image\n"); log_write("failed to download image\n");
} }
}, nullptr, DownloadPriority::High);
} }
});
} break; } break;
case ImageDownloadState::Progress: { case ImageDownloadState::Progress: {
} break; } break;
case ImageDownloadState::Done: { case ImageDownloadState::Done: {
loadThemeImage(theme); image.cached = false;
if (!loadThemeImage(theme)) {
image.state = ImageDownloadState::Failed;
} else {
image_load_count++;
}
} break; } break;
case ImageDownloadState::Failed: { case ImageDownloadState::Failed: {
} break; } break;
} }
} else {
gfx::drawImageRounded(vg, x + xoff, y, 320, 180, image.image);
}
} }
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->elements[text_id].colour, "%s", e.details.name.c_str()); 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()); gfx::drawTextArgs(vg, x + xoff, y + 180 + 55, 18, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s", e.creator.display_name.c_str());
} }
}
nvgRestore(vg); nvgRestore(vg);
});
} }
void Menu::OnFocusGained() { void Menu::OnFocusGained() {
@@ -691,7 +710,7 @@ void Menu::PackListDownload() {
SetSubHeading(subheading); SetSubHeading(subheading);
m_index = 0; m_index = 0;
m_start = 0; m_list->SetYoff(0);
// already downloaded // already downloaded
if (m_pages[m_page_index].m_ready != PageLoadState::None) { if (m_pages[m_page_index].m_ready != PageLoadState::None) {
@@ -706,15 +725,17 @@ void Menu::PackListDownload() {
config.order_index = m_order.Get(); config.order_index = m_order.Get();
config.nsfw = m_nsfw.Get(); config.nsfw = m_nsfw.Get();
const auto packList_url = apiBuildUrlListPacks(config); const auto packList_url = apiBuildUrlListPacks(config);
const auto themeList_url = apiBuildUrlThemeList(config); const auto packlist_path = apiBuildListPacksCache(config);
log_write("\npackList_url: %s\n\n", packList_url.c_str()); log_write("\npackList_url: %s\n\n", packList_url.c_str());
log_write("\nthemeList_url: %s\n\n", themeList_url.c_str());
DownloadClearCache(packList_url); curl::Api().ToFileAsync(
DownloadMemoryAsync(packList_url, "", [this, page_index](std::vector<u8>& data, bool success){ curl::Url{packList_url},
curl::Path{packlist_path},
curl::Flags{curl::Flag_Cache},
curl::OnComplete{[this, page_index](auto& result){
log_write("got themezer data\n"); log_write("got themezer data\n");
if (!success) { if (!result.success) {
auto& page = m_pages[page_index-1]; auto& page = m_pages[page_index-1];
page.m_ready = PageLoadState::Error; page.m_ready = PageLoadState::Error;
log_write("failed to get themezer data...\n"); log_write("failed to get themezer data...\n");
@@ -722,7 +743,7 @@ void Menu::PackListDownload() {
} }
PackList a; PackList a;
from_json(data, a); from_json(result.path, a);
m_pages.resize(a.pagination.page_count); m_pages.resize(a.pagination.page_count);
auto& page = m_pages[page_index-1]; auto& page = m_pages[page_index-1];
@@ -738,7 +759,8 @@ void Menu::PackListDownload() {
log_write("a.pagination.page: %u\n", a.pagination.page); log_write("a.pagination.page: %u\n", a.pagination.page);
log_write("a.pagination.page_count: %u\n", a.pagination.page_count); log_write("a.pagination.page_count: %u\n", a.pagination.page_count);
}, nullptr, DownloadPriority::High); }
});
} }
} // namespace sphaira::ui::menu::themezer } // namespace sphaira::ui::menu::themezer

View File

@@ -11,10 +11,6 @@ NotifEntry::NotifEntry(std::string text, Side side)
, m_side{side} { , m_side{side} {
} }
auto NotifEntry::OnLayoutChange() -> void {
}
auto NotifEntry::Draw(NVGcontext* vg, Theme* theme, float y) -> bool { auto NotifEntry::Draw(NVGcontext* vg, Theme* theme, float y) -> bool {
m_pos.y = y; m_pos.y = y;
Draw(vg, theme); Draw(vg, theme);
@@ -57,11 +53,6 @@ auto NotifEntry::Draw(NVGcontext* vg, Theme* theme) -> void {
gfx::drawText(vg, Vec2{m_pos.x + (m_pos.w / 2.f), m_pos.y + (m_pos.h / 2.f)}, font_size, text_col, m_text.c_str(), NVG_ALIGN_MIDDLE | NVG_ALIGN_CENTER); gfx::drawText(vg, Vec2{m_pos.x + (m_pos.w / 2.f), m_pos.y + (m_pos.h / 2.f)}, font_size, text_col, m_text.c_str(), NVG_ALIGN_MIDDLE | NVG_ALIGN_CENTER);
} }
auto NotifMananger::OnLayoutChange() -> void {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
}
auto NotifMananger::Draw(NVGcontext* vg, Theme* theme) -> void { auto NotifMananger::Draw(NVGcontext* vg, Theme* theme) -> void {
mutexLock(&m_mutex); mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); ON_SCOPE_EXIT(mutexUnlock(&m_mutex));

View File

@@ -77,6 +77,19 @@ inline void drawRectOutlineInternal(NVGcontext* vg, float size, const NVGcolor&
float gradientX, gradientY, color; float gradientX, gradientY, color;
getHighlightAnimation(&gradientX, &gradientY, &color); getHighlightAnimation(&gradientX, &gradientY, &color);
#if 1
// 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);
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, c, false);
#else
const auto strokeWidth = 5.0; const auto strokeWidth = 5.0;
auto v2 = vec; auto v2 = vec;
v2.x -= strokeWidth / 2.0; v2.x -= strokeWidth / 2.0;
@@ -85,8 +98,8 @@ inline void drawRectOutlineInternal(NVGcontext* vg, float size, const NVGcolor&
v2.h += strokeWidth; v2.h += strokeWidth;
const auto corner_radius = 0.5; const auto corner_radius = 0.5;
nvgSave(vg); // nvgSave(vg);
nvgResetScissor(vg); // nvgResetScissor(vg);
// const auto stroke_width = 5.0f; // const auto stroke_width = 5.0f;
// const auto shadow_corner_radius = 6.0f; // const auto shadow_corner_radius = 6.0f;
@@ -155,7 +168,8 @@ inline void drawRectOutlineInternal(NVGcontext* vg, float size, const NVGcolor&
nvgFillColor(vg, c); nvgFillColor(vg, c);
nvgFill(vg); nvgFill(vg);
nvgRestore(vg); // nvgRestore(vg);
#endif
} }
inline void drawRectOutlineInternal(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGpaint& p) { inline void drawRectOutlineInternal(NVGcontext* vg, float size, const NVGcolor& out_col, Vec4 vec, const NVGpaint& p) {
@@ -291,7 +305,7 @@ void textBounds(NVGcontext* vg, float x, float y, float *bounds, const char* str
void dimBackground(NVGcontext* vg) { void dimBackground(NVGcontext* vg) {
// drawRectIntenal(vg, {0.f,0.f,1280.f,720.f}, nvgRGBA(30,30,30,180)); // drawRectIntenal(vg, {0.f,0.f,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(20, 20, 20, 225), false);
drawRectIntenal(vg, {0.f,0.f,1920.f,1080.f}, nvgRGBA(0, 0, 0, 220), false); drawRectIntenal(vg, {0.f,0.f,1920.f,1080.f}, nvgRGBA(0, 0, 0, 230), false);
} }
void drawRect(NVGcontext* vg, float x, float y, float w, float h, Colour c, bool rounded) { void drawRect(NVGcontext* vg, float x, float y, float w, float h, Colour c, bool rounded) {
@@ -453,58 +467,47 @@ void drawTextArgs(NVGcontext* vg, float x, float y, float size, int align, Colou
drawTextIntenal(vg, {x, y}, size, buffer, nullptr, align, getColour(c)); drawTextIntenal(vg, {x, y}, size, buffer, nullptr, align, getColour(c));
} }
void drawButton(NVGcontext* vg, float x, float y, float size, Button button) { void drawScrollbar(NVGcontext* vg, Theme* theme, float x, float y, float h, u32 index_off, u32 count, u32 max_per_page) {
drawText(vg, x, y, size, getButton(button), nullptr, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, getColour(Colour::WHITE)); 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 drawButtons(NVGcontext* vg, const Widget::Actions& _actions, const NVGcolor& c, float start_x) { // only draw scrollbar if needed
nvgBeginPath(vg); if (entry_total > max_entry_display) {
nvgFontSize(vg, 24.f); const float sb_h = 1.f / (float)entry_total * h;
nvgTextAlign(vg, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP); const float sb_y = SCROLL;
nvgFillColor(vg, c); gfx::drawRect(vg, x, y, scc2, h, theme->elements[ThemeEntryID_GRID].colour, false);
gfx::drawRect(vg, x + scw, y + scw + sb_h * sb_y, scc2 - scw * 2, sb_h * float(max_entry_display) - scw * 2, theme->elements[ThemeEntryID_TEXT_SELECTED].colour, false);
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) { void drawScrollbar(NVGcontext* vg, Theme* theme, u32 index_off, u32 count, u32 max_per_page) {
if (action.IsHidden() || action.m_hint.empty()) { // drawScrollbar(vg, SCREEN_WIDTH - 50, 100, 500, index_off, count, max_per_page);
continue; drawScrollbar(vg, theme, SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200, index_off, count, max_per_page);
} }
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; void drawScrollbar2(NVGcontext* vg, Theme* theme, float x, float y, float h, s64 index_off, s64 count, s64 row, s64 page) {
nvgFontSize(vg, 26.f); // round up
nvgTextBounds(vg, x, y - 7.f, getButton(button), nullptr, bounds); if (count % row) {
len = bounds[2] - bounds[0]; count = count + (row - count % row);
nvgText(vg, x, y - 4.f, getButton(button), nullptr); }
x -= len + 34.f;
const float scc2 = 8.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->elements[ThemeEntryID_GRID].colour, false);
gfx::drawRect(vg, x + scw, y + scw + sb_h * sb_y, scc2 - scw * 2, sb_h * float(page) - scw * 2, theme->elements[ThemeEntryID_TEXT_SELECTED].colour, false);
} }
} }
// from gc installer void drawScrollbar2(NVGcontext* vg, Theme* theme, s64 index_off, s64 count, s64 row, s64 page) {
void drawDimBackground(NVGcontext* vg) { drawScrollbar2(vg, theme, SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200, index_off, count, row, page);
// drawRect(vg, 0, 0, 1920, 1080, nvgRGBA(20, 20, 20, 225));
drawRect(vg, 0, 0, 1920, 1080, nvgRGBA(0, 0, 0, 220));
} }
#define HIGHLIGHT_SPEED 350.0 #define HIGHLIGHT_SPEED 350.0

View File

@@ -45,7 +45,7 @@ OptionBox::OptionBox(const std::string& message, const Option& a, const Option&
} }
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, std::size_t index, Callback cb) OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, s64 index, Callback cb)
: m_message{message} : m_message{message}
, m_callback{cb} { , m_callback{cb} {
@@ -70,7 +70,7 @@ OptionBox::OptionBox(const std::string& message, const Option& a, const Option&
} }
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, std::size_t index, Callback cb) OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, s64 index, Callback cb)
: m_message{message} : m_message{message}
, m_callback{cb} { , m_callback{cb} {
@@ -79,26 +79,16 @@ OptionBox::OptionBox(const std::string& message, const Option& a, const Option&
auto OptionBox::Update(Controller* controller, TouchInfo* touch) -> void { auto OptionBox::Update(Controller* controller, TouchInfo* touch) -> void {
Widget::Update(controller, touch); Widget::Update(controller, touch);
// if (!controller->GotDown(Button::ANY_HORIZONTAL)) { if (touch->is_clicked) {
// return; for (s64 i = 0; i < m_entries.size(); i++) {
// } auto& e = m_entries[i];
if (touch->in_range(e.GetPos())) {
// const auto old_index = m_index; SetIndex(i);
FireAction(Button::A);
// if (controller->GotDown(Button::LEFT) && m_index) { break;
// m_index--; }
// } else if (controller->GotDown(Button::RIGHT) && m_index < (m_entries.size() - 1)) { }
// m_index++;
// }
// if (old_index != m_index) {
// m_entries[old_index].Selected(false);
// m_entries[m_index].Selected(true);
// }
} }
auto OptionBox::OnLayoutChange() -> void {
} }
auto OptionBox::Draw(NVGcontext* vg, Theme* theme) -> void { auto OptionBox::Draw(NVGcontext* vg, Theme* theme) -> void {
@@ -118,26 +108,30 @@ auto OptionBox::Draw(NVGcontext* vg, Theme* theme) -> void {
} }
} }
auto OptionBox::Setup(std::size_t index) -> void { auto OptionBox::OnFocusGained() noexcept -> void {
m_index = std::min(m_entries.size() - 1, index); Widget::OnFocusGained();
SetHidden(false);
}
auto OptionBox::OnFocusLost() noexcept -> void {
Widget::OnFocusLost();
SetHidden(true);
}
auto OptionBox::Setup(s64 index) -> void {
m_index = std::min<s64>(m_entries.size() - 1, index);
m_entries[m_index].Selected(true); m_entries[m_index].Selected(true);
m_spacer_line = Vec4{m_pos.x, m_pos.y + 220.f - 2.f, m_pos.w, 2.f}; m_spacer_line = Vec4{m_pos.x, m_pos.y + 220.f - 2.f, m_pos.w, 2.f};
SetActions( SetActions(
std::make_pair(Button::LEFT, Action{[this](){ std::make_pair(Button::LEFT, Action{[this](){
if (m_index) { if (m_index) {
m_entries[m_index].Selected(false); SetIndex(m_index - 1);
m_index--;
m_entries[m_index].Selected(true);
App::PlaySoundEffect(SoundEffect_Focus);
} }
}}), }}),
std::make_pair(Button::RIGHT, Action{[this](){ std::make_pair(Button::RIGHT, Action{[this](){
if (m_index < (m_entries.size() - 1)) { if (m_index < (m_entries.size() - 1)) {
m_entries[m_index].Selected(false); SetIndex(m_index + 1);
m_index++;
m_entries[m_index].Selected(true);
App::PlaySoundEffect(SoundEffect_Focus);
} }
}}), }}),
std::make_pair(Button::A, Action{[this](){ std::make_pair(Button::A, Action{[this](){
@@ -151,4 +145,12 @@ auto OptionBox::Setup(std::size_t index) -> void {
); );
} }
void OptionBox::SetIndex(s64 index) {
if (m_index != index) {
m_entries[m_index].Selected(false);
m_index = index;
m_entries[m_index].Selected(true);
}
}
} // namespace sphaira::ui } // namespace sphaira::ui

View File

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

View File

@@ -5,7 +5,7 @@
namespace sphaira::ui { namespace sphaira::ui {
PopupList::PopupList(std::string title, Items items, std::string& index_str_ref, std::size_t& index_ref) PopupList::PopupList(std::string title, Items items, std::string& index_str_ref, s64& index_ref)
: PopupList{std::move(title), std::move(items), Callback{}, index_ref} { : PopupList{std::move(title), std::move(items), Callback{}, index_ref} {
m_callback = [&index_str_ref, &index_ref, this](auto op_idx) { m_callback = [&index_str_ref, &index_ref, this](auto op_idx) {
@@ -22,7 +22,9 @@ PopupList::PopupList(std::string title, Items items, std::string& index_ref)
const auto it = std::find(m_items.cbegin(), m_items.cend(), index_ref); const auto it = std::find(m_items.cbegin(), m_items.cend(), index_ref);
if (it != m_items.cend()) { if (it != m_items.cend()) {
m_index = std::distance(m_items.cbegin(), it); m_index = std::distance(m_items.cbegin(), it);
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h); if (m_index >= 7) {
m_index_offset = m_index - 6;
}
} }
m_callback = [&index_ref, this](auto op_idx) { m_callback = [&index_ref, this](auto op_idx) {
@@ -32,7 +34,7 @@ PopupList::PopupList(std::string title, Items items, std::string& index_ref)
}; };
} }
PopupList::PopupList(std::string title, Items items, std::size_t& index_ref) PopupList::PopupList(std::string title, Items items, s64& index_ref)
: PopupList{std::move(title), std::move(items), Callback{}, index_ref} { : PopupList{std::move(title), std::move(items), Callback{}, index_ref} {
m_callback = [&index_ref, this](auto op_idx) { m_callback = [&index_ref, this](auto op_idx) {
@@ -47,28 +49,29 @@ PopupList::PopupList(std::string title, Items items, Callback cb, std::string in
const auto it = std::find(m_items.cbegin(), m_items.cend(), index); const auto it = std::find(m_items.cbegin(), m_items.cend(), index);
if (it != m_items.cend()) { if (it != m_items.cend()) {
m_index = std::distance(m_items.cbegin(), it); SetIndex(std::distance(m_items.cbegin(), it));
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h); if (m_index >= 7) {
m_index_offset = m_index - 6;
}
} }
} }
PopupList::PopupList(std::string title, Items items, Callback cb, std::size_t index) PopupList::PopupList(std::string title, Items items, Callback cb, s64 index)
: m_title{std::move(title)} : m_title{std::move(title)}
, m_items{std::move(items)} , m_items{std::move(items)}
, m_callback{cb} , m_callback{cb}
, m_index{index} { , m_index{index} {
this->SetActions(
m_pos.w = 1280.f; std::make_pair(Button::DOWN, Action{[this](){
const float a = std::min(405.f, (60.f * static_cast<float>(m_items.size()))); if (m_list->ScrollDown(m_index, 1, m_items.size())) {
m_pos.h = 80.f + 140.f + a; SetIndex(m_index);
m_pos.y = 720.f - m_pos.h; }
m_line_top = m_pos.y + 70.f; }}),
m_line_bottom = 720.f - 73.f; std::make_pair(Button::UP, Action{[this](){
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h); if (m_list->ScrollUp(m_index, 1, m_items.size())) {
SetIndex(m_index);
m_scrollbar.Setup(Vec4{1220.f, m_line_top, 1.f, m_line_bottom - m_line_top}, m_block.h, m_items.size()); }
}}),
SetActions(
std::make_pair(Button::A, Action{"Select"_i18n, [this](){ std::make_pair(Button::A, Action{"Select"_i18n, [this](){
if (m_callback) { if (m_callback) {
m_callback(m_index); m_callback(m_index);
@@ -76,48 +79,33 @@ PopupList::PopupList(std::string title, Items items, Callback cb, std::size_t in
SetPop(); SetPop();
}}), }}),
std::make_pair(Button::B, Action{"Back"_i18n, [this](){ std::make_pair(Button::B, Action{"Back"_i18n, [this](){
if (m_callback) {
m_callback(std::nullopt);
}
SetPop(); SetPop();
}}) }})
); );
m_pos.w = 1280.f;
const float a = std::min(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;
if (m_index >= 7) {
m_index_offset = m_index - 6;
}
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, 7, pos, v);
m_list->SetScrollBarPos(1250, m_line_top + 20, m_line_bottom - m_line_top - 40);
} }
auto PopupList::Update(Controller* controller, TouchInfo* touch) -> void { auto PopupList::Update(Controller* controller, TouchInfo* touch) -> void {
Widget::Update(controller, touch); Widget::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_items.size(), [this](auto i) {
if (!controller->GotDown(Button::ANY_VERTICAL)) { SetIndex(i);
return; FireAction(Button::A);
} });
const auto old_index = m_index;
if (controller->GotDown(Button::DOWN) && m_index < (m_items.size() - 1)) {
m_index++;
m_selected_y += m_block.h;
} else if (controller->GotDown(Button::UP) && m_index != 0) {
m_index--;
m_selected_y -= m_block.h;
}
if (old_index != m_index) {
App::PlaySoundEffect(SoundEffect_Scroll);
OnLayoutChange();
}
}
auto PopupList::OnLayoutChange() -> void {
if ((m_selected_y + m_block.h) > m_line_bottom) {
m_selected_y -= m_block.h;
m_index_offset++;
m_scrollbar.Move(ScrollBar::Direction::DOWN);
} else if (m_selected_y <= m_line_top) {
m_selected_y += m_block.h;
m_index_offset--;
m_scrollbar.Move(ScrollBar::Direction::UP);
}
// LOG("sely: %.2f, index_off: %lu\n", m_selected_y, m_index_offset);
} }
auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void { auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void {
@@ -127,16 +115,8 @@ auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void {
gfx::drawRect(vg, 30.f, m_line_top, m_line_width, 1.f, theme->elements[ThemeEntryID_TEXT].colour); gfx::drawRect(vg, 30.f, m_line_top, m_line_width, 1.f, theme->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, 30.f, m_line_bottom, m_line_width, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
// todo: cleanup m_list->Draw(vg, theme, m_items.size(), [this](auto* vg, auto* theme, auto v, auto i) {
const float x = m_block.x; const auto& [x, y, w, h] = v;
float y = m_line_top + 1.f + 42.f;
const float h = m_block.h;
const float w = m_block.w;
nvgSave(vg);
nvgScissor(vg, 0, m_line_top, 1280.f, m_line_bottom - m_line_top);
for (std::size_t i = m_index_offset; i < m_items.size(); ++i) {
if (m_index == i) { if (m_index == i) {
gfx::drawRect(vg, x - 4.f, y - 4.f, w + 8.f, h + 8.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour); gfx::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::drawRect(vg, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
@@ -146,15 +126,27 @@ auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void {
gfx::drawRect(vg, x, y + h, 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); 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);
} }
y += h; });
if (y > m_line_bottom) {
break;
}
}
nvgRestore(vg);
m_scrollbar.Draw(vg, theme);
Widget::Draw(vg, theme); Widget::Draw(vg, theme);
} }
auto PopupList::OnFocusGained() noexcept -> void {
Widget::OnFocusGained();
SetHidden(false);
}
auto PopupList::OnFocusLost() noexcept -> void {
Widget::OnFocusLost();
SetHidden(true);
}
void PopupList::SetIndex(s64 index) {
m_index = index;
if (m_index > m_index_offset && m_index - m_index_offset >= 6) {
m_index_offset = m_index - 6;
}
}
} // namespace sphaira::ui } // namespace sphaira::ui

View File

@@ -119,7 +119,7 @@ auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
return *this; return *this;
} }
auto ProgressBox::UpdateTransfer(u64 offset, u64 size) -> ProgressBox& { auto ProgressBox::UpdateTransfer(s64 offset, s64 size) -> ProgressBox& {
mutexLock(&m_mutex); mutexLock(&m_mutex);
m_size = size; m_size = size;
m_offset = offset; m_offset = offset;

View File

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

View File

@@ -7,13 +7,12 @@
namespace sphaira::ui { namespace sphaira::ui {
namespace { namespace {
struct SidebarSpacer : SidebarEntryBase { auto DistanceBetweenY(Vec4 va, Vec4 vb) -> Vec4 {
return Vec4{
}; va.x, va.y,
va.w, vb.y - va.y
struct SidebarHeader : SidebarEntryBase {
}; };
}
} // namespace } // namespace
@@ -127,7 +126,7 @@ SidebarEntryArray::SidebarEntryArray(std::string title, Items items, Callback cb
} }
} }
SidebarEntryArray::SidebarEntryArray(std::string title, Items items, Callback cb, std::size_t index) SidebarEntryArray::SidebarEntryArray(std::string title, Items items, Callback cb, s64 index)
: SidebarEntryBase{std::forward<std::string>(title)} : SidebarEntryBase{std::forward<std::string>(title)}
, m_items{std::move(items)} , m_items{std::move(items)}
, m_callback{cb} , m_callback{cb}
@@ -191,24 +190,12 @@ Sidebar::Sidebar(std::string title, std::string sub, Side side, Items&& items)
m_title_pos = Vec2{m_pos.x + 30.f, m_pos.y + 40.f}; m_title_pos = Vec2{m_pos.x + 30.f, m_pos.y + 40.f};
m_base_pos = Vec4{GetX() + 30.f, GetY() + 170.f, m_pos.w - (30.f * 2.f), 70.f}; m_base_pos = Vec4{GetX() + 30.f, GetY() + 170.f, m_pos.w - (30.f * 2.f), 70.f};
// each item has it's own Action, but we take over B // set button positions
SetAction(Button::B, Action{"Back"_i18n, [this](){ SetUiButtonPos({m_pos.x + m_pos.w - 60.f, 675});
SetPop();
}});
m_selected_y = m_base_pos.y; const Vec4 pos = DistanceBetweenY(m_top_bar, m_bottom_bar);
m_list = std::make_unique<List>(1, 6, pos, m_base_pos);
if (!m_items.empty()) { m_list->SetScrollBarPos(GetX() + GetW() - 20, m_base_pos.y - 10, pos.h - m_base_pos.y + 48);
// setup positions
m_selected_y = m_base_pos.y;
// for (auto&p : m_items) {
// p->SetPos(m_base_pos);
// m_base_pos.y += m_base_pos.h;
// }
// // give focus to first entry.
// m_items[m_index]->OnFocusGained();
}
} }
Sidebar::Sidebar(std::string title, std::string sub, Side side) Sidebar::Sidebar(std::string title, std::string sub, Side side)
@@ -217,46 +204,21 @@ Sidebar::Sidebar(std::string title, std::string sub, Side side)
auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void { auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void {
m_items[m_index]->Update(controller, touch);
Widget::Update(controller, touch); Widget::Update(controller, touch);
// if touched out of bounds, pop the sidebar and all widgets below it.
if (touch->is_clicked && !touch->in_range(GetPos())) {
App::PopToMenu();
} else {
m_list->OnUpdate(controller, touch, m_items.size(), [this](auto i) {
SetIndex(i);
FireAction(Button::A);
});
}
if (m_items[m_index]->ShouldPop()) { if (m_items[m_index]->ShouldPop()) {
SetPop(); SetPop();
} }
const auto old_index = m_index;
if (controller->GotDown(Button::ANY_DOWN) && m_index < (m_items.size() - 1)) {
m_index++;
m_selected_y += m_box_size.y;
} else if (controller->GotDown(Button::ANY_UP) && m_index != 0) {
m_index--;
m_selected_y -= m_box_size.y;
}
// if we moved
if (m_index != old_index) {
App::PlaySoundEffect(SoundEffect_Scroll);
m_items[old_index]->OnFocusLost();
m_items[m_index]->OnFocusGained();
// move offset
if ((m_selected_y + m_box_size.y) >= m_bottom_bar.y) {
m_selected_y -= m_box_size.y;
m_index_offset++;
// LOG("move down\n");
} else if (m_selected_y <= m_top_bar.y) {
// LOG("move up sely %.2f top %.2f\n", m_selected_y, m_top_bar.y);
m_selected_y += m_box_size.y;
m_index_offset--;
}
}
}
auto DistanceBetweenY(Vec4 va, Vec4 vb) -> Vec4 {
return Vec4{
va.x, va.y,
va.w, vb.y - va.y
};
} }
auto Sidebar::Draw(NVGcontext* vg, Theme* theme) -> void { auto Sidebar::Draw(NVGcontext* vg, Theme* theme) -> void {
@@ -268,26 +230,13 @@ auto Sidebar::Draw(NVGcontext* vg, Theme* theme) -> void {
gfx::drawRect(vg, m_top_bar, theme->elements[ThemeEntryID_TEXT].colour); 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_bottom_bar, theme->elements[ThemeEntryID_TEXT].colour);
const auto dist = DistanceBetweenY(m_top_bar, m_bottom_bar); Widget::Draw(vg, theme);
nvgSave(vg);
nvgScissor(vg, dist.x, dist.y, dist.w, dist.h);
// for (std::size_t i = m_index_offset; i < m_items.size(); ++i) { m_list->Draw(vg, theme, m_items.size(), [this](auto* vg, auto* theme, auto v, auto i) {
// m_items[i]->Draw(vg, theme); const auto& [x, y, w, h] = v;
// } m_items[i]->SetY(y);
m_items[i]->Draw(vg, theme);
for (auto&p : m_items) { });
p->Draw(vg, theme);
}
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);
} }
auto Sidebar::OnFocusGained() noexcept -> void { auto Sidebar::OnFocusGained() noexcept -> void {
@@ -303,23 +252,56 @@ auto Sidebar::OnFocusLost() noexcept -> void {
void Sidebar::Add(std::shared_ptr<SidebarEntryBase> entry) { void Sidebar::Add(std::shared_ptr<SidebarEntryBase> entry) {
m_items.emplace_back(entry); m_items.emplace_back(entry);
m_items.back()->SetPos(m_base_pos); m_items.back()->SetPos(m_base_pos);
m_base_pos.y += m_base_pos.h;
// for (auto&p : m_items) {
// p->SetPos(base_pos);
// m_base_pos.y += m_base_pos.h;
// }
// give focus to first entry. // give focus to first entry.
if (m_items.size() == 1) {
m_items[m_index]->OnFocusGained(); 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();
if (m_index > m_index_offset && m_index - m_index_offset >= 5) {
m_index_offset = m_index - 5;
} }
void Sidebar::AddHeader(std::string name) { SetupButtons();
}
}
void Sidebar::SetupButtons() {
RemoveActions();
// add entry actions
for (const auto& [button, action] : m_items[m_index]->GetActions()) {
SetAction(button, action);
}
// add default actions, overriding if needed.
this->SetActions(
std::make_pair(Button::DOWN, Action{[this](){
auto index = m_index;
if (m_list->ScrollDown(index, 1, m_items.size())) {
SetIndex(index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
auto index = m_index;
if (m_list->ScrollUp(index, 1, m_items.size())) {
SetIndex(index);
}
}}),
// each item has it's own Action, but we take over B
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}})
);
} }
} // namespace sphaira::ui } // namespace sphaira::ui

View File

@@ -1,9 +1,22 @@
#include "ui/widget.hpp" #include "ui/widget.hpp"
#include "ui/nvg_util.hpp" #include "ui/nvg_util.hpp"
#include "app.hpp" #include "app.hpp"
#include "log.hpp"
namespace sphaira::ui { 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->elements[ThemeEntryID_TEXT].colour);
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) { void Widget::Update(Controller* controller, TouchInfo* touch) {
for (const auto& [button, action] : m_actions) { for (const auto& [button, action] : m_actions) {
if ((action.m_type & ActionType::DOWN) && controller->GotDown(button)) { if ((action.m_type & ActionType::DOWN) && controller->GotDown(button)) {
@@ -11,28 +24,36 @@ void Widget::Update(Controller* controller, TouchInfo* touch) {
App::PlaySoundEffect(SoundEffect_Focus); App::PlaySoundEffect(SoundEffect_Focus);
} }
action.Invoke(true); action.Invoke(true);
break;
} }
else if ((action.m_type & ActionType::UP) && controller->GotUp(button)) { else if ((action.m_type & ActionType::UP) && controller->GotUp(button)) {
action.Invoke(false); action.Invoke(false);
break;
} }
else if ((action.m_type & ActionType::HELD) && controller->GotHeld(button)) { else if ((action.m_type & ActionType::HELD) && controller->GotHeld(button)) {
action.Invoke(true); 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) { void Widget::Draw(NVGcontext* vg, Theme* theme) {
Actions draw_actions; auto draw_actions = GetUiButtons();
for (const auto& [button, action] : m_actions) { for (auto& e : draw_actions) {
if (!action.IsHidden()) { e.Draw(vg, theme);
draw_actions.emplace(button, action);
} }
} }
gfx::drawButtons(vg, draw_actions, theme->elements[ThemeEntryID_TEXT].colour);
}
auto Widget::HasAction(Button button) const -> bool { auto Widget::HasAction(Button button) const -> bool {
return m_actions.contains(button); return m_actions.contains(button);
} }
@@ -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 } // namespace sphaira::ui