Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70d2e9873c | ||
|
|
705947fefb | ||
|
|
f48f9a527f | ||
|
|
cf95128f0b | ||
|
|
6dbf48d73c | ||
|
|
1614c8e2e4 | ||
|
|
cdebcad4fe | ||
|
|
f824187248 | ||
|
|
54c63d6f3b | ||
|
|
d840a8ddba | ||
|
|
c3b31d0fdd | ||
|
|
dd1a6eb25b | ||
|
|
271fab66f5 | ||
|
|
87642e914e | ||
|
|
45aa7c4e62 | ||
|
|
9b1788d1ec | ||
|
|
389a4cfef5 | ||
|
|
ac06631156 | ||
|
|
bc39e668eb | ||
|
|
e452615c77 | ||
|
|
588eb01379 | ||
|
|
4855a01f1a | ||
|
|
cb7fb0e506 | ||
|
|
cdb38f27a7 | ||
|
|
7804bbbcbc | ||
|
|
5db5f93af1 | ||
|
|
bab4bfce84 | ||
|
|
ec06763e50 | ||
|
|
5e315bd65f | ||
|
|
2edfe91ad6 | ||
|
|
7005118876 | ||
|
|
087d44fb40 | ||
|
|
e3722f2591 | ||
|
|
47855ce7b4 | ||
|
|
ec7caabdbd | ||
|
|
adf0a3b2cd | ||
|
|
f88e354ae8 | ||
|
|
df3d8d3990 |
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: ITotalJustice
|
||||
patreon: totaljustice
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: totaljustice
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
10
.github/workflows/build_presets.yml
vendored
10
.github/workflows/build_presets.yml
vendored
@@ -1,10 +1,6 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -12,7 +8,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
preset: [Release, RelWithDebInfo, MinSizeRel, Debug]
|
||||
preset: [MinSizeRel]
|
||||
runs-on: ${{ matrix.os }}
|
||||
container: devkitpro/devkita64:latest
|
||||
|
||||
@@ -24,7 +20,7 @@ jobs:
|
||||
|
||||
- name: Configure CMake
|
||||
run: |
|
||||
cmake --preset ${{ matrix.preset }}
|
||||
cmake --preset ${{ matrix.preset }} -DUSE_VFS_GC=0
|
||||
|
||||
- name: Build
|
||||
run: cmake --build --preset ${{ matrix.preset }} --parallel 4
|
||||
|
||||
@@ -26,7 +26,8 @@ please include:
|
||||
|
||||
## ftp
|
||||
|
||||
ftp can be enabled via the network menu and listens on port 5000, no username or password is required.
|
||||
ftp can be enabled via the network menu. It uses the same config as ftpsrv `/config/ftpsrv/config.ini`. [See here for the full list
|
||||
of all configs available](https://github.com/ITotalJustice/ftpsrv/blob/master/assets/config.ini.template).
|
||||
|
||||
## mtp
|
||||
|
||||
|
||||
8
assets/romfs/github/ftpsrv.json
Normal file
8
assets/romfs/github/ftpsrv.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"url": "https://github.com/ITotalJustice/ftpsrv",
|
||||
"assets": [
|
||||
{
|
||||
"name": "switch"
|
||||
}
|
||||
]
|
||||
}
|
||||
3
assets/romfs/github/sphaira.json
Normal file
3
assets/romfs/github/sphaira.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"url": "https://github.com/ITotalJustice/sphaira"
|
||||
}
|
||||
3
assets/romfs/github/untitled.json
Normal file
3
assets/romfs/github/untitled.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"url": "https://github.com/ITotalJustice/untitled"
|
||||
}
|
||||
@@ -62,6 +62,8 @@
|
||||
"Music": "Musik",
|
||||
"Network": "Netzwerk",
|
||||
"Network Options": "Netzwerk-Optionen",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink verbunden",
|
||||
"Nxlink Upload": "Nxlink Upload",
|
||||
@@ -199,6 +201,10 @@
|
||||
"Bad Page": "Ungültige Seite",
|
||||
"Download theme?": "Theme herunterladen?",
|
||||
|
||||
"GitHub": "",
|
||||
"Downloading json": "",
|
||||
"Select asset to download for ": "",
|
||||
|
||||
"Installing ": "Installiere ",
|
||||
"Uninstalling ": "Deinstalliere ",
|
||||
"Deleting ": "Lösche ",
|
||||
@@ -211,6 +217,8 @@
|
||||
"Copying ": "Kopiere ",
|
||||
"Trying to load ": "Lade ",
|
||||
"Downloading ": "Lade herunter ",
|
||||
"Downloaded ": "",
|
||||
"Removed ": "",
|
||||
"Checking MD5": "Prüfe MD5",
|
||||
"Loading...": "Lade...",
|
||||
"Loading": "Lade",
|
||||
|
||||
@@ -62,6 +62,8 @@
|
||||
"Music": "Music",
|
||||
"Network": "Network",
|
||||
"Network Options": "Network Options",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink Connected",
|
||||
"Nxlink Upload": "Nxlink Upload",
|
||||
@@ -199,6 +201,10 @@
|
||||
"Bad Page": "Bad Page",
|
||||
"Download theme?": "Download theme?",
|
||||
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Downloading json",
|
||||
"Select asset to download for ": "Select asset to download for ",
|
||||
|
||||
"Installing ": "Installing ",
|
||||
"Uninstalling ": "Uninstalling ",
|
||||
"Deleting ": "Deleting ",
|
||||
@@ -211,6 +217,8 @@
|
||||
"Copying ": "Copying ",
|
||||
"Trying to load ": "Trying to load ",
|
||||
"Downloading ": "Downloading ",
|
||||
"Downloaded ": "Downloaded ",
|
||||
"Removed ": "Removed ",
|
||||
"Checking MD5": "Checking MD5",
|
||||
"Loading...": "Loading...",
|
||||
"Loading": "Loading",
|
||||
|
||||
@@ -9,23 +9,23 @@
|
||||
"OK": "OK",
|
||||
"Back": "Atrás",
|
||||
"Select": "Seleccionar",
|
||||
"Open": "Abierto",
|
||||
"Open": "Abrir",
|
||||
"Launch": "Ejecutar",
|
||||
"Info": "Información",
|
||||
"Install": "Instalar",
|
||||
"Delete": "Borrar",
|
||||
"Restart": "",
|
||||
"Changelog": "Log de Cambios",
|
||||
"Restart": "Reiniciar",
|
||||
"Changelog": "Log de cambios",
|
||||
"Details": "Detalles",
|
||||
"Update": "Actualizar",
|
||||
"Remove": "Borrar",
|
||||
"Download": "Descargar",
|
||||
"Next Page": "Página siguiente",
|
||||
"Prev Page": "Página anterior",
|
||||
"Unstar": "",
|
||||
"Star": "",
|
||||
"System memory": "",
|
||||
"microSD card": "",
|
||||
"Unstar": "Quitar favorito",
|
||||
"Star": "Favorito",
|
||||
"System memory": "Memoria de sistema",
|
||||
"microSD card": "microSD",
|
||||
"Yes": "Sí",
|
||||
"No": "No",
|
||||
"Enabled": "Activado",
|
||||
@@ -38,12 +38,12 @@
|
||||
"Order": "Orden",
|
||||
"Search": "Buscar",
|
||||
"Updated": "Actualizado",
|
||||
"Updated (Star)": "Actualizado (Star)",
|
||||
"Updated (Star)": "Actualizado (favorito)",
|
||||
"Downloads": "Descargas",
|
||||
"Size": "Tamaño",
|
||||
"Size (Star)": "Tamaño (Star)",
|
||||
"Size (Star)": "Tamaño (favorito)",
|
||||
"Alphabetical": "Alfabético",
|
||||
"Alphabetical (Star)": "Alfabético (Star)",
|
||||
"Alphabetical (Star)": "Alfabético (favorito)",
|
||||
"Likes": "Me Gusta",
|
||||
"ID": "ID",
|
||||
"Decending": "Descendente",
|
||||
@@ -53,21 +53,23 @@
|
||||
"Ascending (Up)": "Ascendente (arriba)",
|
||||
"Asc": "Ascendente",
|
||||
|
||||
"Menu Options": "Opciones de Menú",
|
||||
"Header": "Encabezamiento",
|
||||
"Menu Options": "Opciones de menú",
|
||||
"Header": "Encabezado",
|
||||
"Theme": "Tema",
|
||||
"Theme Options": "Opciones de Tema",
|
||||
"Select Theme": "Seleccionar Tema",
|
||||
"Theme Options": "Opciones de tema",
|
||||
"Select Theme": "Seleccionar tema",
|
||||
"Shuffle": "Barajar",
|
||||
"Music": "Música",
|
||||
"Network": "Red",
|
||||
"Network Options": "Opciones de Red",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink Conectado",
|
||||
"Nxlink Upload": "Nxlink Subida",
|
||||
"Nxlink Finished": "Nxlink Finalizado",
|
||||
"Switch-Handheld!": "",
|
||||
"Switch-Docked!": "",
|
||||
"Network Options": "Opciones de red",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "NXlink",
|
||||
"Nxlink Connected": "NXlink conectado",
|
||||
"Nxlink Upload": "NXlink subida",
|
||||
"Nxlink Finished": "NXlink finalizado",
|
||||
"Switch-Handheld!": "¡Switch-Modo-Portátil!",
|
||||
"Switch-Docked!": "¡Switch-Modo-TV!",
|
||||
"Language": "Idioma",
|
||||
"Auto": "Automático",
|
||||
"English": "English",
|
||||
@@ -82,41 +84,41 @@
|
||||
"Portuguese": "Português",
|
||||
"Russian": "Русский",
|
||||
"Swedish": "Svenska",
|
||||
"Logging": "Explotación florestal",
|
||||
"Logging": "Registros",
|
||||
"Replace hbmenu on exit": "Reemplazar hbmenu al salir",
|
||||
"Misc": "Varios",
|
||||
"Misc Options": "Opciones varias",
|
||||
"Web": "Web",
|
||||
"Install forwarders": "",
|
||||
"Install location": "",
|
||||
"Show install warning": "",
|
||||
"Install forwarders": "Instalar forwarders",
|
||||
"Install location": "Ruta de instalación ",
|
||||
"Show install warning": "Mostrar precaución de instalación",
|
||||
|
||||
"FileBrowser": "Explorador de Archivos",
|
||||
"FileBrowser": "Explorador de archivos",
|
||||
"%zd files": "%zd files",
|
||||
"%zd dirs": "%zd dirs",
|
||||
"File Options": "Opciones de Tema",
|
||||
"Show Hidden": "Mostrar Oculto",
|
||||
"File Options": "Opciones de archivo",
|
||||
"Show Hidden": "Mostrar archivos ocultos",
|
||||
"Folders First": "Carpetas primero",
|
||||
"Hidden Last": "Oculto último",
|
||||
"Cut": "Cortar ",
|
||||
"Hidden Last": "Ocultos al final",
|
||||
"Cut": "Cortar",
|
||||
"Copy": "Copiar",
|
||||
"Paste": "Pegar",
|
||||
"Paste ": "Pegar ",
|
||||
" file(s)?": " ¿archivo(s)?",
|
||||
"Rename": "Renombrar",
|
||||
"Set New File Name": "Establecer Nuevo Nombre de Archivo",
|
||||
"Set New File Name": "Establecer nuevo nombre de archivo",
|
||||
"Advanced": "Avanzado",
|
||||
"Advanced Options": "Opciones Avanzadas",
|
||||
"Advanced Options": "Opciones avanzadas",
|
||||
"Create File": "Crear archivo",
|
||||
"Set File Name": "Establecer Nombre de Archivo",
|
||||
"Set File Name": "Establecer nombre de archivo",
|
||||
"Create Folder": "Crear carpeta",
|
||||
"Set Folder Name": "Establecer Nombre de Carpeta",
|
||||
"Set Folder Name": "Establecer nombre de carpeta",
|
||||
"View as text (unfinished)": "Ver como texto (sin terminar)",
|
||||
"Empty...": "Vacío...",
|
||||
"Open with DayBreak?": "Abrir con DayBreak",
|
||||
"Launch ": "",
|
||||
"Launch option for: ": "Opción de ejecución para: ",
|
||||
"Select launcher for: ": "",
|
||||
"Open with DayBreak?": "¿Abrir con DayBreak?",
|
||||
"Launch ": "Abrir ",
|
||||
"Launch option for: ": "Opción de abrir con: ",
|
||||
"Select launcher for: ": "Seleccionar abrir con: ",
|
||||
|
||||
"Homebrew": "Honebrew",
|
||||
"Homebrew Options": "Opciones de Homebrew",
|
||||
@@ -128,16 +130,16 @@
|
||||
"Creating Control": "Creando Control",
|
||||
"Creating Meta": "Creando Meta",
|
||||
"Writing Nca": "Creando NCA",
|
||||
"Updating ncm databse": "Actualizando base de datos ncm ",
|
||||
"Pushing application record": "",
|
||||
"Updating ncm databse": "Actualizando base de datos ncm",
|
||||
"Pushing application record": "Registro de aplicación",
|
||||
"Installed!": "¡Instalado!",
|
||||
"Failed to install forwarder": "Fallo al instalar forwarder",
|
||||
"Unstarred ": "",
|
||||
"Starred ": "",
|
||||
"Unstarred ": "Quitar Favorito",
|
||||
"Starred ": "Favorito",
|
||||
|
||||
"AppStore": "AppStore",
|
||||
"AppStore": "Tienda",
|
||||
"Filter: %s | Sort: %s | Order: %s": "Filtrar: %s | Clasificar: %s | Orden: %s",
|
||||
"AppStore Options": "Opciones de la AppStore",
|
||||
"AppStore Options": "Opciones de la Tienda",
|
||||
"All": "Todo",
|
||||
"Games": "Juegos",
|
||||
"Emulators": "Emuladores",
|
||||
@@ -145,60 +147,64 @@
|
||||
"Themes": "Temas",
|
||||
"Legacy": "Legado",
|
||||
"version: %s": "version: %s",
|
||||
"updated: %s": "updated: %s",
|
||||
"category: %s": "category: %s",
|
||||
"extracted: %.2f MiB": "extracted: %.2f MiB",
|
||||
"updated: %s": "actualizado: %s",
|
||||
"category: %s": "categoría: %s",
|
||||
"extracted: %.2f MiB": "extraído: %.2f MiB",
|
||||
"app_dls: %s": "app_dls: %s",
|
||||
"More by Author": "Mostrar mas del Autor",
|
||||
"Leave Feedback": "Dejar Mensaje",
|
||||
|
||||
"Irs": "IRS",
|
||||
"Ambient Noise Level: ": "Nivel de Ruido",
|
||||
"Ambient Noise Level: ": "Nivel de Ruido Ambiente",
|
||||
"Controller": "Control",
|
||||
"Pad ": "Almohadilla ",
|
||||
" (Available)": " (Disponible)",
|
||||
" (Unsupported)": "",
|
||||
" (Unsupported)": "(No Compatible)",
|
||||
" (Unconnected)": " (Desconectado)",
|
||||
"HandHeld": "Portátil",
|
||||
"Rotation": "Rotación",
|
||||
"0 (Sideways)": "0 (De Lado)",
|
||||
"0 (Sideways)": "0 (De lado)",
|
||||
"90 (Flat)": "90 (Plano)",
|
||||
"180 (-Sideways)": "180 (-De Lado)",
|
||||
"270 (Upside down)": "270 (Al Revés)",
|
||||
"180 (-Sideways)": "180 (-De lado)",
|
||||
"270 (Upside down)": "270 (Al revés)",
|
||||
"Colour": "Color",
|
||||
"Grey": "Gris",
|
||||
"Ironbow": "Paleta Térmica",
|
||||
"Ironbow": "Paleta térmica",
|
||||
"Green": "Verde",
|
||||
"Red": "Rojo",
|
||||
"Blue": "Azul",
|
||||
"Light Target": "Objetivo de Luz",
|
||||
"Light Target": "Objetivo de luz",
|
||||
"All leds": "Todos los leds",
|
||||
"Bright group": "Grupo brillante",
|
||||
"Bright group": "Grupo brillo",
|
||||
"Dim group": "Grupo tenue",
|
||||
"None": "Ninguno",
|
||||
"Gain": "Ganancia",
|
||||
"Negative Image": "Imagen Negativa",
|
||||
"Normal image": "Imagen Normal",
|
||||
"Negative image": "Imagen Negativa",
|
||||
"Negative Image": "Imagen negativa",
|
||||
"Normal image": "Imagen normal",
|
||||
"Negative image": "Imagen negativa",
|
||||
"Format": "Formato",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"Trimming Format": "Formato de Recorte",
|
||||
"External Light Filter": "Filtro de Luz Externa",
|
||||
"Load Default": "Cargar Predeterminado",
|
||||
"Trimming Format": "Formato de recorte",
|
||||
"External Light Filter": "Filtro de luz externa",
|
||||
"Load Default": "Cargar predeterminado",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Opciones de Themezer",
|
||||
"Nsfw": "NSFW",
|
||||
"Page": "Página",
|
||||
"Page %zu / %zu": "Page %zu / %zu",
|
||||
"Page %zu / %zu": "Pág. %zu / %zu",
|
||||
"Enter Page Number": "Ingresar Número de Página",
|
||||
"Bad Page": "Página Errónea",
|
||||
"Download theme?": "¿Descargar Tema?",
|
||||
|
||||
"GitHub": "",
|
||||
"Downloading json": "",
|
||||
"Select asset to download for ": "",
|
||||
|
||||
"Installing ": "Instalando ",
|
||||
"Uninstalling ": "Desinstalando ",
|
||||
"Deleting ": "Borrando ",
|
||||
@@ -209,22 +215,24 @@
|
||||
"Scanning ": "Escaneando ",
|
||||
"Creating ": "Creando ",
|
||||
"Copying ": "Copiando ",
|
||||
"Trying to load ": "",
|
||||
"Trying to load ": "Intentando cargar",
|
||||
"Downloading ": "Descargando ",
|
||||
"Checking MD5": "Chqueando MD5",
|
||||
"Downloaded ": "",
|
||||
"Removed ": "",
|
||||
"Checking MD5": "Chequeando MD5",
|
||||
"Loading...": "Cargando...",
|
||||
"Loading": "Cargando",
|
||||
"Empty!": "¡Vacío!",
|
||||
"Not Ready...": "No Listo Aún...",
|
||||
"Not Ready...": "No listo aún...",
|
||||
"Error loading page!": "¡Error cargando la página!",
|
||||
"Update avaliable: ": "Actualización disponible: ",
|
||||
"Download update: ": "Descargar actualización: ",
|
||||
"Updated to ": "",
|
||||
"Restart Sphaira?": "",
|
||||
"Updated to ": "Actualizado a ",
|
||||
"Restart Sphaira?": "¿Reiniciar Sphaira?",
|
||||
"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",
|
||||
"Are you sure you want to delete ": "¿Estás seguro que quieres eliminar? ",
|
||||
"Are you sure you wish to cancel?": "¿Estás seguro que deseas cancelar?",
|
||||
"If this message appears repeatedly, please open an issue.": ""
|
||||
"If this message appears repeatedly, please open an issue.": "Si este mensaje aparece repetidamente, por favor abrir un 'issue'."
|
||||
}
|
||||
@@ -62,6 +62,8 @@
|
||||
"Music": "Musique",
|
||||
"Network": "Réseau",
|
||||
"Network Options": "Options Réseau",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink Connecté",
|
||||
"Nxlink Upload": "Nxlink téléversement",
|
||||
@@ -199,6 +201,10 @@
|
||||
"Bad Page": "Page inexistante",
|
||||
"Download theme?": "Télécharger le thème?",
|
||||
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Téléchargement du json",
|
||||
"Select asset to download for ": "Sélectionner l'asset pour télécharger ",
|
||||
|
||||
"Installing ": "Installation ",
|
||||
"Uninstalling ": "Désinstallation ",
|
||||
"Deleting ": "Suppression ",
|
||||
@@ -211,6 +217,8 @@
|
||||
"Copying ": "Copie ",
|
||||
"Trying to load ": "Tente de charger ",
|
||||
"Downloading ": "Téléchargement ",
|
||||
"Downloaded ": "Téléchargé",
|
||||
"Removed ": "Supprimé ",
|
||||
"Checking MD5": "Vérification MD5",
|
||||
"Loading...": "Chargement...",
|
||||
"Loading": "Chargement",
|
||||
@@ -227,4 +235,4 @@
|
||||
"Are you sure you want to delete ": "Êtes-vous sûr de vouloir supprimer ",
|
||||
"Are you sure you wish to cancel?": "Souhaitez-vous vraiment annuler?",
|
||||
"If this message appears repeatedly, please open an issue.": "Si ce message apparait en boucle veuillez ouvrir une issue."
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,8 @@
|
||||
"Music": "Musica",
|
||||
"Network": "Rete",
|
||||
"Network Options": "Opzioni di rete",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "",
|
||||
"Nxlink Upload": "",
|
||||
@@ -199,6 +201,10 @@
|
||||
"Bad Page": "",
|
||||
"Download theme?": "",
|
||||
|
||||
"GitHub": "",
|
||||
"Downloading json": "",
|
||||
"Select asset to download for ": "",
|
||||
|
||||
"Installing ": "",
|
||||
"Uninstalling ": "",
|
||||
"Deleting ": "",
|
||||
@@ -211,6 +217,8 @@
|
||||
"Copying ": "",
|
||||
"Trying to load ": "",
|
||||
"Downloading ": "",
|
||||
"Downloaded ": "",
|
||||
"Removed ": "",
|
||||
"Checking MD5": "",
|
||||
"Loading...": "",
|
||||
"Loading": "",
|
||||
|
||||
@@ -62,6 +62,8 @@
|
||||
"Music": "BGM",
|
||||
"Network": "ネットワーク",
|
||||
"Network Options": "ネットワーク設定",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink 接続",
|
||||
"Nxlink Upload": "Nxlink アップロード",
|
||||
@@ -199,6 +201,10 @@
|
||||
"Bad Page": "ページが見つかりません",
|
||||
"Download theme?": "テーマをインストールしますか?",
|
||||
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "JSONからダウンロード",
|
||||
"Select asset to download for ": "ダウンロードアイテムを選択 ",
|
||||
|
||||
"Installing ": "インストール中 ",
|
||||
"Uninstalling ": "アンインストール中 ",
|
||||
"Deleting ": "削除中 ",
|
||||
@@ -211,6 +217,8 @@
|
||||
"Copying ": "コピー中 ",
|
||||
"Trying to load ": "サムネイルを取得中 ",
|
||||
"Downloading ": "ダウンロード中 ",
|
||||
"Downloaded ": "ダウンロード完了 ",
|
||||
"Removed ": "除去完了 ",
|
||||
"Checking MD5": "MD5を確認中 ",
|
||||
"Loading...": "ロード中",
|
||||
"Loading": "ロード中",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"[Applet Mode]": "[애플릿 모드]",
|
||||
"No Internet": "네트워크 연결 없음",
|
||||
"No Internet": "인터넷 연결 없음",
|
||||
"Files": "파일 탐색기",
|
||||
"Apps": "홈브류",
|
||||
"Store": "앱스토어",
|
||||
@@ -32,7 +32,7 @@
|
||||
"Disabled": "",
|
||||
|
||||
"Sort By": "정렬",
|
||||
"Sort Options": "정렬 설정",
|
||||
"Sort Options": "정렬 옵션",
|
||||
"Filter": "필터",
|
||||
"Sort": "분류",
|
||||
"Order": "정렬",
|
||||
@@ -45,7 +45,7 @@
|
||||
"Alphabetical": "알파벳순",
|
||||
"Alphabetical (Star)": "알파벳순 (즐겨찾기)",
|
||||
"Likes": "좋아요순",
|
||||
"ID": "작성자순",
|
||||
"ID": "ID순",
|
||||
"Decending": "내림차순",
|
||||
"Descending (down)": "내림차순",
|
||||
"Desc": "내림차순",
|
||||
@@ -56,18 +56,20 @@
|
||||
"Menu Options": "메뉴",
|
||||
"Header": "헤더",
|
||||
"Theme": "테마",
|
||||
"Theme Options": "테마 설정",
|
||||
"Theme Options": "테마 옵션",
|
||||
"Select Theme": "테마 선택",
|
||||
"Shuffle": "셔플",
|
||||
"Music": "BGM",
|
||||
"Network": "네트워크",
|
||||
"Network Options": "네트워크 설정",
|
||||
"Network Options": "네트워크 옵션",
|
||||
"Ftp": "FTP (무선)",
|
||||
"Mtp": "MTP (유선)",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink 연결됨",
|
||||
"Nxlink Upload": "Nxlink 업로드",
|
||||
"Nxlink Finished": "Nxlink 종료됨",
|
||||
"Switch-Handheld!": "휴대모드로 전환되었습니다!",
|
||||
"Switch-Docked!": "독 모드로 전환되었습니다!",
|
||||
"Switch-Handheld!": "휴대모드로 전환됨!",
|
||||
"Switch-Docked!": "독 모드로 전환됨!",
|
||||
"Language": "언어",
|
||||
"Auto": "자동",
|
||||
"English": "English",
|
||||
@@ -87,14 +89,14 @@
|
||||
"Misc": "기타",
|
||||
"Misc Options": "기타",
|
||||
"Web": "웹 브라우저",
|
||||
"Install forwarders": "바로가기 설치 기능",
|
||||
"Install location": "설치 경로",
|
||||
"Show install warning": "바로가기 설치 경고문 표시",
|
||||
"Install forwarders": "바로가기 설치",
|
||||
"Install location": "설치 위치",
|
||||
"Show install warning": "설치 경고 표시",
|
||||
|
||||
"FileBrowser": "파일 탐색기",
|
||||
"%zd files": "%zd개의 파일",
|
||||
"%zd dirs": "%zd개의 폴더",
|
||||
"File Options": "파일 설정",
|
||||
"%zd files": "%zd 개 파일",
|
||||
"%zd dirs": "%zd 개 폴더",
|
||||
"File Options": "파일 옵션",
|
||||
"Show Hidden": "숨겨진 항목 표시",
|
||||
"Folders First": "폴더 우선 정렬",
|
||||
"Hidden Last": "숨겨진 항목 후순 정렬",
|
||||
@@ -102,34 +104,34 @@
|
||||
"Copy": "복사",
|
||||
"Paste": "붙여넣기",
|
||||
"Paste ": " ",
|
||||
" file(s)?": "개 항목을 붙여넣으시겠습니까?",
|
||||
" file(s)?": "개 항목을 붙여넣을까요?",
|
||||
"Rename": "이름 바꾸기",
|
||||
"Set New File Name": "새 파일명 입력",
|
||||
"Advanced": "고급",
|
||||
"Advanced Options": "고급 설정",
|
||||
"Create File": "파일 생성",
|
||||
"Advanced Options": "고급 옵션",
|
||||
"Create File": "새 파일",
|
||||
"Set File Name": "파일명 입력",
|
||||
"Create Folder": "새 폴더",
|
||||
"Set Folder Name": "폴더명 입력",
|
||||
"View as text (unfinished)": "텍스트로 보기 (미완성)",
|
||||
"Empty...": "비어있습니다...",
|
||||
"Open with DayBreak?": "DayBreak로 여시겠습니까?",
|
||||
"Launch ": "실행하시겠습니까 ",
|
||||
"Launch option for: ": "실행 설정: ",
|
||||
"Empty...": "비어있음...",
|
||||
"Open with DayBreak?": "DayBreak로 열까요?",
|
||||
"Launch ": "실행할까요 ",
|
||||
"Launch option for: ": "실행 옵션: ",
|
||||
"Select launcher for: ": "실행 런처: ",
|
||||
|
||||
"Homebrew": "홈브류",
|
||||
"Homebrew Options": "홈브류 설정",
|
||||
"Homebrew Options": "홈브류 옵션",
|
||||
"Hide Sphaira": "Sphaira 숨기기",
|
||||
"Install Forwarder": "바로가기 설치",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "주의: 시스낸드에서 바로가기 설치시 BAN 위험이 있습니다!",
|
||||
"Installing Forwarder": "바로가기 설치중...",
|
||||
"Creating Program": "프로그램 작성중...",
|
||||
"Creating Control": "컨트롤 작성중...",
|
||||
"Creating Meta": "메타 작성중...",
|
||||
"Writing Nca": "Nca 쓰는중...",
|
||||
"Updating ncm databse": "ncm 데이터베이스 업데이트중...",
|
||||
"Pushing application record": "응용 프로그램 기록 푸시중...",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "경고: 시스낸드에서 바로가기 설치시 밴 위험이 있습니다!",
|
||||
"Installing Forwarder": "바로가기 설치",
|
||||
"Creating Program": "프로그램 생성",
|
||||
"Creating Control": "컨트롤 생성",
|
||||
"Creating Meta": "메타 생성",
|
||||
"Writing Nca": "Nca 쓰기",
|
||||
"Updating ncm databse": "Ncm 데이터베이스 업데이트",
|
||||
"Pushing application record": "응용 프로그램 기록 푸싱",
|
||||
"Installed!": "설치 완료!",
|
||||
"Failed to install forwarder": "바로가기 설치 실패",
|
||||
"Unstarred ": "즐겨찾기 해제: ",
|
||||
@@ -137,40 +139,40 @@
|
||||
|
||||
"AppStore": "앱스토어",
|
||||
"Filter: %s | Sort: %s | Order: %s": "필터: %s | 분류: %s | 정렬: %s",
|
||||
"AppStore Options": "앱스토어 설정",
|
||||
"All": "전체",
|
||||
"AppStore Options": "앱스토어 옵션",
|
||||
"All": "모두",
|
||||
"Games": "게임",
|
||||
"Emulators": "에뮬레이터",
|
||||
"Tools": "도구",
|
||||
"Themes": "테마",
|
||||
"Legacy": "레거시",
|
||||
"version: %s": "버전: %s",
|
||||
"updated: %s": "갱신일: %s",
|
||||
"updated: %s": "업데이트: %s",
|
||||
"category: %s": "카테고리: %s",
|
||||
"extracted: %.2f MiB": "용량: %.2f MiB",
|
||||
"app_dls: %s": "다운로드 횟수: %s",
|
||||
"More by Author": "개발자의 다른 앱 더보기",
|
||||
"More by Author": "개발자의 다른 앱 더 보기",
|
||||
"Leave Feedback": "피드백 남기기",
|
||||
|
||||
"Irs": "Joy-Con IR 카메라",
|
||||
"Ambient Noise Level: ": "노이즈 레벨: ",
|
||||
"Irs": "조이콘 적외선 카메라",
|
||||
"Ambient Noise Level: ": "주변 노이즈 레벨: ",
|
||||
"Controller": "컨트롤러",
|
||||
"Pad ": "조이콘 ",
|
||||
" (Available)": " (사용 가능)",
|
||||
" (Unsupported)": " (지원 안됨)",
|
||||
" (Unconnected)": " (연결 없음)",
|
||||
"HandHeld": "- 본체 연결",
|
||||
"HandHeld": "본체 연결",
|
||||
"Rotation": "화면 회전",
|
||||
"0 (Sideways)": "0 (좌회전)",
|
||||
"90 (Flat)": "90 (정방향)",
|
||||
"180 (-Sideways)": "180 (우회전)",
|
||||
"270 (Upside down)": "270 (역전)",
|
||||
"Colour": "컬러 팔레트",
|
||||
"Grey": "그레이",
|
||||
"0 (Sideways)": "반시계방향 90° 회전",
|
||||
"90 (Flat)": "정방향",
|
||||
"180 (-Sideways)": "시계방향 90° 회전",
|
||||
"270 (Upside down)": "상하반전",
|
||||
"Colour": "색상",
|
||||
"Grey": "회색",
|
||||
"Ironbow": "아이언보우",
|
||||
"Green": "그린",
|
||||
"Red": "레드",
|
||||
"Blue": "블루",
|
||||
"Green": "초록색",
|
||||
"Red": "빨간색",
|
||||
"Blue": "파란색",
|
||||
"Light Target": "반사 표적",
|
||||
"All leds": "모든 LED 켜기",
|
||||
"Bright group": "Bright LED 켜기",
|
||||
@@ -191,40 +193,46 @@
|
||||
"Load Default": "기본값으로 설정",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Themezer 설정",
|
||||
"Themezer Options": "Themezer 옵션",
|
||||
"Nsfw": "선정성 테마",
|
||||
"Page": "페이지",
|
||||
"Page %zu / %zu": "페이지 %zu / %zu",
|
||||
"Enter Page Number": "페이지 번호 입력",
|
||||
"Bad Page": "잘못된 페이지",
|
||||
"Download theme?": "테마를 내려받으시겠습니까?",
|
||||
"Download theme?": "테마를 다운로드할까요?",
|
||||
|
||||
"Installing ": "설치중... ",
|
||||
"Uninstalling ": "설치 제거중... ",
|
||||
"Deleting ": "삭제중... ",
|
||||
"Deleting": "삭제중...",
|
||||
"Pasting ": "붙여넣는중... ",
|
||||
"Pasting": "붙여넣는중...",
|
||||
"Removing ": "제거중... ",
|
||||
"Scanning ": "스캔중... ",
|
||||
"Creating ": "작성중... ",
|
||||
"Copying ": "복사중... ",
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "JSON에서 다운로드",
|
||||
"Select asset to download for ": "다운로드 아이템 선택 ",
|
||||
|
||||
"Installing ": "설치 ",
|
||||
"Uninstalling ": "설치 제거 ",
|
||||
"Deleting ": "삭제 ",
|
||||
"Deleting": "삭제",
|
||||
"Pasting ": "붙여넣기 ",
|
||||
"Pasting": "붙여넣기",
|
||||
"Removing ": "제거 ",
|
||||
"Scanning ": "스캔 ",
|
||||
"Creating ": "생성 ",
|
||||
"Copying ": "복사 ",
|
||||
"Trying to load ": "썸네일 받아오는 중... ",
|
||||
"Downloading ": "다운로드중... ",
|
||||
"Checking MD5": "MD5 확인중... ",
|
||||
"Loading...": "로딩중...",
|
||||
"Loading": "로딩중...",
|
||||
"Downloading ": "다운로드 ",
|
||||
"Downloaded ": "다운로드 완료: ",
|
||||
"Removed ": "제거 됨: ",
|
||||
"Checking MD5": "MD5 확인",
|
||||
"Loading...": "로딩 중...",
|
||||
"Loading": "로딩 중...",
|
||||
"Empty!": "찾을 수 없습니다!",
|
||||
"Not Ready...": "준비되지 않았습니다...",
|
||||
"Not Ready...": "준비되지 않음...",
|
||||
"Error loading page!": "페이지 로딩 오류!",
|
||||
"Update avaliable: ": "업데이트 가능: ",
|
||||
"Download update: ": "업데이트 다운로드: ",
|
||||
"Updated to ": "업데이트: ",
|
||||
"Restart Sphaira?": "Sphaira를 재시작 하시겠습니까?",
|
||||
"Failed to download update": "업데이트 다운로드 실패!",
|
||||
"Delete Selected files?": "정말 삭제하시겠습니까?",
|
||||
"Completely remove ": "제거하시겠습니까 ",
|
||||
"Are you sure you want to delete ": "정말 삭제하시겠습니까 ",
|
||||
"Are you sure you wish to cancel?": "정말 취소하시겠습니까?",
|
||||
"If this message appears repeatedly, please open an issue.": "해당 메시지가 반복해서 나타나는 경우, 이슈를 열어주세요."
|
||||
"Restart Sphaira?": "Sphaira를 재시작할까요?",
|
||||
"Failed to download update": "업데이트 다운로드 실패함",
|
||||
"Delete Selected files?": "선택한 파일을 삭제할까요?",
|
||||
"Completely remove ": "정말 삭제할까요 ",
|
||||
"Are you sure you want to delete ": "정말 삭제할까요 ",
|
||||
"Are you sure you wish to cancel?": "정말 취소할까요?",
|
||||
"If this message appears repeatedly, please open an issue.": "해당 메시지가 반복해서 나타나는 경우, 이슈를 등록하세요."
|
||||
}
|
||||
@@ -62,6 +62,8 @@
|
||||
"Music": "Muziek",
|
||||
"Network": "Netwerk",
|
||||
"Network Options": "Netwerkopties",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "",
|
||||
"Nxlink Upload": "",
|
||||
@@ -199,6 +201,10 @@
|
||||
"Bad Page": "",
|
||||
"Download theme?": "",
|
||||
|
||||
"GitHub": "",
|
||||
"Downloading json": "",
|
||||
"Select asset to download for ": "",
|
||||
|
||||
"Installing ": "",
|
||||
"Uninstalling ": "",
|
||||
"Deleting ": "",
|
||||
@@ -211,6 +217,8 @@
|
||||
"Copying ": "",
|
||||
"Trying to load ": "",
|
||||
"Downloading ": "",
|
||||
"Downloaded ": "",
|
||||
"Removed ": "",
|
||||
"Checking MD5": "",
|
||||
"Loading...": "",
|
||||
"Loading": "",
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
{
|
||||
"[Applet Mode]": "[Modo Applet]",
|
||||
"No Internet": "Sem Internet",
|
||||
"Files": "",
|
||||
"Apps": "",
|
||||
"Store": "",
|
||||
"Files": "Arquivos",
|
||||
"Apps": "Aplicativos",
|
||||
"Store": "Loja",
|
||||
"Menu": "Menu",
|
||||
"Options": "Opções",
|
||||
"OK": "",
|
||||
"OK": "OK",
|
||||
"Back": "Voltar",
|
||||
"Select": "",
|
||||
"Select": "Selecionar",
|
||||
"Open": "Abrir",
|
||||
"Launch": "Iniciar",
|
||||
"Info": "Informações",
|
||||
"Install": "Instalar",
|
||||
"Delete": "Excluir",
|
||||
"Restart": "",
|
||||
"Changelog": "",
|
||||
"Details": "",
|
||||
"Update": "",
|
||||
"Remove": "",
|
||||
"Restart": "Reiniciar",
|
||||
"Changelog": "Changelog",
|
||||
"Details": "Detalhes",
|
||||
"Update": "Atualizar",
|
||||
"Remove": "Remover",
|
||||
"Download": "Download",
|
||||
"Next Page": "Próxima página",
|
||||
"Prev Page": "Página anterior",
|
||||
"Unstar": "",
|
||||
"Star": "",
|
||||
"System memory": "",
|
||||
"microSD card": "",
|
||||
"Unstar": "Desfavoritar",
|
||||
"Star": "Favoritar",
|
||||
"System memory": "Memória do console",
|
||||
"microSD card": "Cartão microSD",
|
||||
"Yes": "Sim",
|
||||
"No": "Não",
|
||||
"Enabled": "Habilitado",
|
||||
@@ -38,20 +38,20 @@
|
||||
"Order": "Ordem",
|
||||
"Search": "Procurar",
|
||||
"Updated": "Atualizado",
|
||||
"Updated (Star)": "",
|
||||
"Updated (Star)": "Atualizado (Favoritos)",
|
||||
"Downloads": "Downloads",
|
||||
"Size": "Tamanho",
|
||||
"Size (Star)": "",
|
||||
"Size (Star)": "Tamanho (Favoritos)",
|
||||
"Alphabetical": "Alfabético",
|
||||
"Alphabetical (Star)": "",
|
||||
"Likes": "",
|
||||
"ID": "",
|
||||
"Alphabetical (Star)": "Alfabético (Favoritos)",
|
||||
"Likes": "Curtidas",
|
||||
"ID": "ID",
|
||||
"Decending": "Decrescente",
|
||||
"Descending (down)": "Decrescente",
|
||||
"Desc": "Decrescente",
|
||||
"Descending (down)": "Decrescente (Baixo)",
|
||||
"Desc": "Decr.",
|
||||
"Ascending": "Ascendente",
|
||||
"Ascending (Up)": "Ascendente",
|
||||
"Asc": "Ascendente",
|
||||
"Ascending (Up)": "Ascendente (Cima)",
|
||||
"Asc": "Asc.",
|
||||
|
||||
"Menu Options": "Opções do menu",
|
||||
"Header": "Cabeçalho",
|
||||
@@ -62,14 +62,16 @@
|
||||
"Music": "Música",
|
||||
"Network": "Rede",
|
||||
"Network Options": "Opções de rede",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "",
|
||||
"Nxlink Upload": "",
|
||||
"Nxlink Finished": "",
|
||||
"Switch-Handheld!": "",
|
||||
"Switch-Docked!": "",
|
||||
"Nxlink Connected": "Nxlink conectado",
|
||||
"Nxlink Upload": "Envio Nxlink",
|
||||
"Nxlink Finished": "Nxlink finalizado",
|
||||
"Switch-Handheld!": "Switch-Portátil",
|
||||
"Switch-Docked!": "Switch-Docado",
|
||||
"Language": "Idioma",
|
||||
"Auto": "",
|
||||
"Auto": "Automático",
|
||||
"English": "English",
|
||||
"Japanese": "日本語",
|
||||
"French": "Français",
|
||||
@@ -83,59 +85,59 @@
|
||||
"Russian": "Русский",
|
||||
"Swedish": "Svenska",
|
||||
"Logging": "Logging",
|
||||
"Replace hbmenu on exit": "Substitua hbmenu ao sair",
|
||||
"Replace hbmenu on exit": "Substituir hbmenu ao sair",
|
||||
"Misc": "Diversos",
|
||||
"Misc Options": "Opções diversas",
|
||||
"Web": "Rede",
|
||||
"Install forwarders": "",
|
||||
"Install location": "",
|
||||
"Show install warning": "",
|
||||
"Web": "Navegador web",
|
||||
"Install forwarders": "Instalar forwarder",
|
||||
"Install location": "Local de instalação",
|
||||
"Show install warning": "Mostrar aviso de instalação",
|
||||
|
||||
"FileBrowser": "Navegador de arquivos",
|
||||
"%zd files": "%zd files",
|
||||
"%zd dirs": "%zd dirs",
|
||||
"%zd files": "%zd arquivo(s)",
|
||||
"%zd dirs": "%zd diretório(s)",
|
||||
"File Options": "Opções de arquivo",
|
||||
"Show Hidden": "Mostrar oculto",
|
||||
"Show Hidden": "Mostrar ocultos",
|
||||
"Folders First": "Pastas primeiro",
|
||||
"Hidden Last": "Oculto por último",
|
||||
"Hidden Last": "Ocultos por último",
|
||||
"Cut": "Cortar",
|
||||
"Copy": "Copiar",
|
||||
"Paste": "",
|
||||
"Paste ": "",
|
||||
" file(s)?": "",
|
||||
"Paste": "Colar",
|
||||
"Paste ": "Colar",
|
||||
" file(s)?": " arquivo(s)?",
|
||||
"Rename": "Renomear",
|
||||
"Set New File Name": "",
|
||||
"Set New File Name": "Definir novo nome do arquivo",
|
||||
"Advanced": "Avançado",
|
||||
"Advanced Options": "Criar arquivo",
|
||||
"Advanced Options": "Opções avançadas",
|
||||
"Create File": "Criar arquivo",
|
||||
"Set File Name": "",
|
||||
"Set File Name": "Definir nome do arquivo",
|
||||
"Create Folder": "Criar pasta",
|
||||
"Set Folder Name": "",
|
||||
"Set Folder Name": "Definir novo nome da pasta",
|
||||
"View as text (unfinished)": "Ver como texto (inacabado)",
|
||||
"Empty...": "",
|
||||
"Open with DayBreak?": "",
|
||||
"Launch ": "",
|
||||
"Launch option for: ": "",
|
||||
"Select launcher for: ": "",
|
||||
"Empty...": "Vazio...",
|
||||
"Open with DayBreak?": "Abrir com DayBreak?",
|
||||
"Launch ": "Iniciar",
|
||||
"Launch option for: ": "Opções de inicialização para: ",
|
||||
"Select launcher for: ": "Selecionar launcher para: ",
|
||||
|
||||
"Homebrew": "Homebrew",
|
||||
"Homebrew Options": "Opções do Homebrew",
|
||||
"Hide Sphaira": "Esconder Sphaira",
|
||||
"Install Forwarder": "Instalar forwarder",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Isso pode resultar em um banimento!",
|
||||
"Installing Forwarder": "",
|
||||
"Creating Program": "",
|
||||
"Creating Control": "",
|
||||
"Creating Meta": "",
|
||||
"Writing Nca": "",
|
||||
"Updating ncm databse": "",
|
||||
"Pushing application record": "",
|
||||
"Installed!": "",
|
||||
"Failed to install forwarder": "",
|
||||
"Unstarred ": "",
|
||||
"Starred ": "",
|
||||
"Installing Forwarder": "Instalando forwarder",
|
||||
"Creating Program": "Criando Program",
|
||||
"Creating Control": "Criando Control",
|
||||
"Creating Meta": "Criando Meta",
|
||||
"Writing Nca": "Escrevendo NCA",
|
||||
"Updating ncm databse": "Atualizando base de dados NCM",
|
||||
"Pushing application record": "Aplicando registro do aplicativo",
|
||||
"Installed!": "Instalado!",
|
||||
"Failed to install forwarder": "Falha ao instalar forwarder",
|
||||
"Unstarred ": "Desfavoritado ",
|
||||
"Starred ": "Favoritado ",
|
||||
|
||||
"AppStore": "",
|
||||
"AppStore": "AppStore",
|
||||
"Filter: %s | Sort: %s | Order: %s": "Filtro: %s | Organizar: %s | Ordem: %s",
|
||||
"AppStore Options": "Opções da AppStore",
|
||||
"All": "Todos",
|
||||
@@ -144,20 +146,20 @@
|
||||
"Tools": "Ferramentas",
|
||||
"Themes": "Temas",
|
||||
"Legacy": "Legado",
|
||||
"version: %s": "version: %s",
|
||||
"updated: %s": "updated: %s",
|
||||
"category: %s": "category: %s",
|
||||
"extracted: %.2f MiB": "extracted: %.2f MiB",
|
||||
"app_dls: %s": "app_dls: %s",
|
||||
"More by Author": "",
|
||||
"Leave Feedback": "",
|
||||
"version: %s": "versão: %s",
|
||||
"updated: %s": "atualizado: %s",
|
||||
"category: %s": "categoria: %s",
|
||||
"extracted: %.2f MiB": "tam. extraído: %.2f MiB",
|
||||
"app_dls: %s": "downloads: %s",
|
||||
"More by Author": "Mais do autor",
|
||||
"Leave Feedback": "Deixar um feedback",
|
||||
|
||||
"Irs": "Irs",
|
||||
"Ambient Noise Level: ": "",
|
||||
"Ambient Noise Level: ": "Nível de ruído ambiente",
|
||||
"Controller": "Controle",
|
||||
"Pad ": "Pad ",
|
||||
" (Available)": " (Disponível)",
|
||||
" (Unsupported)": "",
|
||||
" (Unsupported)": "(Não suportado)",
|
||||
" (Unconnected)": " (Desconectado)",
|
||||
"HandHeld": "Portátil",
|
||||
"Rotation": "Rotação",
|
||||
@@ -180,51 +182,57 @@
|
||||
"Negative Image": "Imagem negativa",
|
||||
"Normal image": "Imagem normal",
|
||||
"Negative image": "Imagem negativa",
|
||||
"Format": "Formatar",
|
||||
"Format": "Formato",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"Trimming Format": "Formato de corte",
|
||||
"External Light Filter": "Filtro de luz externo",
|
||||
"External Light Filter": "Filtro de luz externa",
|
||||
"Load Default": "Carregar padrão",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "",
|
||||
"Nsfw": "",
|
||||
"Page": "",
|
||||
"Themezer Options": "Opções do Themezer",
|
||||
"Nsfw": "NSFW",
|
||||
"Page": "Página",
|
||||
"Page %zu / %zu": "Page %zu / %zu",
|
||||
"Enter Page Number": "",
|
||||
"Bad Page": "",
|
||||
"Download theme?": "",
|
||||
"Enter Page Number": "Digite o número da página",
|
||||
"Bad Page": "Página inválida",
|
||||
"Download theme?": "Baixar tema?",
|
||||
|
||||
"Installing ": "",
|
||||
"Uninstalling ": "",
|
||||
"Deleting ": "",
|
||||
"Deleting": "",
|
||||
"Pasting ": "",
|
||||
"Pasting": "",
|
||||
"Removing ": "",
|
||||
"Scanning ": "",
|
||||
"Creating ": "",
|
||||
"Copying ": "",
|
||||
"Trying to load ": "",
|
||||
"Downloading ": "",
|
||||
"Checking MD5": "",
|
||||
"Loading...": "",
|
||||
"Loading": "",
|
||||
"Empty!": "",
|
||||
"Not Ready...": "",
|
||||
"Error loading page!": "",
|
||||
"Update avaliable: ": "",
|
||||
"Download update: ": "",
|
||||
"Updated to ": "",
|
||||
"Restart Sphaira?": "",
|
||||
"Failed to download update": "",
|
||||
"Delete Selected files?": "",
|
||||
"Completely remove ": "",
|
||||
"Are you sure you want to delete ": "Excluir ",
|
||||
"Are you sure you wish to cancel?": "",
|
||||
"If this message appears repeatedly, please open an issue.": ""
|
||||
"GitHub": "",
|
||||
"Downloading json": "",
|
||||
"Select asset to download for ": "",
|
||||
|
||||
"Installing ": "Instalando ",
|
||||
"Uninstalling ": "Desinstalando ",
|
||||
"Deleting ": "Deletando ",
|
||||
"Deleting": "Deletando ",
|
||||
"Pasting ": "Colando ",
|
||||
"Pasting": "Colando ",
|
||||
"Removing ": "Removendo ",
|
||||
"Scanning ": "Analisando ",
|
||||
"Creating ": "Criando ",
|
||||
"Copying ": "Copiando ",
|
||||
"Trying to load ": "Tentando carregar ",
|
||||
"Downloading ": "Baixando ",
|
||||
"Downloaded ": "",
|
||||
"Removed ": "",
|
||||
"Checking MD5": "Checando MD5",
|
||||
"Loading...": "Carregando...",
|
||||
"Loading": "Carregando",
|
||||
"Empty!": "Vazio!",
|
||||
"Not Ready...": "Não está pronto...",
|
||||
"Error loading page!": "Erro ao carregar página!",
|
||||
"Update avaliable: ": "Atualização disponível: ",
|
||||
"Download update: ": "Baixar autalização: ",
|
||||
"Updated to ": "Atualizado para ",
|
||||
"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."
|
||||
}
|
||||
@@ -62,6 +62,8 @@
|
||||
"Music": "Музыка",
|
||||
"Network": "Сеть",
|
||||
"Network Options": "Параметры сети",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "",
|
||||
"Nxlink Upload": "",
|
||||
@@ -199,6 +201,10 @@
|
||||
"Bad Page": "",
|
||||
"Download theme?": "",
|
||||
|
||||
"GitHub": "",
|
||||
"Downloading json": "",
|
||||
"Select asset to download for ": "",
|
||||
|
||||
"Installing ": "",
|
||||
"Uninstalling ": "",
|
||||
"Deleting ": "",
|
||||
@@ -211,6 +217,8 @@
|
||||
"Copying ": "",
|
||||
"Trying to load ": "",
|
||||
"Downloading ": "",
|
||||
"Downloaded ": "",
|
||||
"Removed ": "",
|
||||
"Checking MD5": "",
|
||||
"Loading...": "",
|
||||
"Loading": "",
|
||||
|
||||
@@ -62,6 +62,8 @@
|
||||
"Music": "Musik",
|
||||
"Network": "Nätverk",
|
||||
"Network Options": "Nätverksalternativ",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink ansluten",
|
||||
"Nxlink Upload": "Nxlink uppladdning",
|
||||
@@ -198,7 +200,11 @@
|
||||
"Enter Page Number": "Ange sidnummer",
|
||||
"Bad Page": "Ogiltig sida",
|
||||
"Download theme?": "Ladda ner tema?",
|
||||
|
||||
|
||||
"GitHub": "",
|
||||
"Downloading json": "",
|
||||
"Select asset to download for ": "",
|
||||
|
||||
"Installing ": "Installerar ",
|
||||
"Uninstalling ": "Avinstallerar ",
|
||||
"Deleting ": "Raderar ",
|
||||
@@ -211,6 +217,8 @@
|
||||
"Copying ": "Kopierar ",
|
||||
"Trying to load ": "",
|
||||
"Downloading ": "Laddar ner ",
|
||||
"Downloaded ": "",
|
||||
"Removed ": "",
|
||||
"Checking MD5": "Kontrollerar MD5",
|
||||
"Loading...": "Laddar...",
|
||||
"Loading": "Laddar",
|
||||
|
||||
@@ -62,6 +62,8 @@
|
||||
"Music": "音乐",
|
||||
"Network": "网络",
|
||||
"Network Options": "网络选项",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink 已连接",
|
||||
"Nxlink Upload": "Nxlink 上传中",
|
||||
@@ -199,6 +201,10 @@
|
||||
"Bad Page": "错误的页面",
|
||||
"Download theme?": "下载该主题?",
|
||||
|
||||
"GitHub": "",
|
||||
"Downloading json": "",
|
||||
"Select asset to download for ": "",
|
||||
|
||||
"Installing ": "正在安装 ",
|
||||
"Uninstalling ": "正在卸载 ",
|
||||
"Deleting ": "正在删除 ",
|
||||
@@ -211,6 +217,8 @@
|
||||
"Copying ": "正在复制 ",
|
||||
"Trying to load ": "",
|
||||
"Downloading ": "正在下载 ",
|
||||
"Downloaded ": "",
|
||||
"Removed ": "",
|
||||
"Checking MD5": "正在校验 MD5",
|
||||
"Loading...": "加载中...",
|
||||
"Loading": "加载中",
|
||||
|
||||
@@ -45,18 +45,18 @@ add_executable(sphaira
|
||||
source/ui/menus/main_menu.cpp
|
||||
source/ui/menus/menu_base.cpp
|
||||
source/ui/menus/themezer.cpp
|
||||
source/ui/menus/ghdl.cpp
|
||||
|
||||
source/ui/error_box.cpp
|
||||
source/ui/notification.cpp
|
||||
source/ui/nvg_util.cpp
|
||||
source/ui/option_box.cpp
|
||||
source/ui/option_list.cpp
|
||||
source/ui/popup_list.cpp
|
||||
source/ui/progress_box.cpp
|
||||
source/ui/scrollable_text.cpp
|
||||
source/ui/scrollbar.cpp
|
||||
source/ui/sidebar.cpp
|
||||
source/ui/widget.cpp
|
||||
source/ui/list.cpp
|
||||
|
||||
source/app.cpp
|
||||
source/download.cpp
|
||||
@@ -85,7 +85,7 @@ set(FETCHCONTENT_QUIET FALSE)
|
||||
|
||||
FetchContent_Declare(ftpsrv
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
|
||||
GIT_TAG 8d5a14e
|
||||
GIT_TAG 1.2.1
|
||||
)
|
||||
|
||||
FetchContent_Declare(libhaze
|
||||
@@ -113,12 +113,12 @@ FetchContent_Declare(yyjson
|
||||
GIT_TAG 0.10.0
|
||||
)
|
||||
|
||||
FetchContent_Declare(minIni-sphaira
|
||||
FetchContent_Declare(minIni
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/minIni-nx.git
|
||||
GIT_TAG 63ec295
|
||||
)
|
||||
|
||||
set(MININI_LIB_NAME minIni-sphaira)
|
||||
set(MININI_LIB_NAME minIni)
|
||||
set(MININI_USE_STDIO ON)
|
||||
set(MININI_USE_NX ON)
|
||||
set(MININI_USE_FLOAT OFF)
|
||||
@@ -139,30 +139,68 @@ set(NANOVG_STBI_STATIC OFF)
|
||||
set(NANOVG_STBTT_STATIC ON)
|
||||
|
||||
set(YYJSON_DISABLE_READER OFF)
|
||||
set(YYJSON_DISABLE_WRITER ON)
|
||||
set(YYJSON_DISABLE_WRITER OFF)
|
||||
set(YYJSON_DISABLE_UTILS ON)
|
||||
set(YYJSON_DISABLE_FAST_FP_CONV ON)
|
||||
set(YYJSON_DISABLE_NON_STANDARD ON)
|
||||
set(YYJSON_DISABLE_UTF8_VALIDATION ON)
|
||||
set(YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS OFF)
|
||||
|
||||
set(FTPSRV_LIB_BUILD TRUE)
|
||||
set(FTPSRV_LIB_SOCK_UNISTD TRUE)
|
||||
set(FTPSRV_LIB_VFS_CUSTOM ${CMAKE_CURRENT_SOURCE_DIR}/include/ftpsrv_helper.hpp)
|
||||
set(FTPSRV_LIB_PATH_SIZE 0x301)
|
||||
set(FTPSRV_LIB_SESSIONS 32)
|
||||
set(FTPSRV_LIB_BUF_SIZE 1024*64)
|
||||
|
||||
FetchContent_MakeAvailable(
|
||||
ftpsrv
|
||||
# ftpsrv
|
||||
libhaze
|
||||
libpulsar
|
||||
nanovg
|
||||
stb
|
||||
minIni-sphaira
|
||||
minIni
|
||||
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
|
||||
add_library(libhaze
|
||||
${libhaze_SOURCE_DIR}/source/async_usb_server.cpp
|
||||
@@ -240,10 +278,10 @@ set_target_properties(sphaira PROPERTIES
|
||||
)
|
||||
|
||||
target_link_libraries(sphaira PRIVATE
|
||||
ftpsrv
|
||||
ftpsrv_helper
|
||||
libhaze
|
||||
libpulsar
|
||||
minIni-sphaira
|
||||
minIni
|
||||
nanovg
|
||||
stb
|
||||
yyjson
|
||||
|
||||
@@ -33,7 +33,9 @@ enum class LaunchType {
|
||||
Forwader_Sphaira,
|
||||
};
|
||||
|
||||
// todo: why is this global???
|
||||
void DrawElement(float x, float y, float w, float h, ThemeEntryID id);
|
||||
void DrawElement(const Vec4&, ThemeEntryID id);
|
||||
|
||||
class App {
|
||||
public:
|
||||
@@ -45,6 +47,8 @@ public:
|
||||
static void ExitRestart();
|
||||
static auto GetVg() -> NVGcontext*;
|
||||
static void Push(std::shared_ptr<ui::Widget>);
|
||||
// pops all widgets above a menu
|
||||
static void PopToMenu();
|
||||
|
||||
// this is thread safe
|
||||
static void Notify(std::string text, ui::NotifEntry::Side side = ui::NotifEntry::Side::RIGHT);
|
||||
@@ -54,8 +58,10 @@ public:
|
||||
static void NotifyFlashLed();
|
||||
|
||||
static auto GetThemeMetaList() -> std::span<ThemeMeta>;
|
||||
static void SetTheme(u64 theme_index);
|
||||
static auto GetThemeIndex() -> u64;
|
||||
static void SetTheme(s64 theme_index);
|
||||
static auto GetThemeIndex() -> s64;
|
||||
|
||||
static auto GetDefaultImage(int* w = nullptr, int* h = nullptr) -> int;
|
||||
|
||||
// returns argv[0]
|
||||
static auto GetExePath() -> fs::FsPath;
|
||||
@@ -119,6 +125,7 @@ public:
|
||||
u64 m_start_timestamp{};
|
||||
u64 m_prev_timestamp{};
|
||||
fs::FsPath m_prev_last_launch{};
|
||||
int m_default_image{};
|
||||
|
||||
bool m_is_launched_via_sphaira_forwader{};
|
||||
|
||||
@@ -138,7 +145,7 @@ public:
|
||||
|
||||
Theme m_theme{};
|
||||
fs::FsPath theme_path{};
|
||||
std::size_t m_theme_index{};
|
||||
s64 m_theme_index{};
|
||||
|
||||
bool m_quit{};
|
||||
|
||||
|
||||
@@ -1,41 +1,194 @@
|
||||
#pragma once
|
||||
|
||||
#include "fs.hpp"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <algorithm>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira {
|
||||
namespace sphaira::curl {
|
||||
|
||||
using DownloadCallback = std::function<void(std::vector<u8>& data, bool success)>;
|
||||
using ProgressCallback = std::function<bool(u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow)>;
|
||||
enum {
|
||||
Flag_None = 0,
|
||||
// requests to download send etag in the header.
|
||||
// the received etag is then saved on success.
|
||||
// this api is only available on downloading to file.
|
||||
Flag_Cache = 1 << 0,
|
||||
};
|
||||
|
||||
enum class DownloadPriority {
|
||||
enum class Priority {
|
||||
Normal, // gets pushed to the back of the queue
|
||||
High, // gets pushed to the front of the queue
|
||||
};
|
||||
|
||||
struct DownloadEventData {
|
||||
DownloadCallback callback;
|
||||
std::vector<u8> data;
|
||||
bool result;
|
||||
struct Api;
|
||||
struct ApiResult;
|
||||
|
||||
using Path = fs::FsPath;
|
||||
using OnComplete = std::function<void(ApiResult& result)>;
|
||||
using OnProgress = std::function<bool(u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow)>;
|
||||
|
||||
struct Url {
|
||||
Url() = default;
|
||||
Url(const std::string& str) : m_str{str} {}
|
||||
std::string m_str;
|
||||
};
|
||||
|
||||
auto DownloadInit() -> bool;
|
||||
void DownloadExit();
|
||||
struct Fields {
|
||||
Fields() = default;
|
||||
Fields(const std::string& str) : m_str{str} {}
|
||||
std::string m_str;
|
||||
};
|
||||
|
||||
struct Header {
|
||||
Header() = default;
|
||||
Header(std::initializer_list<std::pair<const std::string, std::string>> p) : m_map{p} {}
|
||||
std::unordered_map<std::string, std::string> m_map;
|
||||
|
||||
auto Find(const std::string& key) const {
|
||||
return std::find_if(m_map.cbegin(), m_map.cend(), [&key](auto& e) {
|
||||
return !strcasecmp(key.c_str(), e.first.c_str());
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
struct Flags {
|
||||
Flags() = default;
|
||||
Flags(u32 flags) : m_flags{flags} {}
|
||||
u32 m_flags{Flag_None};
|
||||
};
|
||||
|
||||
struct ApiResult {
|
||||
bool success;
|
||||
long code;
|
||||
Header header; // returned headers in request
|
||||
std::vector<u8> data; // empty if downloaded a file
|
||||
fs::FsPath path; // empty if downloaded memory
|
||||
};
|
||||
|
||||
struct DownloadEventData {
|
||||
OnComplete callback;
|
||||
ApiResult result;
|
||||
};
|
||||
|
||||
auto Init() -> bool;
|
||||
void Exit();
|
||||
|
||||
// sync functions
|
||||
auto DownloadMemory(const std::string& url, const std::string& post, ProgressCallback pcallback = nullptr) -> std::vector<u8>;
|
||||
auto DownloadFile(const std::string& url, const std::string& out, const std::string& post, ProgressCallback pcallback = nullptr) -> bool;
|
||||
auto ToMemory(const Api& e) -> ApiResult;
|
||||
auto ToFile(const Api& e) -> ApiResult;
|
||||
|
||||
// async functions
|
||||
// starts the downloads in a new thread, pushes an event when complete
|
||||
// then, the callback will be called on the main thread.
|
||||
// auto DownloadMemoryAsync(const std::string& url, DownloadCallback callback, DownloadPriority prio = DownloadPriority::Normal) -> bool;
|
||||
// auto DownloadFileAsync(const std::string& url, const std::string& out, DownloadCallback callback, DownloadPriority prio = DownloadPriority::Normal) -> bool;
|
||||
auto ToMemoryAsync(const Api& e) -> bool;
|
||||
auto ToFileAsync(const Api& e) -> bool;
|
||||
|
||||
auto DownloadMemoryAsync(const std::string& url, const std::string& post, DownloadCallback callback, ProgressCallback pcallback = nullptr, DownloadPriority prio = DownloadPriority::Normal) -> bool;
|
||||
auto DownloadFileAsync(const std::string& url, const std::string& out, const std::string& post, DownloadCallback callback, ProgressCallback pcallback = nullptr, DownloadPriority prio = DownloadPriority::Normal) -> bool;
|
||||
struct Api {
|
||||
Api() = default;
|
||||
|
||||
void DownloadClearCache(const std::string& url);
|
||||
template <typename... Ts>
|
||||
Api(Ts&&... ts) {
|
||||
Api::set_option(std::forward<Ts>(ts)...);
|
||||
}
|
||||
|
||||
} // namespace sphaira
|
||||
template <typename... Ts>
|
||||
auto To(Ts&&... ts) {
|
||||
if constexpr(std::disjunction_v<std::is_same<Path, Ts>...>) {
|
||||
return ToFile(std::forward<Ts>(ts)...);
|
||||
} else {
|
||||
return ToMemory(std::forward<Ts>(ts)...);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
auto ToAsync(Ts&&... ts) {
|
||||
if constexpr(std::disjunction_v<std::is_same<Path, Ts>...>) {
|
||||
return ToFileAsync(std::forward<Ts>(ts)...);
|
||||
} else {
|
||||
return ToMemoryAsync(std::forward<Ts>(ts)...);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
auto ToMemory(Ts&&... ts) {
|
||||
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
||||
static_assert(!std::disjunction_v<std::is_same<Path, Ts>...>, "Path must not valid for memory");
|
||||
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
|
||||
|
||||
@@ -26,7 +26,7 @@ using EventData = std::variant<
|
||||
ExitEventData,
|
||||
HazeCallbackData,
|
||||
NxlinkCallbackData,
|
||||
DownloadEventData
|
||||
curl::DownloadEventData
|
||||
>;
|
||||
|
||||
// returns number of events
|
||||
|
||||
@@ -171,39 +171,39 @@ static_assert(FsPath::TestFrom(FsPath{"abc"}));
|
||||
|
||||
FsPath AppendPath(const fs::FsPath& root_path, const fs::FsPath& file_path);
|
||||
|
||||
Result CreateFile(FsFileSystem* fs, const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false);
|
||||
Result CreateDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false);
|
||||
Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false);
|
||||
Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false);
|
||||
Result DeleteFile(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false);
|
||||
Result DeleteDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false);
|
||||
Result DeleteDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = false);
|
||||
Result RenameFile(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only = false);
|
||||
Result RenameDirectory(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only = false);
|
||||
Result CreateFile(FsFileSystem* fs, const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = true);
|
||||
Result CreateDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
|
||||
Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
|
||||
Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
|
||||
Result DeleteFile(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
|
||||
Result DeleteDirectory(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
|
||||
Result DeleteDirectoryRecursively(FsFileSystem* fs, const FsPath& path, bool ignore_read_only = true);
|
||||
Result RenameFile(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only = true);
|
||||
Result RenameDirectory(FsFileSystem* fs, const FsPath& src, const FsPath& dst, bool ignore_read_only = true);
|
||||
Result GetEntryType(FsFileSystem* fs, const FsPath& path, FsDirEntryType* out);
|
||||
Result GetFileTimeStampRaw(FsFileSystem* fs, const FsPath& path, FsTimeStampRaw *out);
|
||||
bool FileExists(FsFileSystem* fs, const FsPath& path);
|
||||
bool DirExists(FsFileSystem* fs, const FsPath& path);
|
||||
Result read_entire_file(FsFileSystem* fs, const FsPath& path, std::vector<u8>& out);
|
||||
Result write_entire_file(FsFileSystem* fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = false);
|
||||
Result copy_entire_file(FsFileSystem* fs, const FsPath& dst, const FsPath& src, bool ignore_read_only = false);
|
||||
Result write_entire_file(FsFileSystem* fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = true);
|
||||
Result copy_entire_file(FsFileSystem* fs, const FsPath& dst, const FsPath& src, bool ignore_read_only = true);
|
||||
|
||||
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false);
|
||||
Result CreateDirectory(const FsPath& path, bool ignore_read_only = false);
|
||||
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = false);
|
||||
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = false);
|
||||
Result DeleteFile(const FsPath& path, bool ignore_read_only = false);
|
||||
Result DeleteDirectory(const FsPath& path, bool ignore_read_only = false);
|
||||
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = false);
|
||||
Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = false);
|
||||
Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = false);
|
||||
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = true);
|
||||
Result CreateDirectory(const FsPath& path, bool ignore_read_only = true);
|
||||
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = true);
|
||||
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = true);
|
||||
Result DeleteFile(const FsPath& path, bool ignore_read_only = true);
|
||||
Result DeleteDirectory(const FsPath& path, bool ignore_read_only = true);
|
||||
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = true);
|
||||
Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = true);
|
||||
Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = true);
|
||||
Result GetEntryType(const FsPath& path, FsDirEntryType* out);
|
||||
Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out);
|
||||
bool FileExists(const FsPath& path);
|
||||
bool DirExists(const FsPath& path);
|
||||
Result read_entire_file(const FsPath& path, std::vector<u8>& out);
|
||||
Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = false);
|
||||
Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only = false);
|
||||
Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = true);
|
||||
Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only = true);
|
||||
|
||||
struct Fs {
|
||||
static constexpr inline u32 FsModule = 505;
|
||||
@@ -222,51 +222,64 @@ struct Fs {
|
||||
static constexpr inline Result ResultUnknownStdioError = MAKERESULT(FsModule, 13);
|
||||
static constexpr inline Result ResultReadOnly = MAKERESULT(FsModule, 14);
|
||||
|
||||
virtual Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false) = 0;
|
||||
virtual Result CreateDirectory(const FsPath& path, bool ignore_read_only = false) = 0;
|
||||
virtual Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) = 0;
|
||||
virtual Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = false) = 0;
|
||||
virtual Result DeleteFile(const FsPath& path, bool ignore_read_only = false) = 0;
|
||||
virtual Result DeleteDirectory(const FsPath& path, bool ignore_read_only = false) = 0;
|
||||
virtual Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) = 0;
|
||||
virtual Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) = 0;
|
||||
virtual Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) = 0;
|
||||
Fs(bool ignore_read_only = true) : m_ignore_read_only{ignore_read_only} {}
|
||||
virtual ~Fs() = default;
|
||||
|
||||
virtual Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0) = 0;
|
||||
virtual Result CreateDirectory(const FsPath& path) = 0;
|
||||
virtual Result CreateDirectoryRecursively(const FsPath& path) = 0;
|
||||
virtual Result CreateDirectoryRecursivelyWithPath(const FsPath& path) = 0;
|
||||
virtual Result DeleteFile(const FsPath& path) = 0;
|
||||
virtual Result DeleteDirectory(const FsPath& path) = 0;
|
||||
virtual Result DeleteDirectoryRecursively(const FsPath& path) = 0;
|
||||
virtual Result RenameFile(const FsPath& src, const FsPath& dst) = 0;
|
||||
virtual Result RenameDirectory(const FsPath& src, const FsPath& dst) = 0;
|
||||
virtual Result GetEntryType(const FsPath& path, FsDirEntryType* out) = 0;
|
||||
virtual Result GetFileTimeStampRaw(const FsPath& path, FsTimeStampRaw *out) = 0;
|
||||
virtual bool FileExists(const FsPath& path) = 0;
|
||||
virtual bool DirExists(const FsPath& path) = 0;
|
||||
virtual Result read_entire_file(const FsPath& path, std::vector<u8>& out) = 0;
|
||||
virtual Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = false) = 0;
|
||||
virtual Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only = false) = 0;
|
||||
virtual Result write_entire_file(const FsPath& path, const std::vector<u8>& in) = 0;
|
||||
virtual Result copy_entire_file(const FsPath& dst, const FsPath& src) = 0;
|
||||
|
||||
void SetIgnoreReadOnly(bool enable) {
|
||||
m_ignore_read_only = enable;
|
||||
}
|
||||
|
||||
protected:
|
||||
bool m_ignore_read_only;
|
||||
};
|
||||
|
||||
struct FsStdio : Fs {
|
||||
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false) override {
|
||||
return fs::CreateFile(path, size, option, ignore_read_only);
|
||||
FsStdio(bool ignore_read_only = true) : Fs{ignore_read_only} {}
|
||||
virtual ~FsStdio() = default;
|
||||
|
||||
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0) override {
|
||||
return fs::CreateFile(path, size, option, m_ignore_read_only);
|
||||
}
|
||||
Result CreateDirectory(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::CreateDirectory(path, ignore_read_only);
|
||||
Result CreateDirectory(const FsPath& path) override {
|
||||
return fs::CreateDirectory(path, m_ignore_read_only);
|
||||
}
|
||||
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::CreateDirectoryRecursively(path, ignore_read_only);
|
||||
Result CreateDirectoryRecursively(const FsPath& path) override {
|
||||
return fs::CreateDirectoryRecursively(path, m_ignore_read_only);
|
||||
}
|
||||
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::CreateDirectoryRecursivelyWithPath(path, ignore_read_only);
|
||||
Result CreateDirectoryRecursivelyWithPath(const FsPath& path) override {
|
||||
return fs::CreateDirectoryRecursivelyWithPath(path, m_ignore_read_only);
|
||||
}
|
||||
Result DeleteFile(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::DeleteFile(path, ignore_read_only);
|
||||
Result DeleteFile(const FsPath& path) override {
|
||||
return fs::DeleteFile(path, m_ignore_read_only);
|
||||
}
|
||||
Result DeleteDirectory(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::DeleteDirectory(path, ignore_read_only);
|
||||
Result DeleteDirectory(const FsPath& path) override {
|
||||
return fs::DeleteDirectory(path, m_ignore_read_only);
|
||||
}
|
||||
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::DeleteDirectoryRecursively(path, ignore_read_only);
|
||||
Result DeleteDirectoryRecursively(const FsPath& path) override {
|
||||
return fs::DeleteDirectoryRecursively(path, m_ignore_read_only);
|
||||
}
|
||||
Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) override {
|
||||
return fs::RenameFile(src, dst, ignore_read_only);
|
||||
Result RenameFile(const FsPath& src, const FsPath& dst) override {
|
||||
return fs::RenameFile(src, dst, m_ignore_read_only);
|
||||
}
|
||||
Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) override {
|
||||
return fs::RenameDirectory(src, dst, ignore_read_only);
|
||||
Result RenameDirectory(const FsPath& src, const FsPath& dst) override {
|
||||
return fs::RenameDirectory(src, dst, m_ignore_read_only);
|
||||
}
|
||||
Result GetEntryType(const FsPath& path, FsDirEntryType* out) override {
|
||||
return fs::GetEntryType(path, out);
|
||||
@@ -283,17 +296,17 @@ struct FsStdio : Fs {
|
||||
Result read_entire_file(const FsPath& path, std::vector<u8>& out) override {
|
||||
return fs::read_entire_file(path, out);
|
||||
}
|
||||
Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = false) override {
|
||||
return fs::write_entire_file(path, in, ignore_read_only);
|
||||
Result write_entire_file(const FsPath& path, const std::vector<u8>& in) override {
|
||||
return fs::write_entire_file(path, in, m_ignore_read_only);
|
||||
}
|
||||
Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only = false) override {
|
||||
return fs::copy_entire_file(dst, src, ignore_read_only);
|
||||
Result copy_entire_file(const FsPath& dst, const FsPath& src) override {
|
||||
return fs::copy_entire_file(dst, src, m_ignore_read_only);
|
||||
}
|
||||
};
|
||||
|
||||
struct FsNative : Fs {
|
||||
FsNative() = default;
|
||||
FsNative(FsFileSystem* fs, bool own) : m_fs{*fs}, m_own{own} {}
|
||||
explicit FsNative(bool ignore_read_only = true) : Fs{ignore_read_only} {}
|
||||
explicit FsNative(FsFileSystem* fs, bool own, bool ignore_read_only = true) : Fs{ignore_read_only}, m_fs{*fs}, m_own{own} {}
|
||||
|
||||
virtual ~FsNative() {
|
||||
if (m_own) {
|
||||
@@ -355,32 +368,32 @@ struct FsNative : Fs {
|
||||
return m_open_result;
|
||||
}
|
||||
|
||||
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0, bool ignore_read_only = false) override {
|
||||
return fs::CreateFile(&m_fs, path, size, option, ignore_read_only);
|
||||
Result CreateFile(const FsPath& path, u64 size = 0, u32 option = 0) override {
|
||||
return fs::CreateFile(&m_fs, path, size, option, m_ignore_read_only);
|
||||
}
|
||||
Result CreateDirectory(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::CreateDirectory(&m_fs, path, ignore_read_only);
|
||||
Result CreateDirectory(const FsPath& path) override {
|
||||
return fs::CreateDirectory(&m_fs, path, m_ignore_read_only);
|
||||
}
|
||||
Result CreateDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::CreateDirectoryRecursively(&m_fs, path, ignore_read_only);
|
||||
Result CreateDirectoryRecursively(const FsPath& path) override {
|
||||
return fs::CreateDirectoryRecursively(&m_fs, path, m_ignore_read_only);
|
||||
}
|
||||
Result CreateDirectoryRecursivelyWithPath(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::CreateDirectoryRecursivelyWithPath(&m_fs, path, ignore_read_only);
|
||||
Result CreateDirectoryRecursivelyWithPath(const FsPath& path) override {
|
||||
return fs::CreateDirectoryRecursivelyWithPath(&m_fs, path, m_ignore_read_only);
|
||||
}
|
||||
Result DeleteFile(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::DeleteFile(&m_fs, path, ignore_read_only);
|
||||
Result DeleteFile(const FsPath& path) override {
|
||||
return fs::DeleteFile(&m_fs, path, m_ignore_read_only);
|
||||
}
|
||||
Result DeleteDirectory(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::DeleteDirectory(&m_fs, path, ignore_read_only);
|
||||
Result DeleteDirectory(const FsPath& path) override {
|
||||
return fs::DeleteDirectory(&m_fs, path, m_ignore_read_only);
|
||||
}
|
||||
Result DeleteDirectoryRecursively(const FsPath& path, bool ignore_read_only = false) override {
|
||||
return fs::DeleteDirectoryRecursively(&m_fs, path, ignore_read_only);
|
||||
Result DeleteDirectoryRecursively(const FsPath& path) override {
|
||||
return fs::DeleteDirectoryRecursively(&m_fs, path, m_ignore_read_only);
|
||||
}
|
||||
Result RenameFile(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) override {
|
||||
return fs::RenameFile(&m_fs, src, dst, ignore_read_only);
|
||||
Result RenameFile(const FsPath& src, const FsPath& dst) override {
|
||||
return fs::RenameFile(&m_fs, src, dst, m_ignore_read_only);
|
||||
}
|
||||
Result RenameDirectory(const FsPath& src, const FsPath& dst, bool ignore_read_only = false) override {
|
||||
return fs::RenameDirectory(&m_fs, src, dst, ignore_read_only);
|
||||
Result RenameDirectory(const FsPath& src, const FsPath& dst) override {
|
||||
return fs::RenameDirectory(&m_fs, src, dst, m_ignore_read_only);
|
||||
}
|
||||
Result GetEntryType(const FsPath& path, FsDirEntryType* out) override {
|
||||
return fs::GetEntryType(&m_fs, path, out);
|
||||
@@ -397,11 +410,11 @@ struct FsNative : Fs {
|
||||
Result read_entire_file(const FsPath& path, std::vector<u8>& out) override {
|
||||
return fs::read_entire_file(&m_fs, path, out);
|
||||
}
|
||||
Result write_entire_file(const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = false) override {
|
||||
return fs::write_entire_file(&m_fs, path, in, ignore_read_only);
|
||||
Result write_entire_file(const FsPath& path, const std::vector<u8>& in) override {
|
||||
return fs::write_entire_file(&m_fs, path, in, m_ignore_read_only);
|
||||
}
|
||||
Result copy_entire_file(const FsPath& dst, const FsPath& src, bool ignore_read_only = false) override {
|
||||
return fs::copy_entire_file(&m_fs, dst, src, ignore_read_only);
|
||||
Result copy_entire_file(const FsPath& dst, const FsPath& src) override {
|
||||
return fs::copy_entire_file(&m_fs, dst, src, m_ignore_read_only);
|
||||
}
|
||||
|
||||
FsFileSystem m_fs{};
|
||||
@@ -417,43 +430,28 @@ struct FsNativeSd final : FsNative {
|
||||
};
|
||||
#else
|
||||
struct FsNativeSd final : FsNative {
|
||||
FsNativeSd() : FsNative{fsdevGetDeviceFileSystem("sdmc:"), false} {
|
||||
FsNativeSd(bool ignore_read_only = true) : FsNative{fsdevGetDeviceFileSystem("sdmc:"), false, ignore_read_only} {
|
||||
m_open_result = 0;
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
struct FsNativeBis final : FsNative {
|
||||
FsNativeBis(FsBisPartitionId id, const FsPath& string) {
|
||||
FsNativeBis(FsBisPartitionId id, const FsPath& string, bool ignore_read_only = true) : FsNative{ignore_read_only} {
|
||||
m_open_result = fsOpenBisFileSystem(&m_fs, id, string);
|
||||
}
|
||||
};
|
||||
|
||||
struct FsNativeImage final : FsNative {
|
||||
FsNativeImage(FsImageDirectoryId id) {
|
||||
FsNativeImage(FsImageDirectoryId id, bool ignore_read_only = true) : FsNative{ignore_read_only} {
|
||||
m_open_result = fsOpenImageDirectoryFileSystem(&m_fs, id);
|
||||
}
|
||||
};
|
||||
|
||||
struct FsNativeContentStorage final : FsNative {
|
||||
FsNativeContentStorage(FsContentStorageId id) {
|
||||
FsNativeContentStorage(FsContentStorageId id, bool ignore_read_only = true) : FsNative{ignore_read_only} {
|
||||
m_open_result = fsOpenContentStorageFileSystem(&m_fs, id);
|
||||
}
|
||||
};
|
||||
|
||||
// auto file_exists(const FsPath& path) -> bool;
|
||||
// auto create_file(const FsPath& path, u64 size = 0) -> Result;
|
||||
// auto delete_file(const FsPath& path) -> Result;
|
||||
// auto create_directory(const FsPath& path) -> Result;
|
||||
// auto create_directory_recursively(const FsPath& path) -> Result;
|
||||
// auto delete_directory(const FsPath& path) -> Result;
|
||||
// auto delete_directory_recursively(const FsPath& path) -> Result;
|
||||
// auto rename_file(const FsPath& src, const FsPath& dst) -> Result;
|
||||
// auto rename_directory(const FsPath& src, const FsPath& dst) -> Result;
|
||||
|
||||
// auto read_entire_file(const FsPath& path, std::vector<u8>& out) -> Result;
|
||||
// auto write_entire_file(const FsPath& path, const std::vector<u8>& in) -> Result;
|
||||
// // single threaded one shot copy, only use for very small files!
|
||||
// auto copy_entire_file(const FsPath& dst, const FsPath& src) -> Result;
|
||||
|
||||
} // namespace fs
|
||||
|
||||
@@ -1,33 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
|
||||
struct FtpVfsFile {
|
||||
FsFile fd;
|
||||
s64 off;
|
||||
s64 buf_off;
|
||||
s64 buf_size;
|
||||
bool is_write;
|
||||
bool is_valid;
|
||||
u8 buf[1024 * 1024 * 1];
|
||||
};
|
||||
|
||||
struct FtpVfsDir {
|
||||
FsDir dir;
|
||||
bool is_valid;
|
||||
};
|
||||
|
||||
struct FtpVfsDirEntry {
|
||||
FsDirectoryEntry buf;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
namespace sphaira::ftpsrv {
|
||||
|
||||
bool Init();
|
||||
void Exit();
|
||||
|
||||
} // namespace sphaira::ftpsrv
|
||||
|
||||
#endif // __cplusplus
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
#define sphaira_USE_LOG 1
|
||||
|
||||
#include <cstdarg>
|
||||
|
||||
#if sphaira_USE_LOG
|
||||
auto log_file_init() -> bool;
|
||||
auto log_nxlink_init() -> bool;
|
||||
void log_file_exit();
|
||||
void log_nxlink_exit();
|
||||
void log_write(const char* s, ...) __attribute__ ((format (printf, 1, 2)));
|
||||
void log_write_arg(const char* s, std::va_list& v);
|
||||
#else
|
||||
inline auto log_file_init() -> bool {
|
||||
return true;
|
||||
|
||||
@@ -19,7 +19,6 @@ struct NroEntry {
|
||||
s64 size{};
|
||||
NacpStruct nacp{};
|
||||
|
||||
std::vector<u8> icon{};
|
||||
u64 icon_size{};
|
||||
u64 icon_offset{};
|
||||
|
||||
@@ -76,4 +75,10 @@ auto nro_add_arg_file(std::string arg) -> std::string;
|
||||
// strips sdmc:
|
||||
auto nro_normalise_path(const std::string& p) -> std::string;
|
||||
|
||||
// helpers to find nro entry, will be made methods soon once i convert vector into a struct.
|
||||
auto nro_find(std::span<const NroEntry> array, std::string_view name, std::string_view author, const fs::FsPath& path) -> std::optional<NroEntry>;
|
||||
auto nro_find_name(std::span<const NroEntry> array, std::string_view name) -> std::optional<NroEntry>;
|
||||
auto nro_find_author(std::span<const NroEntry> array, std::string_view author) -> std::optional<NroEntry>;
|
||||
auto nro_find_path(std::span<const NroEntry> array, const fs::FsPath& path) -> std::optional<NroEntry>;
|
||||
|
||||
} // namespace sphaira
|
||||
|
||||
@@ -10,7 +10,6 @@ public:
|
||||
ErrorBox(Result code, const std::string& message);
|
||||
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||
auto OnLayoutChange() -> void override;
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
|
||||
private:
|
||||
|
||||
57
sphaira/include/ui/list.hpp
Normal file
57
sphaira/include/ui/list.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/object.hpp"
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
struct List final : Object {
|
||||
using Callback = std::function<void(NVGcontext* vg, Theme* theme, Vec4 v, s64 index)>;
|
||||
using TouchCallback = std::function<void(s64 index)>;
|
||||
|
||||
List(s64 row, s64 page, const Vec4& pos, const Vec4& v, const Vec2& pad = {});
|
||||
|
||||
void OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCallback callback);
|
||||
|
||||
void Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const;
|
||||
|
||||
auto SetScrollBarPos(float x, float y, float h) {
|
||||
m_scrollbar.x = x;
|
||||
m_scrollbar.y = y;
|
||||
m_scrollbar.h = h;
|
||||
}
|
||||
|
||||
auto ScrollDown(s64& index, s64 step, s64 count) -> bool;
|
||||
auto ScrollUp(s64& index, s64 step, s64 count) -> bool;
|
||||
|
||||
auto GetYoff() const {
|
||||
return m_yoff;
|
||||
}
|
||||
|
||||
void SetYoff(float y = 0) {
|
||||
m_yoff = y;
|
||||
}
|
||||
|
||||
auto GetMaxY() const {
|
||||
return m_v.h + m_pad.y;
|
||||
}
|
||||
|
||||
private:
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override {}
|
||||
auto ClampY(float y, s64 count) const -> float;
|
||||
|
||||
private:
|
||||
const s64 m_row;
|
||||
const s64 m_page;
|
||||
|
||||
Vec4 m_v;
|
||||
Vec2 m_pad;
|
||||
|
||||
Vec4 m_scrollbar{};
|
||||
|
||||
// current y offset.
|
||||
float m_yoff{};
|
||||
// in progress y offset, used when scrolling.
|
||||
float m_y_prog{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "ui/scrollable_text.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include "nro.hpp"
|
||||
#include "fs.hpp"
|
||||
#include <span>
|
||||
@@ -27,6 +28,8 @@ struct LazyImage {
|
||||
~LazyImage();
|
||||
int image{};
|
||||
int w{}, h{};
|
||||
bool tried_cache{};
|
||||
bool cached{};
|
||||
ImageDownloadState state{ImageDownloadState::None};
|
||||
u8 first_pixel[4]{};
|
||||
};
|
||||
@@ -75,7 +78,7 @@ struct EntryMenu final : MenuBase {
|
||||
// void OnFocusGained() override;
|
||||
|
||||
void ShowChangelogAction();
|
||||
void SetIndex(std::size_t index);
|
||||
void SetIndex(s64 index);
|
||||
|
||||
void UpdateOptions();
|
||||
|
||||
@@ -95,10 +98,10 @@ private:
|
||||
const LazyImage& m_default_icon;
|
||||
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;
|
||||
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_changelog;
|
||||
@@ -147,7 +150,7 @@ struct FeedbackMenu final : MenuBase {
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
|
||||
void SetIndex(std::size_t index);
|
||||
void SetIndex(s64 index);
|
||||
void ScanHomebrew();
|
||||
void Sort();
|
||||
|
||||
@@ -155,8 +158,7 @@ private:
|
||||
const std::vector<Entry>& m_package_entries;
|
||||
LazyImage& m_default_image;
|
||||
std::vector<FeedbackEntry> m_entries;
|
||||
std::size_t m_start{};
|
||||
std::size_t m_index{}; // where i am in the array
|
||||
s64 m_index{}; // where i am in the array
|
||||
ImageDownloadState m_repo_download_state{ImageDownloadState::None};
|
||||
};
|
||||
|
||||
@@ -168,7 +170,7 @@ struct Menu final : MenuBase {
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
|
||||
void SetIndex(std::size_t index);
|
||||
void SetIndex(s64 index);
|
||||
void ScanHomebrew();
|
||||
void Sort();
|
||||
|
||||
@@ -199,19 +201,19 @@ private:
|
||||
SortType m_sort{SortType::SortType_Updated};
|
||||
OrderType m_order{OrderType::OrderType_Decending};
|
||||
|
||||
std::size_t m_start{};
|
||||
std::size_t m_index{}; // where i am in the array
|
||||
s64 m_index{}; // where i am in the array
|
||||
LazyImage m_default_image;
|
||||
LazyImage m_update;
|
||||
LazyImage m_get;
|
||||
LazyImage m_local;
|
||||
LazyImage m_installed;
|
||||
ImageDownloadState m_repo_download_state{ImageDownloadState::None};
|
||||
std::unique_ptr<List> m_list;
|
||||
|
||||
std::string m_search_term;
|
||||
std::string m_author_term;
|
||||
u64 m_entry_search_jump_back{};
|
||||
u64 m_entry_author_jump_back{};
|
||||
s64 m_entry_search_jump_back{};
|
||||
s64 m_entry_author_jump_back{};
|
||||
bool m_is_search{};
|
||||
bool m_is_author{};
|
||||
bool m_dirty{}; // if set, does a sort
|
||||
|
||||
@@ -23,8 +23,8 @@ private:
|
||||
|
||||
std::unique_ptr<ScrollableText> m_scroll_text;
|
||||
|
||||
std::size_t m_start{};
|
||||
std::size_t m_index{}; // where i am in the array
|
||||
s64 m_start{};
|
||||
s64 m_index{}; // where i am in the array
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::fileview
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include "nro.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "option.hpp"
|
||||
@@ -9,6 +10,12 @@
|
||||
|
||||
namespace sphaira::ui::menu::filebrowser {
|
||||
|
||||
enum class FsType {
|
||||
Sd,
|
||||
ImageNand,
|
||||
ImageSd,
|
||||
};
|
||||
|
||||
enum class SelectedType {
|
||||
None,
|
||||
Copy,
|
||||
@@ -83,13 +90,23 @@ struct FileAssocEntry {
|
||||
std::vector<std::string> ext; // list of ext
|
||||
std::vector<std::string> database; // list of systems
|
||||
};
|
||||
|
||||
struct LastFile {
|
||||
fs::FsPath name;
|
||||
u64 index;
|
||||
u64 offset;
|
||||
u64 entries_count;
|
||||
s64 index;
|
||||
float offset;
|
||||
s64 entries_count;
|
||||
};
|
||||
|
||||
struct FsDirCollection {
|
||||
fs::FsPath path;
|
||||
fs::FsPath parent_name;
|
||||
std::vector<FsDirectoryEntry> files;
|
||||
std::vector<FsDirectoryEntry> dirs;
|
||||
};
|
||||
|
||||
using FsDirCollections = std::vector<FsDirCollection>;
|
||||
|
||||
struct Menu final : MenuBase {
|
||||
Menu(const std::vector<NroEntry>& nro_entries);
|
||||
~Menu();
|
||||
@@ -103,7 +120,7 @@ struct Menu final : MenuBase {
|
||||
}
|
||||
|
||||
private:
|
||||
void SetIndex(std::size_t index);
|
||||
void SetIndex(s64 index);
|
||||
void InstallForwarder();
|
||||
auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result;
|
||||
|
||||
@@ -115,7 +132,7 @@ private:
|
||||
return GetNewPath(m_path, entry.name);
|
||||
}
|
||||
|
||||
auto GetNewPath(u64 index) const -> fs::FsPath {
|
||||
auto GetNewPath(s64 index) const -> fs::FsPath {
|
||||
return GetNewPath(m_path, GetEntry(index).name);
|
||||
}
|
||||
|
||||
@@ -203,11 +220,19 @@ private:
|
||||
void OnDeleteCallback();
|
||||
void OnPasteCallback();
|
||||
void OnRenameCallback();
|
||||
auto CheckIfUpdateFolder() -> Result;
|
||||
|
||||
auto get_collection(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollection& out, bool inc_file, bool inc_dir, bool inc_size) -> Result;
|
||||
auto get_collections(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out) -> Result;
|
||||
|
||||
void SetFs(const fs::FsPath& new_path, u32 new_type);
|
||||
|
||||
private:
|
||||
static constexpr inline const char* INI_SECTION = "filebrowser";
|
||||
|
||||
const std::vector<NroEntry>& m_nro_entries;
|
||||
std::unique_ptr<fs::FsNative> m_fs;
|
||||
FsType m_fs_type;
|
||||
fs::FsPath m_path;
|
||||
std::vector<FileEntry> m_entries;
|
||||
std::vector<u32> m_entries_index; // files not including hidden
|
||||
@@ -215,6 +240,9 @@ private:
|
||||
std::vector<u32> m_entries_index_search; // files found via search
|
||||
std::span<u32> m_entries_current;
|
||||
|
||||
std::unique_ptr<List> m_list;
|
||||
std::optional<fs::FsPath> m_daybreak_path;
|
||||
|
||||
// search options
|
||||
// show files [X]
|
||||
// show folders [X]
|
||||
@@ -229,9 +257,8 @@ private:
|
||||
// if it does, the index becomes that file.
|
||||
std::vector<LastFile> m_previous_highlighted_file;
|
||||
fs::FsPath m_selected_path;
|
||||
std::size_t m_index{};
|
||||
std::size_t m_index_offset{};
|
||||
std::size_t m_selected_count{};
|
||||
s64 m_index{};
|
||||
s64 m_selected_count{};
|
||||
SelectedType m_selected_type{SelectedType::None};
|
||||
|
||||
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_folders_first{INI_SECTION, "folders_first", true};
|
||||
option::OptionBool m_hidden_last{INI_SECTION, "hidden_last", false};
|
||||
|
||||
option::OptionBool m_search_show_files{INI_SECTION, "search_show_files", true};
|
||||
option::OptionBool m_search_show_folders{INI_SECTION, "search_show_folders", true};
|
||||
option::OptionBool m_search_recursive{INI_SECTION, "search_recursive", false};
|
||||
option::OptionBool m_ignore_read_only{INI_SECTION, "ignore_read_only", false};
|
||||
option::OptionLong m_mount{INI_SECTION, "mount", 0};
|
||||
|
||||
bool m_loaded_assoc_entries{};
|
||||
bool m_is_update_folder{};
|
||||
|
||||
75
sphaira/include/ui/menus/ghdl.hpp
Normal file
75
sphaira/include/ui/menus/ghdl.hpp
Normal 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
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include "nro.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "option.hpp"
|
||||
@@ -29,7 +30,7 @@ struct Menu final : MenuBase {
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
|
||||
void SetIndex(std::size_t index);
|
||||
void SetIndex(s64 index);
|
||||
void InstallHomebrew();
|
||||
void ScanHomebrew();
|
||||
void Sort();
|
||||
@@ -50,8 +51,8 @@ private:
|
||||
static constexpr inline const char* INI_SECTION = "homebrew";
|
||||
|
||||
std::vector<NroEntry> m_entries;
|
||||
std::size_t m_start{};
|
||||
std::size_t m_index{}; // where i am in the array
|
||||
s64 m_index{}; // where i am in the array
|
||||
std::unique_ptr<List> m_list;
|
||||
|
||||
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_AlphabeticalStar};
|
||||
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Decending};
|
||||
|
||||
@@ -61,7 +61,7 @@ private:
|
||||
Rotation m_rotation{Rotation_90};
|
||||
Colour m_colour{Colour_Grey};
|
||||
int m_image{};
|
||||
std::size_t m_index{};
|
||||
s64 m_index{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::irs
|
||||
|
||||
@@ -28,10 +28,13 @@ struct MainMenu final : Widget {
|
||||
void OnFocusGained() override;
|
||||
void OnFocusLost() override;
|
||||
|
||||
auto IsMenu() const -> bool override {
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
void OnLRPress(std::shared_ptr<MenuBase> menu, Button b);
|
||||
void AddOnLPress();
|
||||
void AddOnRPress();
|
||||
void AddOnLRPress();
|
||||
|
||||
private:
|
||||
std::shared_ptr<homebrew::Menu> m_homebrew_menu{};
|
||||
|
||||
@@ -12,15 +12,31 @@ struct MenuBase : Widget {
|
||||
|
||||
virtual void Update(Controller* controller, TouchInfo* touch);
|
||||
virtual void Draw(NVGcontext* vg, Theme* theme);
|
||||
|
||||
auto IsMenu() const -> bool override {
|
||||
return true;
|
||||
}
|
||||
|
||||
void SetTitle(std::string title);
|
||||
void SetTitleSubHeading(std::string sub_heading);
|
||||
void SetSubHeading(std::string sub_heading);
|
||||
|
||||
private:
|
||||
void UpdateVars();
|
||||
|
||||
private:
|
||||
std::string m_title;
|
||||
std::string m_title_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
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "ui/scrollable_text.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include "option.hpp"
|
||||
#include <span>
|
||||
|
||||
@@ -15,28 +16,14 @@ enum class ImageDownloadState {
|
||||
};
|
||||
|
||||
struct LazyImage {
|
||||
LazyImage() = default;
|
||||
~LazyImage();
|
||||
int image{};
|
||||
int w{}, h{};
|
||||
bool tried_cache{};
|
||||
bool cached{};
|
||||
ImageDownloadState state{ImageDownloadState::None};
|
||||
u8 first_pixel[4]{};
|
||||
};
|
||||
|
||||
// "mutation setLike($type: String!, $id: String!, $value: Boolean!) {\n setLike(type: $type, id: $id, value: $value)\n}\n"
|
||||
|
||||
// https://api.themezer.net/?query=query($nsfw:Boolean,$target:String,$page:Int,$limit:Int,$sort:String,$order:String,$query:String){themeList(nsfw:$nsfw,target:$target,page:$page,limit:$limit,sort:$sort,order:$order,query:$query){id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}}&variables={"nsfw":false,"target":null,"page":1,"limit":10,"sort":"updated","order":"desc","query":null}
|
||||
// https://api.themezer.net/?query=query($nsfw:Boolean,$page:Int,$limit:Int,$sort:String,$order:String,$query:String){packList(nsfw:$nsfw,page:$page,limit:$limit,sort:$sort,order:$order,query:$query){id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count,themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}}}&variables={"nsfw":false,"page":1,"limit":10,"sort":"updated","order":"desc","query":null}
|
||||
// https://api.themezer.net/?query=query($id:String!){pack(id:$id){id,creator{display_name},details{name,description},last_updated,categories,dl_count,like_count,themes{id,details{name},layout{id,details{name}},categories,target,preview{original,thumb},last_updated,dl_count,like_count}}}&variables={"id":"16d"}
|
||||
|
||||
// https://api.themezer.net/?query=query{nxinstaller(id:"t9a6"){themes{filename,url,mimetype}}}
|
||||
// https://api.themezer.net/?query=query{downloadTheme(id:"t9a6"){filename,url,mimetype}}
|
||||
// https://api.themezer.net/?query=query{downloadPack(id:"t9a6"){filename,url,mimetype}}
|
||||
|
||||
// {"data":{"setLike":true}}
|
||||
// https://api.themezer.net/?query=mutation{setLike(type:"packs",id:"5",value:true){data{setLike}}}
|
||||
// https://api.themezer.net/?query=mutation($type:String!,$id:String!,$value:Boolean!){setLike(type:$type,id:$id,value:$value){data{setLike}}}&variables={"type":"packs","id":"5","value":true}
|
||||
|
||||
enum MenuState {
|
||||
MenuState_Normal,
|
||||
MenuState_Search,
|
||||
@@ -55,6 +42,11 @@ enum class PageLoadState {
|
||||
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 {
|
||||
std::string id;
|
||||
std::string display_name;
|
||||
@@ -62,11 +54,11 @@ struct Creator {
|
||||
|
||||
struct Details {
|
||||
std::string name;
|
||||
std::string description;
|
||||
// std::string description;
|
||||
};
|
||||
|
||||
struct Preview {
|
||||
std::string original;
|
||||
// std::string original;
|
||||
std::string thumb;
|
||||
LazyImage lazy_image;
|
||||
};
|
||||
@@ -81,13 +73,13 @@ using DownloadTheme = DownloadPack;
|
||||
|
||||
struct ThemeEntry {
|
||||
std::string id;
|
||||
Creator creator;
|
||||
Details details;
|
||||
std::string last_updated;
|
||||
u64 dl_count;
|
||||
u64 like_count;
|
||||
std::vector<std::string> categories;
|
||||
std::string target;
|
||||
// Creator creator;
|
||||
// Details details;
|
||||
// std::string last_updated;
|
||||
// u64 dl_count;
|
||||
// u64 like_count;
|
||||
// std::vector<std::string> categories;
|
||||
// std::string target;
|
||||
Preview preview;
|
||||
};
|
||||
|
||||
@@ -106,10 +98,10 @@ struct PackListEntry {
|
||||
std::string id;
|
||||
Creator creator;
|
||||
Details details;
|
||||
std::string last_updated;
|
||||
std::vector<std::string> categories;
|
||||
u64 dl_count;
|
||||
u64 like_count;
|
||||
// std::string last_updated;
|
||||
// std::vector<std::string> categories;
|
||||
// u64 dl_count;
|
||||
// u64 like_count;
|
||||
std::vector<ThemeEntry> themes;
|
||||
};
|
||||
|
||||
@@ -173,8 +165,11 @@ struct Menu final : MenuBase {
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
|
||||
void SetIndex(std::size_t index) {
|
||||
void SetIndex(s64 index) {
|
||||
m_index = index;
|
||||
if (!m_index) {
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
}
|
||||
|
||||
// void SetSearch(const std::string& term);
|
||||
@@ -189,13 +184,13 @@ private:
|
||||
static constexpr inline u32 MAX_ON_PAGE = 16; // same as website
|
||||
|
||||
std::vector<PageEntry> m_pages;
|
||||
std::size_t m_page_index{};
|
||||
std::size_t m_page_index_max{1};
|
||||
s64 m_page_index{};
|
||||
s64 m_page_index_max{1};
|
||||
|
||||
std::string m_search{};
|
||||
|
||||
std::size_t m_start{};
|
||||
std::size_t m_index{}; // where i am in the array
|
||||
s64 m_index{}; // where i am in the array
|
||||
std::unique_ptr<List> m_list;
|
||||
|
||||
// options
|
||||
option::OptionLong m_sort{INI_SECTION, "sort", 0};
|
||||
|
||||
@@ -19,7 +19,6 @@ public:
|
||||
auto IsDone() const noexcept { return m_count == 0; }
|
||||
|
||||
private:
|
||||
void OnLayoutChange() override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
|
||||
private:
|
||||
@@ -34,7 +33,6 @@ public:
|
||||
NotifMananger() = default;
|
||||
~NotifMananger() = default;
|
||||
|
||||
void OnLayoutChange() override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
|
||||
void Push(const NotifEntry& entry);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "nanovg.h"
|
||||
#include "ui/widget.hpp"
|
||||
#include "ui/types.hpp"
|
||||
|
||||
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);
|
||||
|
||||
auto getButton(Button button) -> const char*;
|
||||
void drawButton(NVGcontext* vg, float x, float y, float size, Button button);
|
||||
void drawButtons(NVGcontext* vg, const Widget::Actions& actions, const NVGcolor& c, float start_x = 1220.f);
|
||||
void drawScrollbar(NVGcontext* vg, Theme* theme, u32 index_off, u32 count, u32 max_per_page);
|
||||
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 getHighlightAnimation(float* gradientX, float* gradientY, float* color);
|
||||
|
||||
@@ -9,8 +9,6 @@ public:
|
||||
Object() = default;
|
||||
virtual ~Object() = default;
|
||||
|
||||
// virtual auto OnLayoutChange() -> void = 0;
|
||||
virtual auto OnLayoutChange() -> void {};
|
||||
virtual auto Draw(NVGcontext* vg, Theme* theme) -> void = 0;
|
||||
|
||||
auto GetPos() const noexcept {
|
||||
|
||||
@@ -12,7 +12,6 @@ public:
|
||||
OptionBoxEntry(const std::string& text, Vec4 pos);
|
||||
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> void override {}
|
||||
auto OnLayoutChange() -> void override {}
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
|
||||
auto Selected(bool enable) -> void;
|
||||
@@ -28,23 +27,25 @@ private:
|
||||
// todo: support upto 4 options.
|
||||
class OptionBox final : public Widget {
|
||||
public:
|
||||
using Callback = std::function<void(std::optional<std::size_t> index)>;
|
||||
using Callback = std::function<void(std::optional<s64> index)>;
|
||||
using Option = std::string;
|
||||
using Options = std::vector<Option>;
|
||||
|
||||
public:
|
||||
OptionBox(const std::string& message, const Option& a, Callback cb); // confirm
|
||||
OptionBox(const std::string& message, const Option& a, Callback cb = [](auto){}); // confirm
|
||||
OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb); // yesno
|
||||
OptionBox(const std::string& message, const Option& a, const Option& b, std::size_t index, Callback cb); // yesno
|
||||
OptionBox(const std::string& message, const Option& a, const Option& b, s64 index, Callback cb); // yesno
|
||||
OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, Callback cb); // tri
|
||||
OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, std::size_t index, Callback cb); // tri
|
||||
OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, s64 index, Callback cb); // tri
|
||||
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||
auto OnLayoutChange() -> void override;
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
auto OnFocusGained() noexcept -> void override;
|
||||
auto OnFocusLost() noexcept -> void override;
|
||||
|
||||
private:
|
||||
auto Setup(std::size_t index) -> void; // common setup values
|
||||
auto Setup(s64 index) -> void; // common setup values
|
||||
void SetIndex(s64 index);
|
||||
|
||||
private:
|
||||
std::string m_message;
|
||||
@@ -52,7 +53,7 @@ private:
|
||||
|
||||
Vec4 m_spacer_line{};
|
||||
|
||||
std::size_t m_index{};
|
||||
s64 m_index{};
|
||||
std::vector<OptionBoxEntry> m_entries;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/widget.hpp"
|
||||
#include <optional>
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
class OptionList final : public Widget {
|
||||
public:
|
||||
using Options = std::vector<std::pair<std::string, std::function<void()>>>;
|
||||
|
||||
public:
|
||||
OptionList(Options _options);
|
||||
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||
auto OnLayoutChange() -> void override;
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
|
||||
protected:
|
||||
Options m_options;
|
||||
std::size_t m_index{};
|
||||
|
||||
private:
|
||||
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/widget.hpp"
|
||||
#include "ui/scrollbar.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include <optional>
|
||||
|
||||
namespace sphaira::ui {
|
||||
@@ -9,18 +9,22 @@ namespace sphaira::ui {
|
||||
class PopupList final : public Widget {
|
||||
public:
|
||||
using Items = std::vector<std::string>;
|
||||
using Callback = std::function<void(std::optional<std::size_t>)>;
|
||||
using Callback = std::function<void(std::optional<s64>)>;
|
||||
|
||||
public:
|
||||
explicit PopupList(std::string title, Items items, Callback cb, std::size_t index = 0);
|
||||
explicit PopupList(std::string title, Items items, Callback cb, s64 index = 0);
|
||||
PopupList(std::string title, Items items, Callback cb, std::string index);
|
||||
PopupList(std::string title, Items items, std::string& index_str_ref, std::size_t& index);
|
||||
PopupList(std::string title, Items items, std::string& index_str_ref, s64& index);
|
||||
PopupList(std::string title, Items items, std::string& index_ref);
|
||||
PopupList(std::string title, Items items, std::size_t& index_ref);
|
||||
PopupList(std::string title, Items items, s64& index_ref);
|
||||
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||
auto OnLayoutChange() -> void override;
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
auto OnFocusGained() noexcept -> void override;
|
||||
auto OnFocusLost() noexcept -> void override;
|
||||
|
||||
private:
|
||||
void SetIndex(s64 index);
|
||||
|
||||
private:
|
||||
static constexpr Vec2 m_title_pos{70.f, 28.f};
|
||||
@@ -31,17 +35,14 @@ private:
|
||||
std::string m_title;
|
||||
Items m_items;
|
||||
Callback m_callback;
|
||||
std::size_t m_index; // index in list array
|
||||
std::size_t m_index_offset{}; // drawing from array start
|
||||
s64 m_index; // index in list array
|
||||
s64 m_index_offset{}; // drawing from array start
|
||||
|
||||
// std::size_t& index_ref;
|
||||
// std::string& index_str_ref;
|
||||
std::unique_ptr<List> m_list;
|
||||
|
||||
float m_selected_y{};
|
||||
float m_yoff{};
|
||||
float m_line_top{};
|
||||
float m_line_bottom{};
|
||||
ScrollBar m_scrollbar;
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui
|
||||
|
||||
@@ -22,7 +22,7 @@ struct ProgressBox final : Widget {
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
|
||||
auto NewTransfer(const std::string& transfer) -> ProgressBox&;
|
||||
auto UpdateTransfer(u64 offset, u64 size) -> ProgressBox&;
|
||||
auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
|
||||
void RequestExit();
|
||||
auto ShouldExit() -> bool;
|
||||
|
||||
@@ -30,6 +30,16 @@ struct ProgressBox final : Widget {
|
||||
auto CopyFile(const fs::FsPath& src, const fs::FsPath& dst) -> Result;
|
||||
void Yield();
|
||||
|
||||
auto OnDownloadProgressCallback() {
|
||||
return [this](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
|
||||
if (this->ShouldExit()) {
|
||||
return false;
|
||||
}
|
||||
this->UpdateTransfer(dlnow, dltotal);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
public:
|
||||
struct ThreadData {
|
||||
ProgressBox* pbox;
|
||||
@@ -45,8 +55,8 @@ private:
|
||||
ProgressBoxDoneCallback m_done{};
|
||||
std::string m_title{};
|
||||
std::string m_transfer{};
|
||||
u64 m_size{};
|
||||
u64 m_offset{};
|
||||
s64 m_size{};
|
||||
s64 m_offset{};
|
||||
bool m_exit_requested{};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/widget.hpp"
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
class ScrollBar final : public Widget {
|
||||
public:
|
||||
enum class Direction { DOWN, UP };
|
||||
|
||||
public:
|
||||
ScrollBar() = default;
|
||||
ScrollBar(Vec4 bounds, float entry_height, std::size_t entries);
|
||||
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> void override {}
|
||||
auto OnLayoutChange() -> void override;
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
|
||||
auto Setup(Vec4 bounds, float entry_height, std::size_t entries) -> void;
|
||||
auto Move(Direction direction) -> void;
|
||||
|
||||
private:
|
||||
auto Setup() -> void;
|
||||
|
||||
private:
|
||||
Vec4 m_bounds{};
|
||||
std::size_t m_entries{};
|
||||
std::size_t m_index{};
|
||||
float m_entry_height{};
|
||||
float m_step_size{};
|
||||
bool m_should_draw{false};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/widget.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include <memory>
|
||||
|
||||
namespace sphaira::ui {
|
||||
@@ -9,7 +10,6 @@ class SidebarEntryBase : public Widget {
|
||||
public:
|
||||
SidebarEntryBase(std::string&& title);
|
||||
virtual auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
virtual auto OnLayoutChange() -> void override {}
|
||||
|
||||
protected:
|
||||
std::string m_title;
|
||||
@@ -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, std::string true_str = "On", std::string false_str = "Off");
|
||||
|
||||
private:
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
|
||||
private:
|
||||
bool m_option;
|
||||
Callback m_callback;
|
||||
std::string m_true_str;
|
||||
@@ -50,10 +50,10 @@ class SidebarEntryArray final : public SidebarEntryBase {
|
||||
public:
|
||||
using Items = std::vector<std::string>;
|
||||
using ListCallback = std::function<void()>;
|
||||
using Callback = std::function<void(std::size_t& index)>;
|
||||
using Callback = std::function<void(s64& index)>;
|
||||
|
||||
public:
|
||||
explicit SidebarEntryArray(std::string title, Items items, Callback cb, std::size_t index = 0);
|
||||
explicit SidebarEntryArray(std::string title, Items items, Callback cb, s64 index = 0);
|
||||
SidebarEntryArray(std::string title, Items items, Callback cb, std::string index);
|
||||
SidebarEntryArray(std::string title, Items items, std::string& index);
|
||||
|
||||
@@ -63,7 +63,7 @@ private:
|
||||
Items m_items;
|
||||
ListCallback m_list_callback;
|
||||
Callback m_callback;
|
||||
std::size_t m_index;
|
||||
s64 m_index;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
@@ -101,33 +101,31 @@ public:
|
||||
Sidebar(std::string title, std::string sub, Side side);
|
||||
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||
auto OnLayoutChange() -> void override {}
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
auto OnFocusGained() noexcept -> void override;
|
||||
auto OnFocusLost() noexcept -> void override;
|
||||
|
||||
void Add(std::shared_ptr<SidebarEntryBase> entry);
|
||||
void AddSpacer();
|
||||
void AddHeader(std::string name);
|
||||
|
||||
private:
|
||||
void SetIndex(std::size_t index);
|
||||
void SetIndex(s64 index);
|
||||
void SetupButtons();
|
||||
|
||||
private:
|
||||
std::string m_title;
|
||||
std::string m_sub;
|
||||
Side m_side;
|
||||
Items m_items;
|
||||
std::size_t m_index{};
|
||||
std::size_t m_index_offset{};
|
||||
s64 m_index{};
|
||||
s64 m_index_offset{};
|
||||
|
||||
std::unique_ptr<List> m_list;
|
||||
|
||||
Vec4 m_top_bar{};
|
||||
Vec4 m_bottom_bar{};
|
||||
Vec2 m_title_pos{};
|
||||
Vec4 m_base_pos{};
|
||||
|
||||
float m_selected_y{};
|
||||
|
||||
static constexpr float m_title_size{28.f};
|
||||
// static constexpr Vec2 box_size{380.f, 70.f};
|
||||
static constexpr Vec2 m_box_size{400.f, 70.f};
|
||||
|
||||
@@ -114,15 +114,34 @@ struct [[nodiscard]] Vec4 {
|
||||
|
||||
struct TimeStamp {
|
||||
TimeStamp() {
|
||||
Update();
|
||||
}
|
||||
|
||||
void Update() {
|
||||
start = armGetSystemTick();
|
||||
}
|
||||
|
||||
auto GetNs() -> u64 {
|
||||
auto GetNs() const -> u64 {
|
||||
const auto end_ticks = armGetSystemTick();
|
||||
return armTicksToNs(end_ticks) - armTicksToNs(start);
|
||||
}
|
||||
|
||||
auto GetSeconds() -> double {
|
||||
auto GetMs() const -> u64 {
|
||||
const auto ns = GetNs();
|
||||
return ns/1000/1000;
|
||||
}
|
||||
|
||||
auto GetSeconds() const -> u64 {
|
||||
const auto ns = GetNs();
|
||||
return ns/1000/1000/1000;
|
||||
}
|
||||
|
||||
auto GetMsD() const -> double {
|
||||
const double ns = GetNs();
|
||||
return ns/1000.0/1000.0;
|
||||
}
|
||||
|
||||
auto GetSecondsD() const -> double {
|
||||
const double ns = GetNs();
|
||||
return ns/1000.0/1000.0/1000.0;
|
||||
}
|
||||
@@ -177,39 +196,31 @@ struct Theme {
|
||||
fs::FsPath path;
|
||||
PLSR_BFSTM music;
|
||||
ElementEntry elements[ThemeEntryID_MAX];
|
||||
|
||||
// NVGcolor background; // bg
|
||||
// NVGcolor lines; // grid lines
|
||||
// NVGcolor spacer; // lines in popup box
|
||||
// NVGcolor text; // text colour
|
||||
// NVGcolor text_info; // description text
|
||||
NVGcolor selected; // selected colours
|
||||
// NVGcolor overlay; // popup overlay colour
|
||||
|
||||
// void DrawElement(float x, float y, float w, float h, ThemeEntryID id);
|
||||
};
|
||||
|
||||
enum class TouchState {
|
||||
Start, // set when touch has started
|
||||
Touching, // set when touch is held longer than 1 frame
|
||||
Stop, // set after touch is released
|
||||
None, // set when there is no touch
|
||||
};
|
||||
// enum class TouchGesture {
|
||||
// None,
|
||||
// Tap,
|
||||
// Scroll,
|
||||
// };
|
||||
|
||||
struct TouchInfo {
|
||||
s32 initial_x;
|
||||
s32 initial_y;
|
||||
HidTouchState initial;
|
||||
HidTouchState cur;
|
||||
|
||||
s32 cur_x;
|
||||
s32 cur_y;
|
||||
auto in_range(const Vec4& v) const -> bool {
|
||||
return cur.x >= v.x && cur.x <= v.x + v.w && cur.y >= v.y && cur.y <= v.y + v.h;
|
||||
}
|
||||
|
||||
s32 prev_x;
|
||||
s32 prev_y;
|
||||
|
||||
u32 finger_id;
|
||||
auto in_range(s32 x, s32 y, s32 w, s32 h) const -> bool {
|
||||
return in_range(Vec4(x, y, w, h));
|
||||
}
|
||||
|
||||
bool is_touching;
|
||||
bool is_tap;
|
||||
bool is_scroll;
|
||||
bool is_clicked;
|
||||
bool is_end;
|
||||
};
|
||||
|
||||
enum class Button : u64 {
|
||||
@@ -340,7 +351,7 @@ struct Controller {
|
||||
m_kup = 0;
|
||||
}
|
||||
|
||||
void UpdateButtonHeld(HidNpadButton buttons) {
|
||||
void UpdateButtonHeld(u64 buttons) {
|
||||
if (m_kdown & buttons) {
|
||||
m_step = 50;
|
||||
m_counter = 0;
|
||||
@@ -348,7 +359,7 @@ struct Controller {
|
||||
m_counter += m_step;
|
||||
|
||||
if (m_counter >= m_MAX) {
|
||||
m_kdown |= buttons;
|
||||
m_kdown |= m_kheld & buttons;
|
||||
m_counter = 0;
|
||||
m_step = std::min(m_step + 50, m_MAX_STEP);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,20 @@
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
struct uiButton final : Object {
|
||||
uiButton(Button button, Action action) : m_button{button}, m_action{action} {}
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
|
||||
Button m_button;
|
||||
Action m_action;
|
||||
Vec4 m_button_pos{};
|
||||
Vec4 m_hint_pos{};
|
||||
};
|
||||
|
||||
struct Widget : public Object {
|
||||
using Actions = std::map<Button, Action>;
|
||||
using uiButtons = std::vector<uiButton>;
|
||||
|
||||
virtual ~Widget() = default;
|
||||
|
||||
virtual void Update(Controller* controller, TouchInfo* touch);
|
||||
@@ -26,6 +39,10 @@ struct Widget : public Object {
|
||||
return m_focus;
|
||||
}
|
||||
|
||||
virtual auto IsMenu() const -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto HasAction(Button button) const -> bool;
|
||||
void SetAction(Button button, Action action);
|
||||
void SetActions(std::same_as<std::pair<Button, Action>> auto ...args) {
|
||||
@@ -45,6 +62,8 @@ struct Widget : public Object {
|
||||
m_actions.clear();
|
||||
}
|
||||
|
||||
auto FireAction(Button button, u8 type = ActionType::DOWN) -> bool;
|
||||
|
||||
void SetPop(bool pop = true) {
|
||||
m_pop = pop;
|
||||
}
|
||||
@@ -53,9 +72,14 @@ struct Widget : public Object {
|
||||
return m_pop;
|
||||
}
|
||||
|
||||
using Actions = std::map<Button, Action>;
|
||||
// using Actions = std::unordered_map<Button, Action>;
|
||||
auto SetUiButtonPos(Vec2 pos) {
|
||||
m_button_pos = pos;
|
||||
}
|
||||
|
||||
auto GetUiButtons() const -> uiButtons;
|
||||
|
||||
Actions m_actions;
|
||||
Vec2 m_button_pos{1220, 675};
|
||||
bool m_focus{false};
|
||||
bool m_pop{false};
|
||||
};
|
||||
|
||||
@@ -39,13 +39,14 @@ constexpr auto cexprHash(const char *str, std::size_t v = 0) noexcept -> std::si
|
||||
JSON_SKIP_IF_NULL_PTR(str); \
|
||||
e.name = str; \
|
||||
} \
|
||||
}
|
||||
} break
|
||||
|
||||
#define JSON_SET_OBJ(name) case cexprHash(#name): { \
|
||||
if (yyjson_is_obj(val)) { \
|
||||
from_json(val, e.name); \
|
||||
} \
|
||||
}
|
||||
} break
|
||||
|
||||
#define JSON_SET_UINT(name) JSON_SET_TYPE(name, uint)
|
||||
#define JSON_SET_STR(name) JSON_SET_TYPE(name, str)
|
||||
#define JSON_SET_BOOL(name) JSON_SET_TYPE(name, bool)
|
||||
@@ -72,7 +73,7 @@ constexpr auto cexprHash(const char *str, std::size_t v = 0) noexcept -> std::si
|
||||
JSON_SET_ARR_TYPE(name, type); \
|
||||
} \
|
||||
} \
|
||||
}
|
||||
} break
|
||||
|
||||
#define JSON_SET_ARR_OBJ2(name, member) case cexprHash(#name): { \
|
||||
if (yyjson_is_arr(val)) { \
|
||||
@@ -87,7 +88,7 @@ constexpr auto cexprHash(const char *str, std::size_t v = 0) noexcept -> std::si
|
||||
from_json(hit, member[idx]); \
|
||||
} \
|
||||
} \
|
||||
}
|
||||
} break
|
||||
|
||||
#define JSON_SET_ARR_OBJ(name) JSON_SET_ARR_OBJ2(name, e.name)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "ui/menus/main_menu.hpp"
|
||||
#include "ui/error_box.hpp"
|
||||
#include "ui/option_box.hpp"
|
||||
|
||||
#include "app.hpp"
|
||||
#include "log.hpp"
|
||||
@@ -228,9 +229,22 @@ void App::Loop() {
|
||||
|
||||
ui::gfx::updateHighlightAnimation();
|
||||
|
||||
auto events = evman::popall();
|
||||
// while (auto e = evman::pop()) {
|
||||
for (auto& e : events) {
|
||||
// fire all events in in a 3ms timeslice
|
||||
TimeStamp ts_event;
|
||||
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){
|
||||
using T = std::decay_t<decltype(arg)>;
|
||||
if constexpr(std::is_same_v<T, evman::LaunchNroEventData>) {
|
||||
@@ -276,13 +290,13 @@ void App::Loop() {
|
||||
App::Notify("Nxlink Finished"_i18n);
|
||||
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");
|
||||
arg.callback(arg.data, arg.result);
|
||||
arg.callback(arg.result);
|
||||
} else {
|
||||
static_assert(false, "non-exhaustive visitor!");
|
||||
}
|
||||
}, e);
|
||||
}, event.value());
|
||||
}
|
||||
|
||||
u32 w{},h{};
|
||||
@@ -325,6 +339,16 @@ auto App::Push(std::shared_ptr<ui::Widget> widget) -> void {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
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->m_theme_index = theme_index;
|
||||
}
|
||||
|
||||
auto App::GetThemeIndex() -> u64 {
|
||||
auto App::GetThemeIndex() -> s64 {
|
||||
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 {
|
||||
return g_app->m_app_path;
|
||||
}
|
||||
@@ -451,7 +479,104 @@ void App::SetLogEnable(bool enable) {
|
||||
}
|
||||
|
||||
void App::SetReplaceHbmenuEnable(bool enable) {
|
||||
g_app->m_replace_hbmenu.Set(enable);
|
||||
if (App::GetReplaceHbmenuEnable() != 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) {
|
||||
@@ -576,59 +701,41 @@ void App::ExitRestart() {
|
||||
void App::Poll() {
|
||||
m_controller.Reset();
|
||||
|
||||
padUpdate(&m_pad);
|
||||
m_controller.m_kdown = padGetButtonsDown(&m_pad);
|
||||
m_controller.m_kheld = padGetButtons(&m_pad);
|
||||
m_controller.m_kup = padGetButtonsUp(&m_pad);
|
||||
HidTouchScreenState state{};
|
||||
hidGetTouchScreenStates(&state, 1);
|
||||
m_touch_info.is_clicked = false;
|
||||
|
||||
// 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;
|
||||
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;
|
||||
// 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;
|
||||
} 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(m_touch_info.initial_x - m_touch_info.cur_x) > 20 ||
|
||||
std::abs(m_touch_info.initial_y - m_touch_info.cur_y) > 20)) {
|
||||
(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;
|
||||
|
||||
// check if we clicked on anything, if so, handle it
|
||||
m_touch_info.is_scroll = false;
|
||||
if (m_touch_info.is_tap) {
|
||||
// todo:
|
||||
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);
|
||||
m_controller.m_kdown = padGetButtonsDown(&m_pad);
|
||||
m_controller.m_kheld = padGetButtons(&m_pad);
|
||||
m_controller.m_kup = padGetButtonsUp(&m_pad);
|
||||
m_controller.UpdateButtonHeld(static_cast<u64>(Button::ANY_DIRECTION));
|
||||
}
|
||||
}
|
||||
|
||||
void App::Update() {
|
||||
@@ -663,10 +770,29 @@ void App::Draw() {
|
||||
nvgBeginFrame(this->vg, s_width, s_height, 1.f);
|
||||
nvgScale(vg, m_scale.x, m_scale.y);
|
||||
|
||||
// NOTE: widgets should never pop themselves from drawing!
|
||||
for (auto& p : m_widgets) {
|
||||
if (!p->IsHidden()) {
|
||||
p->Draw(vg, &m_theme);
|
||||
// find the last menu in the list, start drawing from there
|
||||
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()) {
|
||||
p->Draw(vg, &m_theme);
|
||||
}
|
||||
|
||||
if (it == m_widgets.rbegin()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,17 +808,21 @@ auto App::GetVg() -> NVGcontext* {
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
switch (e.type) {
|
||||
case ElementType::None: {
|
||||
} break;
|
||||
case ElementType::Texture: {
|
||||
const auto paint = nvgImagePattern(g_app->vg, x, y, w, h, 0, e.texture, 1.f);
|
||||
ui::gfx::drawRect(g_app->vg, x, y, w, h, paint);
|
||||
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, v, paint);
|
||||
} break;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -845,6 +975,10 @@ void App::ScanThemes(const std::string& path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (d->d_type != DT_REG) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string name = d->d_name;
|
||||
if (!name.ends_with(".ini")) {
|
||||
continue;
|
||||
@@ -917,6 +1051,8 @@ App::App(const char* argv0) {
|
||||
fs::FsNativeSd fs;
|
||||
fs.CreateDirectoryRecursively("/config/sphaira/assoc");
|
||||
fs.CreateDirectoryRecursively("/config/sphaira/themes");
|
||||
fs.CreateDirectoryRecursively("/config/sphaira/github");
|
||||
fs.CreateDirectoryRecursively("/config/sphaira/i18n");
|
||||
|
||||
if (App::GetLogEnable()) {
|
||||
log_file_init();
|
||||
@@ -935,7 +1071,7 @@ App::App(const char* argv0) {
|
||||
nxlinkInitialize(nxlink_callback);
|
||||
}
|
||||
|
||||
DownloadInit();
|
||||
curl::Init();
|
||||
|
||||
// Create the deko3d device
|
||||
this->device = dk::DeviceMaker{}
|
||||
@@ -1069,6 +1205,15 @@ App::App(const char* argv0) {
|
||||
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>());
|
||||
log_write("finished app constructor\n");
|
||||
}
|
||||
@@ -1090,11 +1235,12 @@ App::~App() {
|
||||
log_write("starting to exit\n");
|
||||
|
||||
i18n::exit();
|
||||
DownloadExit();
|
||||
curl::Exit();
|
||||
|
||||
// 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.
|
||||
m_widgets.clear();
|
||||
nvgDeleteImage(vg, m_default_image);
|
||||
|
||||
appletUnhook(&m_appletHookCookie);
|
||||
|
||||
@@ -1121,45 +1267,53 @@ App::~App() {
|
||||
|
||||
// backup hbmenu if it is not sphaira
|
||||
if (App::GetReplaceHbmenuEnable() && !IsHbmenu()) {
|
||||
NacpStruct nacp;
|
||||
NacpStruct hbmenu_nacp;
|
||||
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");
|
||||
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");
|
||||
}
|
||||
} else {
|
||||
log_write("not backing up\n");
|
||||
}
|
||||
|
||||
Result rc;
|
||||
if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", GetExePath(), true))) {
|
||||
if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", GetExePath()))) {
|
||||
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", GetExePath(), rc, R_MODULE(rc), R_DESCRIPTION(rc));
|
||||
} else {
|
||||
log_write("success with copying over root file!\n");
|
||||
}
|
||||
} else if (IsHbmenu()) {
|
||||
// check we have a version that's newer than current.
|
||||
NacpStruct hbmenu_nacp;
|
||||
fs::FsNativeSd fs;
|
||||
NacpStruct sphaira_nacp;
|
||||
fs::FsPath sphaira_path = "/switch/sphaira/sphaira.nro";
|
||||
Result rc;
|
||||
|
||||
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);
|
||||
}
|
||||
// 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;
|
||||
fs::FsPath sphaira_path = "/switch/sphaira/sphaira.nro";
|
||||
|
||||
// found sphaira, now lets get compare version
|
||||
if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
|
||||
if (std::strcmp(APP_VERSION, sphaira_nacp.display_version) < 0) {
|
||||
if (R_FAILED(rc = fs.copy_entire_file(GetExePath(), sphaira_path, true))) {
|
||||
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");
|
||||
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);
|
||||
}
|
||||
|
||||
// found sphaira, now lets get compare version
|
||||
if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
|
||||
if (std::strcmp(hbmenu_nacp.display_version, sphaira_nacp.display_version) < 0) {
|
||||
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));
|
||||
} else {
|
||||
log_write("success with updating hbmenu!\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log_write("no longer hbmenu!\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <curl/curl.h>
|
||||
#include <yyjson.h>
|
||||
|
||||
namespace sphaira {
|
||||
namespace sphaira::curl {
|
||||
namespace {
|
||||
|
||||
#define CURL_EASY_SETOPT_LOG(handle, opt, v) \
|
||||
@@ -24,9 +25,6 @@ namespace {
|
||||
log_write("curl_share_setopt(%s, %s) msg: %s\n", #opt, #v, curl_share_strerror(r)); \
|
||||
} \
|
||||
|
||||
void DownloadThread(void* p);
|
||||
void DownloadThreadQueue(void* p);
|
||||
|
||||
#define USE_THREAD_QUEUE 1
|
||||
constexpr auto API_AGENT = "ITotalJustice";
|
||||
constexpr u64 CHUNK_SIZE = 1024*1024;
|
||||
@@ -38,42 +36,195 @@ std::atomic_bool g_running{};
|
||||
CURLSH* g_curl_share{};
|
||||
Mutex g_mutex_share[CURL_LOCK_DATA_LAST]{};
|
||||
|
||||
struct UrlCache {
|
||||
auto AddToCache(const std::string& url, bool force = false) {
|
||||
mutexLock(&mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&mutex));
|
||||
auto it = std::find(cache.begin(), cache.end(), url);
|
||||
if (it == cache.end()) {
|
||||
cache.emplace_back(url);
|
||||
struct DataStruct {
|
||||
std::vector<u8> data;
|
||||
s64 offset{};
|
||||
FsFile f{};
|
||||
s64 file_offset{};
|
||||
};
|
||||
|
||||
auto generate_key_from_path(const fs::FsPath& path) -> std::string {
|
||||
const auto key = crc32Calculate(path.s, path.size());
|
||||
return std::to_string(key);
|
||||
}
|
||||
|
||||
struct Cache {
|
||||
using Value = std::pair<std::string, std::string>;
|
||||
|
||||
bool init() {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
if (m_json) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto json_in = yyjson_read_file(JSON_PATH, YYJSON_READ_NOFLAG, nullptr, nullptr);
|
||||
if (json_in) {
|
||||
log_write("loading old json doc\n");
|
||||
m_json = yyjson_doc_mut_copy(json_in, nullptr);
|
||||
yyjson_doc_free(json_in);
|
||||
m_root = yyjson_mut_doc_get_root(m_json);
|
||||
} else {
|
||||
if (force) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
log_write("creating new json doc\n");
|
||||
m_json = yyjson_mut_doc_new(nullptr);
|
||||
m_root = yyjson_mut_obj(m_json);
|
||||
yyjson_mut_doc_set_root(m_json, m_root);
|
||||
}
|
||||
|
||||
return m_json && m_root;
|
||||
}
|
||||
|
||||
void exit() {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
if (!m_json) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!yyjson_mut_write_file(JSON_PATH, m_json, YYJSON_WRITE_NOFLAG, nullptr, nullptr)) {
|
||||
log_write("failed to write etag json: %s\n", JSON_PATH.s);
|
||||
}
|
||||
|
||||
yyjson_mut_doc_free(m_json);
|
||||
m_json = nullptr;
|
||||
m_root = nullptr;
|
||||
}
|
||||
|
||||
void get(const fs::FsPath& path, curl::Header& header) {
|
||||
const auto [etag, last_modified] = get_internal(path);
|
||||
if (!etag.empty()) {
|
||||
header.m_map.emplace("if-none-match", etag);
|
||||
}
|
||||
|
||||
if (!last_modified.empty()) {
|
||||
header.m_map.emplace("if-modified-since", last_modified);
|
||||
}
|
||||
}
|
||||
|
||||
void set(const fs::FsPath& path, const curl::Header& value) {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
std::string etag_str;
|
||||
std::string last_modified_str;
|
||||
|
||||
if (auto it = value.Find(ETAG_STR); it != value.m_map.end()) {
|
||||
etag_str = it->second;
|
||||
}
|
||||
if (auto it = value.Find(LAST_MODIFIED_STR); it != value.m_map.end()) {
|
||||
last_modified_str = it->second;
|
||||
}
|
||||
|
||||
if (!etag_str.empty() || !last_modified_str.empty()) {
|
||||
set_internal(path, Value{etag_str, last_modified_str});
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
auto get_internal(const fs::FsPath& path) -> Value {
|
||||
if (!fs::FsNativeSd().FileExists(path)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto kkey = generate_key_from_path(path);
|
||||
const auto it = m_cache.find(kkey);
|
||||
if (it != m_cache.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
auto hash_key = yyjson_mut_obj_getn(m_root, kkey.c_str(), kkey.length());
|
||||
if (!hash_key) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto etag_key = yyjson_mut_obj_get(hash_key, ETAG_STR);
|
||||
auto last_modified_key = yyjson_mut_obj_get(hash_key, LAST_MODIFIED_STR);
|
||||
|
||||
const auto etag_value = yyjson_mut_get_str(etag_key);
|
||||
const auto etag_value_len = yyjson_mut_get_len(etag_key);
|
||||
const auto last_modified_value = yyjson_mut_get_str(last_modified_key);
|
||||
const auto last_modified_value_len = yyjson_mut_get_len(last_modified_key);
|
||||
|
||||
if ((!etag_value || !etag_value_len) && (!last_modified_value || !last_modified_value_len)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string etag;
|
||||
std::string last_modified;
|
||||
if (etag_value && etag_value_len) {
|
||||
etag.assign(etag_value, etag_value_len);
|
||||
}
|
||||
if (last_modified_value && last_modified_value_len) {
|
||||
last_modified.assign(last_modified_value, last_modified_value_len);
|
||||
}
|
||||
|
||||
const Value ret{etag, last_modified};
|
||||
m_cache.insert_or_assign(it, kkey, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void set_internal(const fs::FsPath& path, const Value& value) {
|
||||
const auto kkey = generate_key_from_path(path);
|
||||
|
||||
// check if we already have this entry
|
||||
const auto it = m_cache.find(kkey);
|
||||
if (it != m_cache.end() && it->second == value) {
|
||||
log_write("already has etag, not updating, path: %s key: %s\n", path.s, kkey.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
if (it != m_cache.end()) {
|
||||
log_write("updating etag, path: %s key: %s\n", path.s, kkey.c_str());
|
||||
} else {
|
||||
log_write("setting new etag, path: %s key: %s\n", path.s, kkey.c_str());
|
||||
}
|
||||
|
||||
// insert new entry into cache, this will never fail.
|
||||
const auto& [jkey, jvalue] = *m_cache.insert_or_assign(it, kkey, value);
|
||||
const auto& [etag, last_modified] = jvalue;
|
||||
|
||||
// check if we need to add a new entry to root or simply update the value.
|
||||
auto hash_key = yyjson_mut_obj_getn(m_root, kkey.c_str(), kkey.length());
|
||||
if (!hash_key) {
|
||||
hash_key = yyjson_mut_obj_add_obj(m_json, m_root, jkey.c_str());
|
||||
}
|
||||
|
||||
if (!hash_key) {
|
||||
log_write("failed to set new cache key obj, path: %s key: %s\n", path.s, jkey.c_str());
|
||||
} else {
|
||||
const auto update_entry = [this, &hash_key](const char* tag, const std::string& value) {
|
||||
if (value.empty()) {
|
||||
return true;
|
||||
} else {
|
||||
auto key = yyjson_mut_obj_get(hash_key, tag);
|
||||
if (!key) {
|
||||
return yyjson_mut_obj_add_str(m_json, hash_key, tag, value.c_str());
|
||||
} else {
|
||||
return yyjson_mut_set_str(key, value.c_str());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!update_entry("etag", etag)) {
|
||||
log_write("failed to set new etag, path: %s key: %s\n", path.s, jkey.c_str());
|
||||
}
|
||||
|
||||
if (!update_entry("last-modified", last_modified)) {
|
||||
log_write("failed to set new last-modified, path: %s key: %s\n", path.s, jkey.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveFromCache(const std::string& url) {
|
||||
mutexLock(&mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&mutex));
|
||||
auto it = std::find(cache.begin(), cache.end(), url);
|
||||
if (it != cache.end()) {
|
||||
cache.erase(it);
|
||||
}
|
||||
}
|
||||
static constexpr inline fs::FsPath JSON_PATH{"/switch/sphaira/cache/cache.json"};
|
||||
static constexpr inline const char* ETAG_STR{"etag"};
|
||||
static constexpr inline const char* LAST_MODIFIED_STR{"last-modified"};
|
||||
|
||||
std::vector<std::string> cache;
|
||||
Mutex mutex{};
|
||||
};
|
||||
|
||||
struct DataStruct {
|
||||
std::vector<u8> data;
|
||||
u64 offset{};
|
||||
FsFileSystem fs{};
|
||||
FsFile f{};
|
||||
s64 file_offset{};
|
||||
Mutex m_mutex{};
|
||||
yyjson_mut_doc* m_json{};
|
||||
yyjson_mut_val* m_root{};
|
||||
std::unordered_map<std::string, Value> m_cache{};
|
||||
};
|
||||
|
||||
struct ThreadEntry {
|
||||
@@ -82,7 +233,7 @@ struct ThreadEntry {
|
||||
R_UNLESS(m_curl != nullptr, 0x1);
|
||||
|
||||
ueventCreate(&m_uevent, true);
|
||||
R_TRY(threadCreate(&m_thread, DownloadThread, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
|
||||
R_TRY(threadCreate(&m_thread, ThreadFunc, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
|
||||
R_TRY(threadStart(&m_thread));
|
||||
R_SUCCEED();
|
||||
}
|
||||
@@ -101,7 +252,7 @@ struct ThreadEntry {
|
||||
return m_in_progress == true;
|
||||
}
|
||||
|
||||
auto Setup(DownloadCallback callback, ProgressCallback pcallback, std::string url, std::string file, std::string post) -> bool {
|
||||
auto Setup(const Api& api) -> bool {
|
||||
assert(m_in_progress == false && "Setting up thread while active");
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
@@ -109,35 +260,25 @@ struct ThreadEntry {
|
||||
if (m_in_progress) {
|
||||
return false;
|
||||
}
|
||||
m_url = url;
|
||||
m_file = file;
|
||||
m_post = post;
|
||||
m_callback = callback;
|
||||
m_pcallback = pcallback;
|
||||
m_api = api;
|
||||
m_in_progress = true;
|
||||
// log_write("started download :)\n");
|
||||
ueventSignal(&m_uevent);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ThreadFunc(void* p);
|
||||
|
||||
CURL* m_curl{};
|
||||
Thread m_thread{};
|
||||
std::string m_url{};
|
||||
std::string m_file{}; // if empty, downloads to buffer
|
||||
std::string m_post{}; // if empty, downloads to buffer
|
||||
DownloadCallback m_callback{};
|
||||
ProgressCallback m_pcallback{};
|
||||
Api m_api{};
|
||||
std::atomic_bool m_in_progress{};
|
||||
Mutex m_mutex{};
|
||||
UEvent m_uevent{};
|
||||
};
|
||||
|
||||
struct ThreadQueueEntry {
|
||||
std::string url;
|
||||
std::string file;
|
||||
std::string post;
|
||||
DownloadCallback callback;
|
||||
ProgressCallback pcallback;
|
||||
Api api;
|
||||
bool m_delete{};
|
||||
};
|
||||
|
||||
@@ -149,7 +290,7 @@ struct ThreadQueue {
|
||||
|
||||
auto Create() -> Result {
|
||||
ueventCreate(&m_uevent, true);
|
||||
R_TRY(threadCreate(&m_thread, DownloadThreadQueue, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
|
||||
R_TRY(threadCreate(&m_thread, ThreadFunc, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
|
||||
R_TRY(threadStart(&m_thread));
|
||||
R_SUCCEED();
|
||||
}
|
||||
@@ -160,22 +301,18 @@ struct ThreadQueue {
|
||||
threadClose(&m_thread);
|
||||
}
|
||||
|
||||
auto Add(DownloadPriority prio, DownloadCallback callback, ProgressCallback pcallback, std::string url, std::string file, std::string post) -> bool {
|
||||
auto Add(const Api& api) -> bool {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
ThreadQueueEntry entry{};
|
||||
entry.url = url;
|
||||
entry.file = file;
|
||||
entry.post = post;
|
||||
entry.callback = callback;
|
||||
entry.pcallback = pcallback;
|
||||
entry.api = api;
|
||||
|
||||
switch (prio) {
|
||||
case DownloadPriority::Normal:
|
||||
switch (api.m_prio) {
|
||||
case Priority::Normal:
|
||||
m_entries.emplace_back(entry);
|
||||
break;
|
||||
case DownloadPriority::High:
|
||||
case Priority::High:
|
||||
m_entries.emplace_front(entry);
|
||||
break;
|
||||
}
|
||||
@@ -183,11 +320,13 @@ struct ThreadQueue {
|
||||
ueventSignal(&m_uevent);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ThreadFunc(void* p);
|
||||
};
|
||||
|
||||
ThreadEntry g_threads[MAX_THREADS]{};
|
||||
ThreadQueue g_thread_queue;
|
||||
UrlCache g_url_cache;
|
||||
Cache g_cache;
|
||||
|
||||
void GetDownloadTempPath(fs::FsPath& buf) {
|
||||
static Mutex mutex{};
|
||||
@@ -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);
|
||||
auto callback = *static_cast<ProgressCallback*>(clientp);
|
||||
auto callback = *static_cast<OnProgress*>(clientp);
|
||||
if (!callback(dltotal, dlnow, ultotal, ulnow)) {
|
||||
return 1;
|
||||
}
|
||||
@@ -283,36 +422,60 @@ auto WriteFileCallback(void *contents, size_t size, size_t num_files, void *user
|
||||
return realsize;
|
||||
}
|
||||
|
||||
auto DownloadInternal(CURL* curl, DataStruct& chunk, ProgressCallback pcallback, const std::string& url, const std::string& file, const std::string& post) -> bool {
|
||||
fs::FsPath safe_buf;
|
||||
fs::FsPath tmp_buf;
|
||||
const bool has_file = !file.empty() && file != "";
|
||||
const bool has_post = !post.empty() && post != "";
|
||||
auto header_callback(char* b, size_t size, size_t nitems, void* userdata) -> size_t {
|
||||
auto header = static_cast<Header*>(userdata);
|
||||
const auto numbytes = size * nitems;
|
||||
|
||||
ON_SCOPE_EXIT(if (has_file) { fsFsClose(&chunk.fs); } );
|
||||
if (b && numbytes) {
|
||||
const auto dilem = (const char*)memchr(b, ':', numbytes);
|
||||
if (dilem) {
|
||||
const int key_len = dilem - b;
|
||||
const int value_len = numbytes - key_len - 4; // "\r\n"
|
||||
if (key_len > 0 && value_len > 0) {
|
||||
const std::string key(b, key_len);
|
||||
const std::string value(dilem + 2, value_len);
|
||||
header->m_map.insert_or_assign(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return numbytes;
|
||||
}
|
||||
|
||||
auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
|
||||
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) {
|
||||
std::strcpy(safe_buf, file.c_str());
|
||||
GetDownloadTempPath(tmp_buf);
|
||||
R_TRY_RESULT(fsOpenSdCardFileSystem(&chunk.fs), false);
|
||||
fs.CreateDirectoryRecursivelyWithPath(tmp_buf);
|
||||
|
||||
fs::CreateDirectoryRecursivelyWithPath(&chunk.fs, tmp_buf);
|
||||
|
||||
if (auto rc = fsFsCreateFile(&chunk.fs, tmp_buf, 0, 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
||||
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 false;
|
||||
return {};
|
||||
}
|
||||
|
||||
if (R_FAILED(fsFsOpenFile(&chunk.fs, tmp_buf, FsOpenMode_Write|FsOpenMode_Append, &chunk.f))) {
|
||||
if (R_FAILED(fs.OpenFile(tmp_buf, FsOpenMode_Write|FsOpenMode_Append, &chunk.f))) {
|
||||
log_write("failed to open file: %s\n", tmp_buf);
|
||||
return false;
|
||||
return {};
|
||||
}
|
||||
|
||||
if (e.m_flags.m_flags & Flag_Cache) {
|
||||
g_cache.get(e.m_path, header_in);
|
||||
}
|
||||
}
|
||||
|
||||
// reserve the first chunk
|
||||
chunk.data.reserve(CHUNK_SIZE);
|
||||
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_reset(curl);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, e.m_url.m_str.c_str());
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_USERAGENT, "TotalJustice");
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
@@ -320,15 +483,42 @@ auto DownloadInternal(CURL* curl, DataStruct& chunk, ProgressCallback pcallback,
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FAILONERROR, 1L);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SHARE, g_curl_share);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_BUFFERSIZE, 1024*512);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERFUNCTION, header_callback);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERDATA, &header_out);
|
||||
|
||||
if (has_post) {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_POSTFIELDS, post.c_str());
|
||||
log_write("setting post field: %s\n", post.c_str());
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_POSTFIELDS, e.m_fields.m_str.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.
|
||||
if (pcallback) {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &pcallback);
|
||||
if (e.m_on_progress) {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &e.m_on_progress);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc2);
|
||||
} else {
|
||||
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);
|
||||
bool success = res == CURLE_OK;
|
||||
|
||||
long http_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
|
||||
if (has_file) {
|
||||
ON_SCOPE_EXIT( fs.DeleteFile(tmp_buf) );
|
||||
if (res == CURLE_OK && chunk.offset) {
|
||||
fsFileWrite(&chunk.f, chunk.file_offset, chunk.data.data(), chunk.offset, FsWriteOption_None);
|
||||
}
|
||||
|
||||
fsFileClose(&chunk.f);
|
||||
if (res != CURLE_OK) {
|
||||
fsFsDeleteFile(&chunk.fs, tmp_buf);
|
||||
} else {
|
||||
fsFsDeleteFile(&chunk.fs, safe_buf);
|
||||
fs::CreateDirectoryRecursivelyWithPath(&chunk.fs, safe_buf);
|
||||
if (R_FAILED(fsFsRenameFile(&chunk.fs, tmp_buf, safe_buf))) {
|
||||
fsFsDeleteFile(&chunk.fs, tmp_buf);
|
||||
success = false;
|
||||
|
||||
if (res == CURLE_OK) {
|
||||
if (http_code == 304) {
|
||||
log_write("cached download: %s\n", e.m_url.m_str.c_str());
|
||||
} else {
|
||||
log_write("un-cached download: %s code: %u\n", e.m_url.m_str.c_str(), http_code);
|
||||
if (e.m_flags.m_flags & Flag_Cache) {
|
||||
g_cache.set(e.m_path, header_out);
|
||||
}
|
||||
|
||||
fs.DeleteFile(e.m_path);
|
||||
fs.CreateDirectoryRecursivelyWithPath(e.m_path);
|
||||
if (R_FAILED(fs.RenameFile(tmp_buf, e.m_path))) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
chunk.data.clear();
|
||||
} else {
|
||||
// empty data if we failed
|
||||
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));
|
||||
return success;
|
||||
log_write("Downloaded %s %s\n", e.m_url.m_str.c_str(), curl_easy_strerror(res));
|
||||
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();
|
||||
if (!curl) {
|
||||
log_write("curl init failed\n");
|
||||
return false;
|
||||
return {};
|
||||
}
|
||||
ON_SCOPE_EXIT(curl_easy_cleanup(curl));
|
||||
return DownloadInternal(curl, chunk, pcallback, url, file, post);
|
||||
return DownloadInternal(curl, e);
|
||||
}
|
||||
|
||||
void DownloadThread(void* p) {
|
||||
void my_lock(CURL *handle, curl_lock_data data, curl_lock_access laccess, void *useptr) {
|
||||
mutexLock(&g_mutex_share[data]);
|
||||
}
|
||||
|
||||
void my_unlock(CURL *handle, curl_lock_data data, void *useptr) {
|
||||
mutexUnlock(&g_mutex_share[data]);
|
||||
}
|
||||
|
||||
void ThreadEntry::ThreadFunc(void* p) {
|
||||
auto data = static_cast<ThreadEntry*>(p);
|
||||
while (g_running) {
|
||||
auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX);
|
||||
@@ -391,11 +602,10 @@ void DownloadThread(void* p) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DataStruct chunk;
|
||||
#if 1
|
||||
const auto result = DownloadInternal(data->m_curl, chunk, data->m_pcallback, data->m_url, data->m_file, data->m_post);
|
||||
const auto result = DownloadInternal(data->m_curl, data->m_api);
|
||||
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);
|
||||
} else {
|
||||
break;
|
||||
@@ -411,7 +621,7 @@ void DownloadThread(void* p) {
|
||||
log_write("exited download thread\n");
|
||||
}
|
||||
|
||||
void DownloadThreadQueue(void* p) {
|
||||
void ThreadQueue::ThreadFunc(void* p) {
|
||||
auto data = static_cast<ThreadQueue*>(p);
|
||||
while (g_running) {
|
||||
auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX);
|
||||
@@ -444,7 +654,7 @@ void DownloadThreadQueue(void* p) {
|
||||
}
|
||||
|
||||
if (!thread.InProgress()) {
|
||||
thread.Setup(entry.callback, entry.pcallback, entry.url, entry.file, entry.post);
|
||||
thread.Setup(entry.api);
|
||||
// log_write("[dl queue] starting download\n");
|
||||
// mark entry for deletion
|
||||
entry.m_delete = true;
|
||||
@@ -454,10 +664,6 @@ void DownloadThreadQueue(void* p) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!g_running) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!keep_going) {
|
||||
break;
|
||||
}
|
||||
@@ -467,29 +673,14 @@ void DownloadThreadQueue(void* p) {
|
||||
for (u32 i = 0; i < pop_count; i++) {
|
||||
data->m_entries.pop_front();
|
||||
}
|
||||
// if (delete_any) {
|
||||
// data->m_entries.clear();
|
||||
// data->m_entries.
|
||||
// data->m_entries.erase(std::remove_if(data->m_entries.begin(), data->m_entries.end(), [](auto& a) {
|
||||
// return a.m_delete;
|
||||
// }));
|
||||
// }
|
||||
}
|
||||
|
||||
log_write("exited download thread queue\n");
|
||||
}
|
||||
|
||||
void my_lock(CURL *handle, curl_lock_data data, curl_lock_access laccess, void *useptr) {
|
||||
mutexLock(&g_mutex_share[data]);
|
||||
}
|
||||
|
||||
void my_unlock(CURL *handle, curl_lock_data data, void *useptr) {
|
||||
mutexUnlock(&g_mutex_share[data]);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto DownloadInit() -> bool {
|
||||
auto Init() -> bool {
|
||||
if (CURLE_OK != curl_global_init(CURL_GLOBAL_DEFAULT)) {
|
||||
return false;
|
||||
}
|
||||
@@ -518,10 +709,15 @@ auto DownloadInit() -> bool {
|
||||
}
|
||||
|
||||
log_write("finished creating threads\n");
|
||||
|
||||
if (!g_cache.init()) {
|
||||
log_write("failed to init json cache\n");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DownloadExit() {
|
||||
void Exit() {
|
||||
g_running = false;
|
||||
|
||||
g_thread_queue.Close();
|
||||
@@ -536,35 +732,26 @@ void DownloadExit() {
|
||||
}
|
||||
|
||||
curl_global_cleanup();
|
||||
g_cache.exit();
|
||||
}
|
||||
|
||||
auto DownloadMemory(const std::string& url, const std::string& post, ProgressCallback pcallback) -> std::vector<u8> {
|
||||
if (g_url_cache.AddToCache(url)) {
|
||||
DataStruct chunk{};
|
||||
if (DownloadInternal(chunk, pcallback, url, "", post)) {
|
||||
return chunk.data;
|
||||
}
|
||||
auto ToMemory(const Api& e) -> ApiResult {
|
||||
if (!e.m_path.empty()) {
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
return DownloadInternal(e);
|
||||
}
|
||||
|
||||
auto DownloadFile(const std::string& url, const std::string& out, const std::string& post, ProgressCallback pcallback) -> bool {
|
||||
if (g_url_cache.AddToCache(url)) {
|
||||
DataStruct chunk{};
|
||||
if (DownloadInternal(chunk, pcallback, url, out, post)) {
|
||||
return true;
|
||||
}
|
||||
auto ToFile(const Api& e) -> ApiResult {
|
||||
if (e.m_path.empty()) {
|
||||
return {};
|
||||
}
|
||||
return false;
|
||||
return DownloadInternal(e);
|
||||
}
|
||||
|
||||
auto DownloadMemoryAsync(const std::string& url, const std::string& post, DownloadCallback callback, ProgressCallback pcallback, DownloadPriority prio) -> bool {
|
||||
auto ToMemoryAsync(const Api& api) -> bool {
|
||||
#if USE_THREAD_QUEUE
|
||||
if (g_url_cache.AddToCache(url)) {
|
||||
return g_thread_queue.Add(prio, callback, pcallback, url, "", post);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return g_thread_queue.Add(api);
|
||||
#else
|
||||
// mutexLock(&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
|
||||
}
|
||||
|
||||
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 (g_url_cache.AddToCache(url)) {
|
||||
return g_thread_queue.Add(prio, callback, pcallback, url, out, post);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return g_thread_queue.Add(e);
|
||||
#else
|
||||
// mutexLock(&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
|
||||
}
|
||||
|
||||
void DownloadClearCache(const std::string& url) {
|
||||
g_url_cache.AddToCache(url);
|
||||
g_url_cache.RemoveFromCache(url);
|
||||
}
|
||||
|
||||
} // namespace sphaira
|
||||
} // namespace sphaira::curl
|
||||
|
||||
@@ -248,10 +248,10 @@ Result read_entire_file(FsFileSystem* _fs, const FsPath& path, std::vector<u8>&
|
||||
Result write_entire_file(FsFileSystem* _fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
|
||||
|
||||
FsNative fs{_fs, false};
|
||||
FsNative fs{_fs, false, ignore_read_only};
|
||||
R_TRY(fs.GetFsOpenResult());
|
||||
|
||||
if (auto rc = fs.CreateFile(path, in.size(), 0, ignore_read_only); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
||||
if (auto rc = fs.CreateFile(path, in.size(), 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
#include "ftpsrv_helper.hpp"
|
||||
#include <ftpsrv.h>
|
||||
#include <ftpsrv_vfs.h>
|
||||
|
||||
#include "app.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <mutex>
|
||||
#include <algorithm>
|
||||
#include <minIni.h>
|
||||
#include <ftpsrv.h>
|
||||
#include <ftpsrv_vfs.h>
|
||||
#include <nx/vfs_nx.h>
|
||||
#include <nx/utils.h>
|
||||
|
||||
namespace {
|
||||
|
||||
const char* INI_PATH = "/config/ftpsrv/config.ini";
|
||||
FtpSrvConfig g_ftpsrv_config = {0};
|
||||
volatile bool g_should_exit = false;
|
||||
bool g_is_running{false};
|
||||
Thread g_thread;
|
||||
std::mutex g_mutex{};
|
||||
FsFileSystem* g_fs;
|
||||
|
||||
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
|
||||
sphaira::App::NotifyFlashLed();
|
||||
@@ -28,32 +29,6 @@ void ftp_progress_callback(void) {
|
||||
sphaira::App::NotifyFlashLed();
|
||||
}
|
||||
|
||||
int vfs_fs_set_errno(Result rc) {
|
||||
switch (rc) {
|
||||
case FsError_TargetLocked: errno = EBUSY; break;
|
||||
case FsError_PathNotFound: errno = ENOENT; break;
|
||||
case FsError_PathAlreadyExists: errno = EEXIST; break;
|
||||
case FsError_UsableSpaceNotEnoughMmcCalibration: errno = ENOSPC; break;
|
||||
case FsError_UsableSpaceNotEnoughMmcSafe: errno = ENOSPC; break;
|
||||
case FsError_UsableSpaceNotEnoughMmcUser: errno = ENOSPC; break;
|
||||
case FsError_UsableSpaceNotEnoughMmcSystem: errno = ENOSPC; break;
|
||||
case FsError_UsableSpaceNotEnoughSdCard: errno = ENOSPC; break;
|
||||
case FsError_OutOfRange: errno = ESPIPE; break;
|
||||
case FsError_TooLongPath: errno = ENAMETOOLONG; break;
|
||||
case FsError_UnsupportedWriteForReadOnlyFileSystem: errno = EROFS; break;
|
||||
default: errno = EIO; break;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
Result flush_buffered_write(struct FtpVfsFile* f) {
|
||||
Result rc;
|
||||
if (R_SUCCEEDED(rc = fsFileSetSize(&f->fd, f->off + f->buf_off))) {
|
||||
rc = fsFileWrite(&f->fd, f->off, f->buf, f->buf_off, FsWriteOption_None);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
void loop(void* arg) {
|
||||
while (!g_should_exit) {
|
||||
ftpsrv_init(&g_ftpsrv_config);
|
||||
@@ -77,16 +52,51 @@ bool Init() {
|
||||
return false;
|
||||
}
|
||||
|
||||
g_fs = fsdevGetDeviceFileSystem("sdmc");
|
||||
if (R_FAILED(fsdev_wrapMountSdmc())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
g_ftpsrv_config.log_callback = ftp_log_callback;
|
||||
g_ftpsrv_config.progress_callback = ftp_progress_callback;
|
||||
g_ftpsrv_config.anon = true;
|
||||
g_ftpsrv_config.timeout = 15;
|
||||
g_ftpsrv_config.port = 5000;
|
||||
g_ftpsrv_config.anon = ini_getbool("Login", "anon", 0, INI_PATH);
|
||||
int user_len = ini_gets("Login", "user", "", g_ftpsrv_config.user, sizeof(g_ftpsrv_config.user), INI_PATH);
|
||||
int pass_len = ini_gets("Login", "pass", "", g_ftpsrv_config.pass, sizeof(g_ftpsrv_config.pass), INI_PATH);
|
||||
g_ftpsrv_config.port = ini_getl("Network", "port", 5000, INI_PATH); // 5000 to keep compat with older sphaira
|
||||
g_ftpsrv_config.timeout = ini_getl("Network", "timeout", 0, INI_PATH);
|
||||
g_ftpsrv_config.use_localtime = ini_getbool("Misc", "use_localtime", 0, INI_PATH);
|
||||
bool log_enabled = ini_getbool("Log", "log", 0, INI_PATH);
|
||||
|
||||
// get nx config
|
||||
bool mount_devices = ini_getbool("Nx", "mount_devices", 1, INI_PATH);
|
||||
bool mount_bis = ini_getbool("Nx", "mount_bis", 0, INI_PATH);
|
||||
bool save_writable = ini_getbool("Nx", "save_writable", 0, INI_PATH);
|
||||
g_ftpsrv_config.port = ini_getl("Nx", "app_port", g_ftpsrv_config.port, INI_PATH); // compat
|
||||
|
||||
// get Nx-App overrides
|
||||
g_ftpsrv_config.anon = ini_getbool("Nx-App", "anon", g_ftpsrv_config.anon, INI_PATH);
|
||||
user_len = ini_gets("Nx-App", "user", g_ftpsrv_config.user, g_ftpsrv_config.user, sizeof(g_ftpsrv_config.user), INI_PATH);
|
||||
pass_len = ini_gets("Nx-App", "pass", g_ftpsrv_config.pass, g_ftpsrv_config.pass, sizeof(g_ftpsrv_config.pass), INI_PATH);
|
||||
g_ftpsrv_config.port = ini_getl("Nx-App", "port", g_ftpsrv_config.port, INI_PATH);
|
||||
g_ftpsrv_config.timeout = ini_getl("Nx-App", "timeout", g_ftpsrv_config.timeout, INI_PATH);
|
||||
g_ftpsrv_config.use_localtime = ini_getbool("Nx-App", "use_localtime", g_ftpsrv_config.use_localtime, INI_PATH);
|
||||
log_enabled = ini_getbool("Nx-App", "log", log_enabled, INI_PATH);
|
||||
mount_devices = ini_getbool("Nx-App", "mount_devices", mount_devices, INI_PATH);
|
||||
mount_bis = ini_getbool("Nx-App", "mount_bis", mount_bis, INI_PATH);
|
||||
save_writable = ini_getbool("Nx-App", "save_writable", save_writable, INI_PATH);
|
||||
|
||||
if (!g_ftpsrv_config.port) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// keep compat with older sphaira
|
||||
if (!user_len && !pass_len) {
|
||||
g_ftpsrv_config.anon = true;
|
||||
}
|
||||
|
||||
vfs_nx_init(mount_devices, save_writable, mount_bis);
|
||||
|
||||
Result rc;
|
||||
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*64, 0x2C, 2))) {
|
||||
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*16, 0x2C, 2))) {
|
||||
log_write("failed to create nxlink thread: 0x%X\n", rc);
|
||||
return false;
|
||||
}
|
||||
@@ -108,232 +118,24 @@ void Exit() {
|
||||
g_should_exit = true;
|
||||
threadWaitForExit(&g_thread);
|
||||
threadClose(&g_thread);
|
||||
|
||||
vfs_nx_exit();
|
||||
fsdev_wrapUnmountAll();
|
||||
}
|
||||
|
||||
} // namespace sphaira::ftpsrv
|
||||
|
||||
extern "C" {
|
||||
|
||||
#define VFS_NX_BUFFER_IO 1
|
||||
|
||||
int ftp_vfs_open(struct FtpVfsFile* f, const char* path, enum FtpVfsOpenMode mode) {
|
||||
u32 open_mode;
|
||||
if (mode == FtpVfsOpenMode_READ) {
|
||||
open_mode = FsOpenMode_Read;
|
||||
f->is_write = false;
|
||||
} else {
|
||||
fsFsCreateFile(g_fs, path, 0, 0);
|
||||
open_mode = FsOpenMode_Write | FsOpenMode_Append;
|
||||
#if !VFS_NX_BUFFER_IO
|
||||
open_mode |= FsOpenMode_Append;
|
||||
#endif
|
||||
f->is_write = true;
|
||||
}
|
||||
|
||||
Result rc;
|
||||
if (R_FAILED(rc = fsFsOpenFile(g_fs, path, open_mode, &f->fd))) {
|
||||
return vfs_fs_set_errno(rc);
|
||||
}
|
||||
|
||||
f->off = f->buf_off = f->buf_size = 0;
|
||||
|
||||
if (mode == FtpVfsOpenMode_WRITE) {
|
||||
if (R_FAILED(rc = fsFileSetSize(&f->fd, 0))) {
|
||||
goto fail_close;
|
||||
}
|
||||
} else if (mode == FtpVfsOpenMode_APPEND) {
|
||||
if (R_FAILED(rc = fsFileGetSize(&f->fd, &f->off))) {
|
||||
goto fail_close;
|
||||
}
|
||||
}
|
||||
|
||||
f->is_valid = true;
|
||||
return 0;
|
||||
|
||||
fail_close:
|
||||
fsFileClose(&f->fd);
|
||||
return vfs_fs_set_errno(rc);
|
||||
void log_file_write(const char* msg) {
|
||||
log_write("%s", msg);
|
||||
}
|
||||
|
||||
int ftp_vfs_read(struct FtpVfsFile* f, void* buf, size_t size) {
|
||||
Result rc;
|
||||
|
||||
#if VFS_NX_BUFFER_IO
|
||||
if (f->buf_off == f->buf_size) {
|
||||
u64 bytes_read;
|
||||
if (R_FAILED(rc = fsFileRead(&f->fd, f->off, f->buf, sizeof(f->buf), FsReadOption_None, &bytes_read))) {
|
||||
return vfs_fs_set_errno(rc);
|
||||
}
|
||||
|
||||
f->buf_off = 0;
|
||||
f->buf_size = bytes_read;
|
||||
}
|
||||
|
||||
if (!f->buf_size) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size = size < f->buf_size - f->buf_off ? size : f->buf_size - f->buf_off;
|
||||
memcpy(buf, f->buf + f->buf_off, size);
|
||||
f->off += size;
|
||||
f->buf_off += size;
|
||||
return size;
|
||||
#else
|
||||
u64 bytes_read;
|
||||
if (R_FAILED(rc = fsFileRead(&f->fd, f->off, buf, size, FsReadOption_None, &bytes_read))) {
|
||||
return vfs_fs_set_errno(rc);
|
||||
}
|
||||
f->off += bytes_read;
|
||||
return bytes_read;
|
||||
#endif
|
||||
}
|
||||
|
||||
int ftp_vfs_write(struct FtpVfsFile* f, const void* buf, size_t size) {
|
||||
Result rc;
|
||||
|
||||
#if VFS_NX_BUFFER_IO
|
||||
const size_t ret = size;
|
||||
while (size) {
|
||||
if (f->buf_off + size > sizeof(f->buf)) {
|
||||
const u64 sz = sizeof(f->buf) - f->buf_off;
|
||||
memcpy(f->buf + f->buf_off, buf, sz);
|
||||
f->buf_off += sz;
|
||||
|
||||
if (R_FAILED(rc = flush_buffered_write(f))) {
|
||||
return vfs_fs_set_errno(rc);
|
||||
}
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wpointer-arith"
|
||||
buf += sz;
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
size -= sz;
|
||||
f->off += f->buf_off;
|
||||
f->buf_off = 0;
|
||||
} else {
|
||||
memcpy(f->buf + f->buf_off, buf, size);
|
||||
f->buf_off += size;
|
||||
size = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
#else
|
||||
if (R_FAILED(rc = fsFileWrite(&f->fd, f->off, buf, size, FsWriteOption_None))) {
|
||||
return vfs_fs_set_errno(rc);
|
||||
}
|
||||
f->off += size;
|
||||
return size;
|
||||
const size_t ret = size;
|
||||
#endif
|
||||
}
|
||||
|
||||
// buf and size is the amount of data sent.
|
||||
int ftp_vfs_seek(struct FtpVfsFile* f, const void* buf, size_t size, size_t off) {
|
||||
#if VFS_NX_BUFFER_IO
|
||||
if (!f->is_write) {
|
||||
f->buf_off -= f->off - off;
|
||||
}
|
||||
#endif
|
||||
f->off = off;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ftp_vfs_close(struct FtpVfsFile* f) {
|
||||
if (!ftp_vfs_isfile_open(f)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (f->is_write && f->buf_off) {
|
||||
flush_buffered_write(f);
|
||||
}
|
||||
|
||||
fsFileClose(&f->fd);
|
||||
f->is_valid = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ftp_vfs_isfile_open(struct FtpVfsFile* f) {
|
||||
return f->is_valid;
|
||||
}
|
||||
|
||||
int ftp_vfs_opendir(struct FtpVfsDir* f, const char* path) {
|
||||
Result rc;
|
||||
if (R_FAILED(rc = fsFsOpenDirectory(g_fs, path, FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize, &f->dir))) {
|
||||
return vfs_fs_set_errno(rc);
|
||||
}
|
||||
f->is_valid = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* ftp_vfs_readdir(struct FtpVfsDir* f, struct FtpVfsDirEntry* entry) {
|
||||
Result rc;
|
||||
s64 total_entries;
|
||||
if (R_FAILED(rc = fsDirRead(&f->dir, &total_entries, 1, &entry->buf))) {
|
||||
vfs_fs_set_errno(rc);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (total_entries <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return entry->buf.name;
|
||||
}
|
||||
|
||||
int ftp_vfs_dirlstat(struct FtpVfsDir* f, const struct FtpVfsDirEntry* entry, const char* path, struct stat* st) {
|
||||
return lstat(path, st);
|
||||
}
|
||||
|
||||
int ftp_vfs_closedir(struct FtpVfsDir* f) {
|
||||
if (!ftp_vfs_isdir_open(f)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
fsDirClose(&f->dir);
|
||||
f->is_valid = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ftp_vfs_isdir_open(struct FtpVfsDir* f) {
|
||||
return f->is_valid;
|
||||
}
|
||||
|
||||
int ftp_vfs_stat(const char* path, struct stat* st) {
|
||||
return stat(path, st);
|
||||
}
|
||||
|
||||
int ftp_vfs_lstat(const char* path, struct stat* st) {
|
||||
return lstat(path, st);
|
||||
}
|
||||
|
||||
int ftp_vfs_mkdir(const char* path) {
|
||||
return mkdir(path, 0777);
|
||||
}
|
||||
|
||||
int ftp_vfs_unlink(const char* path) {
|
||||
return unlink(path);
|
||||
}
|
||||
|
||||
int ftp_vfs_rmdir(const char* path) {
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
int ftp_vfs_rename(const char* src, const char* dst) {
|
||||
return rename(src, dst);
|
||||
}
|
||||
|
||||
int ftp_vfs_readlink(const char* path, char* buf, size_t buflen) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* ftp_vfs_getpwuid(const struct stat* st) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const char* ftp_vfs_getgrgid(const struct stat* st) {
|
||||
return "unknown";
|
||||
void log_file_fwrite(const char* fmt, ...) {
|
||||
std::va_list v{};
|
||||
va_start(v, fmt);
|
||||
log_write_arg(fmt, v);
|
||||
va_end(v);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -21,7 +21,7 @@ std::string get_internal(const char* str, size_t len) {
|
||||
}
|
||||
|
||||
// add default entry
|
||||
g_tr_cache.emplace(kkey, kkey);
|
||||
const auto it = g_tr_cache.emplace(kkey, kkey).first;
|
||||
|
||||
if (!json || !root) {
|
||||
log_write("no json or root\n");
|
||||
@@ -43,7 +43,7 @@ std::string get_internal(const char* str, size_t len) {
|
||||
|
||||
// update entry in cache
|
||||
const std::string ret = {val, val_len};
|
||||
g_tr_cache.insert_or_assign(kkey, ret);
|
||||
g_tr_cache.insert_or_assign(it, kkey, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,16 @@ std::FILE* file{};
|
||||
int nxlink_socket{};
|
||||
std::mutex mutex{};
|
||||
|
||||
void log_write_arg_internal(const char* s, std::va_list& v) {
|
||||
if (file) {
|
||||
std::vfprintf(file, s, v);
|
||||
std::fflush(file);
|
||||
}
|
||||
if (nxlink_socket) {
|
||||
std::vprintf(s, v);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto log_file_init() -> bool {
|
||||
@@ -60,13 +70,17 @@ void log_write(const char* s, ...) {
|
||||
|
||||
std::va_list v{};
|
||||
va_start(v, s);
|
||||
if (file) {
|
||||
std::vfprintf(file, s, v);
|
||||
std::fflush(file);
|
||||
}
|
||||
if (nxlink_socket) {
|
||||
std::vprintf(s, v);
|
||||
}
|
||||
log_write_arg_internal(s, v);
|
||||
va_end(v);
|
||||
}
|
||||
|
||||
void log_write_arg(const char* s, std::va_list& v) {
|
||||
std::scoped_lock lock{mutex};
|
||||
if (!file && !nxlink_socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
log_write_arg_internal(s, v);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -63,6 +63,8 @@ void userAppInit(void) {
|
||||
diagAbortWithResult(rc);
|
||||
if (R_FAILED(rc = hidsysInitialize()))
|
||||
diagAbortWithResult(rc);
|
||||
if (R_FAILED(rc = ncmInitialize()))
|
||||
diagAbortWithResult(rc);
|
||||
|
||||
log_nxlink_init();
|
||||
}
|
||||
@@ -70,6 +72,7 @@ void userAppInit(void) {
|
||||
void userAppExit(void) {
|
||||
log_nxlink_exit();
|
||||
|
||||
ncmExit();
|
||||
hidsysExit();
|
||||
setExit();
|
||||
accountExit();
|
||||
|
||||
@@ -309,4 +309,37 @@ auto nro_normalise_path(const std::string& p) -> std::string {
|
||||
return p;
|
||||
}
|
||||
|
||||
auto nro_find(std::span<const NroEntry> array, std::string_view name, std::string_view author, const fs::FsPath& path) -> std::optional<NroEntry> {
|
||||
const auto it = std::find_if(array.cbegin(), array.cend(), [name, author, path](auto& e){
|
||||
if (!name.empty() && !author.empty() && !path.empty()) {
|
||||
return e.GetName() == name && e.GetAuthor() == author && e.path == path;
|
||||
} else if (!name.empty()) {
|
||||
return e.GetName() == name;
|
||||
} else if (!author.empty()) {
|
||||
return e.GetAuthor() == author;
|
||||
} else if (!path.empty()) {
|
||||
return e.path == path;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (it == array.cend()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return *it;
|
||||
}
|
||||
|
||||
auto nro_find_name(std::span<const NroEntry> array, std::string_view name) -> std::optional<NroEntry> {
|
||||
return nro_find(array, name, {}, {});
|
||||
}
|
||||
|
||||
auto nro_find_author(std::span<const NroEntry> array, std::string_view author) -> std::optional<NroEntry> {
|
||||
return nro_find(array, {}, author, {});
|
||||
}
|
||||
|
||||
auto nro_find_path(std::span<const NroEntry> array, const fs::FsPath& path) -> std::optional<NroEntry> {
|
||||
return nro_find(array, {}, {}, path);
|
||||
}
|
||||
|
||||
} // namespace sphaira
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "app.hpp"
|
||||
#include "ui/progress_box.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
namespace sphaira {
|
||||
namespace {
|
||||
@@ -191,6 +192,40 @@ struct NpdmMeta {
|
||||
u32 acid_size;
|
||||
};
|
||||
|
||||
struct NpdmAcid {
|
||||
u8 rsa_sig[0x100];
|
||||
u8 rsa_pub[0x100];
|
||||
u32 magic; // "ACID"
|
||||
u32 size;
|
||||
u8 version;
|
||||
u8 _0x209[0x1];
|
||||
u8 _0x20A[0x2];
|
||||
u32 flags;
|
||||
u64 program_id_min;
|
||||
u64 program_id_max;
|
||||
u32 fac_offset;
|
||||
u32 fac_size;
|
||||
u32 sac_offset;
|
||||
u32 sac_size;
|
||||
u32 kac_offset;
|
||||
u32 kac_size;
|
||||
u8 _0x238[0x8];
|
||||
};
|
||||
|
||||
struct NpdmAci0 {
|
||||
u32 magic; // "ACI0"
|
||||
u8 _0x4[0xC];
|
||||
u64 program_id;
|
||||
u8 _0x18[0x8];
|
||||
u32 fac_offset;
|
||||
u32 fac_size;
|
||||
u32 sac_offset;
|
||||
u32 sac_size;
|
||||
u32 kac_offset;
|
||||
u32 kac_size;
|
||||
u8 _0x38[0x8];
|
||||
};
|
||||
|
||||
struct NpdmPatch {
|
||||
char title_name[0x10]{"Application"};
|
||||
char product_code[0x10]{};
|
||||
@@ -578,17 +613,56 @@ auto romfs_build(const FileEntries& entries, u64 *out_size) -> std::vector<u8> {
|
||||
return buf.buf;
|
||||
}
|
||||
|
||||
auto npdm_patch_kc(std::vector<u8>& npdm, u32 off, u32 size, u32 bitmask, u32 value) -> bool {
|
||||
const u32 pattern = BIT(bitmask) - 1;
|
||||
const u32 mask = BIT(bitmask) | pattern;
|
||||
|
||||
for (u32 i = 0; i < size; i += 4) {
|
||||
u32 cup;
|
||||
std::memcpy(&cup, npdm.data() + off + i, sizeof(cup));
|
||||
if ((cup & mask) == pattern) {
|
||||
cup = value | pattern;
|
||||
std::memcpy(npdm.data() + off + i, &cup, sizeof(cup));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// todo: manually build npdm
|
||||
void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
|
||||
NpdmMeta meta{};
|
||||
NpdmAci0 aci0{};
|
||||
NpdmAcid acid{};
|
||||
std::memcpy(&meta, npdm.data(), sizeof(meta));
|
||||
std::memcpy(&aci0, npdm.data() + meta.aci0_offset, sizeof(aci0));
|
||||
std::memcpy(&acid, npdm.data() + meta.acid_offset, sizeof(acid));
|
||||
|
||||
// apply patch
|
||||
std::memcpy(npdm.data() + 0x20, &patch.title_name, sizeof(patch.title_name));
|
||||
std::memcpy(npdm.data() + 0x30, &patch.product_code, sizeof(patch.product_code));
|
||||
std::memcpy(npdm.data() + meta.aci0_offset + 0x10, &patch.tid, sizeof(patch.tid));
|
||||
std::memcpy(npdm.data() + meta.acid_offset + 0x210, &patch.tid, sizeof(patch.tid));
|
||||
std::memcpy(npdm.data() + meta.acid_offset + 0x218, &patch.tid, sizeof(patch.tid));
|
||||
std::memcpy(meta.title_name, &patch.title_name, sizeof(meta.title_name));
|
||||
std::memcpy(meta.product_code, &patch.product_code, sizeof(patch.product_code));
|
||||
aci0.program_id = patch.tid;
|
||||
acid.program_id_min = patch.tid;
|
||||
acid.program_id_max = patch.tid;
|
||||
|
||||
// patch debug flags based on ams version
|
||||
// SEE: https://github.com/ITotalJustice/sphaira/issues/67
|
||||
u64 ver{};
|
||||
splInitialize();
|
||||
ON_SCOPE_EXIT(splExit());
|
||||
const auto SplConfigItem_ExosphereVersion = (SplConfigItem)65000;
|
||||
splGetConfig(SplConfigItem_ExosphereVersion, &ver);
|
||||
ver >>= 40;
|
||||
|
||||
if (ver >= MAKEHOSVERSION(1,7,1)) {
|
||||
npdm_patch_kc(npdm, meta.aci0_offset + aci0.kac_offset, aci0.kac_size, 16, BIT(19));
|
||||
npdm_patch_kc(npdm, meta.acid_offset + acid.kac_offset, acid.kac_size, 16, BIT(19));
|
||||
}
|
||||
|
||||
std::memcpy(npdm.data(), &meta, sizeof(meta));
|
||||
std::memcpy(npdm.data() + meta.aci0_offset, &aci0, sizeof(aci0));
|
||||
std::memcpy(npdm.data() + meta.acid_offset, &acid, sizeof(acid));
|
||||
}
|
||||
|
||||
void patch_nacp(NacpStruct& nacp, const NcapPatch& patch) {
|
||||
|
||||
@@ -1134,10 +1134,6 @@ auto ErrorBox::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||
Widget::Update(controller, touch);
|
||||
}
|
||||
|
||||
auto ErrorBox::OnLayoutChange() -> void {
|
||||
|
||||
}
|
||||
|
||||
auto ErrorBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
gfx::dimBackground(vg);
|
||||
gfx::drawRect(vg, m_pos, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
|
||||
189
sphaira/source/ui/list.cpp
Normal file
189
sphaira/source/ui/list.cpp
Normal file
@@ -0,0 +1,189 @@
|
||||
#include "ui/list.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "app.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
List::List(s64 row, s64 page, const Vec4& pos, const Vec4& v, const Vec2& pad)
|
||||
: m_row{row}
|
||||
, m_page{page}
|
||||
, m_v{v}
|
||||
, m_pad{pad} {
|
||||
m_pos = pos;
|
||||
SetScrollBarPos(SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200);
|
||||
}
|
||||
|
||||
auto List::ClampY(float y, s64 count) const -> float {
|
||||
float y_max = 0;
|
||||
|
||||
if (count >= m_page) {
|
||||
// round up
|
||||
if (count % m_row) {
|
||||
count = count + (m_row - count % m_row);
|
||||
}
|
||||
y_max = (count - m_page) / m_row * GetMaxY();
|
||||
}
|
||||
|
||||
if (y < 0) {
|
||||
y = 0;
|
||||
} else if (y > y_max) {
|
||||
y = y_max;
|
||||
}
|
||||
|
||||
return y;
|
||||
}
|
||||
|
||||
void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCallback callback) {
|
||||
if (touch->is_clicked && touch->in_range(GetPos())) {
|
||||
auto v = m_v;
|
||||
v.y -= ClampY(m_yoff + m_y_prog, count);
|
||||
|
||||
for (s64 i = 0; i < count; v.y += v.h + m_pad.y) {
|
||||
if (v.y > GetY() + GetH()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto x = v.x;
|
||||
|
||||
for (; i < count; i++, v.x += v.w + m_pad.x) {
|
||||
// only draw if full x is in bounds
|
||||
if (v.x + v.w > GetX() + GetW()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// skip anything not visible
|
||||
if (v.y + v.h < GetY()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Vec4 vv = v;
|
||||
// if not drawing, only return clipped v as its used for touch
|
||||
vv.w = std::min(v.x + v.w, m_pos.x + m_pos.w) - v.x;
|
||||
vv.h = std::min(v.y + v.h, m_pos.y + m_pos.h) - v.y;
|
||||
|
||||
if (touch->in_range(vv)) {
|
||||
callback(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
v.x = x;
|
||||
}
|
||||
} else if (touch->is_scroll && touch->in_range(GetPos())) {
|
||||
m_y_prog = (float)touch->initial.y - (float)touch->cur.y;
|
||||
} else if (touch->is_end) {
|
||||
m_yoff = ClampY(m_yoff + m_y_prog, count);
|
||||
m_y_prog = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void List::Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const {
|
||||
const auto yoff = ClampY(m_yoff + m_y_prog, count);
|
||||
const s64 start = yoff / GetMaxY() * m_row;
|
||||
gfx::drawScrollbar2(vg, theme, m_scrollbar.x, m_scrollbar.y, m_scrollbar.h, start, count, m_row, m_page);
|
||||
|
||||
auto v = m_v;
|
||||
v.y -= yoff;
|
||||
|
||||
nvgSave(vg);
|
||||
nvgScissor(vg, GetX(), GetY(), GetW(), GetH());
|
||||
|
||||
for (s64 i = 0; i < count; v.y += v.h + m_pad.y) {
|
||||
if (v.y > GetY() + GetH()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto x = v.x;
|
||||
|
||||
for (; i < count; i++, v.x += v.w + m_pad.x) {
|
||||
// only draw if full x is in bounds
|
||||
if (v.x + v.w > GetX() + GetW()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// skip anything not visible
|
||||
if (v.y + v.h < GetY()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
callback(vg, theme, v, i);
|
||||
}
|
||||
|
||||
v.x = x;
|
||||
}
|
||||
|
||||
nvgRestore(vg);
|
||||
}
|
||||
|
||||
auto List::ScrollDown(s64& index, s64 step, s64 count) -> bool {
|
||||
const auto old_index = index;
|
||||
|
||||
if (!count) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index + step < count) {
|
||||
index += step;
|
||||
} else {
|
||||
index = count - 1;
|
||||
}
|
||||
|
||||
if (index != old_index) {
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
s64 delta = index - old_index;
|
||||
s64 start = m_yoff / GetMaxY() * m_row;
|
||||
|
||||
while (index < start) {
|
||||
start -= m_row;
|
||||
m_yoff -= GetMaxY();
|
||||
}
|
||||
|
||||
if (index - start >= m_page) {
|
||||
do {
|
||||
start += m_row;
|
||||
delta -= m_row;
|
||||
m_yoff += GetMaxY();
|
||||
} while (delta > 0 && start + m_page < count);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
auto List::ScrollUp(s64& index, s64 step, s64 count) -> bool {
|
||||
const auto old_index = index;
|
||||
|
||||
if (!count) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index >= step) {
|
||||
index -= step;
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
if (index != old_index) {
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
s64 start = m_yoff / GetMaxY() * m_row;
|
||||
|
||||
while (index < start) {
|
||||
start -= m_row;
|
||||
m_yoff -= GetMaxY();
|
||||
}
|
||||
|
||||
while (index - start >= m_page && start + m_page < count) {
|
||||
start += m_row;
|
||||
m_yoff += GetMaxY();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
@@ -206,17 +206,17 @@ auto LoadAndParseManifest(const Entry& e) -> ManifestEntries {
|
||||
return ParseManifest(std::span{(const char*)data.data(), data.size()});
|
||||
}
|
||||
|
||||
void EntryLoadImageFile(fs::Fs& fs, const fs::FsPath& path, LazyImage& image) {
|
||||
auto EntryLoadImageFile(fs::Fs& fs, const fs::FsPath& path, LazyImage& image) -> bool {
|
||||
// already have the image
|
||||
if (image.image) {
|
||||
log_write("warning, tried to load image: %s when already loaded\n", path);
|
||||
return;
|
||||
// log_write("warning, tried to load image: %s when already loaded\n", path);
|
||||
return true;
|
||||
}
|
||||
auto vg = App::GetVg();
|
||||
|
||||
std::vector<u8> image_buf;
|
||||
if (R_FAILED(fs.read_entire_file(path, image_buf))) {
|
||||
image.state = ImageDownloadState::Failed;
|
||||
log_write("failed to load image from file: %s\n", path.s);
|
||||
} else {
|
||||
int channels_in_file;
|
||||
auto buf = stbi_load_from_memory(image_buf.data(), image_buf.size(), &image.w, &image.h, &channels_in_file, 4);
|
||||
@@ -228,20 +228,21 @@ void EntryLoadImageFile(fs::Fs& fs, const fs::FsPath& path, LazyImage& image) {
|
||||
}
|
||||
|
||||
if (!image.image) {
|
||||
image.state = ImageDownloadState::Failed;
|
||||
log_write("failed to load image from file: %s\n", path);
|
||||
log_write("failed to load image from file: %s\n", path.s);
|
||||
return false;
|
||||
} else {
|
||||
// log_write("loaded image from file: %s\n", path);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void EntryLoadImageFile(const fs::FsPath& path, LazyImage& image) {
|
||||
auto EntryLoadImageFile(const fs::FsPath& path, LazyImage& image) -> bool {
|
||||
if (!strncasecmp("romfs:/", path, 7)) {
|
||||
fs::FsStdio fs;
|
||||
EntryLoadImageFile(fs, path, image);
|
||||
return EntryLoadImageFile(fs, path, image);
|
||||
} else {
|
||||
fs::FsNativeSd fs;
|
||||
EntryLoadImageFile(fs, path, image);
|
||||
return EntryLoadImageFile(fs, path, image);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,45 +271,17 @@ void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, float x, f
|
||||
rounded_image = false;
|
||||
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) {
|
||||
gfx::drawImageRounded(vg, ix, iy, iw, ih, i.image);
|
||||
} else {
|
||||
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) {
|
||||
DrawIcon(vg, l, d, vec.x, vec.y, vec.w, vec.h, rounded, scale);
|
||||
}
|
||||
|
||||
auto ScrollHelperDown(u64& index, u64& start, u64 step, u64 max, u64 size) -> bool {
|
||||
if (size && index < (size - 1)) {
|
||||
if (index < (size - step)) {
|
||||
index = index + step;
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
} else {
|
||||
index = size - 1;
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
}
|
||||
if (index - start >= max) {
|
||||
log_write("moved down\n");
|
||||
start += step;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
auto AppDlToStr(u32 value) -> std::string {
|
||||
auto str = std::to_string(value);
|
||||
u32 inc = 3;
|
||||
@@ -398,17 +371,13 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
||||
log_write("starting download\n");
|
||||
|
||||
const auto url = BuildZipUrl(entry);
|
||||
if (!DownloadFile(url, zip_out, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
|
||||
if (pbox->ShouldExit()) {
|
||||
return false;
|
||||
}
|
||||
pbox->UpdateTransfer(dlnow, dltotal);
|
||||
return true;
|
||||
})) {
|
||||
if (!curl::Api().ToFile(
|
||||
curl::Url{url},
|
||||
curl::Path{zip_out},
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||
).success) {
|
||||
log_write("error with download\n");
|
||||
// push popup error box
|
||||
return false;
|
||||
// return appletEnterFatalSection();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,10 +514,10 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
||||
}
|
||||
|
||||
// create directories
|
||||
fs.CreateDirectoryRecursivelyWithPath(output, true);
|
||||
fs.CreateDirectoryRecursivelyWithPath(output);
|
||||
|
||||
Result rc;
|
||||
if (R_FAILED(rc = fs.CreateFile(output, info.uncompressed_size, 0, true)) && rc != FsError_PathAlreadyExists) {
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
@@ -637,7 +606,7 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
||||
if (!found) {
|
||||
const auto safe_buf = fs::AppendPath("/", old_entry.path);
|
||||
// std::strcat(safe_buf, old_entry.path);
|
||||
if (R_FAILED(fs.DeleteFile(safe_buf, true))) {
|
||||
if (R_FAILED(fs.DeleteFile(safe_buf))) {
|
||||
log_write("failed to delete: %s\n", safe_buf);
|
||||
} else {
|
||||
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 file = BuildFeedbackCachePath(m_entry);
|
||||
|
||||
DownloadFileAsync(URL_POST_FEEDBACK, file, post, [](std::vector<u8>& data, bool success){
|
||||
if (success) {
|
||||
log_write("got feedback!\n");
|
||||
} else {
|
||||
log_write("failed to send feedback :(");
|
||||
curl::Api().ToAsync(
|
||||
curl::Url{URL_POST_FEEDBACK},
|
||||
curl::Path{file},
|
||||
curl::Fields{post},
|
||||
curl::OnComplete{[](auto& result){
|
||||
if (result.success) {
|
||||
log_write("got feedback!\n");
|
||||
} else {
|
||||
log_write("failed to send feedback :(");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, true));
|
||||
App::Push(sidebar);
|
||||
}}),
|
||||
// std::make_pair(Button::A, Action{m_entry.status == EntryStatus::Update ? "Update" : "Install", [this](){
|
||||
// App::Push(std::make_shared<ProgressBox>("App Install", [this](auto pbox){
|
||||
// InstallApp(pbox, m_entry);
|
||||
// }, 2));
|
||||
// }}),
|
||||
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}})
|
||||
);
|
||||
|
||||
// SidebarEntryCallback
|
||||
// if (!m_entries_current.empty() && !GetEntry().url.empty()) {
|
||||
// options->Add(std::make_shared<SidebarEntryCallback>("Show Release Page"))
|
||||
// }
|
||||
|
||||
SetTitleSubHeading("by " + m_entry.author);
|
||||
|
||||
// const char* very_long = "total@fedora:~/dev/switch/sphaira$ nxlink build/MinSizeRel/*.nro total@fedora:~/dev/switch/sphaira$ nxlink build/MinSizeRel/*.nro total@fedora:~/dev/switch/sphaira$ nxlink build/MinSizeRel/*.nro total@fedora:~/dev/switch/sphaira$ nxlink build/MinSizeRel/*.nro";
|
||||
|
||||
m_details = std::make_shared<ScrollableText>(m_entry.details, 0, 374, 250, 768, 18);
|
||||
m_changelog = std::make_shared<ScrollableText>(m_entry.changelog, 0, 374, 250, 768, 18);
|
||||
|
||||
@@ -720,41 +682,32 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
|
||||
|
||||
const auto path = BuildBannerCachePath(m_entry);
|
||||
const auto url = BuildBannerUrl(m_entry);
|
||||
|
||||
if (fs::FsNativeSd().FileExists(path)) {
|
||||
EntryLoadImageFile(path, m_banner);
|
||||
}
|
||||
m_banner.cached = EntryLoadImageFile(path, m_banner);
|
||||
|
||||
// race condition if we pop the widget before the download completes
|
||||
if (!m_banner.image) {
|
||||
DownloadFileAsync(url, path, "", [this, path](std::vector<u8>& data, bool success){
|
||||
if (success) {
|
||||
EntryLoadImageFile(path, m_banner);
|
||||
curl::Api().ToFileAsync(
|
||||
curl::Url{url},
|
||||
curl::Path{path},
|
||||
curl::Flags{curl::Flag_Cache},
|
||||
curl::OnComplete{[this, path](auto& result){
|
||||
if (result.success) {
|
||||
if (result.code == 304) {
|
||||
m_banner.cached = false;
|
||||
} else {
|
||||
EntryLoadImageFile(path, m_banner);
|
||||
}
|
||||
}
|
||||
}, nullptr, DownloadPriority::High);
|
||||
}
|
||||
|
||||
// ignore screen shots, most apps don't have any sadly.
|
||||
#if 0
|
||||
m_screens.resize(m_entry.screens);
|
||||
|
||||
for (u32 i = 0; i < m_screens.size(); i++) {
|
||||
path = BuildScreensCachePath(m_entry.name, i);
|
||||
url = BuildScreensUrl(m_entry.name, i);
|
||||
|
||||
if (fs::file_exists(path.c_str())) {
|
||||
EntryLoadImageFile(path, m_screens[i]);
|
||||
} else {
|
||||
DownloadFileAsync(url.c_str(), path.c_str(), [this, i, path](std::vector<u8>& data, bool success){
|
||||
EntryLoadImageFile(path, m_screens[i]);
|
||||
}, nullptr, DownloadPriority::High);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
});
|
||||
|
||||
SetSubHeading(m_entry.binary);
|
||||
SetSubHeading(m_entry.description);
|
||||
UpdateOptions();
|
||||
|
||||
// todo: see Draw()
|
||||
// const Vec4 v{75, 110, 370, 155};
|
||||
// const Vec2 pad{10, 10};
|
||||
// m_list = std::make_unique<List>(3, 3, v, pad);
|
||||
}
|
||||
|
||||
EntryMenu::~EntryMenu() {
|
||||
@@ -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_entry.image, m_default_icon, icon_vec);
|
||||
|
||||
// gfx::drawImage(vg, icon_vec, m_entry.image.image);
|
||||
constexpr float text_start_x = icon_vec.x;// - 10;
|
||||
float text_start_y = 218 + line_vec.y;
|
||||
const float text_inc_y = 32;
|
||||
const float font_size = 20;
|
||||
|
||||
// gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "%s", m_entry.name.c_str());
|
||||
// gfx::drawTextBox(vg, text_start_x - 20, text_start_y, font_size, icon_vec.w + 20*2, theme->elements[ThemeEntryID_TEXT].colour, m_entry.description.c_str(), NVG_ALIGN_CENTER);
|
||||
// text_start_y += text_inc_y * 2.0;
|
||||
|
||||
// gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "author: %s", m_entry.author.c_str());
|
||||
// text_start_y += text_inc_y;
|
||||
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "version: %s"_i18n.c_str(), m_entry.version.c_str());
|
||||
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());
|
||||
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());
|
||||
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);
|
||||
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());
|
||||
@@ -812,6 +752,7 @@ void EntryMenu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
// for (const auto& option : m_options) {
|
||||
const auto& text_col = theme->elements[ThemeEntryID_TEXT].colour;
|
||||
|
||||
// todo: rewrite this mess and use list
|
||||
constexpr float mm = 0;//20;
|
||||
constexpr Vec4 block{968.f + mm, 110.f, 256.f - mm*2, 60.f};
|
||||
constexpr float text_xoffset{15.f};
|
||||
@@ -863,6 +804,7 @@ void EntryMenu::UpdateOptions() {
|
||||
return InstallApp(pbox, m_entry);
|
||||
}, [this](bool success){
|
||||
if (success) {
|
||||
App::Notify("Downloaded "_i18n + m_entry.title);
|
||||
m_entry.status = EntryStatus::Installed;
|
||||
m_menu.SetDirty();
|
||||
UpdateOptions();
|
||||
@@ -875,6 +817,7 @@ void EntryMenu::UpdateOptions() {
|
||||
return UninstallApp(pbox, m_entry);
|
||||
}, [this](bool success){
|
||||
if (success) {
|
||||
App::Notify("Removed "_i18n + m_entry.title);
|
||||
m_entry.status = EntryStatus::Get;
|
||||
m_menu.SetDirty();
|
||||
UpdateOptions();
|
||||
@@ -913,7 +856,7 @@ void EntryMenu::UpdateOptions() {
|
||||
SetIndex(0);
|
||||
}
|
||||
|
||||
void EntryMenu::SetIndex(std::size_t index) {
|
||||
void EntryMenu::SetIndex(s64 index) {
|
||||
m_index = index;
|
||||
const auto option = m_options[m_index];
|
||||
if (option.confirm_text.empty()) {
|
||||
@@ -941,8 +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/screens");
|
||||
|
||||
// m_span = m_entries;
|
||||
|
||||
this->SetActions(
|
||||
std::make_pair(Button::RIGHT, Action{[this](){
|
||||
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](){
|
||||
if (ScrollHelperDown(m_index, m_start, 3, 9, m_entries_current.size())) {
|
||||
if (m_list->ScrollDown(m_index, 3, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
if (m_entries_current.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_index >= 3) {
|
||||
SetIndex(m_index - 3);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
if (m_index < m_start ) {
|
||||
// log_write("moved up\n");
|
||||
m_start -= 3;
|
||||
}
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::R2, Action{(u8)ActionType::HELD, [this](){
|
||||
if (ScrollHelperDown(m_index, m_start, 9, 9, m_entries_current.size())) {
|
||||
if (m_list->ScrollUp(m_index, 3, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::L2, Action{(u8)ActionType::HELD, [this](){
|
||||
if (m_entries.empty()) {
|
||||
return;
|
||||
std::make_pair(Button::R2, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 9, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
|
||||
if (m_index >= 9) {
|
||||
SetIndex(m_index - 9);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
while (m_index < m_start) {
|
||||
// log_write("moved up\n");
|
||||
m_start -= 3;
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::L2, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 9, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::A, Action{"Info"_i18n, [this](){
|
||||
@@ -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("Ascending"_i18n);
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Filter"_i18n, filter_items, [this, filter_items](std::size_t& index_out){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Filter"_i18n, filter_items, [this, filter_items](s64& index_out){
|
||||
SetFilter((Filter)index_out);
|
||||
}, (std::size_t)m_filter));
|
||||
}, (s64)m_filter));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](std::size_t& index_out){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
|
||||
SetSort((SortType)index_out);
|
||||
}, (std::size_t)m_sort));
|
||||
}, (s64)m_sort));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](std::size_t& index_out){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](s64& index_out){
|
||||
SetOrder((OrderType)index_out);
|
||||
}, (std::size_t)m_order));
|
||||
}, (s64)m_order));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Search"_i18n, [this](){
|
||||
std::string out;
|
||||
@@ -1058,52 +981,12 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
|
||||
);
|
||||
|
||||
m_repo_download_state = ImageDownloadState::Progress;
|
||||
#if 0
|
||||
DownloadMemoryAsync(URL_JSON, [this](std::vector<u8>& data, bool success){
|
||||
if (success) {
|
||||
repo_json = data;
|
||||
repo_json.push_back('\0');
|
||||
m_repo_download_state = ImageDownloadState::Done;
|
||||
if (HasFocus()) {
|
||||
ScanHomebrew();
|
||||
}
|
||||
} else {
|
||||
m_repo_download_state = ImageDownloadState::Failed;
|
||||
}
|
||||
});
|
||||
#else
|
||||
FsTimeStampRaw time_stamp{};
|
||||
u64 current_time{};
|
||||
bool download_file = false;
|
||||
if (R_SUCCEEDED(fs.GetFsOpenResult())) {
|
||||
fs.GetFileTimeStampRaw(REPO_PATH, &time_stamp);
|
||||
timeGetCurrentTime(TimeType_Default, ¤t_time);
|
||||
}
|
||||
|
||||
// this fails if we don't have the file or on fw < 3.0.0
|
||||
if (!time_stamp.is_valid) {
|
||||
download_file = true;
|
||||
} else {
|
||||
// check the date, if older than 1hour, then fetch new file
|
||||
// this relaxes the spam to their server, don't want to fetch repo
|
||||
// every time the user opens the app!
|
||||
const auto time_file = time_stamp.created;
|
||||
const auto time_cur = current_time;
|
||||
const auto day = 60 * 60;
|
||||
if (time_file > time_cur || time_cur - time_file >= day) {
|
||||
log_write("repo.json expired, downloading new! time_file: %zu time_cur: %zu\n", time_file, time_cur);
|
||||
download_file = true;
|
||||
} else {
|
||||
log_write("repo.json not expired! time_file: %zu time_cur: %zu\n", time_file, time_cur);
|
||||
}
|
||||
}
|
||||
|
||||
// todo: remove me soon
|
||||
// download_file = true;
|
||||
|
||||
if (download_file) {
|
||||
DownloadFileAsync(URL_JSON, REPO_PATH, "", [this](std::vector<u8>& data, bool success){
|
||||
if (success) {
|
||||
curl::Api().ToFileAsync(
|
||||
curl::Url{URL_JSON},
|
||||
curl::Path{REPO_PATH},
|
||||
curl::Flags{curl::Flag_Cache},
|
||||
curl::OnComplete{[this](auto& result){
|
||||
if (result.success) {
|
||||
m_repo_download_state = ImageDownloadState::Done;
|
||||
if (HasFocus()) {
|
||||
ScanHomebrew();
|
||||
@@ -1111,16 +994,16 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
|
||||
} else {
|
||||
m_repo_download_state = ImageDownloadState::Failed;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
m_repo_download_state = ImageDownloadState::Done;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
});
|
||||
|
||||
m_filter = (Filter)ini_getl(INI_SECTION, "filter", m_filter, App::CONFIG_PATH);
|
||||
m_sort = (SortType)ini_getl(INI_SECTION, "sort", m_sort, App::CONFIG_PATH);
|
||||
m_order = (OrderType)ini_getl(INI_SECTION, "order", m_order, App::CONFIG_PATH);
|
||||
|
||||
const Vec4 v{75, 110, 370, 155};
|
||||
const Vec2 pad{10, 10};
|
||||
m_list = std::make_unique<List>(3, 9, m_pos, v, pad);
|
||||
Sort();
|
||||
}
|
||||
|
||||
@@ -1130,6 +1013,14 @@ Menu::~Menu() {
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
m_list->OnUpdate(controller, touch, m_entries_current.size(), [this](auto i) {
|
||||
if (m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
SetIndex(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
@@ -1145,97 +1036,108 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
const u64 SCROLL = m_start;
|
||||
const u64 max_entry_display = 9;
|
||||
const u64 nro_total = m_entries_current.size();
|
||||
const u64 cursor_pos = m_index;
|
||||
// max images per frame, in order to not hit io / gpu too hard.
|
||||
const int image_load_max = 2;
|
||||
int image_load_count = 0;
|
||||
|
||||
// only draw scrollbar if needed
|
||||
if (nro_total > max_entry_display) {
|
||||
const auto scrollbar_size = 500.f;
|
||||
const auto sb_h = 3.f / (float)nro_total * scrollbar_size;
|
||||
const auto sb_y = SCROLL / 3.f;
|
||||
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
|
||||
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * 2) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
|
||||
}
|
||||
m_list->Draw(vg, theme, m_entries_current.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
|
||||
const auto& [x, y, w, h] = v;
|
||||
const auto index = m_entries_current[pos];
|
||||
auto& e = m_entries[index];
|
||||
auto& image = e.image;
|
||||
|
||||
for (u64 i = 0, pos = SCROLL, y = 110, w = 370, h = 155; pos < nro_total && i < max_entry_display; y += h + 10) {
|
||||
for (u64 j = 0, x = 75; j < 3 && pos < nro_total && i < max_entry_display; j++, i++, pos++, x += w + 10) {
|
||||
const auto index = m_entries_current[pos];
|
||||
auto& e = m_entries[index];
|
||||
|
||||
// lazy load image
|
||||
if (!e.image.image) {
|
||||
switch (e.image.state) {
|
||||
case ImageDownloadState::None: {
|
||||
const auto path = BuildIconCachePath(e);
|
||||
if (fs::FsNativeSd().FileExists(path)) {
|
||||
EntryLoadImageFile(path, e.image);
|
||||
} else {
|
||||
const auto url = BuildIconUrl(e);
|
||||
e.image.state = ImageDownloadState::Progress;
|
||||
DownloadFileAsync(url, path, "", [this, index](std::vector<u8>& data, bool success) {
|
||||
if (success) {
|
||||
m_entries[index].image.state = ImageDownloadState::Done;
|
||||
} else {
|
||||
m_entries[index].image.state = ImageDownloadState::Failed;
|
||||
log_write("failed to download image\n");
|
||||
}
|
||||
}, nullptr, DownloadPriority::High);
|
||||
}
|
||||
} break;
|
||||
case ImageDownloadState::Progress: {
|
||||
|
||||
} break;
|
||||
case ImageDownloadState::Done: {
|
||||
EntryLoadImageFile(BuildIconCachePath(e), e.image);
|
||||
} break;
|
||||
case ImageDownloadState::Failed: {
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
auto text_id = ThemeEntryID_TEXT;
|
||||
if (pos == cursor_pos) {
|
||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
} else {
|
||||
DrawElement(x, y, w, h, ThemeEntryID_GRID);
|
||||
}
|
||||
|
||||
constexpr double image_scale = 256.0 / 115.0;
|
||||
// const float image_size = 256 / image_scale;
|
||||
// const float image_size_h = 150 / image_scale;
|
||||
DrawIcon(vg, e.image, m_default_image, x + 20, y + 20, 115, 115, true, image_scale);
|
||||
// gfx::drawImage(vg, x + 20, y + 20, image_size, image_size_h, e.image.image ? e.image.image : m_default_image);
|
||||
|
||||
nvgSave(vg);
|
||||
nvgScissor(vg, x, y, w - 30.f, h); // clip
|
||||
{
|
||||
const float font_size = 18;
|
||||
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.title.c_str());
|
||||
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.author.c_str());
|
||||
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.version.c_str());
|
||||
}
|
||||
nvgRestore(vg);
|
||||
|
||||
float i_size = 22;
|
||||
switch (e.status) {
|
||||
case EntryStatus::Get:
|
||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image);
|
||||
break;
|
||||
case EntryStatus::Installed:
|
||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image);
|
||||
break;
|
||||
case EntryStatus::Local:
|
||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image);
|
||||
break;
|
||||
case EntryStatus::Update:
|
||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image);
|
||||
break;
|
||||
// try and load cached image.
|
||||
if (image_load_count < image_load_max && !image.image && !image.tried_cache) {
|
||||
image.tried_cache = true;
|
||||
image.cached = EntryLoadImageFile(BuildIconCachePath(e), image);
|
||||
if (image.cached) {
|
||||
image_load_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lazy load image
|
||||
if (!image.image || image.cached) {
|
||||
switch (image.state) {
|
||||
case ImageDownloadState::None: {
|
||||
const auto path = BuildIconCachePath(e);
|
||||
const auto url = BuildIconUrl(e);
|
||||
image.state = ImageDownloadState::Progress;
|
||||
curl::Api().ToFileAsync(
|
||||
curl::Url{url},
|
||||
curl::Path{path},
|
||||
curl::Flags{curl::Flag_Cache},
|
||||
curl::OnComplete{[this, &image](auto& result) {
|
||||
if (result.success) {
|
||||
image.state = ImageDownloadState::Done;
|
||||
// data hasn't changed
|
||||
if (result.code == 304) {
|
||||
image.cached = false;
|
||||
}
|
||||
} else {
|
||||
image.state = ImageDownloadState::Failed;
|
||||
log_write("failed to download image\n");
|
||||
}
|
||||
}
|
||||
});
|
||||
} break;
|
||||
case ImageDownloadState::Progress: {
|
||||
|
||||
} break;
|
||||
case ImageDownloadState::Done: {
|
||||
if (image_load_count < image_load_max) {
|
||||
image.cached = false;
|
||||
if (!EntryLoadImageFile(BuildIconCachePath(e), e.image)) {
|
||||
image.state = ImageDownloadState::Failed;
|
||||
} else {
|
||||
image_load_count++;
|
||||
}
|
||||
}
|
||||
} break;
|
||||
case ImageDownloadState::Failed: {
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
auto text_id = ThemeEntryID_TEXT;
|
||||
if (pos == m_index) {
|
||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
} else {
|
||||
DrawElement(x, y, w, h, ThemeEntryID_GRID);
|
||||
}
|
||||
|
||||
constexpr double image_scale = 256.0 / 115.0;
|
||||
// const float image_size = 256 / image_scale;
|
||||
// const float image_size_h = 150 / image_scale;
|
||||
DrawIcon(vg, e.image, m_default_image, x + 20, y + 20, 115, 115, true, image_scale);
|
||||
// gfx::drawImage(vg, x + 20, y + 20, image_size, image_size_h, image.image ? image.image : m_default_image);
|
||||
|
||||
nvgSave(vg);
|
||||
nvgIntersectScissor(vg, v.x, v.y, w - 30.f, h); // clip
|
||||
{
|
||||
const float font_size = 18;
|
||||
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.title.c_str());
|
||||
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.author.c_str());
|
||||
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.version.c_str());
|
||||
}
|
||||
nvgRestore(vg);
|
||||
|
||||
float i_size = 22;
|
||||
switch (e.status) {
|
||||
case EntryStatus::Get:
|
||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image);
|
||||
break;
|
||||
case EntryStatus::Installed:
|
||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image);
|
||||
break;
|
||||
case EntryStatus::Local:
|
||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image);
|
||||
break;
|
||||
case EntryStatus::Update:
|
||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Menu::OnFocusGained() {
|
||||
@@ -1264,21 +1166,16 @@ void Menu::OnFocusGained() {
|
||||
if (m_dirty) {
|
||||
m_dirty = false;
|
||||
const auto& current_entry = m_entries[m_entries_current[m_index]];
|
||||
// m_start = 0;
|
||||
// m_index = 0;
|
||||
log_write("\nold index: %zu start: %zu\n", m_index, m_start);
|
||||
// old index: 19 start: 12
|
||||
Sort();
|
||||
|
||||
for (u32 i = 0; i < m_entries_current.size(); i++) {
|
||||
if (current_entry.name == m_entries[m_entries_current[i]].name) {
|
||||
SetIndex(i);
|
||||
if (i >= 9) {
|
||||
m_start = (i - 9) / 3 * 3 + 3;
|
||||
m_list->SetYoff((((i - 9) + 3) / 3) * m_list->GetMaxY());
|
||||
} else {
|
||||
m_start = 0;
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
log_write("\nnew index: %zu start: %zu\n", m_index, m_start);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1286,10 +1183,10 @@ void Menu::OnFocusGained() {
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::SetIndex(std::size_t index) {
|
||||
void Menu::SetIndex(s64 index) {
|
||||
m_index = index;
|
||||
if (!m_index) {
|
||||
m_start = 0;
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
|
||||
this->SetSubHeading(std::to_string(m_index + 1) + " / " + std::to_string(m_entries_current.size()));
|
||||
@@ -1484,9 +1381,10 @@ void Menu::SetSearch(const std::string& term) {
|
||||
SetFilter(m_filter);
|
||||
SetIndex(m_entry_search_jump_back);
|
||||
if (m_entry_search_jump_back >= 9) {
|
||||
m_start = (m_entry_search_jump_back - 9) / 3 * 3 + 3;
|
||||
m_list->SetYoff(0);
|
||||
m_list->SetYoff((((m_entry_search_jump_back - 9) + 3) / 3) * m_list->GetMaxY());
|
||||
} else {
|
||||
m_start = 0;
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
}});
|
||||
|
||||
@@ -1517,11 +1415,12 @@ void Menu::SetAuthor() {
|
||||
} else {
|
||||
SetFilter(m_filter);
|
||||
}
|
||||
|
||||
SetIndex(m_entry_author_jump_back);
|
||||
if (m_entry_author_jump_back >= 9) {
|
||||
m_start = (m_entry_author_jump_back - 9) / 3 * 3 + 3;
|
||||
m_list->SetYoff((((m_entry_author_jump_back - 9) + 3) / 3) * m_list->GetMaxY());
|
||||
} else {
|
||||
m_start = 0;
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
}});
|
||||
|
||||
|
||||
@@ -36,20 +36,11 @@
|
||||
namespace sphaira::ui::menu::filebrowser {
|
||||
namespace {
|
||||
|
||||
struct FsDirCollection {
|
||||
fs::FsPath path;
|
||||
fs::FsPath parent_name;
|
||||
std::vector<FsDirectoryEntry> files;
|
||||
std::vector<FsDirectoryEntry> dirs;
|
||||
};
|
||||
|
||||
struct ExtDbEntry {
|
||||
std::string_view db_name;
|
||||
std::span<const std::string_view> ext;
|
||||
};
|
||||
|
||||
using FsDirCollections = std::vector<FsDirCollection>;
|
||||
|
||||
constexpr std::string_view AUDIO_EXTENSIONS[] = {
|
||||
"mp3", "ogg", "flac", "wav", "aac" "ac3", "aif", "asf", "bfwav",
|
||||
"bfsar", "bfstm",
|
||||
@@ -107,16 +98,6 @@ constexpr RomDatabaseEntry PATHS[]{
|
||||
|
||||
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 {
|
||||
for (auto e : list) {
|
||||
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 (db_idx < 0) {
|
||||
log_write("using nro image\n");
|
||||
@@ -225,24 +206,21 @@ auto GetRomIcon(ProgressBox* pbox, std::string filename, std::string extension,
|
||||
if (!pbox->ShouldExit()) {
|
||||
pbox->NewTransfer("Trying to load "_i18n + ra_thumbnail_path);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// try and download icon
|
||||
if (!pbox->ShouldExit()) {
|
||||
// auto png_image = DownloadMemory(ra_thumbnail_url.c_str());
|
||||
pbox->NewTransfer("Downloading "_i18n + gh_thumbnail_url);
|
||||
auto png_image = DownloadMemory(gh_thumbnail_url, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
|
||||
if (pbox->ShouldExit()) {
|
||||
return false;
|
||||
}
|
||||
pbox->UpdateTransfer(dlnow, dltotal);
|
||||
return true;
|
||||
});
|
||||
if (!png_image.empty()) {
|
||||
return png_image;
|
||||
const auto result = curl::Api().ToMemory(
|
||||
curl::Url{gh_thumbnail_url},
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||
);
|
||||
|
||||
if (result.success && !result.data.empty()) {
|
||||
return result.data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
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](){
|
||||
if (m_index < (m_entries_current.size() - 1)) {
|
||||
SetIndex(m_index + 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
if (m_index - m_index_offset >= 8) {
|
||||
log_write("moved down\n");
|
||||
m_index_offset++;
|
||||
}
|
||||
if (m_list->ScrollDown(m_index, 1, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
if (m_index != 0) {
|
||||
SetIndex(m_index - 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
if (m_index < m_index_offset ) {
|
||||
log_write("moved up\n");
|
||||
m_index_offset--;
|
||||
}
|
||||
if (m_list->ScrollUp(m_index, 1, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DPAD_RIGHT, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 8, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DPAD_LEFT, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 8, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::A, Action{"Open"_i18n, [this](){
|
||||
@@ -379,12 +270,12 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
||||
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){
|
||||
if (op_index && *op_index) {
|
||||
// daybreak uses native fs so do not use nro_add_arg_file
|
||||
// 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;
|
||||
@@ -394,7 +285,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
||||
|
||||
if (entry.type == FsDirEntryType_Dir) {
|
||||
Scan(GetNewPathCurrent());
|
||||
} else {
|
||||
} else if (m_fs_type == FsType::Sd) {
|
||||
// special case for nro
|
||||
if (entry.GetExtension() == "nro") {
|
||||
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("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);
|
||||
SortAndFindLastFile();
|
||||
}, m_sort.Get()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](std::size_t& index_out){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this](s64& index_out){
|
||||
m_order.Set(index_out);
|
||||
SortAndFindLastFile();
|
||||
}, m_order.Get()));
|
||||
@@ -517,12 +408,13 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
||||
App::Push(std::make_shared<OptionBox>(
|
||||
"Delete Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
App::PopToMenu();
|
||||
OnDeleteCallback();
|
||||
}
|
||||
}
|
||||
));
|
||||
log_write("pushed delete\n");
|
||||
}, true));
|
||||
}));
|
||||
}
|
||||
|
||||
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>(
|
||||
buf, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
App::PopToMenu();
|
||||
OnPasteCallback();
|
||||
}
|
||||
}));
|
||||
}, true));
|
||||
}));
|
||||
}
|
||||
|
||||
// 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 name = entry.GetName();
|
||||
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 dst_path = GetNewPath(m_path, out);
|
||||
|
||||
Result rc;
|
||||
if (entry.IsFile()) {
|
||||
rc = fs::FsNativeSd().RenameFile(src_path, dst_path);
|
||||
rc = m_fs->RenameFile(src_path, dst_path);
|
||||
} else {
|
||||
rc = fs::FsNativeSd().RenameDirectory(src_path, dst_path);
|
||||
rc = m_fs->RenameDirectory(src_path, dst_path);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}, true));
|
||||
}));
|
||||
}
|
||||
|
||||
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](){
|
||||
std::string out;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, "Set File Name"_i18n.c_str())) && !out.empty()) {
|
||||
App::PopToMenu();
|
||||
|
||||
fs::FsPath full_path;
|
||||
if (out[0] == '/') {
|
||||
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);
|
||||
}
|
||||
|
||||
fs::FsNativeSd fs;
|
||||
fs.CreateDirectoryRecursivelyWithPath(full_path);
|
||||
if (R_SUCCEEDED(fs.CreateFile(full_path, 0, 0))) {
|
||||
m_fs->CreateDirectoryRecursivelyWithPath(full_path);
|
||||
if (R_SUCCEEDED(m_fs->CreateFile(full_path, 0, 0))) {
|
||||
log_write("created file: %s\n", full_path);
|
||||
Scan(m_path);
|
||||
} else {
|
||||
log_write("failed to create file: %s\n", full_path);
|
||||
}
|
||||
}
|
||||
}, true));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Create Folder"_i18n, [this](){
|
||||
std::string out;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, "Set Folder Name"_i18n.c_str())) && !out.empty()) {
|
||||
App::PopToMenu();
|
||||
|
||||
fs::FsPath full_path;
|
||||
if (out[0] == '/') {
|
||||
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);
|
||||
}
|
||||
|
||||
if (R_SUCCEEDED(fs::FsNativeSd().CreateDirectoryRecursively(full_path))) {
|
||||
if (R_SUCCEEDED(m_fs->CreateDirectoryRecursively(full_path))) {
|
||||
log_write("created dir: %s\n", full_path);
|
||||
Scan(m_path);
|
||||
} else {
|
||||
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](){
|
||||
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())) {
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Install Forwarder"_i18n, [this](){;
|
||||
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;
|
||||
ini_gets("paths", "last_path", "/", buf, sizeof(buf), App::CONFIG_PATH);
|
||||
m_path = buf;
|
||||
SetFs(buf, m_mount.Get());
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
@@ -648,6 +565,14 @@ Menu::~Menu() {
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
m_list->OnUpdate(controller, touch, m_entries_current.size(), [this](auto i) {
|
||||
if (m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
SetIndex(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
@@ -660,43 +585,17 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
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};
|
||||
|
||||
// todo: cleanup
|
||||
const float x = block.x;
|
||||
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++) {
|
||||
m_list->Draw(vg, theme, m_entries_current.size(), [this, text_col](auto* vg, auto* theme, auto v, auto i) {
|
||||
const auto& [x, y, w, h] = v;
|
||||
auto& e = GetEntry(i);
|
||||
|
||||
if (e.IsDir()) {
|
||||
if (e.file_count == -1 && e.dir_count == -1) {
|
||||
const auto full_path = GetNewPath(e);
|
||||
fs::FsNativeSd fs;
|
||||
fs.DirGetEntryCount(full_path, FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize, &e.file_count);
|
||||
fs.DirGetEntryCount(full_path, FsDirOpenMode_ReadDirs | FsDirOpenMode_NoFileSize, &e.dir_count);
|
||||
m_fs->DirGetEntryCount(full_path, FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize, &e.file_count);
|
||||
m_fs->DirGetEntryCount(full_path, FsDirOpenMode_ReadDirs | FsDirOpenMode_NoFileSize, &e.dir_count);
|
||||
}
|
||||
} else if (!e.checked_extension) {
|
||||
e.checked_extension = true;
|
||||
@@ -715,7 +614,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
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) {
|
||||
if (i == m_index) {
|
||||
gfx::drawRect(vg, x, y, 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);
|
||||
const auto txt_clip = std::min(GetY() + GetH(), y + h) - y;
|
||||
nvgScissor(vg, x + text_xoffset+65, y, w-(x+text_xoffset+65+50), txt_clip);
|
||||
nvgIntersectScissor(vg, x + text_xoffset+65, y, w-(x+text_xoffset+65+50), h);
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
} else {
|
||||
if (!e.time_stamp.is_valid) {
|
||||
fs::FsNativeSd fs;
|
||||
const auto path = GetNewPath(e);
|
||||
|
||||
if (R_SUCCEEDED(fs.GetFsOpenResult())) {
|
||||
fs.GetFileTimeStampRaw(path, &e.time_stamp);
|
||||
}
|
||||
m_fs->GetFileTimeStampRaw(path, &e.time_stamp);
|
||||
}
|
||||
const auto t = (time_t)(e.time_stamp.modified);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
y += h;
|
||||
if (!InYBounds(y)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
nvgRestore(vg);
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
if (!m_index) {
|
||||
m_index_offset = 0;
|
||||
m_list->SetYoff();
|
||||
}
|
||||
|
||||
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 = file_name;
|
||||
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));
|
||||
}));
|
||||
@@ -894,42 +781,31 @@ void Menu::InstallForwarder() {
|
||||
auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
|
||||
log_write("new scan path: %s\n", new_path);
|
||||
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);
|
||||
}
|
||||
|
||||
log_write("\nold index: %zu start: %zu\n", m_index, m_index_offset);
|
||||
|
||||
m_path = new_path;
|
||||
m_entries.clear();
|
||||
m_index = 0;
|
||||
m_index_offset = 0;
|
||||
m_list->SetYoff(0);
|
||||
SetTitleSubHeading(m_path);
|
||||
|
||||
if (m_selected_type == SelectedType::None) {
|
||||
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;
|
||||
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));
|
||||
|
||||
s64 count;
|
||||
R_TRY(fs.DirGetEntryCount(&d, &count));
|
||||
R_TRY(m_fs->DirGetEntryCount(&d, &count));
|
||||
|
||||
// we won't run out of memory here (tm)
|
||||
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
|
||||
dir_entries.resize(count);
|
||||
@@ -959,7 +835,7 @@ auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
|
||||
Sort();
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -1029,13 +905,16 @@ void Menu::LoadAssocEntriesPath(const fs::FsPath& path) {
|
||||
return;
|
||||
}
|
||||
ON_SCOPE_EXIT(closedir(dir));
|
||||
fs::FsNativeSd fs;
|
||||
|
||||
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, ".ini")) {
|
||||
continue;
|
||||
@@ -1081,7 +960,7 @@ void Menu::LoadAssocEntriesPath(const fs::FsPath& path) {
|
||||
// if path isn't empty, check if the file exists
|
||||
bool file_exists{};
|
||||
if (!assoc.path.empty()) {
|
||||
file_exists = fs.FileExists(assoc.path);
|
||||
file_exists = m_fs->FileExists(assoc.path);
|
||||
} else {
|
||||
const auto nro_name = assoc.name + ".nro";
|
||||
for (const auto& nro : m_nro_entries) {
|
||||
@@ -1187,7 +1066,7 @@ void Menu::Sort() {
|
||||
void Menu::SortAndFindLastFile() {
|
||||
std::optional<LastFile> last_file;
|
||||
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();
|
||||
@@ -1208,21 +1087,21 @@ void Menu::SetIndexFromLastFile(const LastFile& last_file) {
|
||||
}
|
||||
}
|
||||
if (index >= 0) {
|
||||
if ((u64)index == last_file.index && m_entries_current.size() == last_file.entries_count) {
|
||||
m_index_offset = last_file.offset;
|
||||
if (index == last_file.index && m_entries_current.size() == last_file.entries_count) {
|
||||
m_list->SetYoff(last_file.offset);
|
||||
log_write("index is the same as last time\n");
|
||||
} else {
|
||||
// file position changed!
|
||||
log_write("file position changed\n");
|
||||
// guesstimate where the position is
|
||||
if (index >= 8) {
|
||||
m_index_offset = (index - 8) + 1;
|
||||
m_list->SetYoff(((index - 8) + 1) * m_list->GetMaxY());
|
||||
} else {
|
||||
m_index_offset = 0;
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
}
|
||||
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
|
||||
if (m_selected_files.size() == 1) {
|
||||
fs::FsNativeSd fs;
|
||||
const auto& entry = m_selected_files[0];
|
||||
const auto full_path = GetNewPath(m_selected_path, entry.name);
|
||||
|
||||
if (entry.IsDir()) {
|
||||
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) {
|
||||
fs.DeleteDirectory(full_path);
|
||||
m_fs->DeleteDirectory(full_path);
|
||||
use_progress_box = false;
|
||||
}
|
||||
} else {
|
||||
fs.DeleteFile(full_path);
|
||||
m_fs->DeleteFile(full_path);
|
||||
use_progress_box = false;
|
||||
}
|
||||
}
|
||||
@@ -1259,7 +1137,6 @@ void Menu::OnDeleteCallback() {
|
||||
log_write("did delete\n");
|
||||
} else {
|
||||
App::Push(std::make_shared<ProgressBox>("Deleting"_i18n, [this](auto pbox){
|
||||
fs::FsNativeSd fs;
|
||||
FsDirCollections collections;
|
||||
|
||||
// build list of dirs / files
|
||||
@@ -1292,10 +1169,10 @@ void Menu::OnDeleteCallback() {
|
||||
pbox->NewTransfer("Deleting "_i18n + full_path);
|
||||
if (p.type == FsDirEntryType_Dir) {
|
||||
log_write("deleting dir: %s\n", full_path);
|
||||
fs.DeleteDirectory(full_path);
|
||||
m_fs->DeleteDirectory(full_path);
|
||||
} else {
|
||||
log_write("deleting file: %s\n", full_path);
|
||||
fs.DeleteFile(full_path);
|
||||
m_fs->DeleteFile(full_path);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -1320,10 +1197,10 @@ void Menu::OnDeleteCallback() {
|
||||
|
||||
if (p.IsDir()) {
|
||||
log_write("deleting dir: %s\n", full_path);
|
||||
fs.DeleteDirectory(full_path);
|
||||
m_fs->DeleteDirectory(full_path);
|
||||
} else {
|
||||
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)
|
||||
if (m_selected_files.size() == 1 && m_selected_type == SelectedType::Cut) {
|
||||
fs::FsNativeSd fs;
|
||||
const auto& entry = m_selected_files[0];
|
||||
const auto full_path = GetNewPath(m_selected_path, entry.name);
|
||||
|
||||
if (entry.IsDir()) {
|
||||
fs.RenameDirectory(full_path, GetNewPath(entry));
|
||||
m_fs->RenameDirectory(full_path, GetNewPath(entry));
|
||||
} else {
|
||||
fs.RenameFile(full_path, GetNewPath(entry));
|
||||
m_fs->RenameFile(full_path, GetNewPath(entry));
|
||||
}
|
||||
|
||||
ResetSelection();
|
||||
@@ -1356,7 +1232,6 @@ void Menu::OnPasteCallback() {
|
||||
log_write("did paste\n");
|
||||
} else {
|
||||
App::Push(std::make_shared<ProgressBox>("Pasting"_i18n, [this](auto pbox){
|
||||
fs::FsNativeSd fs;
|
||||
|
||||
if (m_selected_type == SelectedType::Cut) {
|
||||
for (const auto& p : m_selected_files) {
|
||||
@@ -1371,9 +1246,9 @@ void Menu::OnPasteCallback() {
|
||||
pbox->NewTransfer("Pasting "_i18n + src_path);
|
||||
|
||||
if (p.IsDir()) {
|
||||
fs.RenameDirectory(src_path, dst_path);
|
||||
m_fs->RenameDirectory(src_path, dst_path);
|
||||
} else {
|
||||
fs.RenameFile(src_path, dst_path);
|
||||
m_fs->RenameFile(src_path, dst_path);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1407,7 +1282,7 @@ void Menu::OnPasteCallback() {
|
||||
|
||||
if (p.IsDir()) {
|
||||
pbox->NewTransfer("Creating "_i18n + dst_path);
|
||||
fs.CreateDirectory(dst_path);
|
||||
m_fs->CreateDirectory(dst_path);
|
||||
} else {
|
||||
pbox->NewTransfer("Copying "_i18n + src_path);
|
||||
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);
|
||||
pbox->NewTransfer("Creating "_i18n + dst_path);
|
||||
fs.CreateDirectory(dst_path);
|
||||
m_fs->CreateDirectory(dst_path);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// options
|
||||
|
||||
533
sphaira/source/ui/menus/ghdl.cpp
Normal file
533
sphaira/source/ui/menus/ghdl.cpp
Normal 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
|
||||
@@ -43,27 +43,23 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
if (m_index < (m_entries.size() - 1)) {
|
||||
if (m_index < (m_entries.size() - 3)) {
|
||||
SetIndex(m_index + 3);
|
||||
} else {
|
||||
SetIndex(m_entries.size() - 1);
|
||||
}
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
if (m_index - m_start >= 9) {
|
||||
log_write("moved down\n");
|
||||
m_start += 3;
|
||||
}
|
||||
if (m_list->ScrollDown(m_index, 3, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
if (m_index >= 3) {
|
||||
SetIndex(m_index - 3);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
if (m_index < m_start ) {
|
||||
// log_write("moved up\n");
|
||||
m_start -= 3;
|
||||
}
|
||||
if (m_list->ScrollUp(m_index, 3, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::R2, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 9, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::L2, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 9, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::A, Action{"Launch"_i18n, [this](){
|
||||
@@ -90,12 +86,12 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
||||
order_items.push_back("Decending"_i18n);
|
||||
order_items.push_back("Ascending"_i18n);
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](std::size_t& index_out){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
|
||||
m_sort.Set(index_out);
|
||||
SortAndFindLastFile();
|
||||
}, m_sort.Get()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](std::size_t& index_out){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](s64& index_out){
|
||||
m_order.Set(index_out);
|
||||
SortAndFindLastFile();
|
||||
}, m_order.Get()));
|
||||
@@ -145,6 +141,10 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
||||
}
|
||||
}})
|
||||
);
|
||||
|
||||
const Vec4 v{75, 110, 370, 155};
|
||||
const Vec2 pad{10, 10};
|
||||
m_list = std::make_unique<List>(3, 9, m_pos, v, pad);
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
@@ -157,68 +157,71 @@ Menu::~Menu() {
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
m_list->OnUpdate(controller, touch, m_entries.size(), [this](auto i) {
|
||||
if (m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
SetIndex(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
const u64 SCROLL = m_start;
|
||||
const u64 max_entry_display = 9;
|
||||
const u64 nro_total = m_entries.size();
|
||||
const u64 cursor_pos = m_index;
|
||||
fs::FsNativeSd fs;
|
||||
// max images per frame, in order to not hit io / gpu too hard.
|
||||
const int image_load_max = 2;
|
||||
int image_load_count = 0;
|
||||
|
||||
// only draw scrollbar if needed
|
||||
if (nro_total > max_entry_display) {
|
||||
const auto scrollbar_size = 500.f;
|
||||
const auto sb_h = 3.f / (float)nro_total * scrollbar_size;
|
||||
const auto sb_y = SCROLL / 3.f;
|
||||
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
|
||||
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * 2) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
|
||||
}
|
||||
m_list->Draw(vg, theme, m_entries.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
|
||||
const auto& [x, y, w, h] = v;
|
||||
auto& e = m_entries[pos];
|
||||
|
||||
for (u64 i = 0, pos = SCROLL, y = 110, w = 370, h = 155; pos < nro_total && i < max_entry_display; y += h + 10) {
|
||||
for (u64 j = 0, x = 75; j < 3 && pos < nro_total && i < max_entry_display; j++, i++, pos++, x += w + 10) {
|
||||
auto& e = m_entries[pos];
|
||||
|
||||
// lazy load image
|
||||
if (!e.image && e.icon.empty() && e.icon_size && e.icon_offset) {
|
||||
e.icon = nro_get_icon(e.path, e.icon_size, e.icon_offset);
|
||||
if (!e.icon.empty()) {
|
||||
e.image = nvgCreateImageMem(vg, 0, e.icon.data(), e.icon.size());
|
||||
// lazy load image
|
||||
if (image_load_count < image_load_max) {
|
||||
if (!e.image && e.icon_size && e.icon_offset) {
|
||||
// NOTE: it seems that images can be any size. SuperTux uses a 1024x1024
|
||||
// ~300Kb image, which takes a few frames to completely load.
|
||||
// really, switch-tools should handle this by resizing the image before
|
||||
// adding it to the nro, as well as validate its a valid jpeg.
|
||||
const auto icon = nro_get_icon(e.path, e.icon_size, e.icon_offset);
|
||||
if (!icon.empty()) {
|
||||
e.image = nvgCreateImageMem(vg, 0, icon.data(), icon.size());
|
||||
image_load_count++;
|
||||
}
|
||||
}
|
||||
|
||||
auto text_id = ThemeEntryID_TEXT;
|
||||
if (pos == cursor_pos) {
|
||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
} else {
|
||||
DrawElement(x, y, w, h, ThemeEntryID_GRID);
|
||||
}
|
||||
|
||||
const float image_size = 115;
|
||||
gfx::drawImageRounded(vg, x + 20, y + 20, image_size, image_size, e.image);
|
||||
|
||||
nvgSave(vg);
|
||||
nvgScissor(vg, x, y, w - 30.f, h); // clip
|
||||
{
|
||||
bool has_star = false;
|
||||
if (IsStarEnabled()) {
|
||||
if (!e.has_star.has_value()) {
|
||||
e.has_star = fs.FileExists(GenerateStarPath(e.path));
|
||||
}
|
||||
has_star = e.has_star.value();
|
||||
}
|
||||
|
||||
const float font_size = 18;
|
||||
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s%s", has_star ? "\u2605 " : "", e.GetName());
|
||||
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetAuthor());
|
||||
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetDisplayVersion());
|
||||
}
|
||||
nvgRestore(vg);
|
||||
}
|
||||
}
|
||||
|
||||
auto text_id = ThemeEntryID_TEXT;
|
||||
if (pos == m_index) {
|
||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, v, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
} else {
|
||||
DrawElement(v, ThemeEntryID_GRID);
|
||||
}
|
||||
|
||||
const float image_size = 115;
|
||||
gfx::drawImageRounded(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage());
|
||||
|
||||
nvgSave(vg);
|
||||
nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip
|
||||
{
|
||||
bool has_star = false;
|
||||
if (IsStarEnabled()) {
|
||||
if (!e.has_star.has_value()) {
|
||||
e.has_star = fs::FsNativeSd().FileExists(GenerateStarPath(e.path));
|
||||
}
|
||||
has_star = e.has_star.value();
|
||||
}
|
||||
|
||||
const float font_size = 18;
|
||||
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, "%s%s", has_star ? "\u2605 " : "", e.GetName());
|
||||
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetAuthor());
|
||||
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->elements[text_id].colour, e.GetDisplayVersion());
|
||||
}
|
||||
nvgRestore(vg);
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
if (!m_index) {
|
||||
m_start = 0;
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
|
||||
const auto& e = m_entries[m_index];
|
||||
@@ -266,13 +269,13 @@ void Menu::SetIndex(std::size_t index) {
|
||||
|
||||
void Menu::InstallHomebrew() {
|
||||
const auto& nro = m_entries[m_index];
|
||||
InstallHomebrew(nro.path, nro.nacp, nro.icon);
|
||||
InstallHomebrew(nro.path, nro.nacp, nro_get_icon(nro.path, nro.icon_size, nro.icon_offset));
|
||||
}
|
||||
|
||||
void Menu::ScanHomebrew() {
|
||||
TimeStamp ts;
|
||||
nro_scan("/switch", m_entries, m_hide_sphaira.Get());
|
||||
log_write("nros found: %zu time_taken: %.2f\n", m_entries.size(), ts.GetSeconds());
|
||||
log_write("nros found: %zu time_taken: %.2f\n", m_entries.size(), ts.GetSecondsD());
|
||||
|
||||
struct IniUser {
|
||||
std::vector<NroEntry>& entires;
|
||||
@@ -330,6 +333,22 @@ void Menu::Sort() {
|
||||
const auto order = m_order.Get();
|
||||
|
||||
const auto sorter = [this, sort, order](const NroEntry& lhs, const NroEntry& rhs) -> bool {
|
||||
const auto name_cmp = [order](const NroEntry& lhs, const NroEntry& rhs) -> bool {
|
||||
auto r = strcasecmp(lhs.GetName(), rhs.GetName());
|
||||
if (!r) {
|
||||
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) {
|
||||
case SortType_UpdatedStar:
|
||||
if (lhs.has_star.value() && !rhs.has_star.value()) {
|
||||
@@ -348,7 +367,7 @@ void Menu::Sort() {
|
||||
}
|
||||
|
||||
if (lhs_timestamp == rhs_timestamp) {
|
||||
return strcasecmp(lhs.GetName(), rhs.GetName()) < 0;
|
||||
return name_cmp(lhs, rhs);
|
||||
} else if (order == OrderType_Decending) {
|
||||
return lhs_timestamp > rhs_timestamp;
|
||||
} else {
|
||||
@@ -364,7 +383,7 @@ void Menu::Sort() {
|
||||
}
|
||||
case SortType_Size: {
|
||||
if (lhs.size == rhs.size) {
|
||||
return strcasecmp(lhs.GetName(), rhs.GetName()) < 0;
|
||||
return name_cmp(lhs, rhs);
|
||||
} else if (order == OrderType_Decending) {
|
||||
return lhs.size > rhs.size;
|
||||
} else {
|
||||
@@ -379,11 +398,7 @@ void Menu::Sort() {
|
||||
return false;
|
||||
}
|
||||
case SortType_Alphabetical: {
|
||||
if (order == OrderType_Decending) {
|
||||
return strcasecmp(lhs.GetName(), rhs.GetName()) < 0;
|
||||
} else {
|
||||
return strcasecmp(lhs.GetName(), rhs.GetName()) > 0;
|
||||
}
|
||||
return name_cmp(lhs, rhs);
|
||||
} break;
|
||||
}
|
||||
|
||||
@@ -409,9 +424,9 @@ void Menu::SortAndFindLastFile() {
|
||||
if (index >= 0) {
|
||||
// guesstimate where the position is
|
||||
if (index >= 9) {
|
||||
m_start = (index - 9) / 3 * 3 + 3;
|
||||
m_list->SetYoff((((index - 9) + 3) / 3) * m_list->GetMaxY());
|
||||
} else {
|
||||
m_start = 0;
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
SetIndex(index);
|
||||
}
|
||||
|
||||
@@ -139,44 +139,44 @@ Menu::Menu() : MenuBase{"Irs"_i18n} {
|
||||
format_str.emplace_back("20x15"_i18n);
|
||||
}
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Controller"_i18n, controller_str, [this](std::size_t& index){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Controller"_i18n, controller_str, [this](s64& index){
|
||||
irsStopImageProcessor(m_entries[m_index].m_handle);
|
||||
m_index = index;
|
||||
UpdateConfig(&m_config);
|
||||
}, m_index));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Rotation"_i18n, rotation_str, [this](std::size_t& index){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Rotation"_i18n, rotation_str, [this](s64& index){
|
||||
m_rotation = (Rotation)index;
|
||||
}, m_rotation));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Colour"_i18n, colour_str, [this](std::size_t& index){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Colour"_i18n, colour_str, [this](s64& index){
|
||||
m_colour = (Colour)index;
|
||||
updateColourArray();
|
||||
}, m_colour));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Light Target"_i18n, light_target_str, [this](std::size_t& index){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Light Target"_i18n, light_target_str, [this](s64& index){
|
||||
m_config.light_target = index;
|
||||
UpdateConfig(&m_config);
|
||||
}, m_config.light_target));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Gain"_i18n, gain_str, [this](std::size_t& index){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Gain"_i18n, gain_str, [this](s64& index){
|
||||
m_config.gain = GAIN_MIN + index;
|
||||
UpdateConfig(&m_config);
|
||||
}, m_config.gain - GAIN_MIN));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Negative Image"_i18n, is_negative_image_used_str, [this](std::size_t& index){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Negative Image"_i18n, is_negative_image_used_str, [this](s64& index){
|
||||
m_config.is_negative_image_used = index;
|
||||
UpdateConfig(&m_config);
|
||||
}, m_config.is_negative_image_used));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Format"_i18n, format_str, [this](std::size_t& index){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Format"_i18n, format_str, [this](s64& index){
|
||||
m_config.orig_format = index;
|
||||
m_config.trimming_format = index;
|
||||
UpdateConfig(&m_config);
|
||||
}, m_config.orig_format));
|
||||
|
||||
if (hosversionAtLeast(4,0,0)) {
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Trimming Format"_i18n, format_str, [this](std::size_t& index){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Trimming Format"_i18n, format_str, [this](s64& index){
|
||||
// you cannot set trim a larger region than the source
|
||||
if (index < m_config.orig_format) {
|
||||
index = m_config.orig_format;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "ui/menus/main_menu.hpp"
|
||||
#include "ui/menus/irs_menu.hpp"
|
||||
#include "ui/menus/themezer.hpp"
|
||||
#include "ui/menus/ghdl.hpp"
|
||||
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/popup_list.hpp"
|
||||
@@ -22,6 +23,9 @@
|
||||
namespace sphaira::ui::menu::main {
|
||||
namespace {
|
||||
|
||||
constexpr const char* GITHUB_URL{"https://api.github.com/repos/ITotalJustice/sphaira/releases/latest"};
|
||||
constexpr fs::FsPath CACHE_PATH{"/switch/sphaira/cache/sphaira_latest.json"};
|
||||
|
||||
auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string version) -> bool {
|
||||
static fs::FsPath zip_out{"/switch/sphaira/cache/update.zip"};
|
||||
constexpr auto chunk_size = 1024 * 512; // 512KiB
|
||||
@@ -34,16 +38,12 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
|
||||
pbox->NewTransfer("Downloading "_i18n + version);
|
||||
log_write("starting download: %s\n", url.c_str());
|
||||
|
||||
DownloadClearCache(url);
|
||||
if (!DownloadFile(url, zip_out, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
|
||||
if (pbox->ShouldExit()) {
|
||||
return false;
|
||||
}
|
||||
pbox->UpdateTransfer(dlnow, dltotal);
|
||||
return true;
|
||||
})) {
|
||||
if (!curl::Api().ToFile(
|
||||
curl::Url{url},
|
||||
curl::Path{zip_out},
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||
).success) {
|
||||
log_write("error with download\n");
|
||||
// push popup error box
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
|
||||
}
|
||||
|
||||
std::vector<char> buf(chunk_size);
|
||||
u64 offset{};
|
||||
s64 offset{};
|
||||
while (offset < info.uncompressed_size) {
|
||||
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
|
||||
if (bytes_read <= 0) {
|
||||
@@ -142,63 +142,69 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
|
||||
} // namespace
|
||||
|
||||
MainMenu::MainMenu() {
|
||||
DownloadMemoryAsync("https://api.github.com/repos/ITotalJustice/sphaira/releases/latest", "", [this](std::vector<u8>& data, bool success){
|
||||
m_update_state = UpdateState::Error;
|
||||
ON_SCOPE_EXIT( log_write("update status: %u\n", (u8)m_update_state) );
|
||||
curl::Api().ToFileAsync(
|
||||
curl::Url{GITHUB_URL},
|
||||
curl::Path{CACHE_PATH},
|
||||
curl::Flags{curl::Flag_Cache},
|
||||
curl::Header{
|
||||
{ "Accept", "application/vnd.github+json" },
|
||||
},
|
||||
curl::OnComplete{[this](auto& result){
|
||||
log_write("inside github download\n");
|
||||
m_update_state = UpdateState::Error;
|
||||
ON_SCOPE_EXIT( log_write("update status: %u\n", (u8)m_update_state) );
|
||||
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
if (!result.success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto json = yyjson_read((const char*)data.data(), data.size(), 0);
|
||||
R_UNLESS(json, false);
|
||||
ON_SCOPE_EXIT(yyjson_doc_free(json));
|
||||
auto json = yyjson_read_file(CACHE_PATH, YYJSON_READ_NOFLAG, nullptr, nullptr);
|
||||
R_UNLESS(json, false);
|
||||
ON_SCOPE_EXIT(yyjson_doc_free(json));
|
||||
|
||||
auto root = yyjson_doc_get_root(json);
|
||||
R_UNLESS(root, false);
|
||||
auto root = yyjson_doc_get_root(json);
|
||||
R_UNLESS(root, false);
|
||||
|
||||
auto tag_key = yyjson_obj_get(root, "tag_name");
|
||||
R_UNLESS(tag_key, false);
|
||||
auto tag_key = yyjson_obj_get(root, "tag_name");
|
||||
R_UNLESS(tag_key, false);
|
||||
|
||||
const auto version = yyjson_get_str(tag_key);
|
||||
R_UNLESS(version, false);
|
||||
if (std::strcmp(APP_VERSION, version) >= 0) {
|
||||
m_update_state = UpdateState::None;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto body_key = yyjson_obj_get(root, "body");
|
||||
R_UNLESS(body_key, false);
|
||||
|
||||
const auto body = yyjson_get_str(body_key);
|
||||
R_UNLESS(body, false);
|
||||
|
||||
auto assets = yyjson_obj_get(root, "assets");
|
||||
R_UNLESS(assets, false);
|
||||
|
||||
auto idx0 = yyjson_arr_get(assets, 0);
|
||||
R_UNLESS(idx0, false);
|
||||
|
||||
auto url_key = yyjson_obj_get(idx0, "browser_download_url");
|
||||
R_UNLESS(url_key, false);
|
||||
|
||||
const auto url = yyjson_get_str(url_key);
|
||||
R_UNLESS(url, false);
|
||||
|
||||
m_update_version = version;
|
||||
m_update_url = url;
|
||||
m_update_description = body;
|
||||
m_update_state = UpdateState::Update;
|
||||
log_write("found url: %s\n", url);
|
||||
log_write("found body: %s\n", body);
|
||||
App::Notify("Update avaliable: "_i18n + m_update_version);
|
||||
|
||||
const auto version = yyjson_get_str(tag_key);
|
||||
R_UNLESS(version, false);
|
||||
if (std::strcmp(APP_VERSION, version) >= 0) {
|
||||
m_update_state = UpdateState::None;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto body_key = yyjson_obj_get(root, "body");
|
||||
R_UNLESS(body_key, false);
|
||||
|
||||
const auto body = yyjson_get_str(body_key);
|
||||
R_UNLESS(body, false);
|
||||
|
||||
auto assets = yyjson_obj_get(root, "assets");
|
||||
R_UNLESS(assets, false);
|
||||
|
||||
auto idx0 = yyjson_arr_get(assets, 0);
|
||||
R_UNLESS(idx0, false);
|
||||
|
||||
auto url_key = yyjson_obj_get(idx0, "browser_download_url");
|
||||
R_UNLESS(url_key, false);
|
||||
|
||||
const auto url = yyjson_get_str(url_key);
|
||||
R_UNLESS(url, false);
|
||||
|
||||
m_update_version = version;
|
||||
m_update_url = url;
|
||||
m_update_description = body;
|
||||
m_update_state = UpdateState::Update;
|
||||
log_write("found url: %s\n", url);
|
||||
log_write("found body: %s\n", body);
|
||||
App::Notify("Update avaliable: "_i18n + m_update_version);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
AddOnLPress();
|
||||
AddOnRPress();
|
||||
|
||||
this->SetActions(
|
||||
std::make_pair(Button::START, Action{App::Exit}),
|
||||
std::make_pair(Button::Y, Action{"Menu"_i18n, [this](){
|
||||
@@ -220,8 +226,6 @@ MainMenu::MainMenu() {
|
||||
language_items.push_back("Russian"_i18n);
|
||||
language_items.push_back("Swedish"_i18n);
|
||||
|
||||
options->AddHeader("Header"_i18n);
|
||||
options->AddSpacer();
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [this](){
|
||||
SidebarEntryArray::Items theme_items{};
|
||||
const auto theme_meta = App::GetThemeMetaList();
|
||||
@@ -232,7 +236,7 @@ MainMenu::MainMenu() {
|
||||
auto options = std::make_shared<Sidebar>("Theme Options"_i18n, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Select Theme"_i18n, theme_items, [this, theme_items](std::size_t& index_out){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Select Theme"_i18n, theme_items, [this, theme_items](s64& index_out){
|
||||
App::SetTheme(index_out);
|
||||
}, App::GetThemeIndex()));
|
||||
|
||||
@@ -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);
|
||||
}, (std::size_t)App::GetLanguage()));
|
||||
}, (s64)App::GetLanguage()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Misc"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Misc Options"_i18n, Sidebar::Side::LEFT);
|
||||
@@ -297,6 +301,10 @@ MainMenu::MainMenu() {
|
||||
App::Push(std::make_shared<menu::themezer::Menu>());
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("GitHub"_i18n, [](){
|
||||
App::Push(std::make_shared<menu::gh::Menu>());
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Irs"_i18n, [](){
|
||||
App::Push(std::make_shared<menu::irs::Menu>());
|
||||
}));
|
||||
@@ -328,9 +336,9 @@ MainMenu::MainMenu() {
|
||||
App::SetInstallEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Install location"_i18n, install_items, [this](std::size_t& index_out){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Install location"_i18n, install_items, [this](s64& index_out){
|
||||
App::SetInstallSdEnable(index_out);
|
||||
}, (std::size_t)App::GetInstallSdEnable()));
|
||||
}, (s64)App::GetInstallSdEnable()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [this](bool& enable){
|
||||
App::SetInstallPrompt(enable);
|
||||
@@ -344,6 +352,8 @@ MainMenu::MainMenu() {
|
||||
m_app_store_menu = std::make_shared<appstore::Menu>(m_homebrew_menu->GetHomebrewList());
|
||||
m_current_menu = m_homebrew_menu;
|
||||
|
||||
AddOnLRPress();
|
||||
|
||||
for (auto [button, action] : m_actions) {
|
||||
m_current_menu->SetAction(button, action);
|
||||
}
|
||||
@@ -363,11 +373,11 @@ void MainMenu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
|
||||
void MainMenu::OnFocusGained() {
|
||||
Widget::OnFocusGained();
|
||||
this->SetHidden(false);
|
||||
m_current_menu->OnFocusGained();
|
||||
}
|
||||
|
||||
void MainMenu::OnFocusLost() {
|
||||
Widget::OnFocusLost();
|
||||
m_current_menu->OnFocusLost();
|
||||
}
|
||||
|
||||
@@ -376,17 +386,11 @@ void MainMenu::OnLRPress(std::shared_ptr<MenuBase> menu, Button b) {
|
||||
if (m_current_menu == m_homebrew_menu) {
|
||||
m_current_menu = menu;
|
||||
RemoveAction(b);
|
||||
if (b == Button::L) {
|
||||
AddOnRPress();
|
||||
} else {
|
||||
AddOnLPress();
|
||||
}
|
||||
} else {
|
||||
m_current_menu = m_homebrew_menu;
|
||||
AddOnRPress();
|
||||
AddOnLPress();
|
||||
}
|
||||
|
||||
AddOnLRPress();
|
||||
m_current_menu->OnFocusGained();
|
||||
|
||||
for (auto [button, action] : m_actions) {
|
||||
@@ -394,18 +398,20 @@ void MainMenu::OnLRPress(std::shared_ptr<MenuBase> menu, Button b) {
|
||||
}
|
||||
}
|
||||
|
||||
void MainMenu::AddOnLPress() {
|
||||
const auto label = m_current_menu == m_homebrew_menu ? "Files" : "Apps";
|
||||
SetAction(Button::L, Action{i18n::get(label), [this]{
|
||||
OnLRPress(m_filebrowser_menu, Button::L);
|
||||
}});
|
||||
}
|
||||
void MainMenu::AddOnLRPress() {
|
||||
if (m_current_menu != m_filebrowser_menu) {
|
||||
const auto label = m_current_menu == m_homebrew_menu ? "Files" : "Apps";
|
||||
SetAction(Button::L, Action{i18n::get(label), [this]{
|
||||
OnLRPress(m_filebrowser_menu, Button::L);
|
||||
}});
|
||||
}
|
||||
|
||||
void MainMenu::AddOnRPress() {
|
||||
const auto label = m_current_menu == m_homebrew_menu ? "Store" : "Apps";
|
||||
SetAction(Button::R, Action{i18n::get(label), [this]{
|
||||
OnLRPress(m_app_store_menu, Button::R);
|
||||
}});
|
||||
if (m_current_menu != m_app_store_menu) {
|
||||
const auto label = m_current_menu == m_homebrew_menu ? "Store" : "Apps";
|
||||
SetAction(Button::R, Action{i18n::get(label), [this]{
|
||||
OnLRPress(m_app_store_menu, Button::R);
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::main
|
||||
|
||||
@@ -9,8 +9,8 @@ namespace sphaira::ui::menu {
|
||||
MenuBase::MenuBase(std::string title) : m_title{title} {
|
||||
// this->SetParent(this);
|
||||
this->SetPos(30, 87, 1220 - 30, 646 - 87);
|
||||
m_applet_type = appletGetAppletType();
|
||||
SetAction(Button::START, Action{App::Exit});
|
||||
UpdateVars();
|
||||
}
|
||||
|
||||
MenuBase::~MenuBase() {
|
||||
@@ -18,30 +18,17 @@ MenuBase::~MenuBase() {
|
||||
|
||||
void MenuBase::Update(Controller* controller, TouchInfo* touch) {
|
||||
Widget::Update(controller, touch);
|
||||
|
||||
// update every second.
|
||||
if (m_poll_timestamp.GetSeconds() >= 1) {
|
||||
UpdateVars();
|
||||
}
|
||||
}
|
||||
|
||||
void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
|
||||
DrawElement(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, ThemeEntryID_BACKGROUND);
|
||||
Widget::Draw(vg, theme);
|
||||
|
||||
u32 battery_percetange{};
|
||||
|
||||
PsmChargerType charger_type{};
|
||||
NifmInternetConnectionType type{};
|
||||
NifmInternetConnectionStatus status{};
|
||||
u32 strength{};
|
||||
u32 ip{};
|
||||
|
||||
const auto t = time(NULL);
|
||||
struct tm tm{};
|
||||
localtime_r(&t, &tm);
|
||||
|
||||
// todo: app thread poll every 1s and this query the result
|
||||
psmGetBatteryChargePercentage(&battery_percetange);
|
||||
psmGetChargerType(&charger_type);
|
||||
nifmGetInternetConnectionStatus(&type, &strength, &status);
|
||||
nifmGetCurrentIpAddress(&ip);
|
||||
|
||||
const float start_y = 70;
|
||||
const float font_size = 22;
|
||||
const float spacing = 30;
|
||||
@@ -58,14 +45,14 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
|
||||
start_x -= spacing;
|
||||
|
||||
// draw("version %s", APP_VERSION);
|
||||
draw("%u\uFE6A", battery_percetange);
|
||||
draw("%02u:%02u:%02u", tm.tm_hour, tm.tm_min, tm.tm_sec);
|
||||
if (ip) {
|
||||
draw("%u.%u.%u.%u", ip&0xFF, (ip>>8)&0xFF, (ip>>16)&0xFF, (ip>>24)&0xFF);
|
||||
draw("%u\uFE6A", m_battery_percetange);
|
||||
draw("%02u:%02u:%02u", m_tm.tm_hour, m_tm.tm_min, m_tm.tm_sec);
|
||||
if (m_ip) {
|
||||
draw("%u.%u.%u.%u", m_ip&0xFF, (m_ip>>8)&0xFF, (m_ip>>16)&0xFF, (m_ip>>24)&0xFF);
|
||||
} else {
|
||||
draw(("No Internet"_i18n).c_str());
|
||||
}
|
||||
if (m_applet_type == AppletType_LibraryApplet || m_applet_type == AppletType_SystemApplet) {
|
||||
if (!App::IsApplication()) {
|
||||
draw(("[Applet Mode]"_i18n).c_str());
|
||||
}
|
||||
|
||||
@@ -96,4 +83,24 @@ void MenuBase::SetSubHeading(std::string sub_heading) {
|
||||
m_sub_heading = sub_heading;
|
||||
}
|
||||
|
||||
void MenuBase::UpdateVars() {
|
||||
m_tm = {};
|
||||
m_poll_timestamp = {};
|
||||
m_battery_percetange = {};
|
||||
m_charger_type = {};
|
||||
m_type = {};
|
||||
m_status = {};
|
||||
m_strength = {};
|
||||
m_ip = {};
|
||||
|
||||
const auto t = time(NULL);
|
||||
localtime_r(&t, &m_tm);
|
||||
psmGetBatteryChargePercentage(&m_battery_percetange);
|
||||
psmGetChargerType(&m_charger_type);
|
||||
nifmGetInternetConnectionStatus(&m_type, &m_strength, &m_status);
|
||||
nifmGetCurrentIpAddress(&m_ip);
|
||||
|
||||
m_poll_timestamp.Update();
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu
|
||||
|
||||
@@ -53,9 +53,11 @@ constexpr const char* REQUEST_ORDER[]{
|
||||
// https://api.themezer.net/?query=query($nsfw:Boolean,$page:Int,$limit:Int,$sort:String,$order:String,$query:String,$creators:[String!]){packList(nsfw:$nsfw,page:$page,limit:$limit,sort:$sort,order:$order,query:$query,creators:$creators){id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count,themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}}}&variables={"nsfw":false,"page":1,"limit":10,"sort":"updated","order":"desc","query":null,"creators":["695065006068334622"]}
|
||||
|
||||
// i know, this is cursed
|
||||
// todo: send actual POST request rather than GET.
|
||||
auto apiBuildUrlListInternal(const Config& e, bool is_pack) -> std::string {
|
||||
std::string api = "https://api.themezer.net/?query=query";
|
||||
std::string fields = "{id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count";
|
||||
// std::string fields = "{id,creator{id,display_name},details{name,description},last_updated,dl_count,like_count";
|
||||
std::string fields = "{id,creator{id,display_name},details{name}";
|
||||
const char* boolarr[2] = { "false", "true" };
|
||||
|
||||
std::string cmd;
|
||||
@@ -65,7 +67,8 @@ auto apiBuildUrlListInternal(const Config& e, bool is_pack) -> std::string {
|
||||
|
||||
if (is_pack) {
|
||||
cmd = "packList";
|
||||
fields += ",themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}";
|
||||
// fields += ",themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}";
|
||||
fields += ",themes{id, preview{thumb}}";
|
||||
} else {
|
||||
cmd = "themeList";
|
||||
p0 += ",$target:String";
|
||||
@@ -113,11 +116,13 @@ auto apiBuildFilePack(const PackListEntry& e) -> fs::FsPath {
|
||||
return path;
|
||||
}
|
||||
|
||||
#if 0
|
||||
auto apiBuildUrlPack(const PackListEntry& e) -> std::string {
|
||||
char url[2048];
|
||||
std::snprintf(url, sizeof(url), "https://api.themezer.net/?query=query($id:String!){pack(id:$id){id,creator{display_name},details{name,description},last_updated,categories,dl_count,like_count,themes{id,details{name},layout{id,details{name}},categories,target,preview{original,thumb},last_updated,dl_count,like_count}}}&variables={\"id\":\"%s\"}", e.id.c_str());
|
||||
return url;
|
||||
}
|
||||
#endif
|
||||
|
||||
auto apiBuildUrlThemeList(const Config& e) -> std::string {
|
||||
return apiBuildUrlListInternal(e, false);
|
||||
@@ -127,19 +132,25 @@ auto apiBuildUrlListPacks(const Config& e) -> std::string {
|
||||
return apiBuildUrlListInternal(e, true);
|
||||
}
|
||||
|
||||
auto apiBuildListPacksCache(const Config& e) -> fs::FsPath {
|
||||
fs::FsPath path;
|
||||
std::snprintf(path, sizeof(path), "%s/%u_page.json", CACHE_PATH, e.page);
|
||||
return path;
|
||||
}
|
||||
|
||||
auto apiBuildIconCache(const ThemeEntry& e) -> fs::FsPath {
|
||||
fs::FsPath path;
|
||||
std::snprintf(path, sizeof(path), "%s/%s_thumb.jpg", CACHE_PATH, e.id.c_str());
|
||||
return path;
|
||||
}
|
||||
|
||||
auto loadThemeImage(ThemeEntry& e) -> void {
|
||||
auto loadThemeImage(ThemeEntry& e) -> bool {
|
||||
auto& image = e.preview.lazy_image;
|
||||
|
||||
// already have the image
|
||||
if (e.preview.lazy_image.image) {
|
||||
// log_write("warning, tried to load image: %s when already loaded\n", path.c_str());
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
auto vg = App::GetVg();
|
||||
|
||||
@@ -148,43 +159,24 @@ auto loadThemeImage(ThemeEntry& e) -> void {
|
||||
|
||||
const auto path = apiBuildIconCache(e);
|
||||
if (R_FAILED(fs.read_entire_file(path, image_buf))) {
|
||||
e.preview.lazy_image.state = ImageDownloadState::Failed;
|
||||
log_write("failed to load image from file: %s\n", path.s);
|
||||
} else {
|
||||
int channels_in_file;
|
||||
auto buf = stbi_load_from_memory(image_buf.data(), image_buf.size(), &image.w, &image.h, &channels_in_file, 4);
|
||||
if (buf) {
|
||||
ON_SCOPE_EXIT(stbi_image_free(buf));
|
||||
std::memcpy(image.first_pixel, buf, sizeof(image.first_pixel));
|
||||
image.image = nvgCreateImageRGBA(vg, image.w, image.h, 0, buf);
|
||||
}
|
||||
}
|
||||
|
||||
if (!image.image) {
|
||||
image.state = ImageDownloadState::Failed;
|
||||
log_write("failed to load image from file: %s\n", path.s);
|
||||
log_write("failed to load image from file: %s\n", path);
|
||||
return false;
|
||||
} else {
|
||||
// log_write("loaded image from file: %s\n", path);
|
||||
}
|
||||
}
|
||||
|
||||
auto ScrollHelperDown(u64& index, u64& start, u64 step, u64 max, u64 size) -> bool {
|
||||
if (size && index < (size - 1)) {
|
||||
if (index < (size - step)) {
|
||||
index = index + step;
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
} else {
|
||||
index = size - 1;
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
}
|
||||
if (index - start >= max) {
|
||||
log_write("moved down\n");
|
||||
start += step;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void from_json(yyjson_val* json, Creator& e) {
|
||||
@@ -197,13 +189,13 @@ void from_json(yyjson_val* json, Creator& e) {
|
||||
void from_json(yyjson_val* json, Details& e) {
|
||||
JSON_OBJ_ITR(
|
||||
JSON_SET_STR(name);
|
||||
JSON_SET_STR(description);
|
||||
// JSON_SET_STR(description);
|
||||
);
|
||||
}
|
||||
|
||||
void from_json(yyjson_val* json, Preview& e) {
|
||||
JSON_OBJ_ITR(
|
||||
JSON_SET_STR(original);
|
||||
// JSON_SET_STR(original);
|
||||
JSON_SET_STR(thumb);
|
||||
);
|
||||
}
|
||||
@@ -219,13 +211,13 @@ void from_json(yyjson_val* json, DownloadPack& e) {
|
||||
void from_json(yyjson_val* json, ThemeEntry& e) {
|
||||
JSON_OBJ_ITR(
|
||||
JSON_SET_STR(id);
|
||||
JSON_SET_OBJ(creator);
|
||||
JSON_SET_OBJ(details);
|
||||
JSON_SET_STR(last_updated);
|
||||
JSON_SET_UINT(dl_count);
|
||||
JSON_SET_UINT(like_count);
|
||||
JSON_SET_ARR_STR(categories);
|
||||
JSON_SET_STR(target);
|
||||
// JSON_SET_OBJ(creator);
|
||||
// JSON_SET_OBJ(details);
|
||||
// JSON_SET_STR(last_updated);
|
||||
// JSON_SET_UINT(dl_count);
|
||||
// JSON_SET_UINT(like_count);
|
||||
// JSON_SET_ARR_STR(categories);
|
||||
// JSON_SET_STR(target);
|
||||
JSON_SET_OBJ(preview);
|
||||
);
|
||||
}
|
||||
@@ -235,10 +227,10 @@ void from_json(yyjson_val* json, PackListEntry& e) {
|
||||
JSON_SET_STR(id);
|
||||
JSON_SET_OBJ(creator);
|
||||
JSON_SET_OBJ(details);
|
||||
JSON_SET_STR(last_updated);
|
||||
JSON_SET_ARR_STR(categories);
|
||||
JSON_SET_UINT(dl_count);
|
||||
JSON_SET_UINT(like_count);
|
||||
// JSON_SET_STR(last_updated);
|
||||
// JSON_SET_ARR_STR(categories);
|
||||
// JSON_SET_UINT(dl_count);
|
||||
// JSON_SET_UINT(like_count);
|
||||
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) {
|
||||
JSON_INIT_VEC(data, "data");
|
||||
void from_json(const fs::FsPath& path, PackList& e) {
|
||||
JSON_INIT_VEC_FILE(path, "data", nullptr);
|
||||
JSON_OBJ_ITR(
|
||||
JSON_SET_ARR_OBJ(packList);
|
||||
JSON_SET_OBJ(pagination);
|
||||
@@ -272,7 +264,7 @@ void from_json(const std::vector<u8>& data, PackList& e) {
|
||||
}
|
||||
|
||||
auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
|
||||
static fs::FsPath zip_out{"/switch/sphaira/cache/themezer/temp.zip"};
|
||||
static const fs::FsPath zip_out{"/switch/sphaira/cache/themezer/temp.zip"};
|
||||
constexpr auto chunk_size = 1024 * 512; // 512KiB
|
||||
|
||||
fs::FsNativeSd fs;
|
||||
@@ -287,22 +279,17 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
|
||||
|
||||
const auto url = apiBuildUrlDownloadPack(entry);
|
||||
log_write("using url: %s\n", url.c_str());
|
||||
DownloadClearCache(url);
|
||||
const auto data = DownloadMemory(url, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
|
||||
if (pbox->ShouldExit()) {
|
||||
return false;
|
||||
}
|
||||
pbox->UpdateTransfer(dlnow, dltotal);
|
||||
return true;
|
||||
});
|
||||
const auto result = curl::Api().ToMemory(
|
||||
curl::Url{url},
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||
);
|
||||
|
||||
if (data.empty()) {
|
||||
if (!result.success || result.data.empty()) {
|
||||
log_write("error with download: %s\n", url.c_str());
|
||||
// push popup error box
|
||||
return false;
|
||||
}
|
||||
|
||||
from_json(data, download_pack);
|
||||
from_json(result.data, download_pack);
|
||||
}
|
||||
|
||||
// 2. download the zip
|
||||
@@ -310,16 +297,11 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
|
||||
pbox->NewTransfer("Downloading "_i18n + entry.details.name);
|
||||
log_write("starting download: %s\n", download_pack.url.c_str());
|
||||
|
||||
DownloadClearCache(download_pack.url);
|
||||
if (!DownloadFile(download_pack.url, zip_out, "", [pbox](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
|
||||
if (pbox->ShouldExit()) {
|
||||
return false;
|
||||
}
|
||||
pbox->UpdateTransfer(dlnow, dltotal);
|
||||
return true;
|
||||
})) {
|
||||
if (!curl::Api().ToFile(
|
||||
curl::Url{download_pack.url},
|
||||
curl::Path{zip_out},
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()}).success) {
|
||||
log_write("error with download\n");
|
||||
// push popup error box
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -387,7 +369,7 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
|
||||
}
|
||||
|
||||
std::vector<char> buf(chunk_size);
|
||||
u64 offset{};
|
||||
s64 offset{};
|
||||
while (offset < info.uncompressed_size) {
|
||||
if (pbox->ShouldExit()) {
|
||||
return false;
|
||||
@@ -423,6 +405,8 @@ LazyImage::~LazyImage() {
|
||||
}
|
||||
|
||||
Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
fs::FsNativeSd().CreateDirectoryRecursively(CACHE_PATH);
|
||||
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this]{
|
||||
SetPop();
|
||||
}});
|
||||
@@ -445,7 +429,25 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
}}),
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
const auto& page = m_pages[m_page_index];
|
||||
if (ScrollHelperDown(m_index, m_start, 3, 6, page.m_packList.size())) {
|
||||
if (m_list->ScrollDown(m_index, 3, page.m_packList.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
const auto& page = m_pages[m_page_index];
|
||||
if (m_list->ScrollUp(m_index, 3, page.m_packList.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::R2, Action{[this](){
|
||||
const auto& page = m_pages[m_page_index];
|
||||
if (m_list->ScrollDown(m_index, 6, page.m_packList.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::L2, Action{[this](){
|
||||
const auto& page = m_pages[m_page_index];
|
||||
if (m_list->ScrollUp(m_index, 6, page.m_packList.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
@@ -461,12 +463,10 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
|
||||
App::Push(std::make_shared<ProgressBox>("Installing "_i18n + entry.details.name, [this, &entry](auto pbox){
|
||||
return InstallTheme(pbox, entry);
|
||||
}, [this](bool success){
|
||||
// if (success) {
|
||||
// m_entry.status = EntryStatus::Installed;
|
||||
// m_menu.SetDirty();
|
||||
// UpdateOptions();
|
||||
// }
|
||||
}, [this, &entry](bool success){
|
||||
if (success) {
|
||||
App::Notify("Downloaded "_i18n + entry.details.name);
|
||||
}
|
||||
}, 2));
|
||||
}
|
||||
}
|
||||
@@ -492,14 +492,14 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
InvalidateAllPages();
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](std::size_t& index_out){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
|
||||
if (m_sort.Get() != index_out) {
|
||||
m_sort.Set(index_out);
|
||||
InvalidateAllPages();
|
||||
}
|
||||
}, m_sort.Get()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](std::size_t& index_out){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](s64& index_out){
|
||||
if (m_order.Get() != index_out) {
|
||||
m_order.Set(index_out);
|
||||
InvalidateAllPages();
|
||||
@@ -526,16 +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](){
|
||||
m_page_index++;
|
||||
if (m_page_index >= m_page_index_max) {
|
||||
@@ -555,6 +545,10 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
m_page_index = 0;
|
||||
m_pages.resize(1);
|
||||
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() {
|
||||
@@ -563,6 +557,24 @@ Menu::~Menu() {
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
|
||||
if (m_pages.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& page = m_pages[m_page_index];
|
||||
if (page.m_ready != PageLoadState::Done) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_list->OnUpdate(controller, touch, page.m_packList.size(), [this](auto i) {
|
||||
if (m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
SetIndex(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
@@ -589,86 +601,93 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
const u64 SCROLL = m_start;
|
||||
const u64 max_entry_display = 9;
|
||||
const u64 nro_total = page.m_packList.size();// m_entries_current.size();
|
||||
const u64 cursor_pos = m_index;
|
||||
// max images per frame, in order to not hit io / gpu too hard.
|
||||
const int image_load_max = 2;
|
||||
int image_load_count = 0;
|
||||
|
||||
// only draw scrollbar if needed
|
||||
if (nro_total > max_entry_display) {
|
||||
const auto scrollbar_size = 500.f;
|
||||
const auto sb_h = 3.f / (float)(nro_total + 3) * scrollbar_size;
|
||||
const auto sb_y = SCROLL / 3.f;
|
||||
gfx::drawRect(vg, SCREEN_WIDTH - 50, 100, 10, scrollbar_size, theme->elements[ThemeEntryID_GRID].colour);
|
||||
gfx::drawRect(vg, SCREEN_WIDTH - 50+2, 102 + sb_h * sb_y, 10-4, sb_h + (sb_h * 2) - 4, theme->elements[ThemeEntryID_TEXT_SELECTED].colour);
|
||||
}
|
||||
m_list->Draw(vg, theme, page.m_packList.size(), [this, &page, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
|
||||
const auto& [x, y, w, h] = v;
|
||||
auto& e = page.m_packList[pos];
|
||||
|
||||
nvgSave(vg);
|
||||
nvgScissor(vg, 30, 87, 1220 - 30, 646 - 87); // clip
|
||||
auto text_id = ThemeEntryID_TEXT;
|
||||
if (pos == m_index) {
|
||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
} else {
|
||||
DrawElement(x, y, w, h, ThemeEntryID_GRID);
|
||||
}
|
||||
|
||||
for (u64 i = 0, pos = SCROLL, y = 110, w = 350, h = 250; pos < nro_total && i < max_entry_display; y += h + 10) {
|
||||
for (u64 j = 0, x = 75; j < 3 && pos < nro_total && i < max_entry_display; j++, i++, pos++, x += w + 10) {
|
||||
const auto index = pos;
|
||||
auto& e = page.m_packList[index];
|
||||
const float xoff = (350 - 320) / 2;
|
||||
const float yoff = (350 - 320) / 2;
|
||||
|
||||
auto text_id = ThemeEntryID_TEXT;
|
||||
if (pos == cursor_pos) {
|
||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
} else {
|
||||
DrawElement(x, y, w, h, ThemeEntryID_GRID);
|
||||
}
|
||||
// lazy load image
|
||||
if (e.themes.size()) {
|
||||
auto& theme = e.themes[0];
|
||||
auto& image = e.themes[0].preview.lazy_image;
|
||||
|
||||
const float xoff = (350 - 320) / 2;
|
||||
const float yoff = (350 - 320) / 2;
|
||||
|
||||
// lazy load image
|
||||
if (e.themes.size()) {
|
||||
auto& theme = e.themes[0];
|
||||
auto& image = e.themes[0].preview.lazy_image;
|
||||
if (!image.image) {
|
||||
switch (image.state) {
|
||||
case ImageDownloadState::None: {
|
||||
const auto path = apiBuildIconCache(theme);
|
||||
log_write("downloading theme!: %s\n", path);
|
||||
|
||||
if (fs::FsNativeSd().FileExists(path)) {
|
||||
loadThemeImage(theme);
|
||||
} else {
|
||||
const auto url = theme.preview.thumb;
|
||||
log_write("downloading url: %s\n", url.c_str());
|
||||
image.state = ImageDownloadState::Progress;
|
||||
DownloadFileAsync(url, path, "", [this, index, &image](std::vector<u8>& data, bool success) {
|
||||
if (success) {
|
||||
image.state = ImageDownloadState::Done;
|
||||
log_write("downloaded themezer image\n");
|
||||
} else {
|
||||
image.state = ImageDownloadState::Failed;
|
||||
log_write("failed to download image\n");
|
||||
}
|
||||
}, nullptr, DownloadPriority::High);
|
||||
}
|
||||
} break;
|
||||
case ImageDownloadState::Progress: {
|
||||
|
||||
} break;
|
||||
case ImageDownloadState::Done: {
|
||||
loadThemeImage(theme);
|
||||
} break;
|
||||
case ImageDownloadState::Failed: {
|
||||
} break;
|
||||
}
|
||||
} else {
|
||||
gfx::drawImageRounded(vg, x + xoff, y, 320, 180, image.image);
|
||||
// try and load cached image.
|
||||
if (image_load_count < image_load_max && !image.image && !image.tried_cache) {
|
||||
image.tried_cache = true;
|
||||
image.cached = loadThemeImage(theme);
|
||||
if (image.cached) {
|
||||
image_load_count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!image.image || image.cached) {
|
||||
switch (image.state) {
|
||||
case ImageDownloadState::None: {
|
||||
const auto path = apiBuildIconCache(theme);
|
||||
log_write("downloading theme!: %s\n", path);
|
||||
|
||||
const auto url = theme.preview.thumb;
|
||||
log_write("downloading url: %s\n", url.c_str());
|
||||
image.state = ImageDownloadState::Progress;
|
||||
curl::Api().ToFileAsync(
|
||||
curl::Url{url},
|
||||
curl::Path{path},
|
||||
curl::Flags{curl::Flag_Cache},
|
||||
curl::OnComplete{[this, &image](auto& result) {
|
||||
if (result.success) {
|
||||
image.state = ImageDownloadState::Done;
|
||||
// data hasn't changed
|
||||
if (result.code == 304) {
|
||||
image.cached = false;
|
||||
}
|
||||
} else {
|
||||
image.state = ImageDownloadState::Failed;
|
||||
log_write("failed to download image\n");
|
||||
}
|
||||
}
|
||||
});
|
||||
} break;
|
||||
case ImageDownloadState::Progress: {
|
||||
|
||||
} break;
|
||||
case ImageDownloadState::Done: {
|
||||
image.cached = false;
|
||||
if (!loadThemeImage(theme)) {
|
||||
image.state = ImageDownloadState::Failed;
|
||||
} else {
|
||||
image_load_count++;
|
||||
}
|
||||
} break;
|
||||
case ImageDownloadState::Failed: {
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
gfx::drawImageRounded(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage());
|
||||
}
|
||||
|
||||
nvgSave(vg);
|
||||
nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip
|
||||
{
|
||||
gfx::drawTextArgs(vg, x + xoff, y + 180 + 20, 18, NVG_ALIGN_LEFT, theme->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());
|
||||
}
|
||||
}
|
||||
|
||||
nvgRestore(vg);
|
||||
nvgRestore(vg);
|
||||
});
|
||||
}
|
||||
|
||||
void Menu::OnFocusGained() {
|
||||
@@ -691,7 +710,7 @@ void Menu::PackListDownload() {
|
||||
SetSubHeading(subheading);
|
||||
|
||||
m_index = 0;
|
||||
m_start = 0;
|
||||
m_list->SetYoff(0);
|
||||
|
||||
// already downloaded
|
||||
if (m_pages[m_page_index].m_ready != PageLoadState::None) {
|
||||
@@ -706,39 +725,42 @@ void Menu::PackListDownload() {
|
||||
config.order_index = m_order.Get();
|
||||
config.nsfw = m_nsfw.Get();
|
||||
const auto packList_url = apiBuildUrlListPacks(config);
|
||||
const auto themeList_url = apiBuildUrlThemeList(config);
|
||||
const auto packlist_path = apiBuildListPacksCache(config);
|
||||
|
||||
log_write("\npackList_url: %s\n\n", packList_url.c_str());
|
||||
log_write("\nthemeList_url: %s\n\n", themeList_url.c_str());
|
||||
|
||||
DownloadClearCache(packList_url);
|
||||
DownloadMemoryAsync(packList_url, "", [this, page_index](std::vector<u8>& data, bool success){
|
||||
log_write("got themezer data\n");
|
||||
if (!success) {
|
||||
curl::Api().ToFileAsync(
|
||||
curl::Url{packList_url},
|
||||
curl::Path{packlist_path},
|
||||
curl::Flags{curl::Flag_Cache},
|
||||
curl::OnComplete{[this, page_index](auto& result){
|
||||
log_write("got themezer data\n");
|
||||
if (!result.success) {
|
||||
auto& page = m_pages[page_index-1];
|
||||
page.m_ready = PageLoadState::Error;
|
||||
log_write("failed to get themezer data...\n");
|
||||
return;
|
||||
}
|
||||
|
||||
PackList a;
|
||||
from_json(result.path, a);
|
||||
|
||||
m_pages.resize(a.pagination.page_count);
|
||||
auto& page = m_pages[page_index-1];
|
||||
page.m_ready = PageLoadState::Error;
|
||||
log_write("failed to get themezer data...\n");
|
||||
return;
|
||||
|
||||
page.m_packList = a.packList;
|
||||
page.m_pagination = a.pagination;
|
||||
page.m_ready = PageLoadState::Done;
|
||||
m_page_index_max = a.pagination.page_count;
|
||||
|
||||
char subheading[128];
|
||||
std::snprintf(subheading, sizeof(subheading), "Page %zu / %zu"_i18n.c_str(), m_page_index+1, m_page_index_max);
|
||||
SetSubHeading(subheading);
|
||||
|
||||
log_write("a.pagination.page: %u\n", a.pagination.page);
|
||||
log_write("a.pagination.page_count: %u\n", a.pagination.page_count);
|
||||
}
|
||||
|
||||
PackList a;
|
||||
from_json(data, a);
|
||||
|
||||
m_pages.resize(a.pagination.page_count);
|
||||
auto& page = m_pages[page_index-1];
|
||||
|
||||
page.m_packList = a.packList;
|
||||
page.m_pagination = a.pagination;
|
||||
page.m_ready = PageLoadState::Done;
|
||||
m_page_index_max = a.pagination.page_count;
|
||||
|
||||
char subheading[128];
|
||||
std::snprintf(subheading, sizeof(subheading), "Page %zu / %zu"_i18n.c_str(), m_page_index+1, m_page_index_max);
|
||||
SetSubHeading(subheading);
|
||||
|
||||
log_write("a.pagination.page: %u\n", a.pagination.page);
|
||||
log_write("a.pagination.page_count: %u\n", a.pagination.page_count);
|
||||
}, nullptr, DownloadPriority::High);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::themezer
|
||||
|
||||
@@ -11,10 +11,6 @@ NotifEntry::NotifEntry(std::string text, Side side)
|
||||
, m_side{side} {
|
||||
}
|
||||
|
||||
auto NotifEntry::OnLayoutChange() -> void {
|
||||
|
||||
}
|
||||
|
||||
auto NotifEntry::Draw(NVGcontext* vg, Theme* theme, float y) -> bool {
|
||||
m_pos.y = y;
|
||||
Draw(vg, theme);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
auto NotifMananger::OnLayoutChange() -> void {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
}
|
||||
|
||||
auto NotifMananger::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
@@ -77,6 +77,19 @@ inline void drawRectOutlineInternal(NVGcontext* vg, float size, const NVGcolor&
|
||||
float 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;
|
||||
auto v2 = vec;
|
||||
v2.x -= strokeWidth / 2.0;
|
||||
@@ -85,8 +98,8 @@ inline void drawRectOutlineInternal(NVGcontext* vg, float size, const NVGcolor&
|
||||
v2.h += strokeWidth;
|
||||
const auto corner_radius = 0.5;
|
||||
|
||||
nvgSave(vg);
|
||||
nvgResetScissor(vg);
|
||||
// nvgSave(vg);
|
||||
// nvgResetScissor(vg);
|
||||
|
||||
// const auto stroke_width = 5.0f;
|
||||
// const auto shadow_corner_radius = 6.0f;
|
||||
@@ -155,7 +168,8 @@ inline void drawRectOutlineInternal(NVGcontext* vg, float size, const NVGcolor&
|
||||
nvgFillColor(vg, c);
|
||||
nvgFill(vg);
|
||||
|
||||
nvgRestore(vg);
|
||||
// nvgRestore(vg);
|
||||
#endif
|
||||
}
|
||||
|
||||
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) {
|
||||
// drawRectIntenal(vg, {0.f,0.f,1280.f,720.f}, nvgRGBA(30,30,30,180));
|
||||
// drawRectIntenal(vg, {0.f,0.f,1920.f,1080.f}, nvgRGBA(20, 20, 20, 225), false);
|
||||
drawRectIntenal(vg, {0.f,0.f,1920.f,1080.f}, nvgRGBA(0, 0, 0, 220), false);
|
||||
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) {
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
void drawButton(NVGcontext* vg, float x, float y, float size, Button button) {
|
||||
drawText(vg, x, y, size, getButton(button), nullptr, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, getColour(Colour::WHITE));
|
||||
}
|
||||
void drawScrollbar(NVGcontext* vg, Theme* theme, float x, float y, float h, u32 index_off, u32 count, u32 max_per_page) {
|
||||
const s64 SCROLL = index_off;
|
||||
const s64 max_entry_display = max_per_page;
|
||||
const s64 entry_total = count;
|
||||
const float scc2 = 8.0;
|
||||
const float scw = 2.0;
|
||||
|
||||
void drawButtons(NVGcontext* vg, const Widget::Actions& _actions, const NVGcolor& c, float start_x) {
|
||||
nvgBeginPath(vg);
|
||||
nvgFontSize(vg, 24.f);
|
||||
nvgTextAlign(vg, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP);
|
||||
nvgFillColor(vg, c);
|
||||
|
||||
float x = start_x;
|
||||
const float y = 675.f;
|
||||
float bounds[4]{};
|
||||
|
||||
// swaps L/R position, idc how shit this is, it's called once per frame.
|
||||
std::vector<std::pair<Button, Action>> actions;
|
||||
actions.reserve(_actions.size());
|
||||
|
||||
for (const auto a: _actions) {
|
||||
// swap
|
||||
if (a.first == Button::R && actions.size() && actions.back().first == Button::L) {
|
||||
const auto s = actions.back();
|
||||
actions.back() = a;
|
||||
actions.emplace_back(s);
|
||||
|
||||
} else {
|
||||
actions.emplace_back(a);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& [button, action] : actions) {
|
||||
if (action.IsHidden() || action.m_hint.empty()) {
|
||||
continue;
|
||||
}
|
||||
nvgFontSize(vg, 20.f);
|
||||
nvgTextBounds(vg, x, y, action.m_hint.c_str(), nullptr, bounds);
|
||||
auto len = bounds[2] - bounds[0];
|
||||
nvgText(vg, x, y, action.m_hint.c_str(), nullptr);
|
||||
|
||||
x -= len + 8.f;
|
||||
nvgFontSize(vg, 26.f);
|
||||
nvgTextBounds(vg, x, y - 7.f, getButton(button), nullptr, bounds);
|
||||
len = bounds[2] - bounds[0];
|
||||
nvgText(vg, x, y - 4.f, getButton(button), nullptr);
|
||||
x -= len + 34.f;
|
||||
// only draw scrollbar if needed
|
||||
if (entry_total > max_entry_display) {
|
||||
const float sb_h = 1.f / (float)entry_total * h;
|
||||
const float sb_y = SCROLL;
|
||||
gfx::drawRect(vg, x, y, scc2, h, theme->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);
|
||||
}
|
||||
}
|
||||
|
||||
// from gc installer
|
||||
void drawDimBackground(NVGcontext* vg) {
|
||||
// drawRect(vg, 0, 0, 1920, 1080, nvgRGBA(20, 20, 20, 225));
|
||||
drawRect(vg, 0, 0, 1920, 1080, nvgRGBA(0, 0, 0, 220));
|
||||
void drawScrollbar(NVGcontext* vg, Theme* theme, u32 index_off, u32 count, u32 max_per_page) {
|
||||
// drawScrollbar(vg, SCREEN_WIDTH - 50, 100, 500, index_off, count, max_per_page);
|
||||
drawScrollbar(vg, theme, SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200, index_off, count, max_per_page);
|
||||
}
|
||||
|
||||
void drawScrollbar2(NVGcontext* vg, Theme* theme, float x, float y, float h, s64 index_off, s64 count, s64 row, s64 page) {
|
||||
// round up
|
||||
if (count % row) {
|
||||
count = count + (row - count % row);
|
||||
}
|
||||
|
||||
const float scc2 = 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);
|
||||
}
|
||||
}
|
||||
|
||||
void drawScrollbar2(NVGcontext* vg, Theme* theme, s64 index_off, s64 count, s64 row, s64 page) {
|
||||
drawScrollbar2(vg, theme, SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200, index_off, count, row, page);
|
||||
}
|
||||
|
||||
#define HIGHLIGHT_SPEED 350.0
|
||||
|
||||
@@ -45,7 +45,7 @@ OptionBox::OptionBox(const std::string& message, const Option& a, const Option&
|
||||
|
||||
}
|
||||
|
||||
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, std::size_t index, Callback cb)
|
||||
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, s64 index, Callback cb)
|
||||
: m_message{message}
|
||||
, m_callback{cb} {
|
||||
|
||||
@@ -70,7 +70,7 @@ OptionBox::OptionBox(const std::string& message, const Option& a, const Option&
|
||||
|
||||
}
|
||||
|
||||
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, std::size_t index, Callback cb)
|
||||
OptionBox::OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, s64 index, Callback cb)
|
||||
: m_message{message}
|
||||
, m_callback{cb} {
|
||||
|
||||
@@ -79,26 +79,16 @@ OptionBox::OptionBox(const std::string& message, const Option& a, const Option&
|
||||
auto OptionBox::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||
Widget::Update(controller, touch);
|
||||
|
||||
// if (!controller->GotDown(Button::ANY_HORIZONTAL)) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const auto old_index = m_index;
|
||||
|
||||
// if (controller->GotDown(Button::LEFT) && m_index) {
|
||||
// m_index--;
|
||||
// } else if (controller->GotDown(Button::RIGHT) && m_index < (m_entries.size() - 1)) {
|
||||
// m_index++;
|
||||
// }
|
||||
|
||||
// if (old_index != m_index) {
|
||||
// m_entries[old_index].Selected(false);
|
||||
// m_entries[m_index].Selected(true);
|
||||
// }
|
||||
}
|
||||
|
||||
auto OptionBox::OnLayoutChange() -> void {
|
||||
|
||||
if (touch->is_clicked) {
|
||||
for (s64 i = 0; i < m_entries.size(); i++) {
|
||||
auto& e = m_entries[i];
|
||||
if (touch->in_range(e.GetPos())) {
|
||||
SetIndex(i);
|
||||
FireAction(Button::A);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto OptionBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
@@ -118,26 +108,30 @@ auto OptionBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
}
|
||||
}
|
||||
|
||||
auto OptionBox::Setup(std::size_t index) -> void {
|
||||
m_index = std::min(m_entries.size() - 1, index);
|
||||
auto OptionBox::OnFocusGained() noexcept -> void {
|
||||
Widget::OnFocusGained();
|
||||
SetHidden(false);
|
||||
}
|
||||
|
||||
auto OptionBox::OnFocusLost() noexcept -> void {
|
||||
Widget::OnFocusLost();
|
||||
SetHidden(true);
|
||||
}
|
||||
|
||||
auto OptionBox::Setup(s64 index) -> void {
|
||||
m_index = std::min<s64>(m_entries.size() - 1, index);
|
||||
m_entries[m_index].Selected(true);
|
||||
m_spacer_line = Vec4{m_pos.x, m_pos.y + 220.f - 2.f, m_pos.w, 2.f};
|
||||
|
||||
SetActions(
|
||||
std::make_pair(Button::LEFT, Action{[this](){
|
||||
if (m_index) {
|
||||
m_entries[m_index].Selected(false);
|
||||
m_index--;
|
||||
m_entries[m_index].Selected(true);
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
SetIndex(m_index - 1);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::RIGHT, Action{[this](){
|
||||
if (m_index < (m_entries.size() - 1)) {
|
||||
m_entries[m_index].Selected(false);
|
||||
m_index++;
|
||||
m_entries[m_index].Selected(true);
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
SetIndex(m_index + 1);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::A, Action{[this](){
|
||||
@@ -151,4 +145,12 @@ auto OptionBox::Setup(std::size_t index) -> void {
|
||||
);
|
||||
}
|
||||
|
||||
void OptionBox::SetIndex(s64 index) {
|
||||
if (m_index != index) {
|
||||
m_entries[m_index].Selected(false);
|
||||
m_index = index;
|
||||
m_entries[m_index].Selected(true);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
#include "ui/option_list.hpp"
|
||||
#include "app.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "i18n.hpp"
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
OptionList::OptionList(Options options)
|
||||
: m_options{std::move(options)} {
|
||||
SetAction(Button::A, Action{"Select"_i18n, [this](){
|
||||
const auto& [_, func] = m_options[m_index];
|
||||
func();
|
||||
SetPop();
|
||||
}});
|
||||
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}});
|
||||
}
|
||||
|
||||
auto OptionList::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||
|
||||
}
|
||||
|
||||
auto OptionList::OnLayoutChange() -> void {
|
||||
|
||||
}
|
||||
|
||||
auto OptionList::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
PopupList::PopupList(std::string title, Items items, std::string& index_str_ref, std::size_t& index_ref)
|
||||
PopupList::PopupList(std::string title, Items items, std::string& index_str_ref, s64& index_ref)
|
||||
: PopupList{std::move(title), std::move(items), Callback{}, index_ref} {
|
||||
|
||||
m_callback = [&index_str_ref, &index_ref, this](auto op_idx) {
|
||||
@@ -22,7 +22,9 @@ PopupList::PopupList(std::string title, Items items, std::string& index_ref)
|
||||
const auto it = std::find(m_items.cbegin(), m_items.cend(), index_ref);
|
||||
if (it != m_items.cend()) {
|
||||
m_index = std::distance(m_items.cbegin(), it);
|
||||
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h);
|
||||
if (m_index >= 7) {
|
||||
m_index_offset = m_index - 6;
|
||||
}
|
||||
}
|
||||
|
||||
m_callback = [&index_ref, this](auto op_idx) {
|
||||
@@ -32,7 +34,7 @@ PopupList::PopupList(std::string title, Items items, std::string& index_ref)
|
||||
};
|
||||
}
|
||||
|
||||
PopupList::PopupList(std::string title, Items items, std::size_t& index_ref)
|
||||
PopupList::PopupList(std::string title, Items items, s64& index_ref)
|
||||
: PopupList{std::move(title), std::move(items), Callback{}, index_ref} {
|
||||
|
||||
m_callback = [&index_ref, this](auto op_idx) {
|
||||
@@ -47,28 +49,29 @@ PopupList::PopupList(std::string title, Items items, Callback cb, std::string in
|
||||
|
||||
const auto it = std::find(m_items.cbegin(), m_items.cend(), index);
|
||||
if (it != m_items.cend()) {
|
||||
m_index = std::distance(m_items.cbegin(), it);
|
||||
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h);
|
||||
SetIndex(std::distance(m_items.cbegin(), it));
|
||||
if (m_index >= 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_items{std::move(items)}
|
||||
, m_callback{cb}
|
||||
, m_index{index} {
|
||||
|
||||
m_pos.w = 1280.f;
|
||||
const float a = std::min(405.f, (60.f * static_cast<float>(m_items.size())));
|
||||
m_pos.h = 80.f + 140.f + a;
|
||||
m_pos.y = 720.f - m_pos.h;
|
||||
m_line_top = m_pos.y + 70.f;
|
||||
m_line_bottom = 720.f - 73.f;
|
||||
m_selected_y = m_line_top + 1.f + 42.f + (static_cast<float>(m_index) * m_block.h);
|
||||
|
||||
m_scrollbar.Setup(Vec4{1220.f, m_line_top, 1.f, m_line_bottom - m_line_top}, m_block.h, m_items.size());
|
||||
|
||||
SetActions(
|
||||
this->SetActions(
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 1, m_items.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 1, m_items.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::A, Action{"Select"_i18n, [this](){
|
||||
if (m_callback) {
|
||||
m_callback(m_index);
|
||||
@@ -76,48 +79,33 @@ PopupList::PopupList(std::string title, Items items, Callback cb, std::size_t in
|
||||
SetPop();
|
||||
}}),
|
||||
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||
if (m_callback) {
|
||||
m_callback(std::nullopt);
|
||||
}
|
||||
SetPop();
|
||||
}})
|
||||
);
|
||||
|
||||
m_pos.w = 1280.f;
|
||||
const float a = std::min(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 {
|
||||
Widget::Update(controller, touch);
|
||||
|
||||
if (!controller->GotDown(Button::ANY_VERTICAL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto old_index = m_index;
|
||||
|
||||
if (controller->GotDown(Button::DOWN) && m_index < (m_items.size() - 1)) {
|
||||
m_index++;
|
||||
m_selected_y += m_block.h;
|
||||
} else if (controller->GotDown(Button::UP) && m_index != 0) {
|
||||
m_index--;
|
||||
m_selected_y -= m_block.h;
|
||||
}
|
||||
|
||||
if (old_index != m_index) {
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
OnLayoutChange();
|
||||
}
|
||||
}
|
||||
|
||||
auto PopupList::OnLayoutChange() -> void {
|
||||
if ((m_selected_y + m_block.h) > m_line_bottom) {
|
||||
m_selected_y -= m_block.h;
|
||||
m_index_offset++;
|
||||
m_scrollbar.Move(ScrollBar::Direction::DOWN);
|
||||
} else if (m_selected_y <= m_line_top) {
|
||||
m_selected_y += m_block.h;
|
||||
m_index_offset--;
|
||||
m_scrollbar.Move(ScrollBar::Direction::UP);
|
||||
}
|
||||
// LOG("sely: %.2f, index_off: %lu\n", m_selected_y, m_index_offset);
|
||||
m_list->OnUpdate(controller, touch, m_items.size(), [this](auto i) {
|
||||
SetIndex(i);
|
||||
FireAction(Button::A);
|
||||
});
|
||||
}
|
||||
|
||||
auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
@@ -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_bottom, m_line_width, 1.f, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
|
||||
// todo: cleanup
|
||||
const float x = m_block.x;
|
||||
float y = m_line_top + 1.f + 42.f;
|
||||
const float h = m_block.h;
|
||||
const float w = m_block.w;
|
||||
|
||||
nvgSave(vg);
|
||||
nvgScissor(vg, 0, m_line_top, 1280.f, m_line_bottom - m_line_top);
|
||||
|
||||
for (std::size_t i = m_index_offset; i < m_items.size(); ++i) {
|
||||
m_list->Draw(vg, theme, m_items.size(), [this](auto* vg, auto* theme, auto v, auto i) {
|
||||
const auto& [x, y, w, h] = v;
|
||||
if (m_index == i) {
|
||||
gfx::drawRect(vg, x - 4.f, y - 4.f, w + 8.f, h + 8.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour);
|
||||
gfx::drawRect(vg, x, y, w, h, theme->elements[ThemeEntryID_SELECTED].colour);
|
||||
@@ -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::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);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -119,7 +119,7 @@ auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto ProgressBox::UpdateTransfer(u64 offset, u64 size) -> ProgressBox& {
|
||||
auto ProgressBox::UpdateTransfer(s64 offset, s64 size) -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
m_size = size;
|
||||
m_offset = offset;
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
#include "ui/scrollbar.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
ScrollBar::ScrollBar(Vec4 bounds, float entry_height, std::size_t entries)
|
||||
: m_bounds{bounds}
|
||||
, m_entries{entries}
|
||||
, m_entry_height{entry_height} {
|
||||
Setup();
|
||||
}
|
||||
|
||||
auto ScrollBar::OnLayoutChange() -> void {
|
||||
|
||||
}
|
||||
|
||||
auto ScrollBar::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
if (m_should_draw) {
|
||||
gfx::drawRect(vg, m_pos, gfx::Colour::RED);
|
||||
}
|
||||
}
|
||||
|
||||
auto ScrollBar::Setup(Vec4 bounds, float entry_height, std::size_t entries) -> void {
|
||||
m_bounds = bounds;
|
||||
m_entry_height = entry_height;
|
||||
m_entries = entries;
|
||||
Setup();
|
||||
}
|
||||
|
||||
auto ScrollBar::Setup() -> void {
|
||||
m_bounds.y += 5.f;
|
||||
m_bounds.h -= 10.f;
|
||||
|
||||
const float total_size = (m_entry_height) * static_cast<float>(m_entries);
|
||||
if (total_size > m_bounds.h) {
|
||||
m_step_size = total_size / m_entries;
|
||||
m_pos.x = m_bounds.x;
|
||||
m_pos.y = m_bounds.y;
|
||||
m_pos.w = 2.f;
|
||||
m_pos.h = total_size - m_bounds.h;
|
||||
m_should_draw = true;
|
||||
// LOG("total size: %.2f\n", total_size);
|
||||
// LOG("step size: %.2f\n", m_step_size);
|
||||
// LOG("pos y: %.2f\n", m_pos.y);
|
||||
// LOG("pos h: %.2f\n", m_pos.h);
|
||||
} else {
|
||||
// LOG("not big enough for scroll total: %.2f bounds: %.2f\n", total_size, bounds.h);
|
||||
}
|
||||
}
|
||||
|
||||
auto ScrollBar::Move(Direction direction) -> void {
|
||||
switch (direction) {
|
||||
case Direction::DOWN:
|
||||
if (m_index < (m_entries - 1)) {
|
||||
m_index++;
|
||||
m_pos.y += m_step_size;
|
||||
}
|
||||
break;
|
||||
case Direction::UP:
|
||||
if (m_index != 0) {
|
||||
m_index--;
|
||||
m_pos.y -= m_step_size;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
@@ -7,13 +7,12 @@
|
||||
namespace sphaira::ui {
|
||||
namespace {
|
||||
|
||||
struct SidebarSpacer : SidebarEntryBase {
|
||||
|
||||
};
|
||||
|
||||
struct SidebarHeader : SidebarEntryBase {
|
||||
|
||||
};
|
||||
auto DistanceBetweenY(Vec4 va, Vec4 vb) -> Vec4 {
|
||||
return Vec4{
|
||||
va.x, va.y,
|
||||
va.w, vb.y - va.y
|
||||
};
|
||||
}
|
||||
|
||||
} // 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)}
|
||||
, m_items{std::move(items)}
|
||||
, 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_base_pos = Vec4{GetX() + 30.f, GetY() + 170.f, m_pos.w - (30.f * 2.f), 70.f};
|
||||
|
||||
// each item has it's own Action, but we take over B
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}});
|
||||
// set button positions
|
||||
SetUiButtonPos({m_pos.x + m_pos.w - 60.f, 675});
|
||||
|
||||
m_selected_y = m_base_pos.y;
|
||||
|
||||
if (!m_items.empty()) {
|
||||
// setup positions
|
||||
m_selected_y = m_base_pos.y;
|
||||
// for (auto&p : m_items) {
|
||||
// p->SetPos(m_base_pos);
|
||||
// m_base_pos.y += m_base_pos.h;
|
||||
// }
|
||||
|
||||
// // give focus to first entry.
|
||||
// m_items[m_index]->OnFocusGained();
|
||||
}
|
||||
const Vec4 pos = DistanceBetweenY(m_top_bar, m_bottom_bar);
|
||||
m_list = std::make_unique<List>(1, 6, pos, m_base_pos);
|
||||
m_list->SetScrollBarPos(GetX() + GetW() - 20, m_base_pos.y - 10, pos.h - m_base_pos.y + 48);
|
||||
}
|
||||
|
||||
Sidebar::Sidebar(std::string title, std::string sub, Side side)
|
||||
@@ -217,46 +204,21 @@ Sidebar::Sidebar(std::string title, std::string sub, Side side)
|
||||
|
||||
|
||||
auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||
m_items[m_index]->Update(controller, touch);
|
||||
Widget::Update(controller, touch);
|
||||
|
||||
// if touched out of bounds, pop the sidebar and all widgets below it.
|
||||
if (touch->is_clicked && !touch->in_range(GetPos())) {
|
||||
App::PopToMenu();
|
||||
} else {
|
||||
m_list->OnUpdate(controller, touch, m_items.size(), [this](auto i) {
|
||||
SetIndex(i);
|
||||
FireAction(Button::A);
|
||||
});
|
||||
}
|
||||
|
||||
if (m_items[m_index]->ShouldPop()) {
|
||||
SetPop();
|
||||
}
|
||||
|
||||
const auto old_index = m_index;
|
||||
if (controller->GotDown(Button::ANY_DOWN) && m_index < (m_items.size() - 1)) {
|
||||
m_index++;
|
||||
m_selected_y += m_box_size.y;
|
||||
} else if (controller->GotDown(Button::ANY_UP) && m_index != 0) {
|
||||
m_index--;
|
||||
m_selected_y -= m_box_size.y;
|
||||
}
|
||||
|
||||
// if we moved
|
||||
if (m_index != old_index) {
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
m_items[old_index]->OnFocusLost();
|
||||
m_items[m_index]->OnFocusGained();
|
||||
|
||||
// move offset
|
||||
if ((m_selected_y + m_box_size.y) >= m_bottom_bar.y) {
|
||||
m_selected_y -= m_box_size.y;
|
||||
m_index_offset++;
|
||||
// LOG("move down\n");
|
||||
} else if (m_selected_y <= m_top_bar.y) {
|
||||
// LOG("move up sely %.2f top %.2f\n", m_selected_y, m_top_bar.y);
|
||||
m_selected_y += m_box_size.y;
|
||||
m_index_offset--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto DistanceBetweenY(Vec4 va, Vec4 vb) -> Vec4 {
|
||||
return Vec4{
|
||||
va.x, va.y,
|
||||
va.w, vb.y - va.y
|
||||
};
|
||||
}
|
||||
|
||||
auto Sidebar::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
@@ -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_bottom_bar, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
|
||||
const auto dist = DistanceBetweenY(m_top_bar, m_bottom_bar);
|
||||
nvgSave(vg);
|
||||
nvgScissor(vg, dist.x, dist.y, dist.w, dist.h);
|
||||
Widget::Draw(vg, theme);
|
||||
|
||||
// for (std::size_t i = m_index_offset; i < m_items.size(); ++i) {
|
||||
// m_items[i]->Draw(vg, theme);
|
||||
// }
|
||||
|
||||
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);
|
||||
m_list->Draw(vg, theme, m_items.size(), [this](auto* vg, auto* theme, auto v, auto i) {
|
||||
const auto& [x, y, w, h] = v;
|
||||
m_items[i]->SetY(y);
|
||||
m_items[i]->Draw(vg, theme);
|
||||
});
|
||||
}
|
||||
|
||||
auto Sidebar::OnFocusGained() noexcept -> void {
|
||||
@@ -303,23 +252,56 @@ auto Sidebar::OnFocusLost() noexcept -> void {
|
||||
void Sidebar::Add(std::shared_ptr<SidebarEntryBase> entry) {
|
||||
m_items.emplace_back(entry);
|
||||
m_items.back()->SetPos(m_base_pos);
|
||||
m_base_pos.y += m_base_pos.h;
|
||||
|
||||
// for (auto&p : m_items) {
|
||||
// p->SetPos(base_pos);
|
||||
// m_base_pos.y += m_base_pos.h;
|
||||
// }
|
||||
|
||||
// give focus to first entry.
|
||||
m_items[m_index]->OnFocusGained();
|
||||
if (m_items.size() == 1) {
|
||||
m_items[m_index]->OnFocusGained();
|
||||
SetupButtons();
|
||||
}
|
||||
}
|
||||
|
||||
void Sidebar::AddSpacer() {
|
||||
void Sidebar::SetIndex(s64 index) {
|
||||
// if we moved
|
||||
if (m_index != index) {
|
||||
m_items[m_index]->OnFocusLost();
|
||||
m_index = index;
|
||||
m_items[m_index]->OnFocusGained();
|
||||
|
||||
if (m_index > m_index_offset && m_index - m_index_offset >= 5) {
|
||||
m_index_offset = m_index - 5;
|
||||
}
|
||||
|
||||
SetupButtons();
|
||||
}
|
||||
}
|
||||
|
||||
void Sidebar::AddHeader(std::string name) {
|
||||
void Sidebar::SetupButtons() {
|
||||
RemoveActions();
|
||||
|
||||
// add entry actions
|
||||
for (const auto& [button, action] : m_items[m_index]->GetActions()) {
|
||||
SetAction(button, action);
|
||||
}
|
||||
|
||||
// add default actions, overriding if needed.
|
||||
this->SetActions(
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
auto index = m_index;
|
||||
if (m_list->ScrollDown(index, 1, m_items.size())) {
|
||||
SetIndex(index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
auto index = m_index;
|
||||
if (m_list->ScrollUp(index, 1, m_items.size())) {
|
||||
SetIndex(index);
|
||||
}
|
||||
}}),
|
||||
// each item has it's own Action, but we take over B
|
||||
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}})
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
#include "ui/widget.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "app.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
auto uiButton::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
// enable to see button region
|
||||
// gfx::drawRect(vg, m_pos, gfx::Colour::RED);
|
||||
|
||||
nvgTextAlign(vg, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP);
|
||||
nvgFillColor(vg, theme->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) {
|
||||
for (const auto& [button, action] : m_actions) {
|
||||
if ((action.m_type & ActionType::DOWN) && controller->GotDown(button)) {
|
||||
@@ -11,26 +24,34 @@ void Widget::Update(Controller* controller, TouchInfo* touch) {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
}
|
||||
action.Invoke(true);
|
||||
break;
|
||||
}
|
||||
else if ((action.m_type & ActionType::UP) && controller->GotUp(button)) {
|
||||
action.Invoke(false);
|
||||
break;
|
||||
}
|
||||
else if ((action.m_type & ActionType::HELD) && controller->GotHeld(button)) {
|
||||
action.Invoke(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto draw_actions = GetUiButtons();
|
||||
for (auto& e : draw_actions) {
|
||||
if (touch->is_clicked && touch->in_range(e.GetPos())) {
|
||||
log_write("got click: %s\n", e.m_action.m_hint.c_str());
|
||||
FireAction(e.m_button);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Widget::Draw(NVGcontext* vg, Theme* theme) {
|
||||
Actions draw_actions;
|
||||
auto draw_actions = GetUiButtons();
|
||||
|
||||
for (const auto& [button, action] : m_actions) {
|
||||
if (!action.IsHidden()) {
|
||||
draw_actions.emplace(button, action);
|
||||
}
|
||||
for (auto& e : draw_actions) {
|
||||
e.Draw(vg, theme);
|
||||
}
|
||||
|
||||
gfx::drawButtons(vg, draw_actions, theme->elements[ThemeEntryID_TEXT].colour);
|
||||
}
|
||||
|
||||
auto Widget::HasAction(Button button) const -> bool {
|
||||
@@ -47,4 +68,67 @@ void Widget::RemoveAction(Button button) {
|
||||
}
|
||||
}
|
||||
|
||||
auto Widget::FireAction(Button b, u8 type) -> bool {
|
||||
for (const auto& [button, action] : m_actions) {
|
||||
if (button == b && (action.m_type & type)) {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
action.Invoke(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto Widget::GetUiButtons() const -> uiButtons {
|
||||
auto vg = App::GetVg();
|
||||
auto [x, y] = m_button_pos;
|
||||
|
||||
uiButtons draw_actions;
|
||||
draw_actions.reserve(m_actions.size());
|
||||
|
||||
// build array
|
||||
for (const auto& [button, action] : m_actions) {
|
||||
if (action.IsHidden() || action.m_hint.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uiButton ui_button{button, action};
|
||||
|
||||
// swap
|
||||
if (button == Button::R && draw_actions.size() && draw_actions.back().m_button == Button::L) {
|
||||
const auto s = draw_actions.back();
|
||||
draw_actions.back().m_button = button;
|
||||
draw_actions.back().m_action = action;
|
||||
draw_actions.emplace_back(s);
|
||||
} else {
|
||||
draw_actions.emplace_back(ui_button);
|
||||
}
|
||||
}
|
||||
|
||||
float bounds[4]{};
|
||||
for (auto& e : draw_actions) {
|
||||
nvgTextAlign(vg, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP);
|
||||
|
||||
nvgFontSize(vg, 20.f);
|
||||
nvgTextBounds(vg, x, y, e.m_action.m_hint.c_str(), nullptr, bounds);
|
||||
auto len = bounds[2] - bounds[0];
|
||||
e.m_hint_pos = {x, 675, len, 20};
|
||||
|
||||
x -= len + 8.f;
|
||||
nvgFontSize(vg, 26.f);
|
||||
nvgTextBounds(vg, x, y - 7.f, gfx::getButton(e.m_button), nullptr, bounds);
|
||||
len = bounds[2] - bounds[0];
|
||||
e.m_button_pos = {x, 675 - 4.f, len, 26};
|
||||
x -= len + 34.f;
|
||||
|
||||
e.SetPos(e.m_button_pos);
|
||||
e.SetX(e.GetX() - 40);
|
||||
e.SetW(e.m_hint_pos.x - e.m_button_pos.x + len + 25);
|
||||
e.SetY(e.GetY() - 18);
|
||||
e.SetH(26 + 18 * 2);
|
||||
}
|
||||
|
||||
return draw_actions;
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
|
||||
Reference in New Issue
Block a user