1 Commits
touch ... 0.4.0

116 changed files with 3151 additions and 6635 deletions

15
.github/FUNDING.yml vendored
View File

@@ -1,15 +0,0 @@
# These are supported funding model platforms
github: ITotalJustice
patreon: totaljustice
open_collective: # Replace with a single Open Collective username
ko_fi: totaljustice
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

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

View File

@@ -42,12 +42,5 @@ function(dkp_fatal_if_not_found var package)
endif()
endfunction(dkp_fatal_if_not_found var package)
# disable exceptions and rtti in order to shrink final binary size.
add_compile_options(
"$<$<COMPILE_LANGUAGE:C>:-fno-exceptions>"
"$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>"
"$<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>"
)
add_subdirectory(hbl)
add_subdirectory(sphaira)

View File

@@ -2,6 +2,8 @@
A homebrew menu for the switch.
It was built for my usage, as such, features that may seem out of place are included because i found them useful.
[See the gbatemp thread for more details / discussion](https://gbatemp.net/threads/sphaira-hbmenu-replacement.664523/).
## showcase
@@ -24,15 +26,6 @@ please include:
- FW version
- The bug itself and how to reproduce it
## ftp
ftp can be enabled via the network menu. It uses the same config as ftpsrv `/config/ftpsrv/config.ini`. [See here for the full list
of all configs available](https://github.com/ITotalJustice/ftpsrv/blob/master/assets/config.ini.template).
## mtp
mtp can be enabled via the network menu.
## file assoc
sphaira has file assoc support. lets say your app supports loading .png files, then you could write an assoc file, then when using the file browser, clicking on a .png file will launch your app along with the .png file as argv[1]. This was primarly added for rom loading support for emulators / frontends such as retroarch, melonds, mgba etc.
@@ -59,5 +52,4 @@ see `assets/romfs/assoc/` for more examples of file assoc entries
- minIni
- gbatemp
- hb-appstore
- haze
- everyone who has contributed to this project!

View File

@@ -0,0 +1,4 @@
[config]
path=/retroarch/cores/2048_libretro_libnx.nro
supported_extensions=
database=2048

View File

@@ -1,4 +0,0 @@
[config]
path=/retroarch/cores/DoubleCherryGB_libretro_libnx.nro
supported_extensions=cgb|dmg|gb|gbc|sgb
database=Nintendo - Game Boy|Nintendo - Game Boy Color

View File

@@ -1,4 +0,0 @@
[config]
path=/retroarch/cores/ardens_libretro_libnx.nro
supported_extensions=hex|arduboy
database=Arduboy Inc - Arduboy

View File

@@ -1,4 +1,4 @@
[config]
path=/retroarch/cores/arduous_libretro_libnx.nro
supported_extensions=hex
database=Arduboy Inc - Arduboy
database=Arduboy

View File

@@ -1,4 +1,4 @@
[config]
path=/retroarch/cores/atari800_libretro_libnx.nro
supported_extensions=xfd|atr|dcm|cas|bin|a52|zip|atx|car|rom|com|xex|m3u
supported_extensions=xfd|atr|cdm|cas|bin|a52|zip|atx|car|rom|com|xex
database=Atari - 5200|Atari - 8-bit

View File

@@ -1,4 +1,4 @@
[config]
path=/retroarch/cores/bluemsx_libretro_libnx.nro
supported_extensions=rom|ri|mx1|mx2|dsk|col|sg|sc|sf|cas|m3u
database=Microsoft - MSX|Microsoft - MSX2|Coleco - ColecoVision|Sega - SG-1000|Spectravideo - SVI-318 - SVI-328
supported_extensions=rom|ri|mx1|mx2|col|dsk|cas|sg|sc|sf|m3u
database=Microsoft - MSX|Microsoft - MSX2|Coleco - ColecoVision|Sega - SG-1000

View File

@@ -0,0 +1,4 @@
[config]
path=/retroarch/cores/citra_libretro_libnx.nro
supported_extensions=3ds|3dsx|elf|axf|cci|cxi|app
database=Nintendo - Nintendo 3DS

View File

@@ -1,4 +0,0 @@
[config]
path=/retroarch/cores/dosbox_pure_libretro_libnx.nro
supported_extensions=zip|dosz|exe|com|bat|iso|chd|cue|ins|img|ima|vhd|jrc|tc|m3u|m3u8|conf|/
database=DOS

View File

@@ -1,4 +1,4 @@
[config]
path=/retroarch/cores/dosbox_svn_libretro_libnx.nro
supported_extensions=exe|com|bat|conf|cue|iso|img|/
supported_extensions=exe|com|bat|conf|cue|iso
database=DOS

View File

@@ -0,0 +1,3 @@
[config]
path=/retroarch/cores/fbalpha2012_cps1_libretro_libnx.nro
supported_extensions=zip

View File

@@ -0,0 +1,3 @@
[config]
path=/retroarch/cores/fbalpha2012_cps2_libretro_libnx.nro
supported_extensions=zip

View File

@@ -0,0 +1,3 @@
[config]
path=/retroarch/cores/fbalpha2012_libretro_libnx.nro
supported_extensions=iso|zip|7z

View File

@@ -0,0 +1,3 @@
[config]
path=/retroarch/cores/fbalpha2012_neogeo_libretro_libnx.nro
supported_extensions=zip

View File

@@ -1,4 +1,4 @@
[config]
path=/retroarch/cores/frodo_libretro_libnx.nro
supported_extensions=d64|t64|x64|p00|lnx|lyx|zip
supported_extensions=d64|t64|x64|p00|lnx|zip
database=Commodore - 64

View File

@@ -1,4 +1,4 @@
[config]
path=/retroarch/cores/fuse_libretro_libnx.nro
supported_extensions=tzx|tap|z80|rzx|scl|trd|dsk|dck|sna|szx|zip
supported_extensions=tzx|tap|z80|rzx|scl|trd|dsk|zip
database=Sinclair - ZX Spectrum +3|Sinclair - ZX Spectrum

View File

@@ -0,0 +1,3 @@
[config]
path=/retroarch/cores/gme_libretro_libnx.nro
supported_extensions=ay|gbs|gym|hes|kss|nsf|nsfe|sap|spc|vgm|vgz|zip

View File

@@ -0,0 +1,3 @@
[config]
path=/retroarch/cores/gong_libretro_libnx.nro
supported_extensions=

View File

@@ -1,4 +0,0 @@
[config]
path=/retroarch/cores/gpsp_libretro_libnx.nro
supported_extensions=gba|bin
database=Nintendo - Game Boy Advance

View File

@@ -1,4 +1,4 @@
[config]
path=/retroarch/cores/handy_libretro_libnx.nro
supported_extensions=lnx|lyx|o
supported_extensions=lnx|o
database=Atari - Lynx

View File

@@ -1,4 +1,4 @@
[config]
path=/retroarch/cores/mame2000_libretro_libnx.nro
supported_extensions=zip|7z
supported_extensions=zip|7z|chd
database=MAME 2000

View File

@@ -1,4 +1,4 @@
[config]
path=/retroarch/cores/mednafen_lynx_libretro_libnx.nro
supported_extensions=lnx|lyx|o
supported_extensions=lnx|o
database=Atari - Lynx

View File

@@ -0,0 +1,3 @@
[config]
path=/retroarch/cores/minivmac_libretro_libnx.nro
supported_extensions=dsk|img|zip|hvf|cmd

View File

@@ -1,4 +0,0 @@
[config]
path=/retroarch/cores/mrboom_libretro_libnx.nro
supported_extensions=desktop
database=MrBoom

View File

@@ -0,0 +1,3 @@
[config]
path=/retroarch/cores/mu_libretro_libnx.nro
supported_extensions=prc|pqa|img|pdb|zip

View File

@@ -0,0 +1,3 @@
[config]
path=/retroarch/cores/numero_libretro_libnx.nro
supported_extensions=8xp|8xk|8xg

View File

@@ -1,4 +1,4 @@
[config]
path=/retroarch/cores/pcsx_rearmed_libretro_libnx.nro
supported_extensions=bin|cue|img|mdf|pbp|toc|cbn|m3u|ccd|chd|iso|exe
supported_extensions=bin|cue|img|mdf|pbp|toc|cbn|m3u|ccd|chd
database=Sony - PlayStation

View File

@@ -0,0 +1,3 @@
[config]
path=/retroarch/cores/pocketcdg_libretro_libnx.nro
supported_extensions=cdg

View File

@@ -1,4 +1,4 @@
[config]
path=/retroarch/cores/ppsspp_libretro_libnx.nro
supported_extensions=elf|iso|cso|prx|pbp|chd
supported_extensions=elf|iso|cso|prx|pbp
database=Sony - PlayStation Portable

View File

@@ -1,4 +1,4 @@
[config]
path=/retroarch/cores/px68k_libretro_libnx.nro
supported_extensions=dim|img|d88|88d|hdm|dup|2hd|xdf|hdf|cmd|m3u
supported_extensions=dim|zip|img|d88|88d|hdm|dup|2hd|xdf|hdf|cmd|m3u
database=Sharp - X68000

View File

@@ -1,4 +1,3 @@
[config]
path=/retroarch/cores/quasi88_libretro_libnx.nro
supported_extensions=d88|u88|m3u
database=NEC - PC-8001 - PC-8801

View File

@@ -1,4 +1,4 @@
[config]
path=/retroarch/cores/retro8_libretro_libnx.nro
supported_extensions=p8|png
database=PICO-8
database=PICO8

View File

@@ -1,4 +1,4 @@
[config]
path=/retroarch/cores/stella2023_libretro_libnx.nro
path=/retroarch/cores/stella_libretro_libnx.nro
supported_extensions=a26|bin
database=Atari - 2600

View File

@@ -0,0 +1,3 @@
[config]
path=/retroarch/cores/superbroswar_libretro_libnx.nro
supported_extensions=game

View File

@@ -1,4 +0,0 @@
[config]
path=/retroarch/cores/vircon32_libretro_libnx.nro
supported_extensions=v32|V32
database=Vircon32

View File

@@ -1,4 +1,4 @@
[config]
path=/retroarch/cores/x1_libretro_libnx.nro
supported_extensions=dx1|zip|2d|2hd|tfd|d88|88d|hdm|xdf|dup|tap|cmd
database=Sharp - X1
database=Sharp X1

View File

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

View File

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

View File

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

View File

@@ -1,238 +1,114 @@
{
"[Applet Mode]": "[Applet-Modus]",
"No Internet": "Keine Internetverbindung",
"Files": "Dateien",
"Apps": "Apps",
"Store": "Store",
"Menu": "Menü",
"Launch": "Start",
"Options": "Optionen",
"OK": "OK",
"Back": "Zurück",
"Select": "Auswählen",
"Open": "Öffnen",
"Launch": "Starten",
"Info": "Info",
"Install": "Installieren",
"Delete": "Löschen",
"Restart": "Neustart",
"Changelog": "Changelog",
"Details": "Details",
"Update": "Update",
"Remove": "Entfernen",
"Download": "Download",
"Next Page": "Nächste Seite",
"Prev Page": "Vorherige Seite",
"Unstar": "Favorit entfernen",
"Star": "Favorit",
"System memory": "System-Speicher",
"microSD card": "microSD-Karte",
"Yes": "Ja",
"No": "Nein",
"Enabled": "Aktiviert",
"Disabled": "Deaktiviert",
"Homebrew Options": "Homebrew-Optionen",
"Sort By": "Sortieren nach",
"Sort Options": "Sortieroptionen",
"Filter": "Filter",
"Sort": "Sortieren",
"Order": "Reihenfolge",
"Search": "Suchen",
"Updated": "Aktualisiert",
"Updated (Star)": "Aktualisiert (Favoriten)",
"Downloads": "Downloads",
"Size": "Größe",
"Size (Star)": "Größe (Favoriten)",
"Alphabetical": "Alphabetisch",
"Alphabetical (Star)": "Alphabetisch (Favoriten)",
"Likes": "Likes",
"ID": "ID",
"Decending": "Absteigend",
"Descending (down)": "Absteigend",
"Desc": "Abst.",
"Ascending": "Aufsteigend",
"Ascending (Up)": "Aufsteigend",
"Asc": "Aufst.",
"Menu Options": "Menü-Optionen",
"Header": "Header",
"Theme": "Theme",
"Theme Options": "Theme-Optionen",
"Select Theme": "Theme auswählen",
"Shuffle": "Zufällig",
"Sort": "Sortieren",
"Order": "Befehl",
"Info": "Info",
"Delete": "Löschen",
"Hide Sphaira": "Sphaira verstecken",
"Are you sure you want to delete ": "Mit dem Löschvorgang fortfahren?",
"Install Forwarder": "Forwarder installieren",
"WARNING: Installing forwarders will lead to a ban!": "ACHTUNG: Die Installation von Forwardern führt zu einem Ban!",
"Back": "Zurück",
"Install": "Installieren",
"Fs": "Fs",
"App": "App",
"Menu": "Menu",
"Homebrew": "Homebrew",
"FileBrowser": "DateiBrowser",
"Open": "Öffnen",
"Theme Options": "Themenoptionen",
"Select Theme": "Wählen Sie Theme aus",
"Shuffle": "Shuffle",
"Music": "Musik",
"Network": "Netzwerk",
"Network Options": "Netzwerk-Optionen",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink verbunden",
"Nxlink Upload": "Nxlink Upload",
"Nxlink Finished": "Nxlink abgeschlossen",
"Switch-Handheld!": "Switch-Handheld!",
"Switch-Docked!": "Switch-Dock-Modus!",
"Language": "Sprache",
"Auto": "Auto",
"English": "English",
"Japanese": "日本語",
"French": "Français",
"German": "Deutsch",
"Italian": "Italiano",
"Spanish": "Español",
"Chinese": "中文",
"Korean": "한국어",
"Dutch": "Dutch",
"Portuguese": "Português",
"Russian": "Русский",
"Swedish": "Svenska",
"Logging": "Logging",
"Replace hbmenu on exit": "hbmenu beim Beenden ersetzen",
"Misc": "Sonstiges",
"Misc Options": "Weitere Optionen",
"Web": "Web",
"Install forwarders": "Forwarder installieren",
"Install location": "Installationsort",
"Show install warning": "Installationswarnung anzeigen",
"FileBrowser": "Datei-Browser",
"%zd files": "%zd Dateien",
"%zd dirs": "%zd Ordner",
"File Options": "Datei-Optionen",
"Show Hidden": "Versteckte anzeigen",
"Folders First": "Ordner zuerst",
"Hidden Last": "Versteckte zuletzt",
"Hidden Last": "Zuletzt versteckt",
"Yes": "Ja",
"No": "Nein",
"Network Options": "Netzwerkoptionen",
"Nxlink": "Nxlink",
"Check for update": "Nach Updates suchen",
"File Options": "Dateioptionen",
"Cut": "Ausschneiden",
"Copy": "Kopieren",
"Paste": "Einfügen",
"Paste ": "Einfügen ",
" file(s)?": " Datei(en)?",
"Rename": "Umbenennen",
"Set New File Name": "Neuen Dateinamen eingeben",
"Advanced": "Erweitert",
"Advanced Options": "Erweiterte Optionen",
"Create File": "Datei erstellen",
"Set File Name": "Dateinamen eingeben",
"Create Folder": "Ordner erstellen",
"Set Folder Name": "Ordnernamen eingeben",
"View as text (unfinished)": "Als Text anzeigen (Beta)",
"Empty...": "Leer...",
"Open with DayBreak?": "Mit DayBreak öffnen?",
"Launch ": "Starten ",
"Launch option for: ": "Startoption für: ",
"Select launcher for: ": "Launcher auswählen für: ",
"Homebrew": "Homebrew",
"Homebrew Options": "Homebrew-Optionen",
"Hide Sphaira": "Sphaira ausblenden",
"Install Forwarder": "Forwarder installieren",
"WARNING: Installing forwarders will lead to a ban!": "WARNUNG: Installation von Forwardern führt zum Ban!",
"Installing Forwarder": "Installiere Forwarder",
"Creating Program": "Erstelle Programm",
"Creating Control": "Erstelle Control",
"Creating Meta": "Erstelle Meta",
"Writing Nca": "Schreibe NCA",
"Updating ncm databse": "Aktualisiere NCM-Datenbank",
"Pushing application record": "Übertrage Anwendungsdaten",
"Installed!": "Installiert!",
"Failed to install forwarder": "Forwarder-Installation fehlgeschlagen",
"Unstarred ": "Favorit entfernt ",
"Starred ": "Favorit hinzugefügt ",
"AppStore": "AppStore",
"Filter: %s | Sort: %s | Order: %s": "Filter: %s | Sortierung: %s | Reihenfolge: %s",
"View as text": "Als Text anzeigen",
"View as text (unfinished)": "Als Text anzeigen (unfertig)",
"Set Archive Bit": "Archivbit setzen",
"AppStore Options": "AppStore-Optionen",
"All": "Alle",
"Games": "Spiele",
"Emulators": "Emulatoren",
"Tools": "Tools",
"Tools": "Werkzeuge",
"Advanced": "Erweitert",
"Themes": "Themes",
"Legacy": "Legacy",
"version: %s": "Version: %s",
"updated: %s": "Aktualisiert: %s",
"category: %s": "Kategorie: %s",
"extracted: %.2f MiB": "Entpackt: %.2f MiB",
"app_dls: %s": "Downloads: %s",
"More by Author": "Mehr vom Entwickler",
"Leave Feedback": "Feedback geben",
"Irs": "IR-Sensor",
"Ambient Noise Level: ": "Umgebungsrauschen: ",
"Controller": "Controller",
"Pad ": "Pad ",
" (Available)": " (Verfügbar)",
" (Unsupported)": " (Nicht unterstützt)",
"Misc": "Sonstiges",
"Downloads": "Downloads",
"Filter": "Filter",
"Search": "Suchen",
"Menu Options": "Menüoptionen",
"Header": "Header",
"Theme": "Theme",
"Network": "Netzwerk",
"Logging": "Logging",
"Enabled": "Aktiviert",
"Disabled": "Deaktiviert",
"Replace hbmenu on exit": "Ersetzen Sie hbmenu beim Beenden",
"Misc Options": "Verschiedene Optionen",
"Themezer": "Themezer",
"Irs": "Irs",
"Web": "Web",
"Download": "Herunterladen",
"Next Page": "Nächste Seite",
"Prev Page": "Vorherige Seite",
"Pad ": "Unterlage ",
" (Unconnected)": " (Nicht verbunden)",
"HandHeld": "Handheld",
"Rotation": "Rotation",
"0 (Sideways)": "0° (Seitlich)",
"90 (Flat)": "90° (Flach)",
"180 (-Sideways)": "180° (-Seitlich)",
"270 (Upside down)": "270° (Kopfüber)",
"Colour": "Farbe",
" (Available)": " (Verfügbar)",
"0 (Sideways)": "0 (Seitwärts)",
"90 (Flat)": "90 (flach)",
"180 (-Sideways)": "180 (-Seitwärts)",
"270 (Upside down)": "270 (verkehrt herum)",
"Grey": "Grau",
"Ironbow": "Ironbow",
"Ironbow": "Eisenbogen",
"Green": "Grün",
"Red": "Rot",
"Blue": "Blau",
"Light Target": "Lichtziel",
"All leds": "Alle LEDs",
"Bright group": "Helle Gruppe",
"Dim group": "Dunkle Gruppe",
"None": "Keine",
"Gain": "Verstärkung",
"Negative Image": "Negativ-Bild",
"Normal image": "Normal-Bild",
"Negative image": "Negativ-Bild",
"None": "Keiner",
"Normal image": "Normales Bild",
"Negative image": "Negatives Bild",
"320x240": "320x240",
"160x120": "160x120",
"80x60": "80x60",
"40x30": "40x30",
"20x15": "20x15",
"Controller": "Controller",
"Rotation": "Drehung",
"Colour": "Farbe",
"Light Target": "Leichtes Ziel",
"Gain": "Gain",
"Negative Image": "Negatives Bild",
"Format": "Format",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "Beschnitt-Format",
"External Light Filter": "Externes Lichtfilter",
"Load Default": "Standard laden",
"Themezer": "Themezer",
"Themezer Options": "Themezer-Optionen",
"Nsfw": "NSFW",
"Page": "Seite",
"Page %zu / %zu": "Seite %zu / %zu",
"Enter Page Number": "Seitenzahl eingeben",
"Bad Page": "Ungültige Seite",
"Download theme?": "Theme herunterladen?",
"GitHub": "",
"Downloading json": "",
"Select asset to download for ": "",
"Installing ": "Installiere ",
"Uninstalling ": "Deinstalliere ",
"Deleting ": "Lösche ",
"Deleting": "Lösche",
"Pasting ": "Füge ein ",
"Pasting": "Füge ein",
"Removing ": "Entferne ",
"Scanning ": "Scanne ",
"Creating ": "Erstelle ",
"Copying ": "Kopiere ",
"Trying to load ": "Lade ",
"Downloading ": "Lade herunter ",
"Downloaded ": "",
"Removed ": "",
"Checking MD5": "Prüfe MD5",
"Loading...": "Lade...",
"Loading": "Lade",
"Empty!": "Leer!",
"Not Ready...": "Nicht bereit...",
"Error loading page!": "Fehler beim Laden!",
"Update avaliable: ": "Update verfügbar: ",
"Download update: ": "Update herunterladen: ",
"Updated to ": "Aktualisiert auf ",
"Restart Sphaira?": "Sphaira neustarten?",
"Failed to download update": "Update-Download fehlgeschlagen",
"Delete Selected files?": "Ausgewählte Dateien löschen?",
"Completely remove ": "Vollständig entfernen ",
"Are you sure you want to delete ": "Wirklich löschen ",
"Are you sure you wish to cancel?": "Wirklich abbrechen?",
"If this message appears repeatedly, please open an issue.": "Bei wiederholtem Auftreten bitte Issue erstellen."
"Trimming Format": "Zuschneideformat",
"External Light Filter": "Externer Lichtfilter",
"Load Default": "Standardoptionen laden",
"No Internet": "Kein Internet",
"[Applet Mode]": "[Applet-Modus]",
"Language": "Sprache"
}

View File

@@ -1,238 +1,114 @@
{
"[Applet Mode]": "[Applet Mode]",
"No Internet": "No Internet",
"Files": "Files",
"Apps": "Apps",
"Store": "Store",
"Menu": "Menu",
"Options": "Options",
"OK": "OK",
"Back": "Back",
"Select": "Select",
"Open": "Open",
"Launch": "Launch",
"Info": "Info",
"Install": "Install",
"Delete": "Delete",
"Restart": "Restart",
"Changelog": "Changelog",
"Details": "Details",
"Update": "Update",
"Remove": "Remove",
"Download": "Download",
"Next Page": "Next Page",
"Prev Page": "Prev Page",
"Unstar": "Unstar",
"Star": "Star",
"System memory": "System memory",
"microSD card": "microSD card",
"Yes": "Yes",
"No": "No",
"Enabled": "Enabled",
"Disabled": "Disabled",
"Sort By": "Sort By",
"Sort Options": "Sort Options",
"Filter": "Filter",
"Sort": "Sort",
"Order": "Order",
"Search": "Search",
"Updated": "Updated",
"Updated (Star)": "Updated (Star)",
"Downloads": "Downloads",
"Size": "Size",
"Size (Star)": "Size (Star)",
"Alphabetical": "Alphabetical",
"Alphabetical (Star)": "Alphabetical (Star)",
"Likes": "Likes",
"ID": "ID",
"Decending": "Decending",
"Descending (down)": "Descending (down)",
"Desc": "Desc",
"Ascending": "Ascending",
"Ascending (Up)": "Ascending (Up)",
"Asc": "Asc",
"Menu Options": "Menu Options",
"Header": "Header",
"Theme": "Theme",
"Theme Options": "Theme Options",
"Select Theme": "Select Theme",
"Shuffle": "Shuffle",
"Music": "Music",
"Network": "Network",
"Network Options": "Network Options",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink Connected",
"Nxlink Upload": "Nxlink Upload",
"Nxlink Finished": "Nxlink Finished",
"Switch-Handheld!": "Switch-Handheld!",
"Switch-Docked!": "Switch-Docked!",
"Language": "Language",
"Auto": "Auto",
"English": "English",
"Japanese": "日本語",
"French": "Français",
"German": "Deutsch",
"Italian": "Italiano",
"Spanish": "Español",
"Chinese": "中文",
"Korean": "한국어",
"Dutch": "Dutch",
"Portuguese": "Português",
"Russian": "Русский",
"Swedish": "Svenska",
"Logging": "Logging",
"Replace hbmenu on exit": "Replace hbmenu on exit",
"Misc": "Misc",
"Misc Options": "Misc Options",
"Web": "Web",
"Install forwarders": "Install forwarders",
"Install location": "Install location",
"Show install warning": "Show install warning",
"FileBrowser": "FileBrowser",
"%zd files": "%zd files",
"%zd dirs": "%zd dirs",
"File Options": "File Options",
"Show Hidden": "Show Hidden",
"Folders First": "Folders First",
"Hidden Last": "Hidden Last",
"Cut": "Cut",
"Copy": "Copy",
"Paste": "Paste",
"Paste ": "Paste ",
" file(s)?": " file(s)?",
"Rename": "Rename",
"Set New File Name": "Set New File Name",
"Advanced": "Advanced",
"Advanced Options": "Advanced Options",
"Create File": "Create File",
"Set File Name": "Set File Name",
"Create Folder": "Create Folder",
"Set Folder Name": "Set Folder Name",
"View as text (unfinished)": "View as text (unfinished)",
"Empty...": "Empty...",
"Open with DayBreak?": "Open with DayBreak?",
"Launch ": "Launch ",
"Launch option for: ": "Launch option for: ",
"Select launcher for: ": "Select launcher for: ",
"Homebrew": "Homebrew",
"Homebrew Options": "Homebrew Options",
"Hide Sphaira": "Hide Sphaira",
"Install Forwarder": "Install Forwarder",
"WARNING: Installing forwarders will lead to a ban!": "WARNING: Installing forwarders will lead to a ban!",
"Installing Forwarder": "Installing Forwarder",
"Creating Program": "Creating Program",
"Creating Control": "Creating Control",
"Creating Meta": "Creating Meta",
"Writing Nca": "Writing Nca",
"Updating ncm databse": "Updating ncm databse",
"Pushing application record": "Pushing application record",
"Installed!": "Installed!",
"Failed to install forwarder": "Failed to install forwarder",
"Unstarred ": "Unstarred ",
"Starred ": "Starred ",
"AppStore": "AppStore",
"Filter: %s | Sort: %s | Order: %s": "Filter: %s | Sort: %s | Order: %s",
"AppStore Options": "AppStore Options",
"All": "All",
"Games": "Games",
"Emulators": "Emulators",
"Tools": "Tools",
"Themes": "Themes",
"Legacy": "Legacy",
"version: %s": "version: %s",
"updated: %s": "updated: %s",
"category: %s": "category: %s",
"extracted: %.2f MiB": "extracted: %.2f MiB",
"app_dls: %s": "app_dls: %s",
"More by Author": "More by Author",
"Leave Feedback": "Leave Feedback",
"Irs": "Irs",
"Ambient Noise Level: ": "Ambient Noise Level: ",
"Controller": "Controller",
"Pad ": "Pad ",
" (Available)": " (Available)",
" (Unsupported)": " (Unsupported)",
" (Unconnected)": " (Unconnected)",
"HandHeld": "HandHeld",
"Rotation": "Rotation",
"0 (Sideways)": "0 (Sideways)",
"90 (Flat)": "90 (Flat)",
"180 (-Sideways)": "180 (-Sideways)",
"270 (Upside down)": "270 (Upside down)",
"Colour": "Colour",
"Grey": "Grey",
"Ironbow": "Ironbow",
"Green": "Green",
"Red": "Red",
"Blue": "Blue",
"Light Target": "Light Target",
"All leds": "All leds",
"Bright group": "Bright group",
"Dim group": "Dim group",
"None": "None",
"Gain": "Gain",
"Negative Image": "Negative Image",
"Normal image": "Normal image",
"Negative image": "Negative image",
"Format": "Format",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "Trimming Format",
"External Light Filter": "External Light Filter",
"Load Default": "Load Default",
"Themezer": "Themezer",
"Themezer Options": "Themezer Options",
"Nsfw": "Nsfw",
"Page": "Page",
"Page %zu / %zu": "Page %zu / %zu",
"Enter Page Number": "Enter Page Number",
"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 ",
"Deleting": "Deleting",
"Pasting ": "Pasting ",
"Pasting": "Pasting",
"Removing ": "Removing ",
"Scanning ": "Scanning ",
"Creating ": "Creating ",
"Copying ": "Copying ",
"Trying to load ": "Trying to load ",
"Downloading ": "Downloading ",
"Downloaded ": "Downloaded ",
"Removed ": "Removed ",
"Checking MD5": "Checking MD5",
"Loading...": "Loading...",
"Loading": "Loading",
"Empty!": "Empty!",
"Not Ready...": "Not Ready...",
"Error loading page!": "Error loading page!",
"Update avaliable: ": "Update avaliable: ",
"Download update: ": "Download update: ",
"Updated to ": "Updated to ",
"Restart Sphaira?": "Restart Sphaira?",
"Failed to download update": "Failed to download update",
"Delete Selected files?": "Delete Selected files?",
"Completely remove ": "Completely remove ",
"Are you sure you want to delete ": "Are you sure you want to delete ",
"Are you sure you wish to cancel?": "Are you sure you wish to cancel?",
"If this message appears repeatedly, please open an issue.": "If this message appears repeatedly, please open an issue."
"Launch" : "Launch",
"Options" : "Options",
"Homebrew Options" : "Homebrew Options",
"Sort By" : "Sort By",
"Sort Options" : "Sort Options",
"Updated" : "Updated",
"Size" : "Size",
"Alphabetical" : "Alphabetical",
"Decending" : "Decending",
"Ascending" : "Ascending",
"Sort" : "Sort",
"Order" : "Order",
"Info" : "Info",
"Delete" : "Delete",
"Hide Sphaira" : "Hide Sphaira",
"Are you sure you want to delete " : "Are you sure you want to delete ",
"Install Forwarder" : "Install Forwarder",
"WARNING: Installing forwarders will lead to a ban!" : "WARNING: Installing forwarders will lead to a ban!",
"Back" : "Back",
"Install" : "Install",
"Fs" : "Fs",
"App" : "App",
"Menu" : "Menu",
"Homebrew" : "Homebrew",
"FileBrowser" : "FileBrowser",
"Open" : "Open",
"Theme Options" : "Theme Options",
"Select Theme" : "Select Theme",
"Shuffle" : "Shuffle",
"Music" : "Music",
"Show Hidden" : "Show Hidden",
"Folders First" : "Folders First",
"Hidden Last" : "Hidden Last",
"Yes" : "Yes",
"No" : "No",
"Network Options" : "Network Options",
"Nxlink" : "Nxlink",
"Check for update" : "Check for update",
"File Options" : "File Options",
"Cut" : "Cut",
"Copy" : "Copy",
"Rename" : "Rename",
"Advanced Options" : "Create File",
"Create File" : "Create File",
"Create Folder" : "Create Folder",
"View as text" : "View as text",
"View as text (unfinished)" : "View as text (unfinished)",
"Set Archive Bit" : "Set Archive Bit",
"AppStore Options" : "AppStore Options",
"All" : "All",
"Games" : "Games",
"Emulators" : "Emulators",
"Tools" : "Tools",
"Advanced" : "Advanced",
"Themes" : "Themes",
"Legacy" : "Legacy",
"Misc" : "Misc",
"Downloads" : "Downloads",
"Filter" : "Filter",
"Search" : "Search",
"Menu Options" : "Menu Options",
"Header" : "Header",
"Theme" : "Theme",
"Network" : "Network",
"Logging" : "Logging",
"Enabled" : "Enabled",
"Disabled" : "Disabled",
"Replace hbmenu on exit" : "Replace hbmenu on exit",
"Misc Options" : "Misc Options",
"Themezer" : "Themezer",
"Irs" : "Irs",
"Web" : "Web",
"Download" : "Download",
"Next Page" : "Next Page",
"Prev Page" : "Prev Page",
"Pad " : "Pad ",
" (Unconnected)" : " (Unconnected)",
"HandHeld" : "HandHeld",
" (Available)" : " (Available)",
"0 (Sideways)" : "0 (Sideways)",
"90 (Flat)" : "90 (Flat)",
"180 (-Sideways)" : "180 (-Sideways)",
"270 (Upside down)" : "270 (Upside down)",
"Grey" : "Grey",
"Ironbow" : "Ironbow",
"Green" : "Green",
"Red" : "Red",
"Blue" : "Blue",
"All leds" : "All leds",
"Bright group" : "Bright group",
"Dim group" : "Dim group",
"None" : "None",
"Normal image" : "Normal image",
"Negative image" : "Negative image",
"320x240" : "320x240",
"160x120" : "160x120",
"80x60" : "80x60",
"40x30" : "40x30",
"20x15" : "20x15",
"Controller" : "Controller",
"Rotation" : "Rotation",
"Colour" : "Colour",
"Light Target" : "Light Target",
"Gain" : "Gain",
"Negative Image" : "Negative Image",
"Format" : "Format",
"Trimming Format" : "Trimming Format",
"External Light Filter" : "External Light Filter",
"Load Default" : "Load Default",
"No Internet" : "No Internet",
"[Applet Mode]" : "[Applet Mode]",
"Language": "Language"
}

View File

@@ -1,238 +1,114 @@
{
"[Applet Mode]": "[Modo Applet]",
"No Internet": "Sin Internet",
"Files": "Archivos",
"Apps": "Apps",
"Store": "Tienda",
"Menu": "Menú",
"Launch": "Lanzamiento",
"Options": "Opciones",
"OK": "OK",
"Back": "Atrás",
"Select": "Seleccionar",
"Open": "Abrir",
"Launch": "Ejecutar",
"Info": "Información",
"Install": "Instalar",
"Delete": "Borrar",
"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": "Quitar favorito",
"Star": "Favorito",
"System memory": "Memoria de sistema",
"microSD card": "microSD",
"Yes": "Sí",
"No": "No",
"Enabled": "Activado",
"Disabled": "Desactivado",
"Homebrew Options": "Opciones de elaboración casera",
"Sort By": "Ordenar por",
"Sort Options": "Opciones de clasificación",
"Filter": "Filtrar",
"Updated": "Actualizado",
"Size": "Tamaño",
"Alphabetical": "Alfabético",
"Decending": "Descendente",
"Ascending": "Ascendente",
"Sort": "Clasificar",
"Order": "Orden",
"Search": "Buscar",
"Updated": "Actualizado",
"Updated (Star)": "Actualizado (favorito)",
"Downloads": "Descargas",
"Size": "Tamaño",
"Size (Star)": "Tamaño (favorito)",
"Alphabetical": "Alfabético",
"Alphabetical (Star)": "Alfabético (favorito)",
"Likes": "Me Gusta",
"ID": "ID",
"Decending": "Descendente",
"Descending (down)": "Descendente (abajo)",
"Desc": "Descendente",
"Ascending": "Ascendente",
"Ascending (Up)": "Ascendente (arriba)",
"Asc": "Ascendente",
"Menu Options": "Opciones de menú",
"Header": "Encabezado",
"Theme": "Tema",
"Info": "Información",
"Delete": "Borrar",
"Hide Sphaira": "Ocultar Sphaira",
"Are you sure you want to delete ": "¿Estás seguro de que quieres eliminar? ",
"Install Forwarder": "Instalar reenviador",
"WARNING: Installing forwarders will lead to a ban!": "ADVERTENCIA: ¡La instalación de reenviadores dará lugar a una prohibición!",
"Back": "Atrás",
"Install": "Instalar",
"Fs": "fs",
"App": "Aplicación",
"Menu": "Menú",
"Homebrew": "cerveza casera",
"FileBrowser": "Explorador de archivos",
"Open": "Abierto",
"Theme Options": "Opciones de tema",
"Select Theme": "Seleccionar tema",
"Shuffle": "Barajar",
"Music": "Música",
"Network": "Red",
"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",
"Japanese": "日本語",
"French": "Français",
"German": "Deutsch",
"Italian": "Italiano",
"Spanish": "Español",
"Chinese": "中文",
"Korean": "한국어",
"Dutch": "Dutch",
"Portuguese": "Português",
"Russian": "Русский",
"Swedish": "Svenska",
"Logging": "Registros",
"Replace hbmenu on exit": "Reemplazar hbmenu al salir",
"Misc": "Varios",
"Misc Options": "Opciones varias",
"Web": "Web",
"Install forwarders": "Instalar forwarders",
"Install location": "Ruta de instalación ",
"Show install warning": "Mostrar precaución de instalación",
"FileBrowser": "Explorador de archivos",
"%zd files": "%zd files",
"%zd dirs": "%zd dirs",
"File Options": "Opciones de archivo",
"Show Hidden": "Mostrar archivos ocultos",
"Show Hidden": "Mostrar oculto",
"Folders First": "Carpetas primero",
"Hidden Last": "Ocultos al final",
"Hidden Last": "Oculto último",
"Yes": "Sí",
"No": "No",
"Network Options": "Opciones de red",
"Nxlink": "nxenlace",
"Check for update": "Buscar actualizaciones",
"File Options": "Opciones de archivo",
"Cut": "Cortar",
"Copy": "Copiar",
"Paste": "Pegar",
"Paste ": "Pegar ",
" file(s)?": " ¿archivo(s)?",
"Rename": "Renombrar",
"Set New File Name": "Establecer nuevo nombre de archivo",
"Advanced": "Avanzado",
"Advanced Options": "Opciones avanzadas",
"Rename": "Rebautizar",
"Advanced Options": "Crear archivo",
"Create File": "Crear archivo",
"Set File Name": "Establecer nombre de archivo",
"Create Folder": "Crear carpeta",
"Set Folder Name": "Establecer nombre de carpeta",
"View as text": "Ver como texto",
"View as text (unfinished)": "Ver como texto (sin terminar)",
"Empty...": "Vacío...",
"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",
"Hide Sphaira": "Ocultar Sphaira",
"Install Forwarder": "Instalar Forwarder",
"WARNING: Installing forwarders will lead to a ban!": "ADVERTENCIA: ¡La instalación de fordwarders podría producir un baneo de la consola!",
"Installing Forwarder": "Instalando Forwarder",
"Creating Program": "Creando Program",
"Creating Control": "Creando Control",
"Creating Meta": "Creando Meta",
"Writing Nca": "Creando NCA",
"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 ": "Quitar Favorito",
"Starred ": "Favorito",
"AppStore": "Tienda",
"Filter: %s | Sort: %s | Order: %s": "Filtrar: %s | Clasificar: %s | Orden: %s",
"AppStore Options": "Opciones de la Tienda",
"Set Archive Bit": "Establecer bit de archivo",
"AppStore Options": "Opciones de la tienda de aplicaciones",
"All": "Todo",
"Games": "Juegos",
"Emulators": "Emuladores",
"Tools": "Herramientas",
"Advanced": "Avanzado",
"Themes": "Temas",
"Legacy": "Legado",
"version: %s": "version: %s",
"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 Ambiente",
"Controller": "Control",
"Misc": "Varios",
"Downloads": "Descargas",
"Filter": "Filtrar",
"Search": "Buscar",
"Menu Options": "Opciones de menú",
"Header": "Encabezamiento",
"Theme": "Tema",
"Network": "Red",
"Logging": "Explotación florestal",
"Enabled": "Activado",
"Disabled": "Desactivado",
"Replace hbmenu on exit": "Reemplazar hbmenu al salir",
"Misc Options": "Opciones varias",
"Themezer": "Temazer",
"Irs": "irs",
"Web": "Web",
"Download": "Descargar",
"Next Page": "Página siguiente",
"Prev Page": "Página anterior",
"Pad ": "Almohadilla ",
" (Available)": " (Disponible)",
" (Unsupported)": "(No Compatible)",
" (Unconnected)": " (Desconectado)",
"HandHeld": "Portátil",
"Rotation": "Rotación",
"0 (Sideways)": "0 (De lado)",
"90 (Flat)": "90 (Plano)",
"180 (-Sideways)": "180 (-De lado)",
"270 (Upside down)": "270 (Al revés)",
"Colour": "Color",
" (Available)": " (Disponible)",
"0 (Sideways)": "0 (de lado)",
"90 (Flat)": "90 (plano)",
"180 (-Sideways)": "180 (-de lado)",
"270 (Upside down)": "270 (al revés)",
"Grey": "Gris",
"Ironbow": "Paleta térmica",
"Ironbow": "arco de hierro",
"Green": "Verde",
"Red": "Rojo",
"Blue": "Azul",
"Light Target": "Objetivo de luz",
"All leds": "Todos los leds",
"Bright group": "Grupo brillo",
"Dim group": "Grupo tenue",
"All leds": "todos los leds",
"Bright group": "grupo brillante",
"Dim group": "grupo tenue",
"None": "Ninguno",
"Gain": "Ganancia",
"Negative Image": "Imagen negativa",
"Normal image": "Imagen normal",
"Normal image": "imagen normal",
"Negative image": "Imagen negativa",
"320x240": "320x240",
"160x120": "160x120",
"80x60": "80x60",
"40x30": "40x30",
"20x15": "20x15",
"Controller": "Controlador",
"Rotation": "Rotación",
"Colour": "Color",
"Light Target": "Objetivo de luz",
"Gain": "Ganar",
"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",
"External Light Filter": "Filtro de luz externo",
"Load Default": "Cargar predeterminado",
"Themezer": "Themezer",
"Themezer Options": "Opciones de Themezer",
"Nsfw": "NSFW",
"Page": "Página",
"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 ",
"Deleting": "Borrando",
"Pasting ": "Pegando ",
"Pasting": "Pegando",
"Removing ": "Removiendo ",
"Scanning ": "Escaneando ",
"Creating ": "Creando ",
"Copying ": "Copiando ",
"Trying to load ": "Intentando cargar",
"Downloading ": "Descargando ",
"Downloaded ": "",
"Removed ": "",
"Checking MD5": "Chequeando MD5",
"Loading...": "Cargando...",
"Loading": "Cargando",
"Empty!": "¡Vacío!",
"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 ": "Actualizado a ",
"Restart Sphaira?": "¿Reiniciar Sphaira?",
"Failed to download update": "Fallo al descargar actualización",
"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.": "Si este mensaje aparece repetidamente, por favor abrir un 'issue'."
"No Internet": "sin internet",
"[Applet Mode]": "[Modo subprograma]",
"Language": "Idioma"
}

View File

@@ -1,238 +1,114 @@
{
"[Applet Mode]": "[Mode Applet]",
"No Internet": "Pas d'Internet",
"Files": "Fichiers",
"Apps": "Applications",
"Store": "Magasin",
"Menu": "Menu",
"Options": "Options",
"OK": "OK",
"Back": "Retour",
"Select": "Sélectionner",
"Open": "Ouvrir",
"Launch": "Exécuter",
"Info": "Info.",
"Install": "Installer",
"Delete": "Supprimer",
"Restart": "Redémarrer",
"Changelog": "Changelog",
"Details": "Détails",
"Update": "Mise à jour",
"Remove": "Supprimer",
"Download": "Télécharger",
"Next Page": "Page Suiv.",
"Prev Page": "Page Préc.",
"Unstar": "Retirer des favories",
"Star": "Ajouter aux favories",
"System memory": "Mémoire système",
"microSD card": "Carte microSD",
"Yes": "Oui",
"No": "Non",
"Enabled": "Activé(e)",
"Disabled": "Désactivé(e)",
"Options": "Options",
"Homebrew Options": "Options Homebrew",
"Sort By": "Tri Par",
"Sort Options": "Options de Tri",
"Filter": "Filtre",
"Updated": "Mis à jour",
"Size": "Taille",
"Alphabetical": "Alphabétique",
"Decending": "Décroissant",
"Ascending": "Croissant",
"Sort": "Tri",
"Order": "Ordre",
"Search": "Recherche",
"Updated": "Mis à jour",
"Updated (Star)": "Mis à jour (Favories)",
"Downloads": "Téléchargements",
"Size": "Taille",
"Size (Star)": "Taille (Favories)",
"Alphabetical": "Alphabétique",
"Alphabetical (Star)": "Alphabétique (Favories)",
"Likes": "Likes",
"ID": "ID",
"Decending": "Décroissant",
"Descending (down)": "Décroissant",
"Desc": "Décroissant",
"Ascending": "Croissant",
"Ascending (Up)": "Croissant",
"Asc": "Croissant",
"Menu Options": "Options des Menus",
"Header": "En-tête",
"Theme": "Thème",
"Info": "Info.",
"Delete": "Supprimer",
"Hide Sphaira": "Masquer Sphaira",
"Are you sure you want to delete ": "Êtes-vous sûr de vouloir supprimer ",
"Install Forwarder": "Installer le Forwarder",
"WARNING: Installing forwarders will lead to a ban!": "ATTENTION: L'installation de forwarders entraînera un ban!",
"Back": "Retour",
"Install": "Installer",
"Fs": "Fs",
"App": "App.",
"Menu": "Menu",
"Homebrew": "Homebrew",
"FileBrowser": "Navigateur de Fichiers",
"Open": "Ouvrir",
"Theme Options": "Options de Thème",
"Select Theme": "Choisir un Thème",
"Shuffle": "Aléatoire",
"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",
"Nxlink Finished": "Nxlink terminé",
"Switch-Handheld!": "Switch-Portable",
"Switch-Docked!": "Switch-Dockée",
"Language": "Langue",
"Auto": "Auto",
"English": "English",
"Japanese": "日本語",
"French": "Français",
"German": "Deutsch",
"Italian": "Italiano",
"Spanish": "Español",
"Chinese": "中文",
"Korean": "한국어",
"Dutch": "Dutch",
"Portuguese": "Português",
"Russian": "Русский",
"Swedish": "Svenska",
"Logging": "Journalisation",
"Replace hbmenu on exit": "Remplacer hbmenu quand quitté",
"Misc": "Divers",
"Misc Options": "Options Diverses",
"Web": "Web",
"Install forwarders": "Installer les Forwarders",
"Install location": "Emplacement d'installation",
"Show install warning": "Afficher l'avertissement d'installation",
"FileBrowser": "Explorateur de Fichiers",
"%zd files": "%zd fichiers",
"%zd dirs": "%zd dossiers",
"File Options": "Options de Fichier",
"Show Hidden": "Afficher Masqués",
"Folders First": "Dossiers en Premier",
"Hidden Last": "Masqués en Dernier",
"Yes": "Oui",
"No": "Non",
"Network Options": "Options Réseau",
"Nxlink": "Nxlink",
"Check for update": "Vérification d'une mise à jour",
"File Options": "Options de Fichier",
"Cut": "Couper",
"Copy": "Copier",
"Paste": "Coller",
"Paste ": "Coller ",
" file(s)?": " fichier(s)?",
"Rename": "Renommer",
"Set New File Name": "Nouveau Nom Du Fichier",
"Advanced": "Avancé",
"Advanced Options": "Options Avancées",
"Create File": "Créer un Fichier",
"Set File Name": "Nommer Le Fichier",
"Create Folder": "Créer un Dossier",
"Set Folder Name": "Nommer Le Dossier",
"View as text": "Afficher sous forme de texte",
"View as text (unfinished)": "Afficher sous forme de texte (inachevé)",
"Empty...": "Vide...",
"Open with DayBreak?": "Ouvrir avec DayBreak?",
"Launch ": "Lancer ",
"Launch option for: ": "Option de lancement pour: ",
"Select launcher for: ": "Sélectionner le lanceur pour: ",
"Homebrew": "Homebrew",
"Homebrew Options": "Options Homebrew",
"Hide Sphaira": "Masquer Sphaira",
"Install Forwarder": "Installer le Forwarder",
"WARNING: Installing forwarders will lead to a ban!": "ATTENTION: L'installation de forwarders entraînera un ban!",
"Installing Forwarder": "Installation Du Forwarder",
"Creating Program": "Création de Program",
"Creating Control": "Création de Control",
"Creating Meta": "Création de Meta",
"Writing Nca": "Ecriture NCA",
"Updating ncm databse": "Mise à jour de ncm databse",
"Pushing application record": "Ajout de l'enregistrement de l'application",
"Installed!": "Installé!",
"Failed to install forwarder": "Echec de l'installation du forwarder",
"Unstarred ": "Retiré des favories ",
"Starred ": "Ajouté aux favories ",
"AppStore": "AppStore",
"Filter: %s | Sort: %s | Order: %s": "Filtre: %s | Tri: %s | Ordre: %s",
"Set Archive Bit": "Définir le Bit d'Archive",
"AppStore Options": "Options de l'AppStore",
"All": "Tous",
"Games": "Jeux",
"Emulators": "Émulateurs",
"Tools": "Outils",
"Advanced": "Avancé",
"Themes": "Thèmes",
"Legacy": "Legacy",
"version: %s": "version: %s",
"updated: %s": "Mis à jour: %s",
"category: %s": "catégorie: %s",
"extracted: %.2f MiB": "Extrait: %.2f MiB",
"app_dls: %s": "app_dls: %s",
"More by Author": "Plus de cet Auteur",
"Leave Feedback": "Laisser un avis",
"Misc": "Divers",
"Downloads": "Téléchargements",
"Filter": "Filtre",
"Search": "Recherche",
"Menu Options": "Options des Menus",
"Header": "En-tête",
"Theme": "Thème",
"Network": "Réseau",
"Logging": "Journalisation",
"Enabled": "Activé(e)",
"Disabled": "Désactivé(e)",
"Replace hbmenu on exit": "Remplacer hbmenu en sortie",
"Misc Options": "Options Diverses",
"Themezer": "Themezer",
"Irs": "Irs",
"Ambient Noise Level: ": "Niveau De Bruit Ambiant: ",
"Controller": "Contrôleur",
"Web": "Web",
"Download": "Télécharger",
"Next Page": "Page Suiv.",
"Prev Page": "Page Préc.",
"Pad ": "Manette ",
" (Available)": " (Disponible)",
" (Unsupported)": "Non supporté",
" (Unconnected)": " (Non connectée)",
"HandHeld": "Portable",
"Rotation": "Rotation",
" (Available)": " (Disponible)",
"0 (Sideways)": "0 (Paysage)",
"90 (Flat)": "90 (Portrait)",
"180 (-Sideways)": "180 (-Paysage)",
"270 (Upside down)": "270 (Inversé)",
"Colour": "Couleur",
"Grey": "Gris",
"Ironbow": "Ironbow",
"Green": "Vert",
"Red": "Rouge",
"Blue": "Bleu",
"Light Target": "Luminosité",
"All leds": "Toutes les LED",
"Bright group": "Groupe lumineux",
"Dim group": "Groupe sombre",
"None": "Aucun",
"Gain": "Gain",
"Negative Image": "Négatif",
"Normal image": "Image normale",
"Negative image": "Négatif",
"320x240": "320x240",
"160x120": "160x120",
"80x60": "80x60",
"40x30": "40x30",
"20x15": "20x15",
"Controller": "Contrôleur",
"Rotation": "Rotation",
"Colour": "Couleur",
"Light Target": "Luminosité",
"Gain": "Gain",
"Negative Image": "Négatif",
"Format": "Format",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "Format de Découpe",
"External Light Filter": "Filtre de Lumière Externe",
"Load Default": "Charger par Défaut",
"Themezer": "Themezer",
"Themezer Options": "Options Themezer",
"Nsfw": "Nsfw",
"Page": "Page",
"Page %zu / %zu": "Page %zu / %zu",
"Enter Page Number": "Entrez Un Numéro De Page",
"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 ",
"Deleting": "Suppression",
"Pasting ": "Coller ",
"Pasting": "Coller",
"Removing ": "Suppression ",
"Scanning ": "Scan ",
"Creating ": "Création ",
"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",
"Empty!": "Vide!",
"Not Ready...": "Pas prêt",
"Error loading page!": "Erreur du chargement de la page!",
"Update avaliable: ": "Mise à jour disponible: ",
"Download update: ": "Télécharger la mise à jour: ",
"Updated to ": "Mis à jour vers ",
"Restart Sphaira?": "Redémarrer Sphaira?",
"Failed to download update": "Echec du téléchargement de la mise à jour",
"Delete Selected files?": "Supprimer les fichiers sélectionnés?",
"Completely remove ": "Supprimer totalement ",
"Are you sure you want to delete ": "Êtes-vous sûr de vouloir supprimer ",
"Are you sure you wish to cancel?": "Souhaitez-vous vraiment annuler?",
"If this message appears repeatedly, please open an issue.": "Si ce message apparait en boucle veuillez ouvrir une issue."
"No Internet": "Pas d'Internet",
"[Applet Mode]": "[Mode Applet]",
"Language": "Langue"
}

View File

@@ -1,238 +1,114 @@
{
"[Applet Mode]": "[Modalità applet]",
"No Internet": "Niente Internet",
"Files": "",
"Apps": "",
"Store": "",
"Menu": "Menu",
"Options": "Opzioni",
"OK": "",
"Back": "Indietro",
"Select": "",
"Open": "Apri",
"Launch": "Lancia",
"Info": "Informazioni",
"Install": "Installa",
"Delete": "Elimina",
"Restart": "",
"Changelog": "",
"Details": "",
"Update": "",
"Remove": "",
"Download": "Download",
"Next Page": "Pagina successiva",
"Prev Page": "Pagina precedente",
"Unstar": "",
"Star": "",
"System memory": "",
"microSD card": "",
"Yes": "Sì",
"No": "No",
"Enabled": "Abilitato",
"Disabled": "Disabilitato",
"Options": "Opzioni",
"Homebrew Options": "Opzioni Homebrew",
"Sort By": "Ordina per",
"Sort Options": "Opzioni filtro",
"Filter": "Filtro",
"Updated": "Aggiornato",
"Size": "Misurare",
"Alphabetical": "Alfabetico",
"Decending": "Decrescente",
"Ascending": "Crescente",
"Sort": "Riordina",
"Order": "Ordina",
"Search": "Ricerca",
"Updated": "Aggiornato",
"Updated (Star)": "",
"Downloads": "Download",
"Size": "Misurare",
"Size (Star)": "",
"Alphabetical": "Alfabetico",
"Alphabetical (Star)": "",
"Likes": "",
"ID": "",
"Decending": "Decrescente",
"Descending (down)": "Decrescente",
"Desc": "Decrescente",
"Ascending": "Crescente",
"Ascending (Up)": "Crescente",
"Asc": "Crescente",
"Menu Options": "Opzioni menu",
"Header": "Intestazione",
"Theme": "Tema",
"Info": "Informazioni",
"Delete": "Elimina",
"Hide Sphaira": "Nascondi Sphaira",
"Are you sure you want to delete ": "Sei sicuro di voler eliminare? ",
"Install Forwarder": "Installa forwarder",
"WARNING: Installing forwarders will lead to a ban!": "ATTENZIONE: l'installazione di forwarder porterà al ban!",
"Back": "Indietro",
"Install": "Installa",
"Fs": "Fs",
"App": "App",
"Menu": "Menu",
"Homebrew": "Homebrew",
"FileBrowser": "FileBrowser",
"Open": "Apri",
"Theme Options": "Opzioni tema",
"Select Theme": "Seleziona tema",
"Shuffle": "Mescola",
"Music": "Musica",
"Network": "Rete",
"Network Options": "Opzioni di rete",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink",
"Nxlink Connected": "",
"Nxlink Upload": "",
"Nxlink Finished": "",
"Switch-Handheld!": "",
"Switch-Docked!": "",
"Language": "Lingua",
"Auto": "",
"English": "English",
"Japanese": "日本語",
"French": "Français",
"German": "Deutsch",
"Italian": "Italiano",
"Spanish": "Español",
"Chinese": "中文",
"Korean": "한국어",
"Dutch": "Dutch",
"Portuguese": "Português",
"Russian": "Русский",
"Swedish": "Svenska",
"Logging": "Logging",
"Replace hbmenu on exit": "Sostituisci hbmenu all'uscita",
"Misc": "Varie",
"Misc Options": "Opzioni varie",
"Web": "Rete",
"Install forwarders": "",
"Install location": "",
"Show install warning": "",
"FileBrowser": "FileBrowser",
"%zd files": "%zd files",
"%zd dirs": "%zd dirs",
"File Options": "Opzioni file",
"Show Hidden": "Mostra nascosto",
"Folders First": "Prima le cartelle",
"Hidden Last": "Ultimo nascosto",
"Yes": "Sì",
"No": "No",
"Network Options": "Opzioni di rete",
"Nxlink": "Nxlink",
"Check for update": "Controlla aggiornamenti",
"File Options": "Opzioni file",
"Cut": "Taglia",
"Copy": "Copia",
"Paste": "",
"Paste ": "",
" file(s)?": "",
"Rename": "Rinomina",
"Set New File Name": "",
"Advanced": "Avanzato",
"Advanced Options": "Opzioni avanzate",
"Create File": "Crea file",
"Set File Name": "",
"Create Folder": "Crea cartella",
"Set Folder Name": "",
"View as text": "Visualizza come testo",
"View as text (unfinished)": "Visualizza come testo (non finito)",
"Empty...": "",
"Open with DayBreak?": "",
"Launch ": "",
"Launch option for: ": "",
"Select launcher for: ": "",
"Homebrew": "Homebrew",
"Homebrew Options": "Opzioni Homebrew",
"Hide Sphaira": "Nascondi Sphaira",
"Install Forwarder": "Installa forwarder",
"WARNING: Installing forwarders will lead to a ban!": "ATTENZIONE: l'installazione di forwarder porterà al ban!",
"Installing Forwarder": "",
"Creating Program": "",
"Creating Control": "",
"Creating Meta": "",
"Writing Nca": "",
"Updating ncm databse": "",
"Pushing application record": "",
"Installed!": "",
"Failed to install forwarder": "",
"Unstarred ": "",
"Starred ": "",
"AppStore": "",
"Filter: %s | Sort: %s | Order: %s": "Filtro: %s | Riordina: %s | Ordina: %s",
"Set Archive Bit": "Imposta Archive Bit",
"AppStore Options": "Opzioni dell'App Store",
"All": "Tutto",
"Games": "Giochi",
"Emulators": "Emulatori",
"Tools": "Strumenti",
"Advanced": "Avanzato",
"Themes": "Temi",
"Legacy": "Legacy",
"version: %s": "version: %s",
"updated: %s": "updated: %s",
"category: %s": "category: %s",
"extracted: %.2f MiB": "extracted: %.2f MiB",
"app_dls: %s": "app_dls: %s",
"More by Author": "",
"Leave Feedback": "",
"Misc": "Varie",
"Downloads": "Download",
"Filter": "Filtro",
"Search": "Ricerca",
"Menu Options": "Opzioni menu",
"Header": "Intestazione",
"Theme": "Tema",
"Network": "Rete",
"Logging": "Logging",
"Enabled": "Abilitato",
"Disabled": "Disabilitato",
"Replace hbmenu on exit": "Sostituisci hbmenu all'uscita",
"Misc Options": "Opzioni varie",
"Themezer": "Themezer",
"Irs": "Irs",
"Ambient Noise Level: ": "",
"Controller": "Controller",
"Web": "Rete",
"Download": "Download",
"Next Page": "Pagina successiva",
"Prev Page": "Pagina precedente",
"Pad ": "Pad ",
" (Available)": " (Disponibile)",
" (Unsupported)": "",
" (Unconnected)": " (Non connesso)",
"HandHeld": "HandHeld",
"Rotation": "Rotazione",
" (Available)": " (Disponibile)",
"0 (Sideways)": "0 (Di lato)",
"90 (Flat)": "90 (Piatto)",
"180 (-Sideways)": "180 (-Di lato)",
"270 (Upside down)": "270 (Capovolto)",
"Colour": "Colore",
"Grey": "Grigio",
"Ironbow": "Ironbow",
"Green": "Verde",
"Red": "Rosso",
"Blue": "Blu",
"Light Target": "Bersaglio leggero",
"All leds": "Tutti i led",
"Bright group": "Gruppo brillante",
"Dim group": "Gruppo debole",
"None": "Nessuno",
"Gain": "Guadagno",
"Negative Image": "Immagine negativa",
"Normal image": "Immagine normale",
"Negative image": "Immagine negativa",
"Format": "Formato",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"320x240": "320x240",
"160x120": "160x120",
"80x60": "80x60",
"40x30": "40x30",
"20x15": "20×15",
"Controller": "Controller",
"Rotation": "Rotazione",
"Colour": "Colore",
"Light Target": "Bersaglio leggero",
"Gain": "Guadagno",
"Negative Image": "Immagine negativa",
"Format": "Formato",
"Trimming Format": "Formato di ritaglio",
"External Light Filter": "Filtro luce esterno",
"Load Default": "Carica predefinito",
"Themezer": "Themezer",
"Themezer Options": "",
"Nsfw": "",
"Page": "",
"Page %zu / %zu": "Page %zu / %zu",
"Enter Page Number": "",
"Bad Page": "",
"Download theme?": "",
"GitHub": "",
"Downloading json": "",
"Select asset to download for ": "",
"Installing ": "",
"Uninstalling ": "",
"Deleting ": "",
"Deleting": "",
"Pasting ": "",
"Pasting": "",
"Removing ": "",
"Scanning ": "",
"Creating ": "",
"Copying ": "",
"Trying to load ": "",
"Downloading ": "",
"Downloaded ": "",
"Removed ": "",
"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 ": "Sei sicuro di voler eliminare? ",
"Are you sure you wish to cancel?": "",
"If this message appears repeatedly, please open an issue.": ""
"No Internet": "Niente Internet",
"[Applet Mode]": "[Modalità applet]",
"Language": "Lingua"
}

View File

@@ -1,238 +1,114 @@
{
"[Applet Mode]": "Appletモード",
"No Internet": "インターネットなし",
"Files": "ファイル",
"Apps": "アプリ",
"Store": "AppStore",
"Menu": "メニュー",
"Options": "設定",
"OK": "確認",
"Back": "戻る",
"Select": "選択",
"Open": "開く",
"Launch": "起動",
"Info": "情報",
"Install": "インストール",
"Delete": "削除",
"Restart": "再起動",
"Changelog": "リリースノート",
"Details": "詳細",
"Update": "アップデート",
"Remove": "除去",
"Download": "ダウンロード",
"Next Page": "次のページ",
"Prev Page": "前のページ",
"Unstar": "お気に入り解除",
"Star": "お気に入り",
"System memory": "システムメモリ",
"microSD card": "SDメモリーカード",
"Yes": "はい",
"No": "いいえ",
"Enabled": "",
"Disabled": "",
"Options": "設定",
"Homebrew Options": "Homebrew設定",
"Sort By": "並べ替え",
"Sort Options": "並べ替え設定",
"Filter": "フィルター",
"Updated": "最近使った順",
"Size": "ファイルサイズ",
"Alphabetical": "アルファベット順",
"Decending": "降順",
"Ascending": "上昇",
"Sort": "並べ替え",
"Order": "順番",
"Search": "検索",
"Updated": "アップデート順",
"Updated (Star)": "アップデート順(お気に入り)",
"Downloads": "ダウンロード順",
"Size": "ファイルサイズ",
"Size (Star)": "ファイルサイズ(お気に入り)",
"Alphabetical": "アルファベット順",
"Alphabetical (Star)": "アルファベット順(お気に入り)",
"Likes": "いいね順",
"ID": "デベロッパー順",
"Decending": "降順",
"Descending (down)": "降順",
"Desc": "降順",
"Ascending": "上昇",
"Ascending (Up)": "上昇",
"Asc": "上昇",
"Menu Options": "メニュー設定",
"Header": "ヘッダー",
"Theme": "テーマ",
"Info": "情報",
"Delete": "消去",
"Hide Sphaira": "Sphairaを非表示",
"Are you sure you want to delete ": "消去してもよろしいですか ",
"Install Forwarder": "Forwarderのインストール",
"WARNING: Installing forwarders will lead to a ban!": "警告: ForwarderをインストールするとBANされます。",
"Back": "戻る",
"Install": "インストール",
"Fs": "ファイル",
"App": "アプリ",
"Menu": "メニュー",
"Homebrew": "Homebrew",
"FileBrowser": "ファイルブラウザ",
"Open": "開く",
"Theme Options": "テーマ設定",
"Select Theme": "テーマを選ぶ",
"Shuffle": "シャッフル",
"Music": "BGM",
"Network": "ネットワーク",
"Network Options": "ネットワーク設定",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink 接続",
"Nxlink Upload": "Nxlink アップロード",
"Nxlink Finished": "Nxlink 終了",
"Switch-Handheld!": "ハンドヘルド!",
"Switch-Docked!": "ドック接続!",
"Language": "言語",
"Auto": "自動",
"English": "English",
"Japanese": "日本語",
"French": "Français",
"German": "Deutsch",
"Italian": "Italiano",
"Spanish": "Español",
"Chinese": "中文",
"Korean": "한국어",
"Dutch": "Dutch",
"Portuguese": "Português",
"Russian": "Русский",
"Swedish": "Svenska",
"Logging": "ログの取得",
"Replace hbmenu on exit": "終了時に hbmenu を置き換える",
"Misc": "その他",
"Misc Options": "その他",
"Web": "ウェブブラウザ",
"Install forwarders": "Forwarderのインストール機能",
"Install location": "インストール経路",
"Show install warning": "警告文を示す",
"FileBrowser": "ファイルブラウザ",
"%zd files": "%zd個のファイル",
"%zd dirs": "%zd個のフォルダー",
"File Options": "ファイル設定",
"Show Hidden": "非表示ファイルを表示",
"Folders First": "フォルダーを優先",
"Hidden Last": "非表示ファイルを劣後",
"Yes": "はい",
"No": "いいえ",
"Network Options": "ネットワーク設定",
"Nxlink": "Nxlink",
"Check for update": "アップデートの確認",
"File Options": "ファイル設定",
"Cut": "切り取り",
"Copy": "コピー",
"Paste": "ペースト",
"Paste ": " ",
" file(s)?": "個のファイルをペーストしますか?",
"Rename": "名前の変更",
"Set New File Name": "新しい名前を入力",
"Advanced": "高度な",
"Advanced Options": "高度設定",
"Advanced Options": "ファイルの作成",
"Create File": "ファイルの作成",
"Set File Name": "名前を入力",
"Create Folder": "フォルダーの作成",
"Set Folder Name": "名前を入力",
"View as text": "テキストとして表示",
"View as text (unfinished)": "テキストとして表示 (未完成)",
"Empty...": "このフォルダーは空です",
"Open with DayBreak?": "DayBreakで開きますか?",
"Launch ": "起動しますか",
"Launch option for: ": "起動設定: ",
"Select launcher for: ": "起動ランチャーを選ぶ: ",
"Homebrew": "Homebrew",
"Homebrew Options": "Homebrew設定",
"Hide Sphaira": "Sphairaを非表示",
"Install Forwarder": "Forwarderのインストール",
"WARNING: Installing forwarders will lead to a ban!": "警告: ForwarderをインストールするとBANされます。",
"Installing Forwarder": "Forwarderのインストール中",
"Creating Program": "プログラム作成中",
"Creating Control": "コントロール作成中",
"Creating Meta": "メター作成中",
"Writing Nca": "Nca書き取り中",
"Updating ncm databse": "ncmのDBをアップデート中",
"Pushing application record": "アプリの記録をプッシュ中",
"Installed!": "インストール完了",
"Failed to install forwarder": "Forwarderのインストール失敗",
"Unstarred ": "お気に入り解除: ",
"Starred ": "お気に入りに登録: ",
"AppStore": "AppStore",
"Filter: %s | Sort: %s | Order: %s": "フィルター: %s | 並べ替え: %s | 順番: %s",
"Set Archive Bit": "アーカイブビットの設定",
"AppStore Options": "AppStoreの設定",
"All": "全て",
"Games": "ゲーム",
"Emulators": "エミュレータ",
"Tools": "ツール",
"Advanced": "高度な",
"Themes": "テーマ",
"Legacy": "レガシー",
"version: %s": "バージョン: %s",
"updated: %s": "更新日: %s",
"category: %s": "カテゴリー: %s",
"extracted: %.2f MiB": "容量: %.2f MiB",
"app_dls: %s": "ダウンロード: %s",
"More by Author": "ディベロッパーの他のアプリを見る",
"Leave Feedback": "意見を残す",
"Misc": "その他",
"Downloads": "ダウンロード",
"Filter": "フィルター",
"Search": "検索",
"Menu Options": "メニュー設定",
"Header": "ヘッダー",
"Theme": "テーマ",
"Network": "ネットワーク",
"Logging": "ログの取得",
"Enabled": "有効",
"Disabled": "無効",
"Replace hbmenu on exit": "終了時に hbmenu を置き換える",
"Misc Options": "その他",
"Themezer": "Themezer",
"Irs": "Joy-Con IRカメラ",
"Ambient Noise Level: ": "ノイズレベル: ",
"Controller": "コントローラー",
"Pad ": "Joy-Con ",
" (Available)": " (利用可能)",
" (Unsupported)": " (未対応)",
"Web": "ウェブブラウザ",
"Download": "ダウンロード",
"Next Page": "次のページ",
"Prev Page": "前のページ",
"Pad ": "パッド ",
" (Unconnected)": " (未接続)",
"HandHeld": "ハンドヘルド",
"Rotation": "回転",
"0 (Sideways)": "0 (横)",
"90 (Flat)": "90 (フラット)",
" (Available)": " (利用可能)",
"0 (Sideways)": "0(横)",
"90 (Flat)": "90(フラット)",
"180 (-Sideways)": "180 (-横)",
"270 (Upside down)": "270 (上下逆さま)",
"Colour": "色",
"270 (Upside down)": "270上下逆さま",
"Grey": "グレー",
"Ironbow": "アイアンボウ",
"Green": "緑",
"Red": "赤",
"Blue": "青",
"Light Target": "ライトターゲット",
"All leds": "すべてのLED",
"Bright group": "明るいグループ",
"Dim group": "薄暗いグループ",
"None": "なし",
"Gain": "増幅",
"Negative Image": "ネガティブなイメージ",
"Normal image": "通常画像",
"Negative image": "ネガティブなイメージ",
"Format": "解像度",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "トリミングされた解像度",
"20x15": "20x15",
"Controller": "コントローラー",
"Rotation": "回転",
"Colour": "色",
"Light Target": "ライトターゲット",
"Gain": "得",
"Negative Image": "ネガティブなイメージ",
"Format": "形式",
"Trimming Format": "トリミングフォーマット",
"External Light Filter": "外光フィルター",
"Load Default": "基本設定に戻す",
"Themezer": "Themezer",
"Themezer Options": "Themezer設定",
"Nsfw": "アダルトテーマ",
"Page": "ページ",
"Page %zu / %zu": "ページ %zu / %zu",
"Enter Page Number": "ページの番号を入力",
"Bad Page": "ページが見つかりません",
"Download theme?": "テーマをインストールしますか?",
"GitHub": "GitHub",
"Downloading json": "JSONからダウンロード",
"Select asset to download for ": "ダウンロードアイテムを選択 ",
"Installing ": "インストール中 ",
"Uninstalling ": "アンインストール中 ",
"Deleting ": "削除中 ",
"Deleting": "削除中",
"Pasting ": "ペースト中 ",
"Pasting": "ペースト中",
"Removing ": "除去中 ",
"Scanning ": "スキャン中 ",
"Creating ": "作成中 ",
"Copying ": "コピー中 ",
"Trying to load ": "サムネイルを取得中 ",
"Downloading ": "ダウンロード中 ",
"Downloaded ": "ダウンロード完了 ",
"Removed ": "除去完了 ",
"Checking MD5": "MD5を確認中 ",
"Loading...": "ロード中",
"Loading": "ロード中",
"Empty!": "何も見つかりません",
"Not Ready...": "準備ができていません",
"Error loading page!": "ページのロードエラー",
"Update avaliable: ": "アップデート可能: ",
"Download update: ": "アップデートをダウンロード: ",
"Updated to ": "アップデート: ",
"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.": "このメッセージが繰り返し表示される場合は、問題を開いてください。"
"Load Default": "デフォルトをロード",
"No Internet": "インターネットなし",
"[Applet Mode]": "Appletモード",
"Language": "言語"
}

View File

@@ -1,238 +1,114 @@
{
"[Applet Mode]": "[애플릿 모드]",
"No Internet": "인터넷 연결 없음",
"Files": "파일 탐색기",
"Apps": "홈브류",
"Store": "앱스토어",
"Menu": "메뉴",
"Options": "설정",
"OK": "확인",
"Back": "뒤로",
"Select": "선택",
"Open": "열기",
"Launch": "실행",
"Info": "정",
"Install": "설치",
"Delete": "삭제",
"Restart": "재시작",
"Changelog": "변경 내역",
"Details": "상세",
"Update": "업데이트",
"Remove": "제거",
"Download": "다운로드",
"Next Page": "다음 페이지",
"Prev Page": "이전 페이지",
"Unstar": "즐겨찾기 해제",
"Star": "즐겨찾기",
"System memory": "낸드 저장소",
"microSD card": "SD 카드",
"Yes": "예",
"No": "아니요",
"Enabled": "",
"Disabled": "",
"Options": "정",
"Homebrew Options": "홈브류 설정",
"Sort By": "정렬",
"Sort Options": "정렬 옵션",
"Filter": "필터",
"Sort Options": "정렬 설정",
"Updated": "업데이트순",
"Size": "크기순",
"Alphabetical": "알파벳순",
"Decending": "내림차순",
"Ascending": "오름차순",
"Sort": "분류",
"Order": "정렬",
"Search": "검색",
"Updated": "업데이트순",
"Updated (Star)": "업데이트순 (즐겨찾기)",
"Downloads": "다운로드순",
"Size": "크기순",
"Size (Star)": "크기순 (즐겨찾기)",
"Alphabetical": "알파벳순",
"Alphabetical (Star)": "알파벳순 (즐겨찾기)",
"Likes": "좋아요순",
"ID": "ID순",
"Decending": "내림차순",
"Descending (down)": "내림차순",
"Desc": "내림차순",
"Ascending": "오름차순",
"Ascending (Up)": "오름차순",
"Asc": "오름차순",
"Menu Options": "메뉴",
"Header": "헤더",
"Theme": "테마",
"Theme Options": "테마 옵션",
"Info": "정보",
"Delete": "삭제",
"Hide Sphaira": "Sphaira 숨기기",
"Are you sure you want to delete ": "정말 삭제하시겠습니까? ",
"Install Forwarder": "바로가기 설치",
"WARNING: Installing forwarders will lead to a ban!": "주의: 바로가기 설치시 BAN 위험이 있습니다!",
"Back": "뒤로",
"Install": "설치",
"Fs": "파일 탐색기",
"App": "",
"Menu": "메뉴",
"Homebrew": "홈브류",
"FileBrowser": "파일 탐색기",
"Open": "열기",
"Theme Options": "테마 설정",
"Select Theme": "테마 선택",
"Shuffle": "셔플",
"Music": "BGM",
"Network": "네트워크",
"Network Options": "네트워크 옵션",
"Ftp": "FTP (무선)",
"Mtp": "MTP (유선)",
"Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink 연결됨",
"Nxlink Upload": "Nxlink 업로드",
"Nxlink Finished": "Nxlink 종료됨",
"Switch-Handheld!": "휴대모드로 전환됨!",
"Switch-Docked!": "독 모드로 전환됨!",
"Language": "언어",
"Auto": "자동",
"English": "English",
"Japanese": "日本語",
"French": "Français",
"German": "Deutsch",
"Italian": "Italiano",
"Spanish": "Español",
"Chinese": "中文",
"Korean": "한국어",
"Dutch": "Dutch",
"Portuguese": "Português",
"Russian": "Русский",
"Swedish": "Svenska",
"Logging": "로깅",
"Replace hbmenu on exit": "hbmenu  sphaira 교체",
"Misc": "기타",
"Misc Options": "기타",
"Web": "웹 브라우저",
"Install forwarders": "바로가기 설치",
"Install location": "설치 위치",
"Show install warning": "설치 경고 표시",
"FileBrowser": "파일 탐색기",
"%zd files": "%zd 개 파일",
"%zd dirs": "%zd 개 폴더",
"File Options": "파일 옵션",
"Show Hidden": "숨겨진 항목 표시",
"Folders First": "폴더 우선 정렬",
"Hidden Last": "숨겨진 항목 후순 정렬",
"Yes": "예",
"No": "아니요",
"Network Options": "네트워크 설정",
"Nxlink": "Nxlink",
"Check for update": "업데이트 확인",
"File Options": "파일 설정",
"Cut": "잘라내기",
"Copy": "복사",
"Paste": "붙여넣기",
"Paste ": " ",
" file(s)?": "개 항목을 붙여넣을까요?",
"Rename": "이름 바꾸기",
"Set New File Name": "새 파일명 입력",
"Advanced": "고급",
"Advanced Options": "고급 옵션",
"Create File": "새 파일",
"Set File Name": "파일명 입력",
"Advanced Options": "고급 설정",
"Create File": "파일 생성",
"Create Folder": "새 폴더",
"Set Folder Name": "폴더명 입력",
"View as text": "텍스트로 보기",
"View as text (unfinished)": "텍스트로 보기 (미완성)",
"Empty...": "비어있음...",
"Open with DayBreak?": "DayBreak로 열까요?",
"Launch ": "실행할까요 ",
"Launch option for: ": "실행 옵션: ",
"Select launcher for: ": "실행 런처: ",
"Homebrew": "홈브류",
"Homebrew Options": "홈브류 옵션",
"Hide Sphaira": "Sphaira 숨기기",
"Install Forwarder": "바로가기 설치",
"WARNING: Installing forwarders will lead to a ban!": "경고: 시스낸드에서 바로가기 설치시 밴 위험이 있습니다!",
"Installing Forwarder": "바로가기 설치",
"Creating Program": "프로그램 생성",
"Creating Control": "컨트롤 생성",
"Creating Meta": "메타 생성",
"Writing Nca": "Nca 쓰기",
"Updating ncm databse": "Ncm 데이터베이스 업데이트",
"Pushing application record": "응용 프로그램 기록 푸싱",
"Installed!": "설치 완료!",
"Failed to install forwarder": "바로가기 설치 실패",
"Unstarred ": "즐겨찾기 해제: ",
"Starred ": "즐겨찾기 적용: ",
"AppStore": "앱스토어",
"Filter: %s | Sort: %s | Order: %s": "필터: %s | 분류: %s | 정렬: %s",
"AppStore Options": "앱스토어 옵션",
"All": "모두",
"Set Archive Bit": "아카이브 비트 설정",
"AppStore Options": "앱스토어 설정",
"All": "전체",
"Games": "게임",
"Emulators": "에뮬레이터",
"Tools": "도구",
"Advanced": "고급",
"Themes": "테마",
"Legacy": "레거시",
"version: %s": "버전: %s",
"updated: %s": "업데이트: %s",
"category: %s": "카테고리: %s",
"extracted: %.2f MiB": "용량: %.2f MiB",
"app_dls: %s": "다운로드 횟수: %s",
"More by Author": "개발자의 다른 앱 더 보기",
"Leave Feedback": "피드백 남기기",
"Irs": "조이콘 적외선 카메라",
"Ambient Noise Level: ": "주변 노이즈 레벨: ",
"Controller": "컨트롤러",
"Pad ": "조이콘 ",
" (Available)": " (사용 가능)",
" (Unsupported)": " (지원 안됨)",
"Misc": "기타",
"Downloads": "다운로드순",
"Filter": "필터",
"Search": "검색",
"Menu Options": "메뉴",
"Header": "헤더",
"Theme": "테마",
"Network": "네트워크",
"Logging": "로깅",
"Enabled": "",
"Disabled": "",
"Replace hbmenu on exit": "종료 시 hbmenu 교체",
"Misc Options": "기타",
"Themezer": "Themezer",
"Irs": "Joy-Con IR 카메라",
"Web": "웹 브라우저",
"Download": "다운로드",
"Next Page": "다음 페이지",
"Prev Page": "이전 페이지",
"Pad ": "Joy-Con ",
" (Unconnected)": " (연결 없음)",
"HandHeld": "본체 연결",
"Rotation": "화면 회전",
"0 (Sideways)": "반시계방향 90° 회전",
"90 (Flat)": "정방향",
"180 (-Sideways)": "시계방향 90° 회전",
"270 (Upside down)": "상하반전",
"Colour": "색상",
"Grey": "회색",
" (Available)": " (사용 가능)",
"0 (Sideways)": "0 (좌회전)",
"90 (Flat)": "90 (정방향)",
"180 (-Sideways)": "180 (우회전)",
"270 (Upside down)": "270 (역전)",
"Grey": "그레이",
"Ironbow": "아이언보우",
"Green": "초록색",
"Red": "빨간색",
"Blue": "파란색",
"Light Target": "반사 표적",
"Green": "그린",
"Red": "레드",
"Blue": "블루",
"All leds": "모든 LED 켜기",
"Bright group": "Bright LED 켜기",
"Dim group": "Dim LED 켜기",
"None": "LED 끄기",
"Gain": "대비",
"Negative Image": "화상 이미지",
"Normal image": "일반",
"Negative image": "반전",
"Format": "해상도",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Controller": "컨트롤러",
"Rotation": "화면 회전",
"Colour": "팔레트",
"Light Target": "반사 표적",
"Gain": "대비",
"Negative Image": "화상 이미지",
"Format": "해상도",
"Trimming Format": "트리밍 해상도",
"External Light Filter": "외부 조명 필터",
"Load Default": "기본값으로 설정",
"Themezer": "Themezer",
"Themezer Options": "Themezer 옵션",
"Nsfw": "선정성 테마",
"Page": "페이지",
"Page %zu / %zu": "페이지 %zu / %zu",
"Enter Page Number": "페이지 번호 입력",
"Bad Page": "잘못된 페이지",
"Download theme?": "테마를 다운로드할까요?",
"GitHub": "GitHub",
"Downloading json": "JSON에서 다운로드",
"Select asset to download for ": "다운로드 아이템 선택 ",
"Installing ": "설치 ",
"Uninstalling ": "설치 제거 ",
"Deleting ": "삭제 ",
"Deleting": "삭제",
"Pasting ": "붙여넣기 ",
"Pasting": "붙여넣기",
"Removing ": "제거 ",
"Scanning ": "스캔 ",
"Creating ": "생성 ",
"Copying ": "복사 ",
"Trying to load ": "썸네일 받아오는 중... ",
"Downloading ": "다운로드 ",
"Downloaded ": "다운로드 완료: ",
"Removed ": "제거 됨: ",
"Checking MD5": "MD5 확인",
"Loading...": "로딩 중...",
"Loading": "로딩 중...",
"Empty!": "찾을 수 없습니다!",
"Not Ready...": "준비되지 않음...",
"Error loading page!": "페이지 로딩 오류!",
"Update avaliable: ": "업데이트 가능: ",
"Download update: ": "업데이트 다운로드: ",
"Updated to ": "업데이트: ",
"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.": "해당 메시지가 반복해서 나타나는 경우, 이슈를 등록하세요."
"No Internet": "네트워크 연결 없음",
"[Applet Mode]": "[애플릿 모드]",
"Language": "언어"
}

View File

@@ -1,238 +1,114 @@
{
"[Applet Mode]": "[Applet-modus]",
"No Internet": "Geen internet",
"Files": "",
"Apps": "",
"Store": "",
"Menu": "Menu",
"Options": "Opties",
"OK": "",
"Back": "Terug",
"Select": "",
"Open": "Open",
"Launch": "Launch",
"Info": "Info",
"Install": "Installeren",
"Delete": "Verwijderen",
"Restart": "",
"Changelog": "",
"Details": "",
"Update": "",
"Remove": "",
"Download": "Downloaden",
"Next Page": "Volgende pagina",
"Prev Page": "Vorige pagina",
"Unstar": "",
"Star": "",
"System memory": "",
"microSD card": "",
"Yes": "Ja",
"No": "Nee",
"Enabled": "Ingeschakeld",
"Disabled": "Gehandicapt",
"Options": "Opties",
"Homebrew Options": "Homebrew-opties",
"Sort By": "Sorteer op",
"Sort Options": "Sorteeropties",
"Filter": "Filter",
"Updated": "Bijgewerkt",
"Size": "Maat",
"Alphabetical": "Alfabetisch",
"Decending": "Aflopend",
"Ascending": "Oplopend",
"Sort": "Soort",
"Order": "Volgorde",
"Search": "Zoekopdracht",
"Updated": "Bijgewerkt",
"Updated (Star)": "",
"Downloads": "Downloads",
"Size": "Maat",
"Size (Star)": "",
"Alphabetical": "Alfabetisch",
"Alphabetical (Star)": "",
"Likes": "",
"ID": "",
"Decending": "Aflopend",
"Descending (down)": "Aflopend",
"Desc": "Aflopend",
"Ascending": "Oplopend",
"Ascending (Up)": "Oplopend",
"Asc": "Oplopend",
"Menu Options": "Menu-opties",
"Header": "Koptekst",
"Theme": "Thema",
"Info": "Info",
"Delete": "Verwijderen",
"Hide Sphaira": "Verberg Sphaira",
"Are you sure you want to delete ": "Weet u zeker dat u wilt verwijderen ",
"Install Forwarder": "Forwarder installeren",
"WARNING: Installing forwarders will lead to a ban!": "WAARSCHUWING: Het installeren van forwarders leidt tot een ban!",
"Back": "Terug",
"Install": "Installeren",
"Fs": "Fs",
"App": "App",
"Menu": "Menu",
"Homebrew": "Zelf brouwen",
"FileBrowser": "Bestandsbrowser",
"Open": "Open",
"Theme Options": "Thema Opties",
"Select Theme": "Selecteer Thema",
"Shuffle": "Schudden",
"Music": "Muziek",
"Network": "Netwerk",
"Network Options": "Netwerkopties",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink",
"Nxlink Connected": "",
"Nxlink Upload": "",
"Nxlink Finished": "",
"Switch-Handheld!": "",
"Switch-Docked!": "",
"Language": "Taal",
"Auto": "",
"English": "English",
"Japanese": "日本語",
"French": "Français",
"German": "Deutsch",
"Italian": "Italiano",
"Spanish": "Español",
"Chinese": "中文",
"Korean": "한국어",
"Dutch": "Dutch",
"Portuguese": "Português",
"Russian": "Русский",
"Swedish": "Svenska",
"Logging": "Loggen",
"Replace hbmenu on exit": "Vervang hbmenu bij afsluiten",
"Misc": "Diversen",
"Misc Options": "Diverse opties",
"Web": "Web",
"Install forwarders": "",
"Install location": "",
"Show install warning": "",
"FileBrowser": "Bestandsbrowser",
"%zd files": "%zd files",
"%zd dirs": "%zd dirs",
"File Options": "Bestandsopties",
"Show Hidden": "Toon verborgen",
"Folders First": "Mappen eerst",
"Hidden Last": "Verborgen laatste",
"Yes": "Ja",
"No": "Nee",
"Network Options": "Netwerkopties",
"Nxlink": "Nxlink",
"Check for update": "Controleer op update",
"File Options": "Bestandsopties",
"Cut": "Snee",
"Copy": "Kopiëren",
"Paste": "",
"Paste ": "",
" file(s)?": "",
"Rename": "Hernoemen",
"Set New File Name": "",
"Advanced": "Geavanceerd",
"Advanced Options": "Bestand maken",
"Create File": "Bestand maken",
"Set File Name": "",
"Create Folder": "Map maken",
"Set Folder Name": "",
"View as text": "Bekijk als tekst",
"View as text (unfinished)": "Bekijk als tekst (onvoltooid)",
"Empty...": "",
"Open with DayBreak?": "",
"Launch ": "",
"Launch option for: ": "",
"Select launcher for: ": "",
"Homebrew": "Zelf brouwen",
"Homebrew Options": "Homebrew-opties",
"Hide Sphaira": "Verberg Sphaira",
"Install Forwarder": "Forwarder installeren",
"WARNING: Installing forwarders will lead to a ban!": "WAARSCHUWING: Het installeren van forwarders leidt tot een ban!",
"Installing Forwarder": "",
"Creating Program": "",
"Creating Control": "",
"Creating Meta": "",
"Writing Nca": "",
"Updating ncm databse": "",
"Pushing application record": "",
"Installed!": "",
"Failed to install forwarder": "",
"Unstarred ": "",
"Starred ": "",
"AppStore": "",
"Filter: %s | Sort: %s | Order: %s": "Filter: %s | Soort: %s | Volgorde: %s",
"Set Archive Bit": "Archiefbit instellen",
"AppStore Options": "AppStore-opties",
"All": "Alle",
"Games": "Spellen",
"Emulators": "Emulators",
"Tools": "Hulpmiddelen",
"Advanced": "Geavanceerd",
"Themes": "Thema's",
"Legacy": "Nalatenschap",
"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": "",
"Misc": "Diversen",
"Downloads": "Downloads",
"Filter": "Filter",
"Search": "Zoekopdracht",
"Menu Options": "Menu-opties",
"Header": "Koptekst",
"Theme": "Thema",
"Network": "Netwerk",
"Logging": "Loggen",
"Enabled": "Ingeschakeld",
"Disabled": "Gehandicapt",
"Replace hbmenu on exit": "Vervang hbmenu bij afsluiten",
"Misc Options": "Diverse opties",
"Themezer": "Themamaker",
"Irs": "Ir",
"Ambient Noise Level: ": "",
"Controller": "Controleur",
"Web": "Web",
"Download": "Downloaden",
"Next Page": "Volgende pagina",
"Prev Page": "Vorige pagina",
"Pad ": "Pad ",
" (Available)": " (Beschikbaar)",
" (Unsupported)": "",
" (Unconnected)": " (Niet verbonden)",
"HandHeld": "Handbediende",
"Rotation": "Rotatie",
" (Available)": " (Beschikbaar)",
"0 (Sideways)": "0 (zijwaarts)",
"90 (Flat)": "90 (plat)",
"180 (-Sideways)": "180 (-zijwaarts)",
"270 (Upside down)": "270 (ondersteboven)",
"Colour": "Kleur",
"Grey": "Grijs",
"Ironbow": "Ijzerboog",
"Green": "Groente",
"Red": "Rood",
"Blue": "Blauw",
"Light Target": "Licht doel",
"All leds": "Alle leds",
"Bright group": "Heldere groep",
"Dim group": "Dim groep",
"None": "Geen",
"Gain": "Verdienen",
"Negative Image": "Negatief beeld",
"Normal image": "Normaal beeld",
"Negative image": "Negatief beeld",
"320x240": "320x240",
"160x120": "160x120",
"80x60": "80x60",
"40x30": "40x30",
"20x15": "20x15",
"Controller": "Controleur",
"Rotation": "Rotatie",
"Colour": "Kleur",
"Light Target": "Licht doel",
"Gain": "Verdienen",
"Negative Image": "Negatief beeld",
"Format": "Formaat",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "Trimformaat",
"External Light Filter": "Extern lichtfilter",
"Load Default": "Standaard laden",
"Themezer": "Themamaker",
"Themezer Options": "",
"Nsfw": "",
"Page": "",
"Page %zu / %zu": "Page %zu / %zu",
"Enter Page Number": "",
"Bad Page": "",
"Download theme?": "",
"GitHub": "",
"Downloading json": "",
"Select asset to download for ": "",
"Installing ": "",
"Uninstalling ": "",
"Deleting ": "",
"Deleting": "",
"Pasting ": "",
"Pasting": "",
"Removing ": "",
"Scanning ": "",
"Creating ": "",
"Copying ": "",
"Trying to load ": "",
"Downloading ": "",
"Downloaded ": "",
"Removed ": "",
"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 ": "Weet u zeker dat u wilt verwijderen ",
"Are you sure you wish to cancel?": "",
"If this message appears repeatedly, please open an issue.": ""
"No Internet": "Geen internet",
"[Applet Mode]": "[Applet-modus]",
"Language": "Taal"
}

View File

@@ -1,238 +1,114 @@
{
"[Applet Mode]": "[Modo Applet]",
"No Internet": "Sem Internet",
"Files": "Arquivos",
"Apps": "Aplicativos",
"Store": "Loja",
"Menu": "Menu",
"Options": "Opções",
"OK": "OK",
"Back": "Voltar",
"Select": "Selecionar",
"Open": "Abrir",
"Launch": "Iniciar",
"Info": "Informações",
"Install": "Instalar",
"Delete": "Excluir",
"Restart": "Reiniciar",
"Changelog": "Changelog",
"Details": "Detalhes",
"Update": "Atualizar",
"Remove": "Remover",
"Download": "Download",
"Next Page": "Próxima página",
"Prev Page": "Página anterior",
"Unstar": "Desfavoritar",
"Star": "Favoritar",
"System memory": "Memória do console",
"microSD card": "Cartão microSD",
"Yes": "Sim",
"No": "Não",
"Enabled": "Habilitado",
"Disabled": "Desabilitado",
"Options": "Opções",
"Homebrew Options": "Opções do Homebrew",
"Sort By": "Ordenar por",
"Sort Options": "Opções de classificação",
"Filter": "Filtro",
"Updated": "Atualizado",
"Size": "Tamanho",
"Alphabetical": "Alfabético",
"Decending": "Decrescente",
"Ascending": "Ascendente",
"Sort": "Organizar",
"Order": "Ordem",
"Search": "Procurar",
"Updated": "Atualizado",
"Updated (Star)": "Atualizado (Favoritos)",
"Downloads": "Downloads",
"Size": "Tamanho",
"Size (Star)": "Tamanho (Favoritos)",
"Alphabetical": "Alfabético",
"Alphabetical (Star)": "Alfabético (Favoritos)",
"Likes": "Curtidas",
"ID": "ID",
"Decending": "Decrescente",
"Descending (down)": "Decrescente (Baixo)",
"Desc": "Decr.",
"Ascending": "Ascendente",
"Ascending (Up)": "Ascendente (Cima)",
"Asc": "Asc.",
"Menu Options": "Opções do menu",
"Header": "Cabeçalho",
"Theme": "Tema",
"Info": "Informações",
"Delete": "Excluir",
"Hide Sphaira": "Esconder Sphaira",
"Are you sure you want to delete ": "Excluir ",
"Install Forwarder": "Instalar forwarder",
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Isso pode resultar em um banimento!",
"Back": "Voltar",
"Install": "Instalar",
"Fs": "Fs",
"App": "Aplicativo",
"Menu": "Menu",
"Homebrew": "Homebrew",
"FileBrowser": "Navegador de arquivos",
"Open": "Abrir",
"Theme Options": "Opções de tema",
"Select Theme": "Selecionar tema",
"Shuffle": "Embaralhar",
"Music": "Música",
"Network": "Rede",
"Network Options": "Opções de rede",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink conectado",
"Nxlink Upload": "Envio Nxlink",
"Nxlink Finished": "Nxlink finalizado",
"Switch-Handheld!": "Switch-Portátil",
"Switch-Docked!": "Switch-Docado",
"Language": "Idioma",
"Auto": "Automático",
"English": "English",
"Japanese": "日本語",
"French": "Français",
"German": "Deutsch",
"Italian": "Italiano",
"Spanish": "Español",
"Chinese": "中文",
"Korean": "한국어",
"Dutch": "Dutch",
"Portuguese": "Português",
"Russian": "Русский",
"Swedish": "Svenska",
"Logging": "Logging",
"Replace hbmenu on exit": "Substituir hbmenu ao sair",
"Misc": "Diversos",
"Misc Options": "Opções diversas",
"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 arquivo(s)",
"%zd dirs": "%zd diretório(s)",
"File Options": "Opções de arquivo",
"Show Hidden": "Mostrar ocultos",
"Show Hidden": "Mostrar oculto",
"Folders First": "Pastas primeiro",
"Hidden Last": "Ocultos por último",
"Hidden Last": "Oculto por último",
"Yes": "Sim",
"No": "Não",
"Network Options": "Opções de rede",
"Nxlink": "Nxlink",
"Check for update": "Verificar se há atualização",
"File Options": "Opções de arquivo",
"Cut": "Cortar",
"Copy": "Copiar",
"Paste": "Colar",
"Paste ": "Colar",
" file(s)?": " arquivo(s)?",
"Rename": "Renomear",
"Set New File Name": "Definir novo nome do arquivo",
"Advanced": "Avançado",
"Advanced Options": "Opções avançadas",
"Advanced Options": "Criar arquivo",
"Create File": "Criar arquivo",
"Set File Name": "Definir nome do arquivo",
"Create Folder": "Criar pasta",
"Set Folder Name": "Definir novo nome da pasta",
"View as text": "Ver como texto",
"View as text (unfinished)": "Ver como texto (inacabado)",
"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": "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",
"Filter: %s | Sort: %s | Order: %s": "Filtro: %s | Organizar: %s | Ordem: %s",
"Set Archive Bit": "Definir bit de arquivo",
"AppStore Options": "Opções da AppStore",
"All": "Todos",
"Games": "Jogos",
"Emulators": "Emuladores",
"Tools": "Ferramentas",
"Advanced": "Avançado",
"Themes": "Temas",
"Legacy": "Legado",
"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",
"Misc": "Diversos",
"Downloads": "Downloads",
"Filter": "Filtro",
"Search": "Procurar",
"Menu Options": "Opções do menu",
"Header": "Cabeçalho",
"Theme": "Tema",
"Network": "Rede",
"Logging": "Logging",
"Enabled": "Habilitado",
"Disabled": "Desabilitado",
"Replace hbmenu on exit": "Substitua hbmenu ao sair",
"Misc Options": "Opções diversas",
"Themezer": "Themezer",
"Irs": "Irs",
"Ambient Noise Level: ": "Nível de ruído ambiente",
"Controller": "Controle",
"Web": "Rede",
"Download": "Download",
"Next Page": "Próxima página",
"Prev Page": "Página anterior",
"Pad ": "Pad ",
" (Available)": " (Disponível)",
" (Unsupported)": "(Não suportado)",
" (Unconnected)": " (Desconectado)",
"HandHeld": "Portátil",
"Rotation": "Rotação",
" (Available)": " (Disponível)",
"0 (Sideways)": "0 (Lateralmente)",
"90 (Flat)": "90 (plano)",
"180 (-Sideways)": "180 (-Lateralmente)",
"270 (Upside down)": "270 (De cabeça para baixo)",
"Colour": "Cor",
"Grey": "Cinza",
"Ironbow": "Arco de ferro",
"Green": "Verde",
"Red": "Vermelho",
"Blue": "Azul",
"Light Target": "Alvo leve",
"All leds": "Todos os LEDs",
"Bright group": "Grupo claro",
"Dim group": "Grupo escuro",
"None": "Nenhum",
"Gain": "Ganho",
"Negative Image": "Imagem negativa",
"Normal image": "Imagem normal",
"Negative image": "Imagem negativa",
"Format": "Formato",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"320x240": "320x240",
"160x120": "160x120",
"80x60": "80x60",
"40x30": "40x30",
"20x15": "20x15",
"Controller": "Controle",
"Rotation": "Rotação",
"Colour": "Cor",
"Light Target": "Alvo leve",
"Gain": "Ganho",
"Negative Image": "Imagem negativa",
"Format": "Formatar",
"Trimming Format": "Formato de corte",
"External Light Filter": "Filtro de luz externa",
"External Light Filter": "Filtro de luz externo",
"Load Default": "Carregar padrão",
"Themezer": "Themezer",
"Themezer Options": "Opções do Themezer",
"Nsfw": "NSFW",
"Page": "Página",
"Page %zu / %zu": "Page %zu / %zu",
"Enter Page Number": "Digite o número da página",
"Bad Page": "Página inválida",
"Download theme?": "Baixar tema?",
"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."
"No Internet": "Sem Internet",
"[Applet Mode]": "[Modo Applet]",
"Language": "Idioma"
}

View File

@@ -1,238 +1,114 @@
{
"[Applet Mode]": "[Режим апплета]",
"No Internet": "Нет Интернета",
"Files": "",
"Apps": "",
"Store": "",
"Menu": "Меню",
"Options": "Параметры темы",
"OK": "",
"Back": "Назад",
"Select": "",
"Open": "Открыть",
"Launch": "Запуск",
"Info": "Информация",
"Install": "Установить",
"Delete": "Удалить",
"Restart": "",
"Changelog": "",
"Details": "",
"Update": "",
"Remove": "",
"Download": "Скачать",
"Next Page": "Следующая страница",
"Prev Page": "Предыдущая страница",
"Unstar": "",
"Star": "",
"System memory": "",
"microSD card": "",
"Yes": "Да",
"No": "Нет",
"Enabled": "Включено",
"Disabled": "Отключено",
"Options": "Параметры",
"Homebrew Options": "Варианты домашнего пивоварения",
"Sort By": "Сортировать по",
"Sort Options": "Параметры сортировки",
"Filter": "Фильтр",
"Sort": "Сортировать",
"Order": "Порядок",
"Search": "Поиск",
"Updated": "Обновлено",
"Updated (Star)": "",
"Downloads": "Загрузки",
"Size": "Размер",
"Size (Star)": "",
"Alphabetical": "По наименованию",
"Alphabetical (Star)": "",
"Likes": "",
"ID": "",
"Decending": "По убыванию",
"Descending (down)": "По убыванию",
"Desc": "По убыванию",
"Ascending": "По возрастанию",
"Ascending (Up)": "По возрастанию",
"Asc": "По возрастанию",
"Menu Options": "Параметры меню",
"Header": "Заголовок",
"Theme": "Тема",
"Alphabetical": "Алфавитный",
"Decending": "по убыванию",
"Ascending": "восходящий",
"Sort": "Сортировать",
"Order": "Заказ",
"Info": "Информация",
"Delete": "Удалить",
"Hide Sphaira": "Скрыть Сфаиру",
"Are you sure you want to delete ": "Вы уверены, что хотите удалить ",
"Install Forwarder": "Установить переадресатор",
"WARNING: Installing forwarders will lead to a ban!": "ВНИМАНИЕ: Установка форвардеров приведет к бану!",
"Back": "Назад",
"Install": "Установить",
"Fs": "Фс",
"App": "Приложение",
"Menu": "Меню",
"Homebrew": "Домашнее пиво",
"FileBrowser": "ФайлБраузер",
"Open": "Открыть",
"Theme Options": "Параметры темы",
"Select Theme": "Выберите тему",
"Shuffle": "Перетасовать",
"Music": "Музыка",
"Network": "Сеть",
"Network Options": "Параметры сети",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink",
"Nxlink Connected": "",
"Nxlink Upload": "",
"Nxlink Finished": "",
"Switch-Handheld!": "",
"Switch-Docked!": "",
"Language": "Язык",
"Auto": "",
"English": "English",
"Japanese": "日本語",
"French": "Français",
"German": "Deutsch",
"Italian": "Italiano",
"Spanish": "Español",
"Chinese": "中文",
"Korean": "한국어",
"Dutch": "Dutch",
"Portuguese": "Português",
"Russian": "Русский",
"Swedish": "Svenska",
"Logging": "Журналирование",
"Replace hbmenu on exit": "Заменить hbmenu при выходе",
"Misc": "Прочее",
"Misc Options": "Прочие параметры",
"Web": "Интернет",
"Install forwarders": "",
"Install location": "",
"Show install warning": "",
"FileBrowser": "Файловый менеджер",
"%zd files": "%zd files",
"%zd dirs": "%zd dirs",
"File Options": "Параметры файла",
"Show Hidden": "Показать скрытые",
"Show Hidden": "Показать скрытое",
"Folders First": "Папки в первую очередь",
"Hidden Last": "Скрытые в последнюю очередь",
"Cut": "Вырезать",
"Hidden Last": "Скрытый последний",
"Yes": "Да",
"No": "Нет",
"Network Options": "Параметры сети",
"Nxlink": "Нкслинк",
"Check for update": "Проверить наличие обновлений",
"File Options": "Параметры файла",
"Cut": "Резать",
"Copy": "Копировать",
"Paste": "",
"Paste ": "",
" file(s)?": "",
"Rename": "Переименовать",
"Set New File Name": "",
"Advanced": "Продвинутые",
"Advanced Options": "Расширенные параметры",
"Advanced Options": "Создать файл",
"Create File": "Создать файл",
"Set File Name": "",
"Create Folder": "Создать папку",
"Set Folder Name": "",
"View as text": "Посмотреть как текст",
"View as text (unfinished)": "Посмотреть как текст (незакончено)",
"Empty...": "",
"Open with DayBreak?": "",
"Launch ": "",
"Launch option for: ": "",
"Select launcher for: ": "",
"Homebrew": "Homebrew",
"Homebrew Options": "Параметры Homebrew",
"Hide Sphaira": "Скрыть Sphaira",
"Install Forwarder": "Установить форвардер",
"WARNING: Installing forwarders will lead to a ban!": "ВНИМАНИЕ: Установка форвардеров приведет к бану!",
"Installing Forwarder": "Установить форвардер",
"Creating Program": "",
"Creating Control": "",
"Creating Meta": "",
"Writing Nca": "",
"Updating ncm databse": "",
"Pushing application record": "",
"Installed!": "",
"Failed to install forwarder": "",
"Unstarred ": "",
"Starred ": "",
"AppStore": "",
"Filter: %s | Sort: %s | Order: %s": "Фильтр: %s | Сортировать: %s | Порядок: %s",
"Set Archive Bit": "Установить бит архива",
"AppStore Options": "Параметры магазина приложений",
"All": "Все",
"Games": "Игры",
"Emulators": "Эмуляторы",
"Tools": "Инструменты",
"Advanced": "Передовой",
"Themes": "Темы",
"Legacy": "Легаси",
"version: %s": "version: %s",
"updated: %s": "updated: %s",
"category: %s": "category: %s",
"extracted: %.2f MiB": "extracted: %.2f MiB",
"app_dls: %s": "app_dls: %s",
"More by Author": "",
"Leave Feedback": "",
"Irs": "Irs",
"Ambient Noise Level: ": "",
"Controller": "Контроллер",
"Pad ": "Pad ",
" (Available)": " (Доступно)",
" (Unsupported)": "",
"Legacy": "Наследие",
"Misc": "Разное",
"Downloads": "Загрузки",
"Filter": "Фильтр",
"Search": "Поиск",
"Menu Options": "Опции меню",
"Header": "Заголовок",
"Theme": "Тема",
"Network": "Сеть",
"Logging": "Ведение журнала",
"Enabled": "Включено",
"Disabled": "Неполноценный",
"Replace hbmenu on exit": "Заменить hbmenu при выходе",
"Misc Options": "Разные параметры",
"Themezer": "Темезер",
"Irs": "IRS",
"Web": "Интернет",
"Download": "Скачать",
"Next Page": "Следующая страница",
"Prev Page": "Предыдущая страница",
"Pad ": "Подушка ",
" (Unconnected)": " (Не подключено)",
"HandHeld": "Портативный",
"Rotation": "Вращение",
"0 (Sideways)": "0 (набок)",
"90 (Flat)": "90 (ровно)",
"HandHeld": "Ручной",
" (Available)": " (Доступный)",
"0 (Sideways)": "0 (вбок)",
"90 (Flat)": "90 (квартира)",
"180 (-Sideways)": "180 (-вбок)",
"270 (Upside down)": "270 (перевернуто)",
"Colour": "Цвет",
"270 (Upside down)": "270 (перевернутый)",
"Grey": "Серый",
"Ironbow": "Стальной",
"Ironbow": "Железный лук",
"Green": "Зеленый",
"Red": "Красный",
"Blue": "Синий",
"Light Target": "Световая мишень",
"All leds": "Все светодиоды",
"Bright group": "Яркая группа",
"Dim group": "Тусклая группа",
"None": "Никто",
"Normal image": "Обычное изображение",
"Negative image": "Негативный имидж",
"320x240": "320x240",
"160x120": "160x120",
"80x60": "80х60",
"40x30": "40x30",
"20x15": "20x15",
"Controller": "Контроллер",
"Rotation": "Вращение",
"Colour": "Цвет",
"Light Target": "Легкая мишень",
"Gain": "Прирост",
"Negative Image": "Негативное изображение",
"Normal image": "Обычное изображение",
"Negative image": "Негативное изображение",
"Format": "Формат",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "Формат обрезки",
"External Light Filter": "Внешний светофильтр",
"Load Default": "Загрузить умолчания",
"Themezer": "Themezer",
"Themezer Options": "",
"Nsfw": "",
"Page": "",
"Page %zu / %zu": "Page %zu / %zu",
"Enter Page Number": "",
"Bad Page": "",
"Download theme?": "",
"GitHub": "",
"Downloading json": "",
"Select asset to download for ": "",
"Installing ": "",
"Uninstalling ": "",
"Deleting ": "",
"Deleting": "",
"Pasting ": "",
"Pasting": "",
"Removing ": "",
"Scanning ": "",
"Creating ": "",
"Copying ": "",
"Trying to load ": "",
"Downloading ": "",
"Downloaded ": "",
"Removed ": "",
"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 ": "Вы уверены, что хотите удалить ",
"Are you sure you wish to cancel?": "",
"If this message appears repeatedly, please open an issue.": ""
"Load Default": "Загрузить по умолчанию",
"No Internet": "Нет Интернета",
"[Applet Mode]": "[Режим апплета]",
"Language": "Язык"
}

View File

@@ -1,238 +0,0 @@
{
"[Applet Mode]": "[Applet-läge]",
"No Internet": "Ingen internetanslutning",
"Files": "Filer",
"Apps": "Appar",
"Store": "Butik",
"Menu": "Meny",
"Options": "Alternativ",
"OK": "OK",
"Back": "Tillbaka",
"Select": "Välj",
"Open": "Öppna",
"Launch": "Starta",
"Info": "Info",
"Install": "Installera",
"Delete": "Radera",
"Restart": "",
"Changelog": "Ändringslogg",
"Details": "Detaljer",
"Update": "Uppdatera",
"Remove": "Ta bort",
"Download": "Ladda ner",
"Next Page": "Nästa sida",
"Prev Page": "Föregående sida",
"Unstar": "",
"Star": "",
"System memory": "",
"microSD card": "",
"Yes": "Ja",
"No": "Nej",
"Enabled": "Aktiverad",
"Disabled": "Avaktiverad",
"Sort By": "Sortera efter",
"Sort Options": "Sorteringsalternativ",
"Filter": "Filter",
"Sort": "Sortera",
"Order": "Ordning",
"Search": "Sök",
"Updated": "Uppdaterad",
"Updated (Star)": "",
"Downloads": "Nedladdningar",
"Size": "Storlek",
"Size (Star)": "",
"Alphabetical": "Alfabetisk",
"Alphabetical (Star)": "",
"Likes": "Gillar",
"ID": "ID",
"Decending": "Fallande",
"Descending (down)": "Fallande (nedåt)",
"Desc": "Fallande",
"Ascending": "Stigande",
"Ascending (Up)": "Stigande (uppåt)",
"Asc": "Stigande",
"Menu Options": "Menyalternativ",
"Header": "Rubrik",
"Theme": "Tema",
"Theme Options": "Temaalternativ",
"Select Theme": "Välj tema",
"Shuffle": "Blanda",
"Music": "Musik",
"Network": "Nätverk",
"Network Options": "Nätverksalternativ",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink ansluten",
"Nxlink Upload": "Nxlink uppladdning",
"Nxlink Finished": "Nxlink klar",
"Switch-Handheld!": "",
"Switch-Docked!": "",
"Language": "Språk",
"Auto": "Auto",
"English": "Engelska",
"Japanese": "Japanska",
"French": "Franska",
"German": "Tyska",
"Italian": "Italienska",
"Spanish": "Spanska",
"Chinese": "Kinesiska",
"Korean": "Koreanska",
"Dutch": "Holländska",
"Portuguese": "Portugisiska",
"Russian": "Ryska",
"Swedish": "Svenska",
"Logging": "Loggning",
"Replace hbmenu on exit": "Ersätt hbmenu vid avslut",
"Misc": "Övrigt",
"Misc Options": "Övriga alternativ",
"Web": "Webb",
"Install forwarders": "",
"Install location": "",
"Show install warning": "",
"FileBrowser": "Filbläddrare",
"%zd files": "%zd filer",
"%zd dirs": "%zd kataloger",
"File Options": "Filalternativ",
"Show Hidden": "Visa dolda",
"Folders First": "Mappar först",
"Hidden Last": "Dolda sist",
"Cut": "Klipp ut",
"Copy": "Kopiera",
"Paste": "Klistra in",
"Paste ": "Klistra in ",
" file(s)?": " fil(er)?",
"Rename": "Byt namn",
"Set New File Name": "Ange nytt filnamn",
"Advanced": "Avancerat",
"Advanced Options": "Avancerade alternativ",
"Create File": "Skapa fil",
"Set File Name": "Ange filnamn",
"Create Folder": "Skapa mapp",
"Set Folder Name": "Ange mappnamn",
"View as text (unfinished)": "Visa som text (ofärdig)",
"Empty...": "Tom...",
"Open with DayBreak?": "Öppna med DayBreak?",
"Launch ": "",
"Launch option for: ": "Startalternativ för: ",
"Select launcher for: ": "",
"Homebrew": "Homebrew",
"Homebrew Options": "Homebrew-alternativ",
"Hide Sphaira": "Dölj Sphaira",
"Install Forwarder": "Installera forwarder",
"WARNING: Installing forwarders will lead to a ban!": "VARNING: Att installera forwarders leder till en avstängning!",
"Installing Forwarder": "Installerar forwarder",
"Creating Program": "Skapar program",
"Creating Control": "Skapar kontroll",
"Creating Meta": "Skapar meta",
"Writing Nca": "Skriver Nca",
"Updating ncm databse": "Uppdaterar ncm-databas",
"Pushing application record": "Lägger till applikationspost",
"Installed!": "Installerad!",
"Failed to install forwarder": "Misslyckades med att installera forwarder",
"Unstarred ": "",
"Starred ": "",
"AppStore": "AppStore",
"Filter: %s | Sort: %s | Order: %s": "Filter: %s | Sortering: %s | Ordning: %s",
"AppStore Options": "AppStore-alternativ",
"All": "Alla",
"Games": "Spel",
"Emulators": "Emulatorer",
"Tools": "Verktyg",
"Themes": "Teman",
"Legacy": "Legacy",
"version: %s": "version: %s",
"updated: %s": "uppdaterad: %s",
"category: %s": "kategori: %s",
"extracted: %.2f MiB": "extraherad: %.2f MiB",
"app_dls: %s": "app_nedladdningar: %s",
"More by Author": "Mer från författaren",
"Leave Feedback": "Lämna feedback",
"Irs": "Irs",
"Ambient Noise Level: ": "Omgivningsljudnivå: ",
"Controller": "Kontroll",
"Pad ": "Handkontroll ",
" (Available)": " (Tillgänglig)",
" (Unsupported)": "",
" (Unconnected)": " (Ej ansluten)",
"HandHeld": "Handhållen",
"Rotation": "Rotation",
"0 (Sideways)": "0 (Sido)",
"90 (Flat)": "90 (Platt)",
"180 (-Sideways)": "180 (-Sido)",
"270 (Upside down)": "270 (Upp och ner)",
"Colour": "Färg",
"Grey": "Grå",
"Ironbow": "Ironbow",
"Green": "Grön",
"Red": "Röd",
"Blue": "Blå",
"Light Target": "Ljusmål",
"All leds": "Alla lysdioder",
"Bright group": "Ljusstark grupp",
"Dim group": "Dämpad grupp",
"None": "Ingen",
"Gain": "Förstärkning",
"Negative Image": "Negativ bild",
"Normal image": "Normal bild",
"Negative image": "Negativ bild",
"Format": "Format",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "Trimformat",
"External Light Filter": "Extern ljusfilter",
"Load Default": "Ladda standardinställningar",
"Themezer": "Themezer",
"Themezer Options": "Themezer-alternativ",
"Nsfw": "Nsfw",
"Page": "Sida",
"Page %zu / %zu": "Sida %zu / %zu",
"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 ",
"Deleting": "Raderar",
"Pasting ": "Klistrar in ",
"Pasting": "Klistrar in",
"Removing ": "Tar bort ",
"Scanning ": "Skannar ",
"Creating ": "Skapar ",
"Copying ": "Kopierar ",
"Trying to load ": "",
"Downloading ": "Laddar ner ",
"Downloaded ": "",
"Removed ": "",
"Checking MD5": "Kontrollerar MD5",
"Loading...": "Laddar...",
"Loading": "Laddar",
"Empty!": "Tomt!",
"Not Ready...": "Ej redo...",
"Error loading page!": "Fel vid laddning av sida!",
"Update avaliable: ": "Uppdatering tillgänglig: ",
"Download update: ": "Ladda ner uppdatering: ",
"Updated to ": "",
"Restart Sphaira?": "",
"Failed to download update": "Misslyckades med att ladda ner uppdatering",
"Delete Selected files?": "Radera valda filer?",
"Completely remove ": "Ta bort helt ",
"Are you sure you want to delete ": "Är du säker på att du vill radera ",
"Are you sure you wish to cancel?": "Är du säker på att du vill avbryta?",
"If this message appears repeatedly, please open an issue.": ""
}

View File

@@ -1,238 +1,115 @@
{
"[Applet Mode]": "[小程序模式]",
"No Internet": "网络未连接",
"Files": "文件",
"Apps": "应用",
"Store": "商店",
"Menu": "菜单",
"Options": "选项",
"OK": "确定",
"Back": "返回",
"Select": "选择",
"Open": "打开",
"Launch": "启动",
"Info": "信息",
"Install": "安装",
"Delete": "删除",
"Restart": "",
"Changelog": "更新日志",
"Details": "详情",
"Update": "更新",
"Remove": "删除",
"Download": "下载",
"Next Page": "下一页",
"Prev Page": "上一页",
"Unstar": "取消星标",
"Star": "星标",
"System memory": "主机内存",
"microSD card": "SD卡",
"Yes": "是",
"No": "否",
"Enabled": "启用",
"Disabled": "禁用",
"Options": "选项",
"Homebrew Options": "插件选项",
"Sort By": "排序方式",
"Sort Options": "排序选项",
"Filter": "筛选",
"Updated": "最近使用",
"Size": "大小",
"Alphabetical": "按字母顺序",
"Decending": "降序",
"Ascending": "升序",
"Sort": "排序",
"Order": "顺序",
"Search": "搜索",
"Updated": "最近使用",
"Updated (Star)": "最近更新(星标优先)",
"Downloads": "下载",
"Size": "按大小",
"Size (Star)": "按大小(星标优先)",
"Alphabetical": "按字母顺序",
"Alphabetical (Star)": "按字母顺序(星标优先)",
"Likes": "点赞量",
"ID": "ID",
"Decending": "降序",
"Descending (down)": "降序",
"Desc": "降序",
"Ascending": "升序",
"Ascending (Up)": "升序",
"Asc": "升序",
"Menu Options": "菜单选项",
"Header": "标题",
"Theme": "主题",
"Info": "信息",
"Delete": "删除",
"Hide Sphaira": "在插件列表中隐藏Sphaira",
"Are you sure you want to delete ": "您确定要删除吗 ",
"Install Forwarder": "安装前端应用",
"WARNING: Installing forwarders will lead to a ban!": "警告安装前端应用可能导致ban机",
"Back": "返回",
"Install": "安装",
"Fs": "文件系统",
"App": "插件",
"Menu": "菜单",
"Homebrew": "插件列表",
"AppStore": "插件商店",
"FileBrowser": "文件浏览",
"Open": "打开",
"Theme Options": "主题选项",
"Select Theme": "选择主题",
"Shuffle": "随机播放",
"Music": "音乐",
"Network": "网络",
"Network Options": "网络选项",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink",
"Nxlink Connected": "Nxlink 已连接",
"Nxlink Upload": "Nxlink 上传中",
"Nxlink Finished": "Nxlink 已结束",
"Switch-Handheld!": "",
"Switch-Docked!": "",
"Language": "语言",
"Auto": "自动",
"English": "English",
"Japanese": "日本語",
"French": "Français",
"German": "Deutsch",
"Italian": "Italiano",
"Spanish": "Español",
"Chinese": "中文",
"Korean": "한국어",
"Dutch": "Dutch",
"Portuguese": "Português",
"Russian": "Русский",
"Swedish": "Svenska",
"Logging": "日志",
"Replace hbmenu on exit": "退出后用Sphaira替换hbmenu",
"Misc": "杂项",
"Misc Options": "杂项设置",
"Web": "网页浏览器",
"Install forwarders": "允许安装前端应用",
"Install location": "安装位置",
"Show install warning": "显示安装警告",
"FileBrowser": "文件浏览",
"%zd files": "%zd 个文件",
"%zd dirs": "%zd 个文件夹",
"File Options": "文件选项",
"Show Hidden": "显示隐藏项目",
"Folders First": "文件夹靠前",
"Hidden Last": "隐藏项目置后",
"Yes": "是",
"No": "否",
"Network Options": "网络选项",
"Nxlink": "Nxlink开发工具",
"Check for update": "检查更新",
"File Options": "文件选项",
"Cut": "剪切",
"Copy": "复制",
"Paste": "粘贴",
"Paste ": "粘贴 ",
" file(s)?": "个文件(夹)",
"Rename": "重命名",
"Set New File Name": "输入新命名",
"Advanced": "高级",
"Advanced Options": "高级选项",
"Create File": "新建文件",
"Set File Name": "输入文件名",
"Create Folder": "新建文件夹",
"Set Folder Name": "输入文件夹名",
"View as text": "以文本形式查看",
"View as text (unfinished)": "以文本形式查看(未完善)",
"Empty...": "空...",
"Open with DayBreak?": "使用DayBreak打开",
"Launch ": "",
"Launch option for: ": "启动选项:",
"Select launcher for: ": "",
"Homebrew": "应用列表",
"Homebrew Options": "应用选项",
"Hide Sphaira": "在应用列表中隐藏Sphaira",
"Install Forwarder": "安装前端应用",
"WARNING: Installing forwarders will lead to a ban!": "警告安装前端应用可能导致ban机",
"Installing Forwarder": "正在生成前端应用",
"Creating Program": "正在创建程序",
"Creating Control": "正在创建控制器",
"Creating Meta": "正在创建元数据",
"Writing Nca": "正在写入Nca",
"Updating ncm databse": "正在更新ncm数据库",
"Pushing application record": "正在推送应用记录",
"Installed!": "安装完成!",
"Failed to install forwarder": "前端应用安装失败",
"Unstarred ": "",
"Starred ": "",
"AppStore": "应用商店",
"Filter: %s | Sort: %s | Order: %s": "筛选: %s | 排序: %s | 顺序: %s",
"AppStore Options": "应用商店选项",
"Set Archive Bit": "设置存档标志",
"AppStore Options": "插件商店选项",
"All": "全部",
"Games": "游戏",
"Emulators": "模拟器",
"Tools": "工具",
"Advanced": "高级",
"Themes": "主题",
"Legacy": "可更新",
"version: %s": "版本: %s",
"updated: %s": "更新时间: %s",
"category: %s": "分类: %s",
"extracted: %.2f MiB": "应用大小: %.2f MiB",
"app_dls: %s": "下载量: %s",
"More by Author": "作者更多作品",
"Leave Feedback": "留言反馈",
"Misc": "杂项",
"Downloads": "下载",
"Filter": "筛选",
"Search": "搜索",
"Menu Options": "菜单选项",
"Header": "标题",
"Theme": "主题",
"Network": "网络",
"Logging": "日志",
"Enabled": "启用",
"Disabled": "禁用",
"Replace hbmenu on exit": "退出后用Sphaira替换hbmenu",
"Misc Options": "杂项设置",
"Themezer": "在线主题",
"Irs": "红外成像",
"Ambient Noise Level: ": "环境噪声等级:",
"Controller": "控制器",
"Web": "网页浏览器",
"Download": "下载",
"Next Page": "下一页",
"Prev Page": "上一页",
"Pad ": "手柄 ",
" (Available)": " (可用的)",
" (Unsupported)": "",
" (Unconnected)": " (未连接)",
"HandHeld": "掌机模式",
"Rotation": "旋转",
"HandHeld": "手持式",
" (Available)": " (可用的)",
"0 (Sideways)": "0度",
"90 (Flat)": "90度",
"180 (-Sideways)": "180度",
"270 (Upside down)": "270度",
"Colour": "颜色",
"Grey": "灰色",
"Ironbow": "紫黄",
"Green": "绿色",
"Red": "红色",
"Blue": "蓝色",
"Light Target": "光源目标",
"All leds": "全部",
"Bright group": "亮色组",
"Dim group": "暗色组",
"None": "无",
"Gain": "增益",
"Negative Image": "负片图像",
"Normal image": "正常图像",
"Negative image": "负片图像",
"320x240": "320x240",
"160x120": "160x120",
"80x60": "80x60",
"40x30": "40x30",
"20x15": "20x15",
"Controller": "控制器",
"Rotation": "旋转",
"Colour": "颜色",
"Light Target": "光源目标",
"Gain": "增益",
"Negative Image": "负片图像",
"Format": "格式",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "裁剪格式",
"External Light Filter": "外部光滤镜",
"Load Default": "加载默认值",
"Themezer": "在线主题",
"Themezer Options": "在线主题选项",
"Nsfw": "公共场合不宜的主题",
"Page": "页面",
"Page %zu / %zu": "页面 %zu / %zu",
"Enter Page Number": "输入跳转的页码",
"Bad Page": "错误的页面",
"Download theme?": "下载该主题?",
"GitHub": "",
"Downloading json": "",
"Select asset to download for ": "",
"Installing ": "正在安装 ",
"Uninstalling ": "正在卸载 ",
"Deleting ": "正在删除 ",
"Deleting": "正在删除",
"Pasting ": "正在粘贴 ",
"Pasting": "正在粘贴",
"Removing ": "正在移除 ",
"Scanning ": "正在扫描 ",
"Creating ": "正在创建 ",
"Copying ": "正在复制 ",
"Trying to load ": "",
"Downloading ": "正在下载 ",
"Downloaded ": "",
"Removed ": "",
"Checking MD5": "正在校验 MD5",
"Loading...": "加载中...",
"Loading": "加载中",
"Empty!": "空!",
"Not Ready...": "尚未准备好...",
"Error loading page!": "页面加载失败!",
"Update avaliable: ": "有可用更新!",
"Download update: ": "下载更新:",
"Updated to ": "",
"Restart 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.": ""
"No Internet": "网络未连接",
"[Applet Mode]": "[小程序模式]",
"Language": "语言"
}

View File

@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.13)
set(sphaira_VERSION 0.5.0)
set(sphaira_VERSION 0.4.0)
project(sphaira
VERSION ${sphaira_VERSION}
@@ -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
@@ -72,7 +72,6 @@ add_executable(sphaira
source/swkbd.cpp
source/web.cpp
source/i18n.cpp
source/ftpsrv_helper.cpp
)
target_compile_definitions(sphaira PRIVATE
@@ -83,16 +82,6 @@ target_compile_definitions(sphaira PRIVATE
include(FetchContent)
set(FETCHCONTENT_QUIET FALSE)
FetchContent_Declare(ftpsrv
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
GIT_TAG 1.2.1
)
FetchContent_Declare(libhaze
GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git
GIT_TAG 3244b9e
)
FetchContent_Declare(libpulsar
GIT_REPOSITORY https://github.com/ITotalJustice/switch-libpulsar.git
GIT_TAG d729be3
@@ -113,12 +102,12 @@ FetchContent_Declare(yyjson
GIT_TAG 0.10.0
)
FetchContent_Declare(minIni
FetchContent_Declare(minIni-sphaira
GIT_REPOSITORY https://github.com/ITotalJustice/minIni-nx.git
GIT_TAG 63ec295
)
set(MININI_LIB_NAME minIni)
set(MININI_LIB_NAME minIni-sphaira)
set(MININI_USE_STDIO ON)
set(MININI_USE_NX ON)
set(MININI_USE_FLOAT OFF)
@@ -139,7 +128,7 @@ set(NANOVG_STBI_STATIC OFF)
set(NANOVG_STBTT_STATIC ON)
set(YYJSON_DISABLE_READER OFF)
set(YYJSON_DISABLE_WRITER OFF)
set(YYJSON_DISABLE_WRITER ON)
set(YYJSON_DISABLE_UTILS ON)
set(YYJSON_DISABLE_FAST_FP_CONV ON)
set(YYJSON_DISABLE_NON_STANDARD ON)
@@ -147,85 +136,13 @@ set(YYJSON_DISABLE_UTF8_VALIDATION ON)
set(YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS OFF)
FetchContent_MakeAvailable(
# ftpsrv
libhaze
libpulsar
nanovg
stb
minIni
minIni-sphaira
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
${libhaze_SOURCE_DIR}/source/device_properties.cpp
${libhaze_SOURCE_DIR}/source/event_reactor.cpp
${libhaze_SOURCE_DIR}/source/haze.cpp
${libhaze_SOURCE_DIR}/source/ptp_object_database.cpp
${libhaze_SOURCE_DIR}/source/ptp_object_heap.cpp
${libhaze_SOURCE_DIR}/source/ptp_responder_android_operations.cpp
${libhaze_SOURCE_DIR}/source/ptp_responder_mtp_operations.cpp
${libhaze_SOURCE_DIR}/source/ptp_responder_ptp_operations.cpp
${libhaze_SOURCE_DIR}/source/ptp_responder.cpp
${libhaze_SOURCE_DIR}/source/usb_session.cpp
)
target_include_directories(libhaze PUBLIC ${libhaze_SOURCE_DIR}/include)
set_target_properties(libhaze PROPERTIES
C_STANDARD 11
C_EXTENSIONS ON
CXX_STANDARD 20
CXX_EXTENSIONS ON
# force optimisations in debug mode as otherwise vapor errors
# due to force_inline attribute failing...
COMPILE_OPTIONS "$<$<CONFIG:Debug>:-Os>"
)
# todo: upstream cmake
add_library(libpulsar
${libpulsar_SOURCE_DIR}/src/archive/archive_file.c
@@ -278,10 +195,8 @@ set_target_properties(sphaira PROPERTIES
)
target_link_libraries(sphaira PRIVATE
ftpsrv_helper
libhaze
libpulsar
minIni
minIni-sphaira
nanovg
stb
yyjson

View File

@@ -33,9 +33,7 @@ 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:
@@ -44,50 +42,36 @@ public:
void Loop();
static void Exit();
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
// this is thread safe (todo: make it thread safe)
static void Notify(std::string text, ui::NotifEntry::Side side = ui::NotifEntry::Side::RIGHT);
static void Notify(ui::NotifEntry entry);
static void NotifyPop(ui::NotifEntry::Side side = ui::NotifEntry::Side::RIGHT);
static void NotifyClear(ui::NotifEntry::Side side = ui::NotifEntry::Side::RIGHT);
static void NotifyFlashLed();
static auto GetThemeMetaList() -> std::span<ThemeMeta>;
static void SetTheme(s64 theme_index);
static auto GetThemeIndex() -> s64;
static auto GetDefaultImage(int* w = nullptr, int* h = nullptr) -> int;
static void SetTheme(u64 theme_index);
static auto GetThemeIndex() -> u64;
// returns argv[0]
static auto GetExePath() -> fs::FsPath;
// returns true if we are hbmenu.
static auto IsHbmenu() -> bool;
static auto GetMtpEnable() -> bool;
static auto GetFtpEnable() -> bool;
static auto GetNxlinkEnable() -> bool;
static auto GetLogEnable() -> bool;
static auto GetReplaceHbmenuEnable() -> bool;
static auto GetInstallEnable() -> bool;
static auto GetInstallSdEnable() -> bool;
static auto GetInstallPrompt() -> bool;
static auto GetThemeShuffleEnable() -> bool;
static auto GetThemeMusicEnable() -> bool;
static auto GetLanguage() -> long;
static void SetMtpEnable(bool enable);
static void SetFtpEnable(bool enable);
static void SetNxlinkEnable(bool enable);
static void SetLogEnable(bool enable);
static void SetReplaceHbmenuEnable(bool enable);
static void SetInstallEnable(bool enable);
static void SetInstallSdEnable(bool enable);
static void SetInstallPrompt(bool enable);
static void SetThemeShuffleEnable(bool enable);
static void SetThemeMusicEnable(bool enable);
static void SetLanguage(long index);
@@ -101,6 +85,9 @@ public:
void Update();
void Poll();
void DrawBackground();
void DrawTouch();
// void DrawElement(float x, float y, float w, float h, ui::ThemeEntryID id);
auto LoadElementImage(std::string_view value) -> ElementEntry;
auto LoadElementColour(std::string_view value) -> ElementEntry;
@@ -125,7 +112,6 @@ 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{};
@@ -145,18 +131,14 @@ public:
Theme m_theme{};
fs::FsPath theme_path{};
s64 m_theme_index{};
std::size_t m_theme_index{};
bool m_quit{};
option::OptionBool m_nxlink_enabled{INI_SECTION, "nxlink_enabled", true};
option::OptionBool m_mtp_enabled{INI_SECTION, "mtp_enabled", false};
option::OptionBool m_ftp_enabled{INI_SECTION, "ftp_enabled", false};
option::OptionBool m_log_enabled{INI_SECTION, "log_enabled", false};
option::OptionBool m_replace_hbmenu{INI_SECTION, "replace_hbmenu", false};
option::OptionBool m_install{INI_SECTION, "install", false};
option::OptionBool m_install_sd{INI_SECTION, "install_sd", true};
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
option::OptionBool m_theme_shuffle{INI_SECTION, "theme_shuffle", false};
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto

View File

@@ -224,18 +224,18 @@ enum SvcError {
};
enum FsError {
FsError_PathNotFound = 0x202,
FsError_PathAlreadyExists = 0x402,
FsError_TargetLocked = 0xE02,
FsError_ResultPathNotFound = 0x202,
FsError_ResultPathAlreadyExists = 0x402,
FsError_ResultTargetLocked = 0xE02,
FsError_UsableSpaceNotEnoughMmcCalibration = 0x4602,
FsError_UsableSpaceNotEnoughMmcSafe = 0x4802,
FsError_UsableSpaceNotEnoughMmcUser = 0x4A02,
FsError_UsableSpaceNotEnoughMmcSystem = 0x4C02,
FsError_UsableSpaceNotEnoughSdCard = 0x4E02,
FsError_UnsupportedSdkVersion = 0x6402,
FsError_MountNameAlreadyExists = 0x7802,
FsError_PartitionNotFound = 0x7D202,
FsError_TargetNotFound = 0x7D402,
FsError_ResultUsableSpaceNotEnoughSdCard = 0x4E02,
FsError_ResultUnsupportedSdkVersion = 0x6402,
FsError_ResultMountNameAlreadyExists = 0x7802,
FsError_ResultPartitionNotFound = 0x7D202,
FsError_ResultTargetNotFound = 0x7D402,
FsError_PortSdCardNoDevice = 0xFA202,
FsError_GameCardCardNotInserted = 0x13B002,
FsError_GameCardCardNotActivated = 0x13B402,
@@ -286,9 +286,9 @@ enum FsError {
FsError_GameCardFsCheckHandleInGetStatusFailure = 0x171402,
FsError_GameCardFsCheckHandleInCreateReadOnlyFailure = 0x172002,
FsError_GameCardFsCheckHandleInCreateSecureReadOnlyFailure = 0x172202,
FsError_NotImplemented = 0x177202,
FsError_AlreadyExists = 0x177602,
FsError_OutOfRange = 0x177A02,
FsError_ResultNotImplemented = 0x177202,
FsError_ResultAlreadyExists = 0x177602,
FsError_ResultOutOfRange = 0x177A02,
FsError_AllocationMemoryFailedInFatFileSystemA = 0x190202,
FsError_AllocationMemoryFailedInFatFileSystemB = 0x190402,
FsError_AllocationMemoryFailedInFatFileSystemC = 0x190602,
@@ -348,18 +348,18 @@ enum FsError {
FsError_FatFsFormatIllegalSectorsC = 0x280C02,
FsError_FatFsFormatIllegalSectorsD = 0x280E02,
FsError_UnexpectedInMountTableA = 0x296A02,
FsError_TooLongPath = 0x2EE602,
FsError_InvalidCharacter = 0x2EE802,
FsError_InvalidPathFormat = 0x2EEA02,
FsError_DirectoryUnobtainable = 0x2EEC02,
FsError_InvalidOffset = 0x2F5A02,
FsError_InvalidSize = 0x2F5C02,
FsError_NullptrArgument = 0x2F5E02,
FsError_InvalidAlignment = 0x2F6002,
FsError_InvalidMountName = 0x2F6202,
FsError_ExtensionSizeTooLarge = 0x2F6402,
FsError_ExtensionSizeInvalid = 0x2F6602,
FsError_FileExtensionWithoutOpenModeAllowAppend = 0x307202,
FsError_ResultTooLongPath = 0x2EE602,
FsError_ResultInvalidCharacter = 0x2EE802,
FsError_ResultInvalidPathFormat = 0x2EEA02,
FsError_ResultDirectoryUnobtainable = 0x2EEC02,
FsError_ResultInvalidOffset = 0x2F5A02,
FsError_ResultInvalidSize = 0x2F5C02,
FsError_ResultNullptrArgument = 0x2F5E02,
FsError_ResultInvalidAlignment = 0x2F6002,
FsError_ResultInvalidMountName = 0x2F6202,
FsError_ResultExtensionSizeTooLarge = 0x2F6402,
FsError_ResultExtensionSizeInvalid = 0x2F6602,
FsError_ResultFileExtensionWithoutOpenModeAllowAppend = 0x307202,
FsError_UnsupportedCommitTarget = 0x313A02,
FsError_UnsupportedSetSizeForNotResizableSubStorage = 0x313C02,
FsError_UnsupportedSetSizeForResizableSubStorage = 0x313E02,
@@ -444,14 +444,14 @@ enum FsError {
FsError_UnsupportedCommitProvisionallyForDirectorySaveDataFileSystem = 0x31E002,
FsError_UnsupportedWriteForZeroBitmapHashStorageFile = 0x31E202,
FsError_UnsupportedSetSizeForZeroBitmapHashStorageFile = 0x31E402,
FsError_NcaExternalKeyUnregisteredDeprecated = 0x326602,
FsError_FileNotClosed = 0x326E02,
FsError_DirectoryNotClosed = 0x327002,
FsError_WriteModeFileNotClosed = 0x327202,
FsError_AllocatorAlreadyRegistered = 0x327402,
FsError_DefaultAllocatorAlreadyUsed = 0x327602,
FsError_AllocatorAlignmentViolation = 0x327A02,
FsError_UserNotExist = 0x328202,
FsError_ResultNcaExternalKeyUnregisteredDeprecated = 0x326602,
FsError_ResultFileNotClosed = 0x326E02,
FsError_ResultDirectoryNotClosed = 0x327002,
FsError_ResultWriteModeFileNotClosed = 0x327202,
FsError_ResultAllocatorAlreadyRegistered = 0x327402,
FsError_ResultDefaultAllocatorAlreadyUsed = 0x327602,
FsError_ResultAllocatorAlignmentViolation = 0x327A02,
FsError_ResultUserNotExist = 0x328202,
FsError_FileNotFound = 0x339402,
FsError_DirectoryNotFound = 0x339602,
FsError_MappingTableFull = 0x346402,

View File

@@ -1,194 +1,41 @@
#pragma once
#include "fs.hpp"
#include <vector>
#include <string>
#include <functional>
#include <unordered_map>
#include <algorithm>
#include <switch.h>
namespace sphaira::curl {
namespace sphaira {
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,
};
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 class Priority {
enum class DownloadPriority {
Normal, // gets pushed to the back of the queue
High, // gets pushed to the front of the queue
};
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;
};
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;
DownloadCallback callback;
std::vector<u8> data;
bool result;
};
auto Init() -> bool;
void Exit();
auto DownloadInit() -> bool;
void DownloadExit();
// sync functions
auto ToMemory(const Api& e) -> ApiResult;
auto ToFile(const Api& e) -> ApiResult;
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;
// async functions
auto ToMemoryAsync(const Api& e) -> bool;
auto ToFileAsync(const Api& e) -> bool;
// 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;
struct Api {
Api() = default;
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;
template <typename... Ts>
Api(Ts&&... ts) {
Api::set_option(std::forward<Ts>(ts)...);
}
void DownloadClearCache(const std::string& url);
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
} // namespace sphaira

View File

@@ -7,7 +7,6 @@
#include <string>
#include <switch.h>
#include <nxlink.h>
#include <haze.h>
#include "download.hpp"
namespace sphaira::evman {
@@ -24,9 +23,8 @@ struct ExitEventData {
using EventData = std::variant<
LaunchNroEventData,
ExitEventData,
HazeCallbackData,
NxlinkCallbackData,
curl::DownloadEventData
DownloadEventData
>;
// returns number of events

View File

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

View File

@@ -1,8 +0,0 @@
#pragma once
namespace sphaira::ftpsrv {
bool Init();
void Exit();
} // namespace sphaira::ftpsrv

View File

@@ -2,15 +2,12 @@
#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;

View File

@@ -4,7 +4,6 @@
#include <vector>
#include <string>
#include <span>
#include <optional>
#include "fs.hpp"
namespace sphaira {
@@ -19,6 +18,7 @@ struct NroEntry {
s64 size{};
NacpStruct nacp{};
std::vector<u8> icon{};
u64 icon_size{};
u64 icon_offset{};
@@ -28,7 +28,6 @@ struct NroEntry {
int image{}; // nvg image
int x,y,w,h{}; // image
bool is_nacp_valid{};
std::optional<bool> has_star{std::nullopt};
auto GetName() const -> const char* {
return nacp.lang[0].name;
@@ -75,10 +74,4 @@ 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

View File

@@ -10,6 +10,7 @@ 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:

View File

@@ -1,57 +0,0 @@
#pragma once
#include "ui/object.hpp"
namespace sphaira::ui {
struct List final : Object {
using Callback = std::function<void(NVGcontext* vg, Theme* theme, Vec4 v, s64 index)>;
using TouchCallback = std::function<void(s64 index)>;
List(s64 row, s64 page, const Vec4& pos, const Vec4& v, const Vec2& pad = {});
void OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCallback callback);
void Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const;
auto SetScrollBarPos(float x, float y, float h) {
m_scrollbar.x = x;
m_scrollbar.y = y;
m_scrollbar.h = h;
}
auto ScrollDown(s64& index, s64 step, s64 count) -> bool;
auto ScrollUp(s64& index, s64 step, s64 count) -> bool;
auto GetYoff() const {
return m_yoff;
}
void SetYoff(float y = 0) {
m_yoff = y;
}
auto GetMaxY() const {
return m_v.h + m_pad.y;
}
private:
auto Draw(NVGcontext* vg, Theme* theme) -> void override {}
auto ClampY(float y, s64 count) const -> float;
private:
const s64 m_row;
const s64 m_page;
Vec4 m_v;
Vec2 m_pad;
Vec4 m_scrollbar{};
// current y offset.
float m_yoff{};
// in progress y offset, used when scrolling.
float m_y_prog{};
};
} // namespace sphaira::ui

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "ui/list.hpp"
#include "nro.hpp"
#include "fs.hpp"
#include "option.hpp"
@@ -10,12 +9,6 @@
namespace sphaira::ui::menu::filebrowser {
enum class FsType {
Sd,
ImageNand,
ImageSd,
};
enum class SelectedType {
None,
Copy,
@@ -90,23 +83,13 @@ struct FileAssocEntry {
std::vector<std::string> ext; // list of ext
std::vector<std::string> database; // list of systems
};
struct LastFile {
fs::FsPath name;
s64 index;
float offset;
s64 entries_count;
u64 index;
u64 offset;
u64 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();
@@ -120,25 +103,26 @@ struct Menu final : MenuBase {
}
private:
void SetIndex(s64 index);
void SetIndex(std::size_t index);
void InstallForwarder();
auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result;
void LoadAssocEntriesPath(const fs::FsPath& path);
void LoadAssocEntries();
auto FindFileAssocFor() -> std::vector<FileAssocEntry>;
void OnIndexChange();
auto GetNewPath(const FileEntry& entry) const -> fs::FsPath {
return GetNewPath(m_path, entry.name);
}
};
auto GetNewPath(s64 index) const -> fs::FsPath {
auto GetNewPath(u64 index) const -> fs::FsPath {
return GetNewPath(m_path, GetEntry(index).name);
}
};
auto GetNewPathCurrent() const -> fs::FsPath {
return GetNewPath(m_index);
}
};
auto GetSelectedEntries() const -> std::vector<FileEntry> {
if (!m_selected_count) {
@@ -220,19 +204,11 @@ 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
@@ -240,9 +216,6 @@ 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]
@@ -257,8 +230,9 @@ private:
// if it does, the index becomes that file.
std::vector<LastFile> m_previous_highlighted_file;
fs::FsPath m_selected_path;
s64 m_index{};
s64 m_selected_count{};
std::size_t m_index{};
std::size_t m_index_offset{};
std::size_t m_selected_count{};
SelectedType m_selected_type{SelectedType::None};
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Alphabetical};
@@ -266,8 +240,10 @@ 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_ignore_read_only{INI_SECTION, "ignore_read_only", false};
option::OptionLong m_mount{INI_SECTION, "mount", 0};
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};
bool m_loaded_assoc_entries{};
bool m_is_update_folder{};

View File

@@ -1,75 +0,0 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "ui/list.hpp"
#include "fs.hpp"
#include "option.hpp"
#include <vector>
#include <string>
namespace sphaira::ui::menu::gh {
struct AssetEntry {
std::string name;
std::string path;
std::string pre_install_message;
std::string post_install_message;
};
struct Entry {
fs::FsPath json_path;
std::string url;
std::string owner;
std::string repo;
std::string tag;
std::string pre_install_message;
std::string post_install_message;
std::vector<AssetEntry> assets;
};
struct GhApiAsset {
std::string name;
std::string content_type;
u64 size;
u64 download_count;
std::string browser_download_url;
};
struct GhApiEntry {
std::string tag_name;
std::string name;
std::vector<GhApiAsset> assets;
};
struct Menu final : MenuBase {
Menu();
~Menu();
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
private:
void SetIndex(s64 index);
void Scan();
void LoadEntriesFromPath(const fs::FsPath& path);
auto GetEntry() -> Entry& {
return m_entries[m_index];
}
auto GetEntry() const -> const Entry& {
return m_entries[m_index];
}
void Sort();
void UpdateSubheading();
private:
std::vector<Entry> m_entries;
s64 m_index{};
s64 m_index_offset{};
std::unique_ptr<List> m_list;
};
} // namespace sphaira::ui::menu::gh

View File

@@ -1,7 +1,6 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "ui/list.hpp"
#include "nro.hpp"
#include "fs.hpp"
#include "option.hpp"
@@ -10,11 +9,8 @@ namespace sphaira::ui::menu::homebrew {
enum SortType {
SortType_Updated,
SortType_Alphabetical,
SortType_Size,
SortType_UpdatedStar,
SortType_AlphabeticalStar,
SortType_SizeStar,
SortType_Alphabetical,
};
enum OrderType {
@@ -30,7 +26,7 @@ struct Menu final : MenuBase {
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
void SetIndex(s64 index);
void SetIndex(std::size_t index);
void InstallHomebrew();
void ScanHomebrew();
void Sort();
@@ -40,10 +36,6 @@ struct Menu final : MenuBase {
return m_entries;
}
auto IsStarEnabled() -> bool {
return m_sort.Get() >= SortType_UpdatedStar;
}
static Result InstallHomebrew(const fs::FsPath& path, const NacpStruct& nacp, const std::vector<u8>& icon);
static Result InstallHomebrewFromPath(const fs::FsPath& path);
@@ -51,12 +43,12 @@ private:
static constexpr inline const char* INI_SECTION = "homebrew";
std::vector<NroEntry> m_entries;
s64 m_index{}; // where i am in the array
std::unique_ptr<List> m_list;
std::size_t m_start{};
std::size_t m_index{}; // where i am in the array
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_AlphabeticalStar};
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Updated};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Decending};
option::OptionBool m_hide_sphaira{INI_SECTION, "hide_sphaira", false};
};
option::OptionBool m_hide_sphaira{INI_SECTION, "hide_sphaira", false};}
;
} // namespace sphaira::ui::menu::homebrew

View File

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

View File

@@ -14,8 +14,6 @@ enum class UpdateState {
None,
// update available!
Update,
// there was an error whilst checking for updates.
Error,
};
// this holds 2 menus and allows for switching between them
@@ -28,13 +26,10 @@ 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 AddOnLRPress();
void AddOnLPress();
void AddOnRPress();
private:
std::shared_ptr<homebrew::Menu> m_homebrew_menu{};

View File

@@ -12,31 +12,15 @@ 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;
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{};
AppletType m_applet_type;
};
} // namespace sphaira::ui::menu

View File

@@ -2,7 +2,6 @@
#include "ui/menus/menu_base.hpp"
#include "ui/scrollable_text.hpp"
#include "ui/list.hpp"
#include "option.hpp"
#include <span>
@@ -16,14 +15,28 @@ 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,
@@ -42,11 +55,6 @@ 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;
@@ -54,11 +62,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;
};
@@ -73,13 +81,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;
};
@@ -98,10 +106,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;
};
@@ -165,11 +173,8 @@ struct Menu final : MenuBase {
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
void SetIndex(s64 index) {
void SetIndex(std::size_t index) {
m_index = index;
if (!m_index) {
m_list->SetYoff(0);
}
}
// void SetSearch(const std::string& term);
@@ -184,13 +189,13 @@ private:
static constexpr inline u32 MAX_ON_PAGE = 16; // same as website
std::vector<PageEntry> m_pages;
s64 m_page_index{};
s64 m_page_index_max{1};
std::size_t m_page_index{};
std::size_t m_page_index_max{1};
std::string m_search{};
s64 m_index{}; // where i am in the array
std::unique_ptr<List> m_list;
std::size_t m_start{};
std::size_t m_index{}; // where i am in the array
// options
option::OptionLong m_sort{INI_SECTION, "sort", 0};

View File

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

View File

@@ -1,7 +1,7 @@
#pragma once
#include "nanovg.h"
#include "ui/types.hpp"
#include "ui/widget.hpp"
namespace sphaira::ui::gfx {
@@ -81,11 +81,10 @@ 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 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 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 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 drawDimBackground(NVGcontext* vg);
void updateHighlightAnimation();
void getHighlightAnimation(float* gradientX, float* gradientY, float* color);

View File

@@ -9,6 +9,8 @@ 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 {

View File

@@ -12,6 +12,7 @@ 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;
@@ -27,25 +28,23 @@ private:
// todo: support upto 4 options.
class OptionBox final : public Widget {
public:
using Callback = std::function<void(std::optional<s64> index)>;
using Callback = std::function<void(std::optional<std::size_t> index)>;
using Option = std::string;
using Options = std::vector<Option>;
public:
OptionBox(const std::string& message, const Option& a, Callback cb = [](auto){}); // confirm
OptionBox(const std::string& message, const Option& a, Callback cb); // 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, s64 index, 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, const Option& c, Callback cb); // tri
OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, s64 index, Callback cb); // tri
OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, std::size_t 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(s64 index) -> void; // common setup values
void SetIndex(s64 index);
auto Setup(std::size_t index) -> void; // common setup values
private:
std::string m_message;
@@ -53,7 +52,7 @@ private:
Vec4 m_spacer_line{};
s64 m_index{};
std::size_t m_index{};
std::vector<OptionBoxEntry> m_entries;
};

View File

@@ -0,0 +1,27 @@
#pragma once
#include "ui/widget.hpp"
#include <optional>
namespace sphaira::ui {
class OptionList final : public Widget {
public:
using Options = std::vector<std::pair<std::string, std::function<void()>>>;
public:
OptionList(Options _options);
auto Update(Controller* controller, TouchInfo* touch) -> void override;
auto OnLayoutChange() -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
protected:
Options m_options;
std::size_t m_index{};
private:
};
} // namespace sphaira::ui

View File

@@ -1,7 +1,7 @@
#pragma once
#include "ui/widget.hpp"
#include "ui/list.hpp"
#include "ui/scrollbar.hpp"
#include <optional>
namespace sphaira::ui {
@@ -9,22 +9,18 @@ namespace sphaira::ui {
class PopupList final : public Widget {
public:
using Items = std::vector<std::string>;
using Callback = std::function<void(std::optional<s64>)>;
using Callback = std::function<void(std::optional<std::size_t>)>;
public:
explicit PopupList(std::string title, Items items, Callback cb, s64 index = 0);
explicit PopupList(std::string title, Items items, Callback cb, std::size_t index = 0);
PopupList(std::string title, Items items, Callback cb, std::string index);
PopupList(std::string title, Items items, std::string& index_str_ref, s64& 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_ref);
PopupList(std::string title, Items items, s64& index_ref);
PopupList(std::string title, Items items, std::size_t& 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};
@@ -35,14 +31,17 @@ private:
std::string m_title;
Items m_items;
Callback m_callback;
s64 m_index; // index in list array
s64 m_index_offset{}; // drawing from array start
std::size_t m_index; // index in list array
std::size_t m_index_offset{}; // drawing from array start
std::unique_ptr<List> m_list;
// std::size_t& index_ref;
// std::string& index_str_ref;
float m_selected_y{};
float m_yoff{};
float m_line_top{};
float m_line_bottom{};
ScrollBar m_scrollbar;
};
} // namespace sphaira::ui

View File

@@ -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(s64 offset, s64 size) -> ProgressBox&;
auto UpdateTransfer(u64 offset, u64 size) -> ProgressBox&;
void RequestExit();
auto ShouldExit() -> bool;
@@ -30,16 +30,6 @@ 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;
@@ -55,8 +45,8 @@ private:
ProgressBoxDoneCallback m_done{};
std::string m_title{};
std::string m_transfer{};
s64 m_size{};
s64 m_offset{};
u64 m_size{};
u64 m_offset{};
bool m_exit_requested{};
};

View File

@@ -0,0 +1,34 @@
#pragma once
#include "ui/widget.hpp"
namespace sphaira::ui {
class ScrollBar final : public Widget {
public:
enum class Direction { DOWN, UP };
public:
ScrollBar() = default;
ScrollBar(Vec4 bounds, float entry_height, std::size_t entries);
auto Update(Controller* controller, TouchInfo* touch) -> void override {}
auto OnLayoutChange() -> void override;
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
auto Setup(Vec4 bounds, float entry_height, std::size_t entries) -> void;
auto Move(Direction direction) -> void;
private:
auto Setup() -> void;
private:
Vec4 m_bounds{};
std::size_t m_entries{};
std::size_t m_index{};
float m_entry_height{};
float m_step_size{};
bool m_should_draw{false};
};
} // namespace sphaira::ui

View File

@@ -1,7 +1,6 @@
#pragma once
#include "ui/widget.hpp"
#include "ui/list.hpp"
#include <memory>
namespace sphaira::ui {
@@ -10,6 +9,7 @@ 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(s64& index)>;
using Callback = std::function<void(std::size_t& index)>;
public:
explicit SidebarEntryArray(std::string title, Items items, Callback cb, s64 index = 0);
explicit SidebarEntryArray(std::string title, Items items, Callback cb, std::size_t 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;
s64 m_index;
std::size_t m_index;
};
template <typename T>
@@ -101,31 +101,33 @@ 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(s64 index);
void SetupButtons();
void SetIndex(std::size_t index);
private:
std::string m_title;
std::string m_sub;
Side m_side;
Items m_items;
s64 m_index{};
s64 m_index_offset{};
std::unique_ptr<List> m_list;
std::size_t m_index{};
std::size_t m_index_offset{};
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};

View File

@@ -114,34 +114,15 @@ struct [[nodiscard]] Vec4 {
struct TimeStamp {
TimeStamp() {
Update();
}
void Update() {
start = armGetSystemTick();
}
auto GetNs() const -> u64 {
auto GetNs() -> u64 {
const auto end_ticks = armGetSystemTick();
return armTicksToNs(end_ticks) - armTicksToNs(start);
}
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 {
auto GetSeconds() -> double {
const double ns = GetNs();
return ns/1000.0/1000.0/1000.0;
}
@@ -196,31 +177,39 @@ 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 TouchGesture {
// None,
// Tap,
// Scroll,
// };
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
};
struct TouchInfo {
HidTouchState initial;
HidTouchState cur;
s32 initial_x;
s32 initial_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 cur_x;
s32 cur_y;
auto in_range(s32 x, s32 y, s32 w, s32 h) const -> bool {
return in_range(Vec4(x, y, w, h));
}
s32 prev_x;
s32 prev_y;
u32 finger_id;
bool is_touching;
bool is_tap;
bool is_scroll;
bool is_clicked;
bool is_end;
};
enum class Button : u64 {
@@ -351,7 +340,7 @@ struct Controller {
m_kup = 0;
}
void UpdateButtonHeld(u64 buttons) {
void UpdateButtonHeld(HidNpadButton buttons) {
if (m_kdown & buttons) {
m_step = 50;
m_counter = 0;
@@ -359,7 +348,7 @@ struct Controller {
m_counter += m_step;
if (m_counter >= m_MAX) {
m_kdown |= m_kheld & buttons;
m_kdown |= buttons;
m_counter = 0;
m_step = std::min(m_step + 50, m_MAX_STEP);
}

View File

@@ -8,20 +8,7 @@
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);
@@ -39,10 +26,6 @@ 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) {
@@ -62,8 +45,6 @@ struct Widget : public Object {
m_actions.clear();
}
auto FireAction(Button button, u8 type = ActionType::DOWN) -> bool;
void SetPop(bool pop = true) {
m_pop = pop;
}
@@ -72,14 +53,9 @@ struct Widget : public Object {
return m_pop;
}
auto SetUiButtonPos(Vec2 pos) {
m_button_pos = pos;
}
auto GetUiButtons() const -> uiButtons;
using Actions = std::map<Button, Action>;
// using Actions = std::unordered_map<Button, Action>;
Actions m_actions;
Vec2 m_button_pos{1220, 675};
bool m_focus{false};
bool m_pop{false};
};

View File

@@ -39,14 +39,13 @@ 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)
@@ -73,7 +72,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)) { \
@@ -88,7 +87,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)

View File

@@ -1,6 +1,5 @@
#include "ui/menus/main_menu.hpp"
#include "ui/error_box.hpp"
#include "ui/option_box.hpp"
#include "app.hpp"
#include "log.hpp"
@@ -13,12 +12,10 @@
#include "fs.hpp"
#include "defines.hpp"
#include "i18n.hpp"
#include "ftpsrv_helper.hpp"
#include <nanovg_dk.h>
#include <minIni.h>
#include <pulsar.h>
#include <haze.h>
#include <algorithm>
#include <cassert>
#include <cstring>
@@ -110,12 +107,12 @@ void on_applet_operation_mode(App* app) {
switch (appletGetOperationMode()) {
case AppletOperationMode_Handheld:
log_write("[APPLET] AppletOperationMode_Handheld\n");
App::Notify("Switch-Handheld!"_i18n);
App::Notify("Switch-Handheld!");
break;
case AppletOperationMode_Console:
log_write("[APPLET] AppletOperationMode_Console\n");
App::Notify("Switch-Docked!"_i18n);
App::Notify("Switch-Docked!");
break;
}
}
@@ -203,13 +200,7 @@ auto GetNroIcon(const std::vector<u8>& nro_icon) -> std::vector<u8> {
return nro_icon;
}
void haze_callback(const HazeCallbackData *data) {
App::NotifyFlashLed();
evman::push(*data, false);
}
void nxlink_callback(const NxlinkCallbackData *data) {
App::NotifyFlashLed();
evman::push(*data, false);
}
@@ -229,22 +220,9 @@ void App::Loop() {
ui::gfx::updateHighlightAnimation();
// 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;
}
auto events = evman::popall();
// while (auto e = evman::pop()) {
for (auto& e : events) {
std::visit([this](auto&& arg){
using T = std::decay_t<decltype(arg)>;
if constexpr(std::is_same_v<T, evman::LaunchNroEventData>) {
@@ -269,34 +247,31 @@ void App::Loop() {
} else if constexpr(std::is_same_v<T, evman::ExitEventData>) {
log_write("[ExitEventData] got event\n");
m_quit = true;
} else if constexpr(std::is_same_v<T, HazeCallbackData>) {
// log_write("[ExitEventData] got event\n");
// m_quit = true;
} else if constexpr(std::is_same_v<T, NxlinkCallbackData>) {
switch (arg.type) {
case NxlinkCallbackType_Connected:
log_write("[NxlinkCallbackType_Connected]\n");
App::Notify("Nxlink Connected"_i18n);
App::Notify("Nxlink Connected");
break;
case NxlinkCallbackType_WriteBegin:
log_write("[NxlinkCallbackType_WriteBegin] %s\n", arg.file.filename);
App::Notify("Nxlink Upload"_i18n);
App::Notify("Nxlink Upload");
break;
case NxlinkCallbackType_WriteProgress:
// log_write("[NxlinkCallbackType_WriteProgress]\n");
break;
case NxlinkCallbackType_WriteEnd:
log_write("[NxlinkCallbackType_WriteEnd] %s\n", arg.file.filename);
App::Notify("Nxlink Finished"_i18n);
App::Notify("Nxlink Finished");
break;
}
} else if constexpr(std::is_same_v<T, curl::DownloadEventData>) {
} else if constexpr(std::is_same_v<T, DownloadEventData>) {
log_write("[DownloadEventData] got event\n");
arg.callback(arg.result);
arg.callback(arg.data, arg.result);
} else {
static_assert(false, "non-exhaustive visitor!");
}
}, event.value());
}, e);
}
u32 w{},h{};
@@ -339,16 +314,6 @@ 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});
}
@@ -365,45 +330,19 @@ void App::NotifyClear(ui::NotifEntry::Side side) {
g_app->m_notif_manager.Clear(side);
}
void App::NotifyFlashLed() {
static const HidsysNotificationLedPattern pattern = {
.baseMiniCycleDuration = 0x1, // 12.5ms.
.totalMiniCycles = 0x1, // 1 mini cycle(s).
.totalFullCycles = 0x1, // 1 full run(s).
.startIntensity = 0xF, // 100%.
.miniCycles = {{
.ledIntensity = 0xF, // 100%.
.transitionSteps = 0xF, // 1 step(s). Total 12.5ms.
.finalStepDuration = 0xF, // Forced 12.5ms.
}}
};
s32 total;
HidsysUniquePadId unique_pad_ids[16] = {0};
if (R_SUCCEEDED(hidsysGetUniquePadIds(unique_pad_ids, 16, &total))) {
for (int i = 0; i < total; i++) {
hidsysSetNotificationLedPattern(&pattern, unique_pad_ids[i]);
}
}
}
auto App::GetThemeMetaList() -> std::span<ThemeMeta> {
return g_app->m_theme_meta_entries;
}
void App::SetTheme(s64 theme_index) {
void App::SetTheme(u64 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() -> s64 {
auto App::GetThemeIndex() -> u64 {
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;
}
@@ -424,18 +363,10 @@ auto App::GetReplaceHbmenuEnable() -> bool {
return g_app->m_replace_hbmenu.Get();
}
auto App::GetInstallEnable() -> bool {
return g_app->m_install.Get();
}
auto App::GetInstallSdEnable() -> bool {
return g_app->m_install_sd.Get();
}
auto App::GetInstallPrompt() -> bool {
return g_app->m_install_prompt.Get();
}
auto App::GetThemeShuffleEnable() -> bool {
return g_app->m_theme_shuffle.Get();
}
@@ -444,14 +375,6 @@ auto App::GetThemeMusicEnable() -> bool {
return g_app->m_theme_music.Get();
}
auto App::GetMtpEnable() -> bool {
return g_app->m_mtp_enabled.Get();
}
auto App::GetFtpEnable() -> bool {
return g_app->m_ftp_enabled.Get();
}
auto App::GetLanguage() -> long {
return g_app->m_language.Get();
}
@@ -479,118 +402,13 @@ void App::SetLogEnable(bool enable) {
}
void App::SetReplaceHbmenuEnable(bool 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) {
g_app->m_install.Set(enable);
}
void App::SetInstallSdEnable(bool enable) {
g_app->m_install_sd.Set(enable);
}
void App::SetInstallPrompt(bool enable) {
g_app->m_install_prompt.Set(enable);
}
void App::SetThemeShuffleEnable(bool enable) {
g_app->m_theme_shuffle.Set(enable);
}
@@ -600,28 +418,6 @@ void App::SetThemeMusicEnable(bool enable) {
PlaySoundEffect(SoundEffect::SoundEffect_Music);
}
void App::SetMtpEnable(bool enable) {
if (App::GetMtpEnable() != enable) {
g_app->m_mtp_enabled.Set(enable);
if (enable) {
hazeInitialize(haze_callback);
} else {
hazeExit();
}
}
}
void App::SetFtpEnable(bool enable) {
if (App::GetFtpEnable() != enable) {
g_app->m_ftp_enabled.Set(enable);
if (enable) {
ftpsrv::Init();
} else {
ftpsrv::Exit();
}
}
}
void App::SetLanguage(long index) {
if (App::GetLanguage() != index) {
g_app->m_language.Set(index);
@@ -650,10 +446,10 @@ auto App::Install(OwoConfig& config) -> Result {
if (R_FAILED(rc)) {
App::PlaySoundEffect(SoundEffect_Error);
App::Push(std::make_shared<ui::ErrorBox>(rc, "Failed to install forwarder"_i18n));
App::Push(std::make_shared<ui::ErrorBox>(rc, "Failed to install forwarder"));
} else {
App::PlaySoundEffect(SoundEffect_Install);
App::Notify("Installed!"_i18n);
App::Notify("Installed!");
}
return rc;
@@ -680,10 +476,10 @@ auto App::Install(ui::ProgressBox* pbox, OwoConfig& config) -> Result {
if (R_FAILED(rc)) {
App::PlaySoundEffect(SoundEffect_Error);
App::Push(std::make_shared<ui::ErrorBox>(rc, "Failed to install forwarder"_i18n));
App::Push(std::make_shared<ui::ErrorBox>(rc, "Failed to install forwarder"));
} else {
App::PlaySoundEffect(SoundEffect_Install);
App::Notify("Installed!"_i18n);
App::Notify("Installed!");
}
return rc;
@@ -693,48 +489,61 @@ void App::Exit() {
g_app->m_quit = true;
}
void App::ExitRestart() {
nro_launch(GetExePath());
Exit();
}
void App::Poll() {
m_controller.Reset();
HidTouchScreenState state{};
hidGetTouchScreenStates(&state, 1);
m_touch_info.is_clicked = false;
if (state.count == 1 && !m_touch_info.is_touching) {
m_touch_info.initial = m_touch_info.cur = state.touches[0];
m_touch_info.is_touching = true;
m_touch_info.is_tap = true;
} else if (state.count >= 1 && m_touch_info.is_touching) {
m_touch_info.cur = state.touches[0];
if (m_touch_info.is_tap &&
(std::abs((s32)m_touch_info.initial.x - (s32)m_touch_info.cur.x) > 20 ||
std::abs((s32)m_touch_info.initial.y - (s32)m_touch_info.cur.y) > 20)) {
m_touch_info.is_tap = false;
m_touch_info.is_scroll = true;
}
} else if (m_touch_info.is_touching) {
m_touch_info.is_touching = false;
m_touch_info.is_scroll = false;
if (m_touch_info.is_tap) {
m_touch_info.is_clicked = true;
} else {
m_touch_info.is_end = true;
}
}
// todo: better implement this to match hos
if (!m_touch_info.is_touching && !m_touch_info.is_clicked) {
padUpdate(&m_pad);
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));
// dpad
m_controller.UpdateButtonHeld(HidNpadButton_Left);
m_controller.UpdateButtonHeld(HidNpadButton_Right);
m_controller.UpdateButtonHeld(HidNpadButton_Down);
m_controller.UpdateButtonHeld(HidNpadButton_Up);
// ls
m_controller.UpdateButtonHeld(HidNpadButton_StickLLeft);
m_controller.UpdateButtonHeld(HidNpadButton_StickLRight);
m_controller.UpdateButtonHeld(HidNpadButton_StickLDown);
m_controller.UpdateButtonHeld(HidNpadButton_StickLUp);
// rs
m_controller.UpdateButtonHeld(HidNpadButton_StickRLeft);
m_controller.UpdateButtonHeld(HidNpadButton_StickRRight);
m_controller.UpdateButtonHeld(HidNpadButton_StickRDown);
m_controller.UpdateButtonHeld(HidNpadButton_StickRUp);
HidTouchScreenState touch_state{};
hidGetTouchScreenStates(&touch_state, 1);
if (touch_state.count == 1 && !m_touch_info.is_touching) {
m_touch_info.initial_x = m_touch_info.prev_x = m_touch_info.cur_x = touch_state.touches[0].x;
m_touch_info.initial_y = m_touch_info.prev_y = m_touch_info.cur_y = touch_state.touches[0].y;
m_touch_info.finger_id = touch_state.touches[0].finger_id;
m_touch_info.is_touching = true;
m_touch_info.is_tap = true;
// PlaySoundEffect(SoundEffect_Limit);
} else if (touch_state.count >= 1 && m_touch_info.is_touching && m_touch_info.finger_id == touch_state.touches[0].finger_id) {
m_touch_info.prev_x = m_touch_info.cur_x;
m_touch_info.prev_y = m_touch_info.cur_y;
m_touch_info.cur_x = touch_state.touches[0].x;
m_touch_info.cur_y = touch_state.touches[0].y;
if (m_touch_info.is_tap &&
(std::abs(m_touch_info.initial_x - m_touch_info.cur_x) > 20 ||
std::abs(m_touch_info.initial_y - m_touch_info.cur_y) > 20)) {
m_touch_info.is_tap = false;
}
} else if (m_touch_info.is_touching) {
m_touch_info.is_touching = false;
// check if we clicked on anything, if so, handle it
if (m_touch_info.is_tap) {
// todo:
}
}
}
@@ -770,30 +579,11 @@ void App::Draw() {
nvgBeginFrame(this->vg, s_width, s_height, 1.f);
nvgScale(vg, m_scale.x, m_scale.y);
// 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.
// NOTE: widgets should never pop themselves from drawing!
for (auto& p : m_widgets) {
if (!p->IsHidden()) {
p->Draw(vg, &m_theme);
}
if (it == m_widgets.rbegin()) {
break;
}
}
}
m_notif_manager.Draw(vg, &m_theme);
@@ -808,21 +598,17 @@ 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, v.x, v.y, v.w, v.h, 0, e.texture, 1.f);
ui::gfx::drawRect(g_app->vg, v, paint);
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);
} break;
case ElementType::Colour: {
ui::gfx::drawRect(g_app->vg, v, e.colour);
ui::gfx::drawRect(g_app->vg, x, y, w, h, e.colour);
} break;
}
}
@@ -975,10 +761,6 @@ 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;
@@ -1051,27 +833,17 @@ 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();
log_write("hello world\n");
}
if (App::GetMtpEnable()) {
hazeInitialize(haze_callback);
}
if (App::GetFtpEnable()) {
ftpsrv::Init();
}
if (App::GetNxlinkEnable()) {
nxlinkInitialize(nxlink_callback);
}
curl::Init();
DownloadInit();
// Create the deko3d device
this->device = dk::DeviceMaker{}
@@ -1205,15 +977,6 @@ 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");
}
@@ -1235,12 +998,11 @@ App::~App() {
log_write("starting to exit\n");
i18n::exit();
curl::Exit();
DownloadExit();
// 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);
@@ -1267,34 +1029,29 @@ App::~App() {
// backup hbmenu if it is not sphaira
if (App::GetReplaceHbmenuEnable() && !IsHbmenu()) {
NacpStruct hbmenu_nacp;
NacpStruct nacp;
fs::FsNativeSd fs;
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(rc = fs.copy_entire_file("/switch/hbmenu.nro", "/hbmenu.nro"))) {
log_write("failed to backup hbmenu.nro\n");
if (R_SUCCEEDED(nro_get_nacp("/hbmenu.nro", nacp)) && std::strcmp(nacp.lang[0].name, "sphaira")) {
log_write("backing up hbmenu\n");
if (R_FAILED(fs.copy_entire_file("/switch/hbmenu.nro", "/hbmenu.nro", true))) {
log_write("failed to copy sphaire.nro to hbmenu.nro\n");
}
} else {
log_write("not backing up\n");
}
if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", GetExePath()))) {
Result rc;
if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", GetExePath(), true))) {
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", GetExePath(), rc, R_MODULE(rc), R_DESCRIPTION(rc));
} 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;
Result rc;
// ensure that are still sphaira
if (R_SUCCEEDED(rc = nro_get_nacp("/hbmenu.nro", hbmenu_nacp)) && !std::strcmp(hbmenu_nacp.lang[0].name, "sphaira")) {
NacpStruct sphaira_nacp;
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")) {
@@ -1304,27 +1061,14 @@ App::~App() {
// 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))) {
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");
}
}
}
} else {
log_write("no longer hbmenu!\n");
}
}
if (App::GetMtpEnable()) {
log_write("closing mtp\n");
hazeExit();
}
if (App::GetFtpEnable()) {
log_write("closing ftp\n");
ftpsrv::Exit();
}
if (App::GetNxlinkEnable()) {

View File

@@ -10,9 +10,8 @@
#include <deque>
#include <mutex>
#include <curl/curl.h>
#include <yyjson.h>
namespace sphaira::curl {
namespace sphaira {
namespace {
#define CURL_EASY_SETOPT_LOG(handle, opt, v) \
@@ -25,6 +24,9 @@ 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;
@@ -36,195 +38,42 @@ std::atomic_bool g_running{};
CURLSH* g_curl_share{};
Mutex g_mutex_share[CURL_LOCK_DATA_LAST]{};
struct DataStruct {
std::vector<u8> data;
s64 offset{};
FsFile f{};
s64 file_offset{};
struct UrlCache {
auto AddToCache(const std::string& url, bool force = false) {
mutexLock(&mutex);
ON_SCOPE_EXIT(mutexUnlock(&mutex));
auto it = std::find(cache.begin(), cache.end(), url);
if (it == cache.end()) {
cache.emplace_back(url);
return true;
} else {
if (force) {
return true;
} else {
return false;
}
}
}
void RemoveFromCache(const std::string& url) {
mutexLock(&mutex);
ON_SCOPE_EXIT(mutexUnlock(&mutex));
auto it = std::find(cache.begin(), cache.end(), url);
if (it != cache.end()) {
cache.erase(it);
}
}
std::vector<std::string> cache;
Mutex mutex{};
};
auto generate_key_from_path(const fs::FsPath& path) -> std::string {
const auto key = crc32Calculate(path.s, path.size());
return std::to_string(key);
}
struct Cache {
using Value = std::pair<std::string, std::string>;
bool init() {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (m_json) {
return true;
}
auto json_in = yyjson_read_file(JSON_PATH, YYJSON_READ_NOFLAG, nullptr, nullptr);
if (json_in) {
log_write("loading old json doc\n");
m_json = yyjson_doc_mut_copy(json_in, nullptr);
yyjson_doc_free(json_in);
m_root = yyjson_mut_doc_get_root(m_json);
} else {
log_write("creating new json doc\n");
m_json = yyjson_mut_doc_new(nullptr);
m_root = yyjson_mut_obj(m_json);
yyjson_mut_doc_set_root(m_json, m_root);
}
return m_json && m_root;
}
void exit() {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
if (!m_json) {
return;
}
if (!yyjson_mut_write_file(JSON_PATH, m_json, YYJSON_WRITE_NOFLAG, nullptr, nullptr)) {
log_write("failed to write etag json: %s\n", JSON_PATH.s);
}
yyjson_mut_doc_free(m_json);
m_json = nullptr;
m_root = nullptr;
}
void get(const fs::FsPath& path, curl::Header& header) {
const auto [etag, last_modified] = get_internal(path);
if (!etag.empty()) {
header.m_map.emplace("if-none-match", etag);
}
if (!last_modified.empty()) {
header.m_map.emplace("if-modified-since", last_modified);
}
}
void set(const fs::FsPath& path, const curl::Header& value) {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
std::string etag_str;
std::string last_modified_str;
if (auto it = value.Find(ETAG_STR); it != value.m_map.end()) {
etag_str = it->second;
}
if (auto it = value.Find(LAST_MODIFIED_STR); it != value.m_map.end()) {
last_modified_str = it->second;
}
if (!etag_str.empty() || !last_modified_str.empty()) {
set_internal(path, Value{etag_str, last_modified_str});
}
}
private:
auto get_internal(const fs::FsPath& path) -> Value {
if (!fs::FsNativeSd().FileExists(path)) {
return {};
}
const auto kkey = generate_key_from_path(path);
const auto it = m_cache.find(kkey);
if (it != m_cache.end()) {
return it->second;
}
auto hash_key = yyjson_mut_obj_getn(m_root, kkey.c_str(), kkey.length());
if (!hash_key) {
return {};
}
auto etag_key = yyjson_mut_obj_get(hash_key, ETAG_STR);
auto last_modified_key = yyjson_mut_obj_get(hash_key, LAST_MODIFIED_STR);
const auto etag_value = yyjson_mut_get_str(etag_key);
const auto etag_value_len = yyjson_mut_get_len(etag_key);
const auto last_modified_value = yyjson_mut_get_str(last_modified_key);
const auto last_modified_value_len = yyjson_mut_get_len(last_modified_key);
if ((!etag_value || !etag_value_len) && (!last_modified_value || !last_modified_value_len)) {
return {};
}
std::string etag;
std::string last_modified;
if (etag_value && etag_value_len) {
etag.assign(etag_value, etag_value_len);
}
if (last_modified_value && last_modified_value_len) {
last_modified.assign(last_modified_value, last_modified_value_len);
}
const Value ret{etag, last_modified};
m_cache.insert_or_assign(it, kkey, ret);
return ret;
}
void set_internal(const fs::FsPath& path, const Value& value) {
const auto kkey = generate_key_from_path(path);
// check if we already have this entry
const auto it = m_cache.find(kkey);
if (it != m_cache.end() && it->second == value) {
log_write("already has etag, not updating, path: %s key: %s\n", path.s, kkey.c_str());
return;
}
if (it != m_cache.end()) {
log_write("updating etag, path: %s key: %s\n", path.s, kkey.c_str());
} else {
log_write("setting new etag, path: %s key: %s\n", path.s, kkey.c_str());
}
// insert new entry into cache, this will never fail.
const auto& [jkey, jvalue] = *m_cache.insert_or_assign(it, kkey, value);
const auto& [etag, last_modified] = jvalue;
// check if we need to add a new entry to root or simply update the value.
auto hash_key = yyjson_mut_obj_getn(m_root, kkey.c_str(), kkey.length());
if (!hash_key) {
hash_key = yyjson_mut_obj_add_obj(m_json, m_root, jkey.c_str());
}
if (!hash_key) {
log_write("failed to set new cache key obj, path: %s key: %s\n", path.s, jkey.c_str());
} else {
const auto update_entry = [this, &hash_key](const char* tag, const std::string& value) {
if (value.empty()) {
return true;
} else {
auto key = yyjson_mut_obj_get(hash_key, tag);
if (!key) {
return yyjson_mut_obj_add_str(m_json, hash_key, tag, value.c_str());
} else {
return yyjson_mut_set_str(key, value.c_str());
}
}
};
if (!update_entry("etag", etag)) {
log_write("failed to set new etag, path: %s key: %s\n", path.s, jkey.c_str());
}
if (!update_entry("last-modified", last_modified)) {
log_write("failed to set new last-modified, path: %s key: %s\n", path.s, jkey.c_str());
}
}
}
static constexpr inline fs::FsPath JSON_PATH{"/switch/sphaira/cache/cache.json"};
static constexpr inline const char* ETAG_STR{"etag"};
static constexpr inline const char* LAST_MODIFIED_STR{"last-modified"};
Mutex m_mutex{};
yyjson_mut_doc* m_json{};
yyjson_mut_val* m_root{};
std::unordered_map<std::string, Value> m_cache{};
struct DataStruct {
std::vector<u8> data;
u64 offset{};
FsFileSystem fs{};
FsFile f{};
s64 file_offset{};
};
struct ThreadEntry {
@@ -233,7 +82,7 @@ struct ThreadEntry {
R_UNLESS(m_curl != nullptr, 0x1);
ueventCreate(&m_uevent, true);
R_TRY(threadCreate(&m_thread, ThreadFunc, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
R_TRY(threadCreate(&m_thread, DownloadThread, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
R_TRY(threadStart(&m_thread));
R_SUCCEED();
}
@@ -252,7 +101,7 @@ struct ThreadEntry {
return m_in_progress == true;
}
auto Setup(const Api& api) -> bool {
auto Setup(DownloadCallback callback, ProgressCallback pcallback, std::string url, std::string file, std::string post) -> bool {
assert(m_in_progress == false && "Setting up thread while active");
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
@@ -260,25 +109,35 @@ struct ThreadEntry {
if (m_in_progress) {
return false;
}
m_api = api;
m_url = url;
m_file = file;
m_post = post;
m_callback = callback;
m_pcallback = pcallback;
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{};
Api m_api{};
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{};
std::atomic_bool m_in_progress{};
Mutex m_mutex{};
UEvent m_uevent{};
};
struct ThreadQueueEntry {
Api api;
std::string url;
std::string file;
std::string post;
DownloadCallback callback;
ProgressCallback pcallback;
bool m_delete{};
};
@@ -290,7 +149,7 @@ struct ThreadQueue {
auto Create() -> Result {
ueventCreate(&m_uevent, true);
R_TRY(threadCreate(&m_thread, ThreadFunc, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
R_TRY(threadCreate(&m_thread, DownloadThreadQueue, this, nullptr, 1024*32, THREAD_PRIO, THREAD_CORE));
R_TRY(threadStart(&m_thread));
R_SUCCEED();
}
@@ -301,18 +160,22 @@ struct ThreadQueue {
threadClose(&m_thread);
}
auto Add(const Api& api) -> bool {
auto Add(DownloadPriority prio, DownloadCallback callback, ProgressCallback pcallback, std::string url, std::string file, std::string post) -> bool {
mutexLock(&m_mutex);
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
ThreadQueueEntry entry{};
entry.api = api;
entry.url = url;
entry.file = file;
entry.post = post;
entry.callback = callback;
entry.pcallback = pcallback;
switch (api.m_prio) {
case Priority::Normal:
switch (prio) {
case DownloadPriority::Normal:
m_entries.emplace_back(entry);
break;
case Priority::High:
case DownloadPriority::High:
m_entries.emplace_front(entry);
break;
}
@@ -320,13 +183,11 @@ struct ThreadQueue {
ueventSignal(&m_uevent);
return true;
}
static void ThreadFunc(void* p);
};
ThreadEntry g_threads[MAX_THREADS]{};
ThreadQueue g_thread_queue;
Cache g_cache;
UrlCache g_url_cache;
void GetDownloadTempPath(fs::FsPath& buf) {
static Mutex mutex{};
@@ -355,7 +216,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<OnProgress*>(clientp);
auto callback = *static_cast<ProgressCallback*>(clientp);
if (!callback(dltotal, dlnow, ultotal, ulnow)) {
return 1;
}
@@ -422,60 +283,36 @@ auto WriteFileCallback(void *contents, size_t size, size_t num_files, void *user
return realsize;
}
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;
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 {
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 = !e.m_path.empty() && e.m_path != "";
const bool has_post = !e.m_fields.m_str.empty() && e.m_fields.m_str != "";
const bool has_file = !file.empty() && file != "";
const bool has_post = !post.empty() && post != "";
DataStruct chunk;
Header header_in = e.m_header;
Header header_out;
fs::FsNativeSd fs;
ON_SCOPE_EXIT(if (has_file) { fsFsClose(&chunk.fs); } );
if (has_file) {
std::strcpy(safe_buf, file.c_str());
GetDownloadTempPath(tmp_buf);
fs.CreateDirectoryRecursivelyWithPath(tmp_buf);
R_TRY_RESULT(fsOpenSdCardFileSystem(&chunk.fs), false);
if (auto rc = fs.CreateFile(tmp_buf, 0, 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
fs::CreateDirectoryRecursivelyWithPath(&chunk.fs, tmp_buf);
if (auto rc = fsFsCreateFile(&chunk.fs, tmp_buf, 0, 0); R_FAILED(rc) && rc != FsError_ResultPathAlreadyExists) {
log_write("failed to create file: %s\n", tmp_buf);
return {};
return false;
}
if (R_FAILED(fs.OpenFile(tmp_buf, FsOpenMode_Write|FsOpenMode_Append, &chunk.f))) {
if (R_FAILED(fsFsOpenFile(&chunk.fs, tmp_buf, FsOpenMode_Write|FsOpenMode_Append, &chunk.f))) {
log_write("failed to open file: %s\n", tmp_buf);
return {};
}
if (e.m_flags.m_flags & Flag_Cache) {
g_cache.get(e.m_path, header_in);
return false;
}
}
// reserve the first chunk
chunk.data.reserve(CHUNK_SIZE);
curl_easy_reset(curl);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, e.m_url.m_str.c_str());
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, url.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);
@@ -483,42 +320,15 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
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, 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);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_POSTFIELDS, post.c_str());
log_write("setting post field: %s\n", post.c_str());
}
// progress calls.
if (e.m_on_progress) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &e.m_on_progress);
if (pcallback) {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &pcallback);
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc2);
} else {
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc1);
@@ -533,34 +343,21 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
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) {
if (http_code == 304) {
log_write("cached download: %s\n", e.m_url.m_str.c_str());
if (res != CURLE_OK) {
fsFsDeleteFile(&chunk.fs, tmp_buf);
} 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))) {
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;
}
}
}
chunk.data.clear();
} else {
// empty data if we failed
if (res != CURLE_OK) {
@@ -568,29 +365,21 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
}
}
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};
log_write("Downloaded %s %s\n", url.c_str(), curl_easy_strerror(res));
return success;
}
auto DownloadInternal(const Api& e) -> ApiResult {
auto DownloadInternal(DataStruct& chunk, ProgressCallback pcallback, const std::string& url, const std::string& file, const std::string& post) -> bool {
auto curl = curl_easy_init();
if (!curl) {
log_write("curl init failed\n");
return {};
return false;
}
ON_SCOPE_EXIT(curl_easy_cleanup(curl));
return DownloadInternal(curl, e);
return DownloadInternal(curl, chunk, pcallback, url, file, post);
}
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) {
void DownloadThread(void* p) {
auto data = static_cast<ThreadEntry*>(p);
while (g_running) {
auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX);
@@ -602,10 +391,11 @@ void ThreadEntry::ThreadFunc(void* p) {
continue;
}
DataStruct chunk;
#if 1
const auto result = DownloadInternal(data->m_curl, data->m_api);
const auto result = DownloadInternal(data->m_curl, chunk, data->m_pcallback, data->m_url, data->m_file, data->m_post);
if (g_running) {
const DownloadEventData event_data{data->m_api.m_on_complete, result};
DownloadEventData event_data{data->m_callback, std::move(chunk.data), result};
evman::push(std::move(event_data), false);
} else {
break;
@@ -621,7 +411,7 @@ void ThreadEntry::ThreadFunc(void* p) {
log_write("exited download thread\n");
}
void ThreadQueue::ThreadFunc(void* p) {
void DownloadThreadQueue(void* p) {
auto data = static_cast<ThreadQueue*>(p);
while (g_running) {
auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX);
@@ -654,7 +444,7 @@ void ThreadQueue::ThreadFunc(void* p) {
}
if (!thread.InProgress()) {
thread.Setup(entry.api);
thread.Setup(entry.callback, entry.pcallback, entry.url, entry.file, entry.post);
// log_write("[dl queue] starting download\n");
// mark entry for deletion
entry.m_delete = true;
@@ -664,6 +454,10 @@ void ThreadQueue::ThreadFunc(void* p) {
}
}
if (!g_running) {
return;
}
if (!keep_going) {
break;
}
@@ -673,14 +467,29 @@ void ThreadQueue::ThreadFunc(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 Init() -> bool {
auto DownloadInit() -> bool {
if (CURLE_OK != curl_global_init(CURL_GLOBAL_DEFAULT)) {
return false;
}
@@ -709,15 +518,10 @@ auto Init() -> bool {
}
log_write("finished creating threads\n");
if (!g_cache.init()) {
log_write("failed to init json cache\n");
}
return true;
}
void Exit() {
void DownloadExit() {
g_running = false;
g_thread_queue.Close();
@@ -732,26 +536,35 @@ void Exit() {
}
curl_global_cleanup();
g_cache.exit();
}
auto ToMemory(const Api& e) -> ApiResult {
if (!e.m_path.empty()) {
return {};
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;
}
return DownloadInternal(e);
}
auto ToFile(const Api& e) -> ApiResult {
if (e.m_path.empty()) {
return {};
}
return DownloadInternal(e);
return {};
}
auto ToMemoryAsync(const Api& api) -> bool {
auto DownloadFile(const std::string& url, const std::string& out, const std::string& post, ProgressCallback pcallback) -> bool {
if (g_url_cache.AddToCache(url)) {
DataStruct chunk{};
if (DownloadInternal(chunk, pcallback, url, out, post)) {
return true;
}
}
return false;
}
auto DownloadMemoryAsync(const std::string& url, const std::string& post, DownloadCallback callback, ProgressCallback pcallback, DownloadPriority prio) -> bool {
#if USE_THREAD_QUEUE
return g_thread_queue.Add(api);
if (g_url_cache.AddToCache(url)) {
return g_thread_queue.Add(prio, callback, pcallback, url, "", post);
} else {
return false;
}
#else
// mutexLock(&g_thread_queue.m_mutex);
// ON_SCOPE_EXIT(mutexUnlock(&g_thread_queue.m_mutex));
@@ -767,9 +580,13 @@ auto ToMemoryAsync(const Api& api) -> bool {
#endif
}
auto ToFileAsync(const Api& e) -> bool {
auto DownloadFileAsync(const std::string& url, const std::string& out, const std::string& post, DownloadCallback callback, ProgressCallback pcallback, DownloadPriority prio) -> bool {
#if USE_THREAD_QUEUE
return g_thread_queue.Add(e);
if (g_url_cache.AddToCache(url)) {
return g_thread_queue.Add(prio, callback, pcallback, url, out, post);
} else {
return false;
}
#else
// mutexLock(&g_thread_queue.m_mutex);
// ON_SCOPE_EXIT(mutexUnlock(&g_thread_queue.m_mutex));
@@ -785,4 +602,9 @@ auto ToFileAsync(const Api& e) -> bool {
#endif
}
} // namespace sphaira::curl
void DownloadClearCache(const std::string& url) {
g_url_cache.AddToCache(url);
g_url_cache.RemoveFromCache(url);
}
} // namespace sphaira

View File

@@ -134,7 +134,7 @@ Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& _path, bool ig
rc = CreateDirectory(path, ignore_read_only);
}
if (R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
if (R_FAILED(rc) && rc != FsError_ResultPathAlreadyExists) {
log_write("failed to create folder: %s\n", path);
return rc;
}
@@ -166,7 +166,7 @@ Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& _path,
rc = CreateDirectory(path, ignore_read_only);
}
if (R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
if (R_FAILED(rc) && rc != FsError_ResultPathAlreadyExists) {
log_write("failed to create folder recursively: %s\n", path);
return rc;
}
@@ -248,10 +248,10 @@ Result read_entire_file(FsFileSystem* _fs, const FsPath& path, std::vector<u8>&
Result write_entire_file(FsFileSystem* _fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only) {
R_UNLESS(ignore_read_only || !is_read_only(path), Fs::ResultReadOnly);
FsNative fs{_fs, false, ignore_read_only};
FsNative fs{_fs, false};
R_TRY(fs.GetFsOpenResult());
if (auto rc = fs.CreateFile(path, in.size(), 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
if (auto rc = fs.CreateFile(path, in.size(), 0, ignore_read_only); R_FAILED(rc) && rc != FsError_ResultPathAlreadyExists) {
return rc;
}

View File

@@ -1,141 +0,0 @@
#include "ftpsrv_helper.hpp"
#include "app.hpp"
#include "fs.hpp"
#include "log.hpp"
#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{};
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
sphaira::App::NotifyFlashLed();
}
void ftp_progress_callback(void) {
sphaira::App::NotifyFlashLed();
}
void loop(void* arg) {
while (!g_should_exit) {
ftpsrv_init(&g_ftpsrv_config);
while (!g_should_exit) {
if (ftpsrv_loop(100) != FTP_API_LOOP_ERROR_OK) {
svcSleepThread(1e+6);
break;
}
}
ftpsrv_exit();
}
}
} // namespace
namespace sphaira::ftpsrv {
bool Init() {
std::scoped_lock lock{g_mutex};
if (g_is_running) {
return false;
}
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 = 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*16, 0x2C, 2))) {
log_write("failed to create nxlink thread: 0x%X\n", rc);
return false;
}
if (R_FAILED(rc = threadStart(&g_thread))) {
log_write("failed to start nxlink thread: 0x%X\n", rc);
threadClose(&g_thread);
return false;
}
return g_is_running = true;
}
void Exit() {
std::scoped_lock lock{g_mutex};
if (g_is_running) {
g_is_running = false;
}
g_should_exit = true;
threadWaitForExit(&g_thread);
threadClose(&g_thread);
vfs_nx_exit();
fsdev_wrapUnmountAll();
}
} // namespace sphaira::ftpsrv
extern "C" {
void log_file_write(const char* msg) {
log_write("%s", msg);
}
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"

View File

@@ -3,7 +3,6 @@
#include "log.hpp"
#include <yyjson.h>
#include <vector>
#include <unordered_map>
namespace sphaira::i18n {
namespace {
@@ -11,52 +10,37 @@ namespace {
std::vector<u8> g_i18n_data;
yyjson_doc* json;
yyjson_val* root;
std::unordered_map<std::string, std::string> g_tr_cache;
std::string get_internal(const char* str, size_t len) {
const std::string kkey = {str, len};
if (auto it = g_tr_cache.find(kkey); it != g_tr_cache.end()) {
return it->second;
}
// add default entry
const auto it = g_tr_cache.emplace(kkey, kkey).first;
if (!json || !root) {
log_write("no json or root\n");
return kkey;
return str;
}
auto key = yyjson_obj_getn(root, str, len);
if (!key) {
log_write("\tfailed to find key: [%s]\n", kkey.c_str());
return kkey;
log_write("\tfailed to find key: [%.*s]\n", len, str);
return str;
}
auto val = yyjson_get_str(key);
auto val_len = yyjson_get_len(key);
if (!val || !val_len) {
log_write("\tfailed to get value: [%s]\n", kkey.c_str());
return kkey;
log_write("\tfailed to get value: [%.*s]\n", len, str);
return str;
}
// update entry in cache
const std::string ret = {val, val_len};
g_tr_cache.insert_or_assign(it, kkey, ret);
return ret;
return {val, val_len};
}
} // namespace
bool init(long index) {
g_tr_cache.clear();
R_TRY_RESULT(romfsInit(), false);
ON_SCOPE_EXIT( romfsExit() );
u64 languageCode;
SetLanguage setLanguage = SetLanguage_ENGB;
std::string lang_name = "en";
switch (index) {
case 0: // auto
@@ -76,9 +60,9 @@ bool init(long index) {
case 9: setLanguage = SetLanguage_NL; break; // "Dutch"
case 10: setLanguage = SetLanguage_PT; break; // "Portuguese"
case 11: setLanguage = SetLanguage_RU; break; // "Russian"
case 12: lang_name = "se"; break; // "Swedish"
}
std::string lang_name;
switch (setLanguage) {
case SetLanguage_JA: lang_name = "ja"; break;
case SetLanguage_FR: lang_name = "fr"; break;
@@ -91,6 +75,7 @@ bool init(long index) {
case SetLanguage_PT: lang_name = "pt"; break;
case SetLanguage_RU: lang_name = "ru"; break;
case SetLanguage_ZHTW: lang_name = "zh"; break;
default: lang_name = "en"; break;
}
const fs::FsPath sdmc_path = "/config/sphaira/i18n/" + lang_name + ".json";

View File

@@ -14,16 +14,6 @@ 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 {
@@ -70,17 +60,13 @@ void log_write(const char* s, ...) {
std::va_list v{};
va_start(v, s);
log_write_arg_internal(s, v);
if (file) {
std::vfprintf(file, s, v);
std::fflush(file);
}
if (nxlink_socket) {
std::vprintf(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

View File

@@ -59,12 +59,6 @@ void userAppInit(void) {
diagAbortWithResult(rc);
if (R_FAILED(rc = accountInitialize(is_application ? AccountServiceType_Application : AccountServiceType_System)))
diagAbortWithResult(rc);
if (R_FAILED(rc = setInitialize()))
diagAbortWithResult(rc);
if (R_FAILED(rc = hidsysInitialize()))
diagAbortWithResult(rc);
if (R_FAILED(rc = ncmInitialize()))
diagAbortWithResult(rc);
log_nxlink_init();
}
@@ -72,9 +66,6 @@ void userAppInit(void) {
void userAppExit(void) {
log_nxlink_exit();
ncmExit();
hidsysExit();
setExit();
accountExit();
nifmExit();
psmExit();

View File

@@ -309,37 +309,4 @@ 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

View File

@@ -17,7 +17,6 @@
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <poll.h>
namespace {
@@ -115,7 +114,7 @@ auto recvall(int sock, void* buf, int size) -> bool {
if (errno != EWOULDBLOCK && errno != EAGAIN) {
return false;
}
svcSleepThread(1e+6);
svcSleepThread(YieldType_WithoutCoreMigration);
} else {
got += len;
left -= len;
@@ -133,7 +132,7 @@ auto sendall(Socket sock, const void* buf, int size) -> bool {
if (errno != EWOULDBLOCK && errno != EAGAIN) {
return false;
}
svcSleepThread(1e+6);
svcSleepThread(YieldType_WithoutCoreMigration);
}
sent += len;
left -= len;
@@ -172,6 +171,28 @@ auto get_file_data(Socket sock, int max) -> std::vector<u8> {
return buf;
}
#if 0
auto create_directories(fs::FsNative& fs, const std::string& path) -> Result {
std::size_t pos{};
// no sane person creates 20 directories deep
for (int i = 0; i < 20; i++) {
pos = path.find_first_of("/", pos);
if (pos == std::string::npos) {
break;
}
pos++;
fs::FsPath safe_buf;
std::strcpy(safe_buf, path.substr(0, pos).c_str());
const auto rc = fs.CreateDirectory(safe_buf);
R_UNLESS(R_SUCCEEDED(rc) || rc == FsError_ResultPathAlreadyExists, rc);
}
R_SUCCEED();
}
#endif
void loop(void* args) {
log_write("in nxlink thread func\n");
const sockaddr_in servaddr{
@@ -203,7 +224,7 @@ void loop(void* args) {
};
while (!g_quit) {
svcSleepThread(1e+8);
svcSleepThread(1000000);
if (poll_network_change()) {
continue;
@@ -245,36 +266,21 @@ void loop(void* args) {
sockaddr_in sa_remote{};
pollfd pfds[2];
pfds[0].fd = sock;
pfds[0].events = POLLIN;
pfds[1].fd = sock_udp;
pfds[1].events = POLLIN;
while (!g_quit) {
auto poll_rc = poll(pfds, std::size(pfds), 1000/60);
if (poll_rc < 0) {
break;
} else if (poll_rc == 0) {
continue;
} else if ((pfds[0].revents & (POLLERR|POLLHUP|POLLNVAL)) || (pfds[1].revents & (POLLERR|POLLHUP|POLLNVAL))) {
svcSleepThread(10000);
if (poll_network_change()) {
break;
}
if (pfds[1].revents & POLLIN) {
char recvbuf[6];
char recvbuf[256];
socklen_t from_len = sizeof(sa_remote);
auto udp_len = recvfrom(sock_udp, recvbuf, sizeof(recvbuf), 0, (sockaddr*)&sa_remote, &from_len);
if (udp_len == sizeof(recvbuf) && !std::strncmp(recvbuf, UDP_MAGIC_SERVER, std::strlen(UDP_MAGIC_SERVER))) {
const auto udp_len = recvfrom(sock_udp, recvbuf, sizeof(recvbuf), 0, (sockaddr*)&sa_remote, &from_len);
if (udp_len > 0 && !std::strncmp(recvbuf, UDP_MAGIC_SERVER, std::strlen(UDP_MAGIC_SERVER))) {
// log_write("got udp len: %d - %.*s\n", udp_len, udp_len, recvbuf);
sa_remote.sin_family = AF_INET;
sa_remote.sin_port = htons(NXLINK_CLIENT_PORT);
udp_len = sendto(sock_udp, UDP_MAGIC_CLIENT, std::strlen(UDP_MAGIC_CLIENT), 0, (const sockaddr*)&sa_remote, sizeof(sa_remote));
if (udp_len != std::strlen(UDP_MAGIC_CLIENT)) {
log_write("nxlink failed to send udp packet\n");
continue;
}
}
sendto(sock_udp, UDP_MAGIC_CLIENT, std::strlen(UDP_MAGIC_CLIENT), 0, (sockaddr*)&sa_remote, sizeof(sa_remote));
}
socklen_t accept_len = sizeof(sa_remote);
@@ -310,9 +316,8 @@ void loop(void* args) {
}
// check that we have enough space
fs::FsNativeSd fs;
s64 sd_storage_space_free;
if (R_FAILED(fs.GetFreeSpace("/", &sd_storage_space_free)) || filesize >= sd_storage_space_free) {
if (R_FAILED(fs::FsNativeSd().GetFreeSpace("/", &sd_storage_space_free)) || filesize >= sd_storage_space_free) {
sendall(connfd, &ERR_SPACE, sizeof(ERR_SPACE));
continue;
}
@@ -340,6 +345,16 @@ void loop(void* args) {
path = name;
}
// std::strcat(temp_path, "~");
fs::FsNativeSd fs;
if (R_FAILED(rc = fs.GetFsOpenResult())) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
log_write("failed to open fs: 0x%X\n", socketGetLastResult());
continue;
}
// if (R_FAILED(rc = create_directories(fs, path))) {
if (R_FAILED(rc = fs.CreateDirectoryRecursivelyWithPath(path))) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
@@ -349,7 +364,7 @@ void loop(void* args) {
// this is the path we will write to
const auto temp_path = path + "~";
if (R_FAILED(rc = fs.CreateFile(temp_path, file_data.size(), 0)) && rc != FsError_PathAlreadyExists) {
if (R_FAILED(rc = fs.CreateFile(temp_path, file_data.size(), 0)) && rc != FsError_ResultPathAlreadyExists) {
sendall(connfd, &ERR_FILE, sizeof(ERR_FILE));
log_write("failed to create file: %X\n", rc);
continue;
@@ -393,7 +408,7 @@ void loop(void* args) {
}
}
if (R_FAILED(rc = fs.DeleteFile(path)) && rc != FsError_PathNotFound) {
if (R_FAILED(rc = fs.DeleteFile(path)) && rc != FsError_ResultPathNotFound) {
log_write("failed to delete %X\n", rc);
continue;
}
@@ -458,14 +473,13 @@ bool nxlinkInitialize(NxlinkCallback callback) {
g_callback = callback;
g_quit = false;
Result rc;
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*64, 0x2C, 2))) {
log_write("failed to create nxlink thread: 0x%X\n", rc);
if (R_FAILED(threadCreate(&g_thread, loop, nullptr, nullptr, 1024*64, 0x2C, 2))) {
log_write("failed to create nxlink thread: 0x%X\n", socketGetLastResult());
return false;
}
if (R_FAILED(rc = threadStart(&g_thread))) {
log_write("failed to start nxlink thread: 0x%X\n", rc);
if (R_FAILED(threadStart(&g_thread))) {
log_write("failed to start nxlink thread: 0x%X\n", socketGetLastResult());
threadClose(&g_thread);
return false;
}

View File

@@ -11,8 +11,6 @@
#include "defines.hpp"
#include "app.hpp"
#include "ui/progress_box.hpp"
#include "i18n.hpp"
#include "log.hpp"
namespace sphaira {
namespace {
@@ -192,40 +190,6 @@ 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]{};
@@ -613,56 +577,17 @@ 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(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));
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));
}
void patch_nacp(NacpStruct& nacp, const NcapPatch& patch) {
@@ -1102,7 +1027,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
R_UNLESS(!config.main.empty(), OwoError_BadArgs);
R_UNLESS(!config.npdm.empty(), OwoError_BadArgs);
pbox->NewTransfer("Creating Program"_i18n).UpdateTransfer(0, 8);
pbox->NewTransfer("Creating Program").UpdateTransfer(0, 8);
FileEntries exefs;
add_file_entry(exefs, "main", config.main);
add_file_entry(exefs, "main.npdm", config.npdm);
@@ -1134,7 +1059,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
// create control
{
pbox->NewTransfer("Creating Control"_i18n).UpdateTransfer(1, 8);
pbox->NewTransfer("Creating Control").UpdateTransfer(1, 8);
// patch nacp
NcapPatch nacp_patch{};
nacp_patch.tid = tid;
@@ -1157,7 +1082,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
NcmContentStorageRecord content_storage_record;
NcmContentMetaData content_meta_data;
{
pbox->NewTransfer("Creating Meta"_i18n).UpdateTransfer(2, 8);
pbox->NewTransfer("Creating Meta").UpdateTransfer(2, 8);
const auto meta_entry = create_meta_nca(tid, key, storage_id, nca_entries);
nca_entries.emplace_back(meta_entry.nca_entry);
@@ -1174,7 +1099,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
ON_SCOPE_EXIT(ncmContentStorageClose(&cs));
for (const auto& nca : nca_entries) {
pbox->NewTransfer("Writing Nca"_i18n).UpdateTransfer(3, 8);
pbox->NewTransfer("Writing Nca").UpdateTransfer(3, 8);
NcmContentId content_id;
NcmPlaceHolderId placeholder_id;
std::memcpy(&content_id, nca.hash, sizeof(content_id));
@@ -1189,7 +1114,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
// setup database
{
pbox->NewTransfer("Updating ncm databse"_i18n).UpdateTransfer(4, 8);
pbox->NewTransfer("Updating ncm databse").UpdateTransfer(4, 8);
NcmContentMetaDatabase db;
R_TRY(ncmOpenContentMetaDatabase(&db, storage_id));
ON_SCOPE_EXIT(ncmContentMetaDatabaseClose(&db));
@@ -1200,7 +1125,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
// push record
{
pbox->NewTransfer("Pushing application record"_i18n).UpdateTransfer(5, 8);
pbox->NewTransfer("Pushing application record").UpdateTransfer(5, 8);
Service srv{}, *srv_ptr = &srv;
bool already_installed{};
@@ -1241,7 +1166,7 @@ auto install_forwarder(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId st
}
auto install_forwarder(OwoConfig& config, NcmStorageId storage_id) -> Result {
App::Push(std::make_shared<ui::ProgressBox>("Installing Forwarder"_i18n, [config, storage_id](auto pbox) mutable -> bool {
App::Push(std::make_shared<ui::ProgressBox>("Installing Forwarder", [config, storage_id](auto pbox) mutable -> bool {
return R_SUCCEEDED(install_forwarder(pbox, config, storage_id));
}));
R_SUCCEED();

View File

@@ -1,7 +1,6 @@
#include "ui/error_box.hpp"
#include "ui/nvg_util.hpp"
#include "app.hpp"
#include "i18n.hpp"
namespace sphaira::ui {
namespace {
@@ -1134,6 +1133,10 @@ 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);
@@ -1144,10 +1147,10 @@ auto ErrorBox::Draw(NVGcontext* vg, Theme* theme) -> void {
gfx::drawTextArgs(vg, center_x, 180, 63, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::RED, "\uE140");
gfx::drawTextArgs(vg, center_x, 270, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "Error code: 0x%X Module: %s Description: %s", m_code, m_module_str.c_str(), m_description_str.c_str());
gfx::drawTextArgs(vg, center_x, 325, 23, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "%s", m_message.c_str());
gfx::drawTextArgs(vg, center_x, 380, 20, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::SILVER, "If this message appears repeatedly, please open an issue."_i18n.c_str());
gfx::drawTextArgs(vg, center_x, 380, 20, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::SILVER, "If this message appears repeatedly, please open an issue.");
gfx::drawTextArgs(vg, center_x, 415, 20, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, gfx::Colour::SILVER, "https://github.com/ITotalJustice/sphaira/issues");
gfx::drawRectOutline(vg, 4.f, theme->elements[ThemeEntryID_SELECTED_OVERLAY].colour, box, theme->elements[ThemeEntryID_SELECTED].colour);
gfx::drawTextArgs(vg, center_x, box.y + box.h/2, 23, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::WHITE, "OK"_i18n.c_str());
gfx::drawTextArgs(vg, center_x, box.y + box.h/2, 23, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::WHITE, "OK");
}
} // namespace sphaira::ui

View File

@@ -1,189 +0,0 @@
#include "ui/list.hpp"
#include "ui/nvg_util.hpp"
#include "app.hpp"
#include "log.hpp"
namespace sphaira::ui {
List::List(s64 row, s64 page, const Vec4& pos, const Vec4& v, const Vec2& pad)
: m_row{row}
, m_page{page}
, m_v{v}
, m_pad{pad} {
m_pos = pos;
SetScrollBarPos(SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200);
}
auto List::ClampY(float y, s64 count) const -> float {
float y_max = 0;
if (count >= m_page) {
// round up
if (count % m_row) {
count = count + (m_row - count % m_row);
}
y_max = (count - m_page) / m_row * GetMaxY();
}
if (y < 0) {
y = 0;
} else if (y > y_max) {
y = y_max;
}
return y;
}
void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCallback callback) {
if (touch->is_clicked && touch->in_range(GetPos())) {
auto v = m_v;
v.y -= ClampY(m_yoff + m_y_prog, count);
for (s64 i = 0; i < count; v.y += v.h + m_pad.y) {
if (v.y > GetY() + GetH()) {
break;
}
const auto x = v.x;
for (; i < count; i++, v.x += v.w + m_pad.x) {
// only draw if full x is in bounds
if (v.x + v.w > GetX() + GetW()) {
break;
}
// skip anything not visible
if (v.y + v.h < GetY()) {
continue;
}
Vec4 vv = v;
// if not drawing, only return clipped v as its used for touch
vv.w = std::min(v.x + v.w, m_pos.x + m_pos.w) - v.x;
vv.h = std::min(v.y + v.h, m_pos.y + m_pos.h) - v.y;
if (touch->in_range(vv)) {
callback(i);
return;
}
}
v.x = x;
}
} else if (touch->is_scroll && touch->in_range(GetPos())) {
m_y_prog = (float)touch->initial.y - (float)touch->cur.y;
} else if (touch->is_end) {
m_yoff = ClampY(m_yoff + m_y_prog, count);
m_y_prog = 0;
}
}
void List::Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const {
const auto yoff = ClampY(m_yoff + m_y_prog, count);
const s64 start = yoff / GetMaxY() * m_row;
gfx::drawScrollbar2(vg, theme, m_scrollbar.x, m_scrollbar.y, m_scrollbar.h, start, count, m_row, m_page);
auto v = m_v;
v.y -= yoff;
nvgSave(vg);
nvgScissor(vg, GetX(), GetY(), GetW(), GetH());
for (s64 i = 0; i < count; v.y += v.h + m_pad.y) {
if (v.y > GetY() + GetH()) {
break;
}
const auto x = v.x;
for (; i < count; i++, v.x += v.w + m_pad.x) {
// only draw if full x is in bounds
if (v.x + v.w > GetX() + GetW()) {
break;
}
// skip anything not visible
if (v.y + v.h < GetY()) {
continue;
}
callback(vg, theme, v, i);
}
v.x = x;
}
nvgRestore(vg);
}
auto List::ScrollDown(s64& index, s64 step, s64 count) -> bool {
const auto old_index = index;
if (!count) {
return false;
}
if (index + step < count) {
index += step;
} else {
index = count - 1;
}
if (index != old_index) {
App::PlaySoundEffect(SoundEffect_Scroll);
s64 delta = index - old_index;
s64 start = m_yoff / GetMaxY() * m_row;
while (index < start) {
start -= m_row;
m_yoff -= GetMaxY();
}
if (index - start >= m_page) {
do {
start += m_row;
delta -= m_row;
m_yoff += GetMaxY();
} while (delta > 0 && start + m_page < count);
}
return true;
}
return false;
}
auto List::ScrollUp(s64& index, s64 step, s64 count) -> bool {
const auto old_index = index;
if (!count) {
return false;
}
if (index >= step) {
index -= step;
} else {
index = 0;
}
if (index != old_index) {
App::PlaySoundEffect(SoundEffect_Scroll);
s64 start = m_yoff / GetMaxY() * m_row;
while (index < start) {
start -= m_row;
m_yoff -= GetMaxY();
}
while (index - start >= m_page && start + m_page < count) {
start += m_row;
m_yoff += GetMaxY();
}
return true;
}
return false;
}
} // namespace sphaira::ui

View File

@@ -206,17 +206,17 @@ auto LoadAndParseManifest(const Entry& e) -> ManifestEntries {
return ParseManifest(std::span{(const char*)data.data(), data.size()});
}
auto EntryLoadImageFile(fs::Fs& fs, const fs::FsPath& path, LazyImage& image) -> bool {
void EntryLoadImageFile(fs::Fs& fs, const fs::FsPath& path, LazyImage& image) {
// already have the image
if (image.image) {
// log_write("warning, tried to load image: %s when already loaded\n", path);
return true;
log_write("warning, tried to load image: %s when already loaded\n", path);
return;
}
auto vg = App::GetVg();
std::vector<u8> image_buf;
if (R_FAILED(fs.read_entire_file(path, image_buf))) {
log_write("failed to load image from file: %s\n", path.s);
image.state = ImageDownloadState::Failed;
} 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,21 +228,20 @@ auto EntryLoadImageFile(fs::Fs& fs, const fs::FsPath& path, LazyImage& image) ->
}
if (!image.image) {
log_write("failed to load image from file: %s\n", path.s);
return false;
image.state = ImageDownloadState::Failed;
log_write("failed to load image from file: %s\n", path);
} else {
// log_write("loaded image from file: %s\n", path);
return true;
}
}
auto EntryLoadImageFile(const fs::FsPath& path, LazyImage& image) -> bool {
void EntryLoadImageFile(const fs::FsPath& path, LazyImage& image) {
if (!strncasecmp("romfs:/", path, 7)) {
fs::FsStdio fs;
return EntryLoadImageFile(fs, path, image);
EntryLoadImageFile(fs, path, image);
} else {
fs::FsNativeSd fs;
return EntryLoadImageFile(fs, path, image);
EntryLoadImageFile(fs, path, image);
}
}
@@ -271,17 +270,45 @@ 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;
@@ -343,7 +370,7 @@ auto UninstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
// remove directory, this will also delete manifest and info
const auto dir = BuildPackageCachePath(entry);
pbox->NewTransfer("Removing "_i18n + dir);
pbox->NewTransfer("Removing " + dir);
if (R_FAILED(fs.DeleteDirectoryRecursively(dir))) {
log_write("failed to delete folder: %s\n", dir);
} else {
@@ -367,25 +394,29 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
// 1. download the zip
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Downloading "_i18n + entry.title);
pbox->NewTransfer("Downloading " + entry.title);
log_write("starting download\n");
const auto url = BuildZipUrl(entry);
if (!curl::Api().ToFile(
curl::Url{url},
curl::Path{zip_out},
curl::OnProgress{pbox->OnDownloadProgressCallback()}
).success) {
log_write("error with download\n");
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;
})) {
log_write("error with download\n");
// push popup error box
return false;
// return appletEnterFatalSection();
}
}
ON_SCOPE_EXIT(fs.DeleteFile(zip_out));
// 2. md5 check the zip
if (!pbox->ShouldExit()) {
pbox->NewTransfer("Checking MD5"_i18n);
pbox->NewTransfer("Checking MD5");
log_write("starting md5 check\n");
FsFile f;
@@ -441,7 +472,7 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
}
if (strncasecmp(md5_str, entry.md5.data(), entry.md5.length())) {
log_write("bad md5: %.*s vs %.*s\n", 32, md5_str, 32, entry.md5.c_str());
log_write("bad md5: %.*s vs %.*s\n", 32, md5_str, 32, entry.md5);
return false;
}
}
@@ -514,10 +545,10 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
}
// create directories
fs.CreateDirectoryRecursivelyWithPath(output);
fs.CreateDirectoryRecursivelyWithPath(output, true);
Result rc;
if (R_FAILED(rc = fs.CreateFile(output, info.uncompressed_size, 0)) && rc != FsError_PathAlreadyExists) {
if (R_FAILED(rc = fs.CreateFile(output, info.uncompressed_size, 0, true)) && rc != FsError_ResultPathAlreadyExists) {
log_write("failed to create file: %s 0x%04X\n", output, rc);
return false;
}
@@ -606,7 +637,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))) {
if (R_FAILED(fs.DeleteFile(safe_buf, true))) {
log_write("failed to delete: %s\n", safe_buf);
} else {
log_write("deleted file: %s\n", safe_buf);
@@ -651,29 +682,36 @@ 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);
curl::Api().ToAsync(
curl::Url{URL_POST_FEEDBACK},
curl::Path{file},
curl::Fields{post},
curl::OnComplete{[](auto& result){
if (result.success) {
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 :(");
}
}
});
}
}, 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);
@@ -682,32 +720,41 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
const auto path = BuildBannerCachePath(m_entry);
const auto url = BuildBannerUrl(m_entry);
m_banner.cached = EntryLoadImageFile(path, m_banner);
// race condition if we pop the widget before the download completes
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 {
if (fs::FsNativeSd().FileExists(path)) {
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);
}
}, 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() {
@@ -733,26 +780,38 @@ 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, "version: %s"_i18n.c_str(), m_entry.version.c_str());
// gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "%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", m_entry.version.c_str());
text_start_y += text_inc_y;
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "updated: %s"_i18n.c_str(), m_entry.updated.c_str());
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "updated: %s", m_entry.updated.c_str());
text_start_y += text_inc_y;
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "category: %s"_i18n.c_str(), m_entry.category.c_str());
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "category: %s", 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, "extracted: %.2f MiB"_i18n.c_str(), (double)m_entry.extracted / 1024.0);
// gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "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", (double)m_entry.extracted / 1024.0);
text_start_y += text_inc_y;
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "app_dls: %s"_i18n.c_str(), AppDlToStr(m_entry.app_dls).c_str());
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->elements[ThemeEntryID_TEXT].colour, "app_dls: %s", AppDlToStr(m_entry.app_dls).c_str());
text_start_y += text_inc_y;
// for (const auto& option : m_options) {
const auto& text_col = theme->elements[ThemeEntryID_TEXT].colour;
// todo: rewrite this mess and use list
constexpr float mm = 0;//20;
constexpr Vec4 block{968.f + mm, 110.f, 256.f - mm*2, 60.f};
constexpr float text_xoffset{15.f};
@@ -804,7 +863,6 @@ 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();
@@ -817,7 +875,6 @@ 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();
@@ -856,7 +913,7 @@ void EntryMenu::UpdateOptions() {
SetIndex(0);
}
void EntryMenu::SetIndex(s64 index) {
void EntryMenu::SetIndex(std::size_t index) {
m_index = index;
const auto option = m_options[m_index];
if (option.confirm_text.empty()) {
@@ -878,12 +935,14 @@ auto toLower(const std::string& str) -> std::string {
return lower;
}
Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}, m_nro_entries{nro_entries} {
Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"}, m_nro_entries{nro_entries} {
fs::FsNativeSd fs;
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/icons");
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()) {
@@ -908,23 +967,41 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
}
}}),
std::make_pair(Button::DOWN, Action{[this](){
if (m_list->ScrollDown(m_index, 3, m_entries_current.size())) {
if (ScrollHelperDown(m_index, m_start, 3, 9, m_entries_current.size())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::UP, Action{[this](){
if (m_list->ScrollUp(m_index, 3, m_entries_current.size())) {
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())) {
SetIndex(m_index);
}
}}),
std::make_pair(Button::R2, Action{[this](){
if (m_list->ScrollDown(m_index, 9, m_entries_current.size())) {
SetIndex(m_index);
std::make_pair(Button::L2, Action{(u8)ActionType::HELD, [this](){
if (m_entries.empty()) {
return;
}
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](){
@@ -958,17 +1035,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](s64& index_out){
options->Add(std::make_shared<SidebarEntryArray>("Filter"_i18n, filter_items, [this, filter_items](std::size_t& index_out){
SetFilter((Filter)index_out);
}, (s64)m_filter));
}, (std::size_t)m_filter));
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](std::size_t& index_out){
SetSort((SortType)index_out);
}, (s64)m_sort));
}, (std::size_t)m_sort));
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](s64& index_out){
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](std::size_t& index_out){
SetOrder((OrderType)index_out);
}, (s64)m_order));
}, (std::size_t)m_order));
options->Add(std::make_shared<SidebarEntryCallback>("Search"_i18n, [this](){
std::string out;
@@ -981,12 +1058,11 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
);
m_repo_download_state = ImageDownloadState::Progress;
curl::Api().ToFileAsync(
curl::Url{URL_JSON},
curl::Path{REPO_PATH},
curl::Flags{curl::Flag_Cache},
curl::OnComplete{[this](auto& result){
if (result.success) {
#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();
@@ -994,16 +1070,64 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
} 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, &current_time);
}
// this fails if we don't have the file or on fw < 3.0.0
if (!time_stamp.is_valid) {
download_file = true;
} else {
// check the date, if older than 1day, 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_t)time_stamp.created;
const auto time_cur = (time_t)current_time;
const auto tm_file = *gmtime(&time_file);
const auto tm_cur = *gmtime(&time_cur);
if (tm_cur.tm_yday > tm_file.tm_yday || tm_cur.tm_year > tm_file.tm_year) {
log_write("repo.json expired, downloading new! cur_yday: %d file_yday: %d | cur_year: %d file_year: %d\n", tm_cur.tm_yday, tm_file.tm_yday, tm_cur.tm_year, tm_file.tm_year);
download_file = true;
} else {
log_write("repo.json not expired! cur_yday: %d file_yday: %d | cur_year: %d file_year: %d\n", tm_cur.tm_yday, tm_file.tm_yday, tm_cur.tm_year, tm_file.tm_year);
// time_file = (time_t)time_stamp.modified;
// tm_file = *gmtime(&time_file);
// log_write("repo.json not expired! cur_yday: %d file_yday: %d | cur_year: %d file_year: %d\n", tm_cur.tm_yday, tm_file.tm_yday, tm_cur.tm_year, tm_file.tm_year);
// time_file = (time_t)time_stamp.accessed;
// tm_file = *gmtime(&time_file);
// log_write("repo.json not expired! cur_yday: %d file_yday: %d | cur_year: %d file_year: %d\n", tm_cur.tm_yday, tm_file.tm_yday, tm_cur.tm_year, tm_file.tm_year);
}
}
// todo: remove me soon
// download_file = true;
if (download_file) {
DownloadFileAsync(URL_JSON, REPO_PATH, "", [this](std::vector<u8>& data, bool success){
if (success) {
m_repo_download_state = ImageDownloadState::Done;
if (HasFocus()) {
ScanHomebrew();
}
} else {
m_repo_download_state = ImageDownloadState::Failed;
}
});
} else {
m_repo_download_state = ImageDownloadState::Done;
}
#endif
m_filter = (Filter)ini_getl(INI_SECTION, "filter", m_filter, App::CONFIG_PATH);
m_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();
}
@@ -1013,85 +1137,65 @@ Menu::~Menu() {
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_entries_current.size(), [this](auto i) {
if (m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
SetIndex(i);
}
});
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
if (m_entries.empty()) {
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::YELLOW, "Loading..."_i18n.c_str());
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::YELLOW, "Loading...");
return;
}
if (m_entries_current.empty()) {
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::YELLOW, "Empty!"_i18n.c_str());
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, gfx::Colour::YELLOW, "Empty!");
return;
}
// max images per frame, in order to not hit io / gpu too hard.
const int image_load_max = 2;
int image_load_count = 0;
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;
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;
// 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);
}
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];
auto& image = e.image;
// try and load cached image.
if (image_load_count < image_load_max && !image.image && !image.tried_cache) {
image.tried_cache = true;
image.cached = EntryLoadImageFile(BuildIconCachePath(e), image);
if (image.cached) {
image_load_count++;
}
}
// lazy load image
if (!image.image || image.cached) {
switch (image.state) {
if (!e.image.image) {
switch (e.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;
}
if (fs::FsNativeSd().FileExists(path)) {
EntryLoadImageFile(path, e.image);
} else {
image.state = ImageDownloadState::Failed;
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: {
if (image_load_count < image_load_max) {
image.cached = false;
if (!EntryLoadImageFile(BuildIconCachePath(e), e.image)) {
image.state = ImageDownloadState::Failed;
} else {
image_load_count++;
}
}
EntryLoadImageFile(BuildIconCachePath(e), e.image);
} break;
case ImageDownloadState::Failed: {
} break;
@@ -1099,7 +1203,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
}
auto text_id = ThemeEntryID_TEXT;
if (pos == m_index) {
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 {
@@ -1110,10 +1214,10 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
// 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);
// gfx::drawImage(vg, x + 20, y + 20, image_size, image_size_h, e.image.image ? e.image.image : m_default_image);
nvgSave(vg);
nvgIntersectScissor(vg, v.x, v.y, w - 30.f, h); // clip
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());
@@ -1137,7 +1241,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image);
break;
}
});
}
}
}
void Menu::OnFocusGained() {
@@ -1166,16 +1271,21 @@ 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_list->SetYoff((((i - 9) + 3) / 3) * m_list->GetMaxY());
m_start = (i - 9) / 3 * 3 + 3;
} else {
m_list->SetYoff(0);
m_start = 0;
}
log_write("\nnew index: %zu start: %zu\n", m_index, m_start);
break;
}
}
@@ -1183,10 +1293,10 @@ void Menu::OnFocusGained() {
}
}
void Menu::SetIndex(s64 index) {
void Menu::SetIndex(std::size_t index) {
m_index = index;
if (!m_index) {
m_list->SetYoff(0);
m_start = 0;
}
this->SetSubHeading(std::to_string(m_index + 1) + " / " + std::to_string(m_entries_current.size()));
@@ -1328,7 +1438,7 @@ void Menu::Sort() {
char subheader[128]{};
std::snprintf(subheader, sizeof(subheader), "Filter: %s | Sort: %s | Order: %s"_i18n.c_str(), i18n::get(FILTER_STR[m_filter]).c_str(), i18n::get(SORT_STR[m_sort]).c_str(), i18n::get(ORDER_STR[m_order]).c_str());
std::snprintf(subheader, sizeof(subheader), "Sort: %s | Filter: %s | Order: %s", i18n::get(SORT_STR[m_sort]), i18n::get(FILTER_STR[m_filter]), i18n::get(ORDER_STR[m_order]));
SetTitleSubHeading(subheader);
std::sort(m_entries_current.begin(), m_entries_current.end(), sorter);
@@ -1381,10 +1491,9 @@ void Menu::SetSearch(const std::string& term) {
SetFilter(m_filter);
SetIndex(m_entry_search_jump_back);
if (m_entry_search_jump_back >= 9) {
m_list->SetYoff(0);
m_list->SetYoff((((m_entry_search_jump_back - 9) + 3) / 3) * m_list->GetMaxY());
m_start = (m_entry_search_jump_back - 9) / 3 * 3 + 3;
} else {
m_list->SetYoff(0);
m_start = 0;
}
}});
@@ -1415,12 +1524,11 @@ void Menu::SetAuthor() {
} else {
SetFilter(m_filter);
}
SetIndex(m_entry_author_jump_back);
if (m_entry_author_jump_back >= 9) {
m_list->SetYoff((((m_entry_author_jump_back - 9) + 3) / 3) * m_list->GetMaxY());
m_start = (m_entry_author_jump_back - 9) / 3 * 3 + 3;
} else {
m_list->SetYoff(0);
m_start = 0;
}
}});

View File

@@ -1,5 +1,4 @@
#include "ui/menus/file_viewer.hpp"
#include "i18n.hpp"
namespace sphaira::ui::menu::fileview {
namespace {
@@ -7,7 +6,7 @@ namespace {
} // namespace
Menu::Menu(const fs::FsPath& path) : MenuBase{path}, m_path{path} {
SetAction(Button::B, Action{"Back"_i18n, [this](){
SetAction(Button::B, Action{"Back", [this](){
SetPop();
}});

Some files were not shown because too many files have changed in this diff Show More