86 Commits
0.3.0 ... touch

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

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

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

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

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

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

---------

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

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

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

- themezer requests now only request the data needed.

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

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

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

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

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

- github json files have changed from a name + url to a repo + author pair.
- drawing widgets now starts from the last file in the array. as a menu takes up the whole screen, it
 is pointless drawing menu's underneath. this halves gpu usage.
- download url caching has been removed. this was added to fix a race condition when opening /
  closing a widget which starts a download when created. this would result in 2 same files being
  downloaded at the same time. this is no longer an issue and was overhead per download request.
2024-12-29 00:33:31 +00:00
ITotalJustice
2edfe91ad6 re-write download code to support headers, needed for etag support.
etag support will be added later. github supports it and themezer probably does as well.
appstore does not sadly...
2024-12-27 02:28:44 +00:00
ITotalJustice
7005118876 Create FUNDING.yml 2024-12-26 19:23:39 +00:00
DDinghoya
087d44fb40 Update ko.json (#55)
. Fixed some awkward words
. If there is no abbreviation "...", fixed progressive tense to noun.
2024-12-26 18:34:47 +00:00
ITotalJustice
e3722f2591 include name when prompting the user to select which asset to download 2024-12-26 18:29:03 +00:00
ITotalJustice
47855ce7b4 change workflow to only build MinSizeRel 2024-12-26 18:11:53 +00:00
ITotalJustice
ec7caabdbd add GitHub downloader, fix yyjson helper missing break, hide popup list when out of focus 2024-12-26 18:11:03 +00:00
Yorunokyujitsu
adf0a3b2cd Update ko.json and ja.json and add MTP/FTP strings (#54)
Co-authored-by: Yorunokyujitsu <seeonmini1315@gmail.com>
2024-12-26 04:58:17 +00:00
Ny'hrarr
f88e354ae8 Translate missing fields (Portuguese) (#51)
* Update pt.json
2024-12-26 03:29:33 +00:00
cucholix
df3d8d3990 Update es.json (#49)
Further improvements for ver 0.5.0
2024-12-26 03:29:08 +00:00
ITotalJustice
f01dbf7c67 silence warning, add credit to readme, bump version for release 2024-12-25 22:27:25 +00:00
glitched_nx
7c273f30f3 Solve conflicts in de.json, fr.json files - and complete them (#48) 2024-12-25 22:20:31 +00:00
ITotalJustice
37890f157d add mtp (haze) ftp (ftpsrv), update RA file assoc, nxlink now polls for connection. 2024-12-25 22:17:21 +00:00
shadow2560
a2c9b63dfd Some fixes and update french language (#46)
* Fix starred homebrew list logging, fix hbmenu identification when backing up it and logging it correctly, update french language.

Signed-off-by: shadow2560 <24191064+shadow2560@users.noreply.github.com>
2024-12-24 22:21:54 +00:00
ITotalJustice
3df676df0f New strings added and update ko, ja, se(https://github.com/ITotalJustice/sphaira/pull/36), es(https://github.com/ITotalJustice/sphaira/pull/38), zh(https://github.com/ITotalJustice/sphaira/pull/42), fr(https://github.com/ITotalJustice/sphaira/pull/45) translations. (#44)
Co-authored-by: Yorunokyujitsu <seonmini1315@gamil.com>
2024-12-24 08:55:29 +00:00
do-kiss
276ee36bfe Chinese translation update (#42)
* Update zh.json
2024-12-23 04:56:16 +00:00
cucholix
17b622833a Update es.json (#38)
Replaced some lower case chars to match English style.
Replaced some words like Homebrew, AppStore, IRS, etc… that doesn’t have direct meaning in Spanish, literal translation sound awkward so it’s better leave them in English.
Filled out missing translations.
2024-12-23 04:55:38 +00:00
HenryBaby
536c169255 Updated Swedish translation (#36) 2024-12-21 21:49:21 +00:00
shadow2560
38640ea696 Update french translation (#35)
Signed-off-by: shadow2560 <24191064+shadow2560@users.noreply.github.com>
2024-12-21 21:49:01 +00:00
ITotalJustice
372399a27d remove more unused strings 2024-12-21 21:18:13 +00:00
ITotalJustice
483b2b3ce0 add option to restart sphaira upon installing an update 2024-12-21 20:36:59 +00:00
ITotalJustice
e0040b625e remove unused strings from translations 2024-12-21 20:11:22 +00:00
ITotalJustice
10f079e881 make progress popup a little nicer, store json timeout is now 1h, initial work on storing update changelog
also removed the ability to cancel an update whilst unzipping the files, as this would result
in a corrupted sphaira.nro install, which we don't want.
2024-12-21 19:44:43 +00:00
ITotalJustice
4a058d3caf disable install by default, enabled via advanced menu. disable web browser if applet. 2024-12-21 18:49:40 +00:00
ITotalJustice
986ffdcd9c remove option to set archive bit in filebrowser
removed as likely no one needs this feature, and an cause problems for users
that do not know what they're doing
2024-12-21 18:04:19 +00:00
ITotalJustice
97085ef282 add option in config to remove install warning prompt 2024-12-21 18:01:57 +00:00
ITotalJustice
d02fbcf282 fix performance regression caused by #32
the issue isn't with the pr itself, however it did expose the lack of caching with the translation strings, it will
continue to search for strings, even if we already found them.
to solve this, i used a map to cache said strings
2024-12-21 17:31:19 +00:00
Yorunokyujitsu
c8ae2a7872 Almost all strings for translation. (#32)
* Almost all strings for translation

* Remove nonexistent strings.

---------

Co-authored-by: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com>
2024-12-21 16:49:48 +00:00
HenryBaby
79da00e098 Swedish translation (#26) 2024-12-21 16:47:26 +00:00
ITotalJustice
0edd7c400f add support for swedish translations (needed for pr merge) 2024-12-21 16:46:10 +00:00
ITotalJustice
aa03256fd4 better naming for menu tabs (Fs -> Files, Apps, App -> Store)
fixes #30
2024-12-21 16:35:26 +00:00
ITotalJustice
8f1084b24f fix text bounds in option box
fixes #28
2024-12-21 16:32:36 +00:00
ITotalJustice
55c952a51f add stars in homebrew menu (hbmenu feature)
fixes #22
2024-12-21 16:30:32 +00:00
ITotalJustice
dd6371997c fix core3 being pinned at 100% due to nxlink polling.
this was caused due to 9966e57e12
2024-12-21 01:21:45 +00:00
shadow2560
c8c4a273c9 Init and close Set service so auto language work now. (#31)
* Init and close Set service so auto language work now.

Signed-off-by: shadow2560 <24191064+shadow2560@users.noreply.github.com>

* Init and close Set service place moved into services init/close order.

Signed-off-by: shadow2560 <24191064+shadow2560@users.noreply.github.com>

---------

Signed-off-by: shadow2560 <24191064+shadow2560@users.noreply.github.com>
2024-12-20 17:42:28 +00:00
ITotalJustice
0570c14343 bump version for release 2024-12-20 11:17:15 +00:00
ITotalJustice
66f2171995 use localtime instead of gmtime
fixes #23
2024-12-18 17:51:08 +00:00
J0hnTR
3178f11596 Update ru.json (#29)
Native RU-speaker here, just verified autotranslation a bit.
2024-12-18 14:36:50 +00:00
ITotalJustice
e2d9db8928 fix appstore sort strings not being rendered 2024-12-18 00:28:59 +00:00
ITotalJustice
945d1f3ae6 add updater, remove white theme (it was unfished), remove more dead code, bump version for release 2024-12-18 00:04:27 +00:00
Battosai94
0585bec6e5 Update fr.json (#17)
Minor changes to FR translation
2024-12-17 23:15:53 +00:00
Yorunokyujitsu
11f4f3000a Korean translation (#19) 2024-12-17 23:15:30 +00:00
Sanras
474843915c Add OLED Black Theme (#20) 2024-12-17 23:12:37 +00:00
Ny'hrarr
3146b951f2 New icon (#18)
* New icon
2024-12-17 22:54:45 +00:00
ITotalJustice
2db9b72416 improve fr translation (credit to @Battosai94)
see: https://github.com/ITotalJustice/sphaira/issues/2#issuecomment-2549628348
2024-12-17 21:12:26 +00:00
ITotalJustice
9b4710d386 show time in main menu, change battery % symbol to use a small version 2024-12-17 21:04:36 +00:00
Aurelia
ddf5b94f4d i18n: improve de locale (#16)
revised some wording, false friends and neologisms
2024-12-17 20:50:15 +00:00
ITotalJustice
ecb2567757 add workflow 2024-12-17 20:05:33 +00:00
ITotalJustice
b59a162473 fix not exiting to home menu whilst replacing hbmenu 2024-12-17 19:54:49 +00:00
ITotalJustice
ef5ff520d1 Add files via upload (#11)
Corrected Japanese translation

Co-authored-by: HoRy205 <101063179+HoRy205@users.noreply.github.com>
2024-12-17 16:56:00 +00:00
ITotalJustice
433c2e220c fix overlapping text in fs
fixes #13
2024-12-17 16:21:14 +00:00
do-kiss
98ad2f485b Chinese translation (#12)
Chinese translation
2024-12-17 16:05:07 +00:00
ITotalJustice
9966e57e12 option to install nro from fs, swap LR display, load translations locally, fix scrolling sound, add file name to rename swkdb
- reduce nxlink svcsleep to reduce latency between polling.
- translations can now be loaded from /config/sphaira/i18n/name.json, this is to help aid those creating translations.
- swap LR position in display. the fix is a hack, but it'll do for now.
- sound effects are now consistent throught the app.
- renaming a file will now show the current file name in swkbd, makes it easier to rename from config.ini.template -> config.ini
- removed some dead code that was unused.
- add credits to the readme.
- speed up playlog ini parsing by browsing the ini rather that doing a query for each entry.
2024-12-17 16:03:05 +00:00
shadow2560
c11990e1bd Improve french language (#10)
Signed-off-by: shadow2560 <24191064+shadow2560@users.noreply.github.com>
2024-12-17 13:29:18 +00:00
LNLenost
9b1c0226e1 Added Italian translation (#8)
* Delete assets/romfs/i18n/it.json (old Italian translation)

* Uploaded new, correct Italian translations.
2024-12-17 13:13:36 +00:00
WE1ZARD
f38a671a7f use translatation from native Chinese (#3) 2024-12-17 12:58:17 +00:00
Ny'hrarr
fe952dc9f2 Improve Portuguese translation (#1)
* Improved Portuguese translation

* Update pt.json
2024-12-17 01:49:21 +00:00
ITotalJustice
d063ffcb20 remove old theme entries, make "Back" be the default hovered option when installing forwarder 2024-12-16 22:51:58 +00:00
122 changed files with 7021 additions and 3443 deletions

15
.github/FUNDING.yml vendored Normal file
View File

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

31
.github/workflows/build_presets.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: build
on: [push, pull_request]
jobs:
build:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
preset: [MinSizeRel]
runs-on: ${{ matrix.os }}
container: devkitpro/devkita64:latest
steps:
- uses: actions/checkout@v3
# fetch latest cmake
- uses: lukka/get-cmake@latest
- name: Configure CMake
run: |
cmake --preset ${{ matrix.preset }} -DUSE_VFS_GC=0
- name: Build
run: cmake --build --preset ${{ matrix.preset }} --parallel 4
- uses: actions/upload-artifact@master
with:
name: sphaira-${{ matrix.preset }}
path: build/${{ matrix.preset }}/sphaira.nro

View File

@@ -42,5 +42,12 @@ function(dkp_fatal_if_not_found var package)
endif() endif()
endfunction(dkp_fatal_if_not_found var package) 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(hbl)
add_subdirectory(sphaira) add_subdirectory(sphaira)

View File

@@ -2,8 +2,6 @@
A homebrew menu for the switch. 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/). [See the gbatemp thread for more details / discussion](https://gbatemp.net/threads/sphaira-hbmenu-replacement.664523/).
## showcase ## showcase
@@ -26,6 +24,15 @@ please include:
- FW version - FW version
- The bug itself and how to reproduce it - 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 ## 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. 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.
@@ -51,3 +58,6 @@ see `assets/romfs/assoc/` for more examples of file assoc entries
- libpulsar - libpulsar
- minIni - minIni
- gbatemp - gbatemp
- hb-appstore
- haze
- everyone who has contributed to this project!

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

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

View File

@@ -0,0 +1,4 @@
[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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,114 +1,238 @@
{ {
"Launch": "Start", "[Applet Mode]": "[Applet-Modus]",
"No Internet": "Keine Internetverbindung",
"Files": "Dateien",
"Apps": "Apps",
"Store": "Store",
"Menu": "Menü",
"Options": "Optionen", "Options": "Optionen",
"Homebrew Options": "Homebrew-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",
"Sort By": "Sortieren nach", "Sort By": "Sortieren nach",
"Sort Options": "Sortieroptionen", "Sort Options": "Sortieroptionen",
"Updated": "Aktualisiert", "Filter": "Filter",
"Size": "Größe",
"Alphabetical": "Alphabetisch",
"Decending": "Absteigend",
"Ascending": "Aufsteigend",
"Sort": "Sortieren", "Sort": "Sortieren",
"Order": "Befehl", "Order": "Reihenfolge",
"Info": "Info", "Search": "Suchen",
"Delete": "Löschen", "Updated": "Aktualisiert",
"Hide Sphaira": "Sphaira verstecken", "Updated (Star)": "Aktualisiert (Favoriten)",
"Are you sure you want to delete ": "Sind Sie sicher, dass Sie löschen möchten? ", "Downloads": "Downloads",
"Install Forwarder": "Weiterleitung installieren", "Size": "Größe",
"WARNING: Installing forwarders will lead to a ban!": "ACHTUNG: Der Einbau von Forwardern führt zu einem Verbot!", "Size (Star)": "Größe (Favoriten)",
"Back": "Zurück", "Alphabetical": "Alphabetisch",
"Install": "Installieren", "Alphabetical (Star)": "Alphabetisch (Favoriten)",
"Fs": "Fs", "Likes": "Likes",
"App": "App", "ID": "ID",
"Menu": "Speisekarte", "Decending": "Absteigend",
"Homebrew": "Homebrew", "Descending (down)": "Absteigend",
"FileBrowser": "DateiBrowser", "Desc": "Abst.",
"Open": "Offen", "Ascending": "Aufsteigend",
"Theme Options": "Themenoptionen", "Ascending (Up)": "Aufsteigend",
"Select Theme": "Wählen Sie Thema aus", "Asc": "Aufst.",
"Shuffle": "Shuffle",
"Menu Options": "Menü-Optionen",
"Header": "Header",
"Theme": "Theme",
"Theme Options": "Theme-Optionen",
"Select Theme": "Theme auswählen",
"Shuffle": "Zufällig",
"Music": "Musik", "Music": "Musik",
"Show Hidden": "Versteckt anzeigen", "Network": "Netzwerk",
"Folders First": "Ordner zuerst", "Network Options": "Netzwerk-Optionen",
"Hidden Last": "Zuletzt versteckt", "Ftp": "FTP",
"Yes": "Ja", "Mtp": "MTP",
"No": "NEIN",
"Network Options": "Netzwerkoptionen",
"Nxlink": "Nxlink", "Nxlink": "Nxlink",
"Check for update": "Suchen Sie nach Updates", "Nxlink Connected": "Nxlink verbunden",
"File Options": "Dateioptionen", "Nxlink Upload": "Nxlink Upload",
"Cut": "Schneiden", "Nxlink Finished": "Nxlink abgeschlossen",
"Copy": "Kopie", "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",
"Cut": "Ausschneiden",
"Copy": "Kopieren",
"Paste": "Einfügen",
"Paste ": "Einfügen ",
" file(s)?": " Datei(en)?",
"Rename": "Umbenennen", "Rename": "Umbenennen",
"Advanced Options": "Datei erstellen", "Set New File Name": "Neuen Dateinamen eingeben",
"Advanced": "Erweitert",
"Advanced Options": "Erweiterte Optionen",
"Create File": "Datei erstellen", "Create File": "Datei erstellen",
"Set File Name": "Dateinamen eingeben",
"Create Folder": "Ordner erstellen", "Create Folder": "Ordner erstellen",
"View as text": "Als Text anzeigen", "Set Folder Name": "Ordnernamen eingeben",
"View as text (unfinished)": "Als Text anzeigen (unvollendet)", "View as text (unfinished)": "Als Text anzeigen (Beta)",
"Set Archive Bit": "Archivbit setzen", "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",
"AppStore Options": "AppStore-Optionen", "AppStore Options": "AppStore-Optionen",
"All": "Alle", "All": "Alle",
"Games": "Spiele", "Games": "Spiele",
"Emulators": "Emulatoren", "Emulators": "Emulatoren",
"Tools": "Werkzeuge", "Tools": "Tools",
"Advanced": "Fortschrittlich", "Themes": "Themes",
"Themes": "Themen", "Legacy": "Legacy",
"Legacy": "Vermächtnis", "version: %s": "Version: %s",
"Misc": "Sonstiges", "updated: %s": "Aktualisiert: %s",
"Downloads": "Downloads", "category: %s": "Kategorie: %s",
"Filter": "Filter", "extracted: %.2f MiB": "Entpackt: %.2f MiB",
"Search": "Suchen", "app_dls: %s": "Downloads: %s",
"Menu Options": "Menüoptionen", "More by Author": "Mehr vom Entwickler",
"Header": "Kopfzeile", "Leave Feedback": "Feedback geben",
"Theme": "Thema",
"Network": "Netzwerk", "Irs": "IR-Sensor",
"Logging": "Protokollierung", "Ambient Noise Level: ": "Umgebungsrauschen: ",
"Enabled": "Ermöglicht", "Controller": "Controller",
"Disabled": "Deaktiviert", "Pad ": "Pad ",
"Replace hbmenu on exit": "Ersetzen Sie hbmenu beim Beenden", " (Available)": " (Verfügbar)",
"Misc Options": "Verschiedene Optionen", " (Unsupported)": " (Nicht unterstützt)",
"Themezer": "Themezer",
"Irs": "Irs",
"Web": "Web",
"Download": "Herunterladen",
"Next Page": "Nächste Seite",
"Prev Page": "Vorherige Seite",
"Pad ": "Unterlage ",
" (Unconnected)": " (Nicht verbunden)", " (Unconnected)": " (Nicht verbunden)",
"HandHeld": "Handheld", "HandHeld": "Handheld",
" (Available)": " (Verfügbar)", "Rotation": "Rotation",
"0 (Sideways)": "0 (Seitwärts)", "0 (Sideways)": "0° (Seitlich)",
"90 (Flat)": "90 (flach)", "90 (Flat)": "90° (Flach)",
"180 (-Sideways)": "180 (-Seitwärts)", "180 (-Sideways)": "180° (-Seitlich)",
"270 (Upside down)": "270 (verkehrt herum)", "270 (Upside down)": "270° (Kopfüber)",
"Colour": "Farbe",
"Grey": "Grau", "Grey": "Grau",
"Ironbow": "Eisenbogen", "Ironbow": "Ironbow",
"Green": "Grün", "Green": "Grün",
"Red": "Rot", "Red": "Rot",
"Blue": "Blau", "Blue": "Blau",
"Light Target": "Lichtziel",
"All leds": "Alle LEDs", "All leds": "Alle LEDs",
"Bright group": "Helle Gruppe", "Bright group": "Helle Gruppe",
"Dim group": "Dunkle Gruppe", "Dim group": "Dunkle Gruppe",
"None": "Keiner", "None": "Keine",
"Normal image": "Normales Bild", "Gain": "Verstärkung",
"Negative image": "Negatives Bild", "Negative Image": "Negativ-Bild",
"320x240": "320x240", "Normal image": "Normal-Bild",
"160x120": "160x120", "Negative image": "Negativ-Bild",
"80x60": "80x60",
"40x30": "40x30",
"20x15": "20x15",
"Controller": "Regler",
"Rotation": "Drehung",
"Colour": "Farbe",
"Light Target": "Leichtes Ziel",
"Gain": "Gewinnen",
"Negative Image": "Negatives Bild",
"Format": "Format", "Format": "Format",
"Trimming Format": "Zuschneideformat", "320x240": "320×240",
"External Light Filter": "Externer Lichtfilter", "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", "Load Default": "Standard laden",
"No Internet": "Kein Internet",
"[Applet Mode]": "[Applet-Modus]", "Themezer": "Themezer",
"Language": "Sprache" "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."
} }

View File

@@ -1,114 +1,238 @@
{ {
"Launch" : "Launch", "[Applet Mode]": "[Applet Mode]",
"Options" : "Options", "No Internet": "No Internet",
"Homebrew Options" : "Homebrew Options", "Files": "Files",
"Sort By" : "Sort By", "Apps": "Apps",
"Sort Options" : "Sort Options", "Store": "Store",
"Updated" : "Updated", "Menu": "Menu",
"Size" : "Size", "Options": "Options",
"Alphabetical" : "Alphabetical", "OK": "OK",
"Decending" : "Decending", "Back": "Back",
"Ascending" : "Ascending", "Select": "Select",
"Sort" : "Sort", "Open": "Open",
"Order" : "Order", "Launch": "Launch",
"Info" : "Info", "Info": "Info",
"Delete" : "Delete", "Install": "Install",
"Hide Sphaira" : "Hide Sphaira", "Delete": "Delete",
"Are you sure you want to delete " : "Are you sure you want to delete ", "Restart": "Restart",
"Install Forwarder" : "Install Forwarder", "Changelog": "Changelog",
"WARNING: Installing forwarders will lead to a ban!" : "WARNING: Installing forwarders will lead to a ban!", "Details": "Details",
"Back" : "Back", "Update": "Update",
"Install" : "Install", "Remove": "Remove",
"Fs" : "Fs", "Download": "Download",
"App" : "App", "Next Page": "Next Page",
"Menu" : "Menu", "Prev Page": "Prev Page",
"Homebrew" : "Homebrew", "Unstar": "Unstar",
"FileBrowser" : "FileBrowser", "Star": "Star",
"Open" : "Open", "System memory": "System memory",
"Theme Options" : "Theme Options", "microSD card": "microSD card",
"Select Theme" : "Select Theme", "Yes": "Yes",
"Shuffle" : "Shuffle", "No": "No",
"Music" : "Music", "Enabled": "Enabled",
"Show Hidden" : "Show Hidden", "Disabled": "Disabled",
"Folders First" : "Folders First",
"Hidden Last" : "Hidden Last", "Sort By": "Sort By",
"Yes" : "Yes", "Sort Options": "Sort Options",
"No" : "No", "Filter": "Filter",
"Network Options" : "Network Options", "Sort": "Sort",
"Nxlink" : "Nxlink", "Order": "Order",
"Check for update" : "Check for update", "Search": "Search",
"File Options" : "File Options", "Updated": "Updated",
"Cut" : "Cut", "Updated (Star)": "Updated (Star)",
"Copy" : "Copy", "Downloads": "Downloads",
"Rename" : "Rename", "Size": "Size",
"Advanced Options" : "Create File", "Size (Star)": "Size (Star)",
"Create File" : "Create File", "Alphabetical": "Alphabetical",
"Create Folder" : "Create Folder", "Alphabetical (Star)": "Alphabetical (Star)",
"View as text" : "View as text", "Likes": "Likes",
"View as text (unfinished)" : "View as text (unfinished)", "ID": "ID",
"Set Archive Bit" : "Set Archive Bit", "Decending": "Decending",
"AppStore Options" : "AppStore Options", "Descending (down)": "Descending (down)",
"All" : "All", "Desc": "Desc",
"Games" : "Games", "Ascending": "Ascending",
"Emulators" : "Emulators", "Ascending (Up)": "Ascending (Up)",
"Tools" : "Tools", "Asc": "Asc",
"Advanced" : "Advanced",
"Themes" : "Themes", "Menu Options": "Menu Options",
"Legacy" : "Legacy", "Header": "Header",
"Misc" : "Misc", "Theme": "Theme",
"Downloads" : "Downloads", "Theme Options": "Theme Options",
"Filter" : "Filter", "Select Theme": "Select Theme",
"Search" : "Search", "Shuffle": "Shuffle",
"Menu Options" : "Menu Options", "Music": "Music",
"Header" : "Header", "Network": "Network",
"Theme" : "Theme", "Network Options": "Network Options",
"Network" : "Network", "Ftp": "FTP",
"Logging" : "Logging", "Mtp": "MTP",
"Enabled" : "Enabled", "Nxlink": "Nxlink",
"Disabled" : "Disabled", "Nxlink Connected": "Nxlink Connected",
"Replace hbmenu on exit" : "Replace hbmenu on exit", "Nxlink Upload": "Nxlink Upload",
"Misc Options" : "Misc Options", "Nxlink Finished": "Nxlink Finished",
"Themezer" : "Themezer", "Switch-Handheld!": "Switch-Handheld!",
"Irs" : "Irs", "Switch-Docked!": "Switch-Docked!",
"Web" : "Web", "Language": "Language",
"Download" : "Download", "Auto": "Auto",
"Next Page" : "Next Page", "English": "English",
"Prev Page" : "Prev Page", "Japanese": "日本語",
"Pad " : "Pad ", "French": "Français",
" (Unconnected)" : " (Unconnected)", "German": "Deutsch",
"HandHeld" : "HandHeld", "Italian": "Italiano",
" (Available)" : " (Available)", "Spanish": "Español",
"0 (Sideways)" : "0 (Sideways)", "Chinese": "中文",
"90 (Flat)" : "90 (Flat)", "Korean": "한국어",
"180 (-Sideways)" : "180 (-Sideways)", "Dutch": "Dutch",
"270 (Upside down)" : "270 (Upside down)", "Portuguese": "Português",
"Grey" : "Grey", "Russian": "Русский",
"Ironbow" : "Ironbow", "Swedish": "Svenska",
"Green" : "Green", "Logging": "Logging",
"Red" : "Red", "Replace hbmenu on exit": "Replace hbmenu on exit",
"Blue" : "Blue", "Misc": "Misc",
"All leds" : "All leds", "Misc Options": "Misc Options",
"Bright group" : "Bright group", "Web": "Web",
"Dim group" : "Dim group", "Install forwarders": "Install forwarders",
"None" : "None", "Install location": "Install location",
"Normal image" : "Normal image", "Show install warning": "Show install warning",
"Negative image" : "Negative image",
"320x240" : "320x240", "FileBrowser": "FileBrowser",
"160x120" : "160x120", "%zd files": "%zd files",
"80x60" : "80x60", "%zd dirs": "%zd dirs",
"40x30" : "40x30", "File Options": "File Options",
"20x15" : "20x15", "Show Hidden": "Show Hidden",
"Controller" : "Controller", "Folders First": "Folders First",
"Rotation" : "Rotation", "Hidden Last": "Hidden Last",
"Colour" : "Colour", "Cut": "Cut",
"Light Target" : "Light Target", "Copy": "Copy",
"Gain" : "Gain", "Paste": "Paste",
"Negative Image" : "Negative Image", "Paste ": "Paste ",
"Format" : "Format", " file(s)?": " file(s)?",
"Trimming Format" : "Trimming Format", "Rename": "Rename",
"External Light Filter" : "External Light Filter", "Set New File Name": "Set New File Name",
"Load Default" : "Load Default", "Advanced": "Advanced",
"No Internet" : "No Internet", "Advanced Options": "Advanced Options",
"[Applet Mode]" : "[Applet Mode]", "Create File": "Create File",
"Language": "Language" "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."
} }

View File

@@ -1,114 +1,238 @@
{ {
"Launch": "Lanzamiento", "[Applet Mode]": "[Modo Applet]",
"No Internet": "Sin Internet",
"Files": "Archivos",
"Apps": "Apps",
"Store": "Tienda",
"Menu": "Menú",
"Options": "Opciones", "Options": "Opciones",
"Homebrew Options": "Opciones de elaboración casera", "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",
"Sort By": "Ordenar por", "Sort By": "Ordenar por",
"Sort Options": "Opciones de clasificación", "Sort Options": "Opciones de clasificación",
"Updated": "Actualizado", "Filter": "Filtrar",
"Size": "Tamaño",
"Alphabetical": "Alfabético",
"Decending": "Descendente",
"Ascending": "Ascendente",
"Sort": "Clasificar", "Sort": "Clasificar",
"Order": "Orden", "Order": "Orden",
"Info": "Información", "Search": "Buscar",
"Delete": "Borrar", "Updated": "Actualizado",
"Hide Sphaira": "Ocultar Sphaira", "Updated (Star)": "Actualizado (favorito)",
"Are you sure you want to delete ": "¿Estás seguro de que quieres eliminar? ", "Downloads": "Descargas",
"Install Forwarder": "Instalar reenviador", "Size": "Tamaño",
"WARNING: Installing forwarders will lead to a ban!": "ADVERTENCIA: ¡La instalación de reenviadores dará lugar a una prohibición!", "Size (Star)": "Tamaño (favorito)",
"Back": "Atrás", "Alphabetical": "Alfabético",
"Install": "Instalar", "Alphabetical (Star)": "Alfabético (favorito)",
"Fs": "fs", "Likes": "Me Gusta",
"App": "Aplicación", "ID": "ID",
"Menu": "Menú", "Decending": "Descendente",
"Homebrew": "cerveza casera", "Descending (down)": "Descendente (abajo)",
"FileBrowser": "Explorador de archivos", "Desc": "Descendente",
"Open": "Abierto", "Ascending": "Ascendente",
"Ascending (Up)": "Ascendente (arriba)",
"Asc": "Ascendente",
"Menu Options": "Opciones de menú",
"Header": "Encabezado",
"Theme": "Tema",
"Theme Options": "Opciones de tema", "Theme Options": "Opciones de tema",
"Select Theme": "Seleccionar tema", "Select Theme": "Seleccionar tema",
"Shuffle": "Barajar", "Shuffle": "Barajar",
"Music": "Música", "Music": "Música",
"Show Hidden": "Mostrar oculto", "Network": "Red",
"Folders First": "Carpetas primero",
"Hidden Last": "Oculto último",
"Yes": "Sí",
"No": "No",
"Network Options": "Opciones de red", "Network Options": "Opciones de red",
"Nxlink": "nxenlace", "Ftp": "FTP",
"Check for update": "Buscar actualizaciones", "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", "File Options": "Opciones de archivo",
"Show Hidden": "Mostrar archivos ocultos",
"Folders First": "Carpetas primero",
"Hidden Last": "Ocultos al final",
"Cut": "Cortar", "Cut": "Cortar",
"Copy": "Copiar", "Copy": "Copiar",
"Rename": "Rebautizar", "Paste": "Pegar",
"Advanced Options": "Crear archivo", "Paste ": "Pegar ",
" file(s)?": " ¿archivo(s)?",
"Rename": "Renombrar",
"Set New File Name": "Establecer nuevo nombre de archivo",
"Advanced": "Avanzado",
"Advanced Options": "Opciones avanzadas",
"Create File": "Crear archivo", "Create File": "Crear archivo",
"Set File Name": "Establecer nombre de archivo",
"Create Folder": "Crear carpeta", "Create Folder": "Crear carpeta",
"View as text": "Ver como texto", "Set Folder Name": "Establecer nombre de carpeta",
"View as text (unfinished)": "Ver como texto (sin terminar)", "View as text (unfinished)": "Ver como texto (sin terminar)",
"Set Archive Bit": "Establecer bit de archivo", "Empty...": "Vacío...",
"AppStore Options": "Opciones de la tienda de aplicaciones", "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",
"All": "Todo", "All": "Todo",
"Games": "Juegos", "Games": "Juegos",
"Emulators": "Emuladores", "Emulators": "Emuladores",
"Tools": "Herramientas", "Tools": "Herramientas",
"Advanced": "Avanzado",
"Themes": "Temas", "Themes": "Temas",
"Legacy": "Legado", "Legacy": "Legado",
"Misc": "Varios", "version: %s": "version: %s",
"Downloads": "Descargas", "updated: %s": "actualizado: %s",
"Filter": "Filtrar", "category: %s": "categoría: %s",
"Search": "Buscar", "extracted: %.2f MiB": "extraído: %.2f MiB",
"Menu Options": "Opciones de menú", "app_dls: %s": "app_dls: %s",
"Header": "Encabezamiento", "More by Author": "Mostrar mas del Autor",
"Theme": "Tema", "Leave Feedback": "Dejar Mensaje",
"Network": "Red",
"Logging": "Explotación florestal", "Irs": "IRS",
"Enabled": "Activado", "Ambient Noise Level: ": "Nivel de Ruido Ambiente",
"Disabled": "Desactivado", "Controller": "Control",
"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 ", "Pad ": "Almohadilla ",
" (Available)": " (Disponible)",
" (Unsupported)": "(No Compatible)",
" (Unconnected)": " (Desconectado)", " (Unconnected)": " (Desconectado)",
"HandHeld": "Portátil", "HandHeld": "Portátil",
" (Available)": " (Disponible)", "Rotation": "Rotación",
"0 (Sideways)": "0 (de lado)", "0 (Sideways)": "0 (De lado)",
"90 (Flat)": "90 (plano)", "90 (Flat)": "90 (Plano)",
"180 (-Sideways)": "180 (-de lado)", "180 (-Sideways)": "180 (-De lado)",
"270 (Upside down)": "270 (al revés)", "270 (Upside down)": "270 (Al revés)",
"Colour": "Color",
"Grey": "Gris", "Grey": "Gris",
"Ironbow": "arco de hierro", "Ironbow": "Paleta térmica",
"Green": "Verde", "Green": "Verde",
"Red": "Rojo", "Red": "Rojo",
"Blue": "Azul", "Blue": "Azul",
"All leds": "todos los leds",
"Bright group": "grupo brillante",
"Dim group": "grupo tenue",
"None": "Ninguno",
"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", "Light Target": "Objetivo de luz",
"Gain": "Ganar", "All leds": "Todos los leds",
"Bright group": "Grupo brillo",
"Dim group": "Grupo tenue",
"None": "Ninguno",
"Gain": "Ganancia",
"Negative Image": "Imagen negativa", "Negative Image": "Imagen negativa",
"Normal image": "Imagen normal",
"Negative image": "Imagen negativa",
"Format": "Formato", "Format": "Formato",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "Formato de recorte", "Trimming Format": "Formato de recorte",
"External Light Filter": "Filtro de luz externo", "External Light Filter": "Filtro de luz externa",
"Load Default": "Cargar predeterminado", "Load Default": "Cargar predeterminado",
"No Internet": "sin internet",
"[Applet Mode]": "[Modo subprograma]", "Themezer": "Themezer",
"Language": "Idioma" "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'."
} }

View File

@@ -1,114 +1,238 @@
{ {
"Launch": "Lancement", "[Applet Mode]": "[Mode Applet]",
"Options": "Possibilités", "No Internet": "Pas d'Internet",
"Homebrew Options": "Options de brassage maison", "Files": "Fichiers",
"Sort By": "Trier par", "Apps": "Applications",
"Sort Options": "Options de tri", "Store": "Magasin",
"Updated": "Mis à jour",
"Size": "Taille",
"Alphabetical": "Alphabétique",
"Decending": "Décroissant",
"Ascending": "Ascendant",
"Sort": "Trier",
"Order": "Commande",
"Info": "Informations",
"Delete": "Supprimer",
"Hide Sphaira": "Masquer Sphaira",
"Are you sure you want to delete ": "Etes-vous sûr de vouloir supprimer ",
"Install Forwarder": "Installer le redirecteur",
"WARNING: Installing forwarders will lead to a ban!": "ATTENTION : L'installation de transitaires entraînera une interdiction !",
"Back": "Dos",
"Install": "Installer",
"Fs": "Fs",
"App": "Application",
"Menu": "Menu", "Menu": "Menu",
"Homebrew": "Homebrew", "Options": "Options",
"FileBrowser": "Navigateur de fichiers", "OK": "OK",
"Back": "Retour",
"Select": "Sélectionner",
"Open": "Ouvrir", "Open": "Ouvrir",
"Theme Options": "Options de thème", "Launch": "Exécuter",
"Select Theme": "Sélectionnez un thème", "Info": "Info.",
"Shuffle": "Mélanger", "Install": "Installer",
"Music": "Musique", "Delete": "Supprimer",
"Show Hidden": "Afficher masqué", "Restart": "Redémarrer",
"Folders First": "Les dossiers d'abord", "Changelog": "Changelog",
"Hidden Last": "Dernier caché", "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", "Yes": "Oui",
"No": "Non", "No": "Non",
"Network Options": "Options réseau", "Enabled": "Activé(e)",
"Nxlink": "Nxlien", "Disabled": "Désactivé(e)",
"Check for update": "Vérifier la mise à jour",
"File Options": "Options de fichier", "Sort By": "Tri Par",
"Sort Options": "Options de Tri",
"Filter": "Filtre",
"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",
"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",
"Cut": "Couper", "Cut": "Couper",
"Copy": "Copie", "Copy": "Copier",
"Rename": "Rebaptiser", "Paste": "Coller",
"Advanced Options": "Créer un fichier", "Paste ": "Coller ",
"Create File": "Créer un fichier", " file(s)?": " fichier(s)?",
"Create Folder": "Créer un dossier", "Rename": "Renommer",
"View as text": "Afficher sous forme de texte", "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 (unfinished)": "Afficher sous forme de texte (inachevé)", "View as text (unfinished)": "Afficher sous forme de texte (inachevé)",
"Set Archive Bit": "Définir le bit d'archive", "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",
"AppStore Options": "Options de l'AppStore", "AppStore Options": "Options de l'AppStore",
"All": "Tous", "All": "Tous",
"Games": "Jeux", "Games": "Jeux",
"Emulators": "Émulateurs", "Emulators": "Émulateurs",
"Tools": "Outils", "Tools": "Outils",
"Advanced": "Avancé",
"Themes": "Thèmes", "Themes": "Thèmes",
"Legacy": "Héritage", "Legacy": "Legacy",
"Misc": "Divers", "version: %s": "version: %s",
"Downloads": "Téléchargements", "updated: %s": "Mis à jour: %s",
"Filter": "Filtre", "category: %s": "catégorie: %s",
"Search": "Recherche", "extracted: %.2f MiB": "Extrait: %.2f MiB",
"Menu Options": "Options des menus", "app_dls: %s": "app_dls: %s",
"Header": "En-tête", "More by Author": "Plus de cet Auteur",
"Theme": "Thème", "Leave Feedback": "Laisser un avis",
"Network": "Réseau",
"Logging": "Enregistrement",
"Enabled": "Activé",
"Disabled": "Désactivé",
"Replace hbmenu on exit": "Remplacer hbmenu à la sortie",
"Misc Options": "Diverses options",
"Themezer": "Thème",
"Irs": "Irs", "Irs": "Irs",
"Web": "Web", "Ambient Noise Level: ": "Niveau De Bruit Ambiant: ",
"Download": "Télécharger", "Controller": "Contrôleur",
"Next Page": "Page suivante", "Pad ": "Manette ",
"Prev Page": "Page précédente",
"Pad ": "Tampon ",
" (Unconnected)": " (Sans rapport)",
"HandHeld": "Portable",
" (Available)": " (Disponible)", " (Available)": " (Disponible)",
"0 (Sideways)": "0 (latéralement)", " (Unsupported)": "Non supporté",
"90 (Flat)": "90 (plat)", " (Unconnected)": " (Non connectée)",
"180 (-Sideways)": "180 (-Côté)", "HandHeld": "Portable",
"270 (Upside down)": "270 (à l'envers)", "Rotation": "Rotation",
"0 (Sideways)": "0 (Paysage)",
"90 (Flat)": "90 (Portrait)",
"180 (-Sideways)": "180 (-Paysage)",
"270 (Upside down)": "270 (Inversé)",
"Colour": "Couleur",
"Grey": "Gris", "Grey": "Gris",
"Ironbow": "Arc de fer", "Ironbow": "Ironbow",
"Green": "Vert", "Green": "Vert",
"Red": "Rouge", "Red": "Rouge",
"Blue": "Bleu", "Blue": "Bleu",
"Light Target": "Luminosité",
"All leds": "Toutes les LED", "All leds": "Toutes les LED",
"Bright group": "Groupe lumineux", "Bright group": "Groupe lumineux",
"Dim group": "Groupe de gradation", "Dim group": "Groupe sombre",
"None": "Aucun", "None": "Aucun",
"Gain": "Gain",
"Negative Image": "Négatif",
"Normal image": "Image normale", "Normal image": "Image normale",
"Negative image": "Image négative", "Negative image": "Négatif",
"320x240": "320x240",
"160x120": "160x120",
"80x60": "80x60",
"40x30": "40x30",
"20x15": "20x15",
"Controller": "Contrôleur",
"Rotation": "Rotation",
"Colour": "Couleur",
"Light Target": "Cible légère",
"Gain": "Gagner",
"Negative Image": "Image négative",
"Format": "Format", "Format": "Format",
"Trimming Format": "Format de découpage", "320x240": "320×240",
"External Light Filter": "Filtre de lumière externe", "160x120": "160×120",
"Load Default": "Charger par défaut", "80x60": "80×60",
"No Internet": "Pas d'Internet", "40x30": "40×30",
"[Applet Mode]": "[Mode Applet]", "20x15": "20×15",
"Language": "Langue" "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."
} }

View File

@@ -1,114 +1,238 @@
{ {
"Launch": "Lancio", "[Applet Mode]": "[Modalità applet]",
"Options": "Opzioni", "No Internet": "Niente Internet",
"Homebrew Options": "Opzioni per l'homebrew", "Files": "",
"Sort By": "Ordina per", "Apps": "",
"Sort Options": "Opzioni di ordinamento", "Store": "",
"Updated": "Aggiornato",
"Size": "Misurare",
"Alphabetical": "Alfabetico",
"Decending": "Decrescente",
"Ascending": "Ascendente",
"Sort": "Ordinare",
"Order": "Ordine",
"Info": "Informazioni",
"Delete": "Eliminare",
"Hide Sphaira": "Nascondi Sphaira",
"Are you sure you want to delete ": "Sei sicuro di voler eliminare? ",
"Install Forwarder": "Installa lo spedizioniere",
"WARNING: Installing forwarders will lead to a ban!": "ATTENZIONE: l'installazione di forwarder porterà al ban!",
"Back": "Indietro",
"Install": "Installare",
"Fs": "Fs",
"App": "App",
"Menu": "Menu", "Menu": "Menu",
"Homebrew": "Birra fatta in casa", "Options": "Opzioni",
"FileBrowser": "FileBrowser", "OK": "",
"Open": "Aprire", "Back": "Indietro",
"Theme Options": "Opzioni del tema", "Select": "",
"Select Theme": "Seleziona Tema", "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",
"Sort By": "Ordina per",
"Sort Options": "Opzioni filtro",
"Filter": "Filtro",
"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",
"Theme Options": "Opzioni tema",
"Select Theme": "Seleziona tema",
"Shuffle": "Mescola", "Shuffle": "Mescola",
"Music": "Musica", "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", "Show Hidden": "Mostra nascosto",
"Folders First": "Prima le cartelle", "Folders First": "Prima le cartelle",
"Hidden Last": "Ultimo nascosto", "Hidden Last": "Ultimo nascosto",
"Yes": "", "Cut": "Taglia",
"No": "NO",
"Network Options": "Opzioni di rete",
"Nxlink": "Nxlink",
"Check for update": "Controlla l'aggiornamento",
"File Options": "Opzioni file",
"Cut": "Taglio",
"Copy": "Copia", "Copy": "Copia",
"Rename": "Rinominare", "Paste": "",
"Advanced Options": "Crea file", "Paste ": "",
" file(s)?": "",
"Rename": "Rinomina",
"Set New File Name": "",
"Advanced": "Avanzato",
"Advanced Options": "Opzioni avanzate",
"Create File": "Crea file", "Create File": "Crea file",
"Set File Name": "",
"Create Folder": "Crea cartella", "Create Folder": "Crea cartella",
"View as text": "Visualizza come testo", "Set Folder Name": "",
"View as text (unfinished)": "Visualizza come testo (non finito)", "View as text (unfinished)": "Visualizza come testo (non finito)",
"Set Archive Bit": "Imposta bit di archivio", "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",
"AppStore Options": "Opzioni dell'App Store", "AppStore Options": "Opzioni dell'App Store",
"All": "Tutto", "All": "Tutto",
"Games": "Giochi", "Games": "Giochi",
"Emulators": "Emulatori", "Emulators": "Emulatori",
"Tools": "Utensili", "Tools": "Strumenti",
"Advanced": "Avanzato",
"Themes": "Temi", "Themes": "Temi",
"Legacy": "Eredità", "Legacy": "Legacy",
"Misc": "Varie", "version: %s": "version: %s",
"Downloads": "Download", "updated: %s": "updated: %s",
"Filter": "Filtro", "category: %s": "category: %s",
"Search": "Ricerca", "extracted: %.2f MiB": "extracted: %.2f MiB",
"Menu Options": "Opzioni del menu", "app_dls: %s": "app_dls: %s",
"Header": "Intestazione", "More by Author": "",
"Theme": "Tema", "Leave Feedback": "",
"Network": "Rete",
"Logging": "Registrazione",
"Enabled": "Abilitato",
"Disabled": "Disabilitato",
"Replace hbmenu on exit": "Sostituisci hbmenu all'uscita",
"Misc Options": "Opzioni varie",
"Themezer": "Themezer",
"Irs": "Irs", "Irs": "Irs",
"Web": "Rete", "Ambient Noise Level: ": "",
"Download": "Scaricamento", "Controller": "Controller",
"Next Page": "Pagina successiva",
"Prev Page": "Pagina precedente",
"Pad ": "Pad ", "Pad ": "Pad ",
" (Unconnected)": " (Non connesso)",
"HandHeld": "Tenuto in mano",
" (Available)": " (Disponibile)", " (Available)": " (Disponibile)",
"0 (Sideways)": "0 (lateralmente)", " (Unsupported)": "",
" (Unconnected)": " (Non connesso)",
"HandHeld": "HandHeld",
"Rotation": "Rotazione",
"0 (Sideways)": "0 (Di lato)",
"90 (Flat)": "90 (Piatto)", "90 (Flat)": "90 (Piatto)",
"180 (-Sideways)": "180 (-lateralmente)", "180 (-Sideways)": "180 (-Di lato)",
"270 (Upside down)": "270 (Capovolto)", "270 (Upside down)": "270 (Capovolto)",
"Colour": "Colore",
"Grey": "Grigio", "Grey": "Grigio",
"Ironbow": "Arco di ferro", "Ironbow": "Ironbow",
"Green": "Verde", "Green": "Verde",
"Red": "Rosso", "Red": "Rosso",
"Blue": "Blu", "Blue": "Blu",
"Light Target": "Bersaglio leggero",
"All leds": "Tutti i led", "All leds": "Tutti i led",
"Bright group": "Gruppo brillante", "Bright group": "Gruppo brillante",
"Dim group": "Gruppo debole", "Dim group": "Gruppo debole",
"None": "Nessuno", "None": "Nessuno",
"Normal image": "Immagine normale",
"Negative image": "Immagine negativa",
"320x240": "320x240",
"160x120": "160x120",
"80x60": "80x60",
"40x30": "40x30",
"20x15": "20×15",
"Controller": "Controllore",
"Rotation": "Rotazione",
"Colour": "Colore",
"Light Target": "Bersaglio leggero",
"Gain": "Guadagno", "Gain": "Guadagno",
"Negative Image": "Immagine negativa", "Negative Image": "Immagine negativa",
"Normal image": "Immagine normale",
"Negative image": "Immagine negativa",
"Format": "Formato", "Format": "Formato",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "Formato di ritaglio", "Trimming Format": "Formato di ritaglio",
"External Light Filter": "Filtro luce esterno", "External Light Filter": "Filtro luce esterno",
"Load Default": "Carica predefinito", "Load Default": "Carica predefinito",
"No Internet": "Niente Internet",
"[Applet Mode]": "[Modalità applet]", "Themezer": "Themezer",
"Language": "Lingua" "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.": ""
} }

View File

@@ -1,114 +1,238 @@
{ {
"Launch": "打ち上げ", "[Applet Mode]": "Appletモード",
"Options": "オプション", "No Internet": "インターネットなし",
"Homebrew Options": "自作オプション", "Files": "ファイル",
"Sort By": "並べ替え", "Apps": "アプリ",
"Sort Options": "並べ替えオプション", "Store": "AppStore",
"Updated": "更新されました",
"Size": "サイズ",
"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": "Fs",
"App": "アプリ",
"Menu": "メニュー", "Menu": "メニュー",
"Homebrew": "自作", "Options": "設定",
"FileBrowser": "ファイルブラウザ", "OK": "確認",
"Open": "開ける", "Back": "る",
"Theme Options": "テーマのオプション", "Select": "選択",
"Select Theme": "テーマの選択", "Open": "開く",
"Shuffle": "シャッフル", "Launch": "起動",
"Music": "音楽", "Info": "情報",
"Show Hidden": "非表示を表示", "Install": "インストール",
"Folders First": "フォルダーを最初に", "Delete": "削除",
"Hidden Last": "隠された最後", "Restart": "再起動",
"Changelog": "リリースノート",
"Details": "詳細",
"Update": "アップデート",
"Remove": "除去",
"Download": "ダウンロード",
"Next Page": "次のページ",
"Prev Page": "前のページ",
"Unstar": "お気に入り解除",
"Star": "お気に入り",
"System memory": "システムメモリ",
"microSD card": "SDメモリーカード",
"Yes": "はい", "Yes": "はい",
"No": "いいえ", "No": "いいえ",
"Network Options": "ネットワークオプション", "Enabled": "",
"Disabled": "",
"Sort By": "並べ替え",
"Sort Options": "並べ替え設定",
"Filter": "フィルター",
"Sort": "並べ替え",
"Order": "順番",
"Search": "検索",
"Updated": "アップデート順",
"Updated (Star)": "アップデート順(お気に入り)",
"Downloads": "ダウンロード順",
"Size": "ファイルサイズ",
"Size (Star)": "ファイルサイズ(お気に入り)",
"Alphabetical": "アルファベット順",
"Alphabetical (Star)": "アルファベット順(お気に入り)",
"Likes": "いいね順",
"ID": "デベロッパー順",
"Decending": "降順",
"Descending (down)": "降順",
"Desc": "降順",
"Ascending": "上昇",
"Ascending (Up)": "上昇",
"Asc": "上昇",
"Menu Options": "メニュー設定",
"Header": "ヘッダー",
"Theme": "テーマ",
"Theme Options": "テーマ設定",
"Select Theme": "テーマを選ぶ",
"Shuffle": "シャッフル",
"Music": "BGM",
"Network": "ネットワーク",
"Network Options": "ネットワーク設定",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink", "Nxlink": "Nxlink",
"Check for update": "アップデートを確認する", "Nxlink Connected": "Nxlink 接続",
"File Options": "ファイルオプション", "Nxlink Upload": "Nxlink アップロード",
"Cut": "カット", "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": "非表示ファイルを劣後",
"Cut": "切り取り",
"Copy": "コピー", "Copy": "コピー",
"Paste": "ペースト",
"Paste ": " ",
" file(s)?": "個のファイルをペーストしますか?",
"Rename": "名前の変更", "Rename": "名前の変更",
"Advanced Options": "ファイルの作成", "Set New File Name": "新しい名前を入力",
"Advanced": "高度な",
"Advanced Options": "高度設定",
"Create File": "ファイルの作成", "Create File": "ファイルの作成",
"Set File Name": "名前を入力",
"Create Folder": "フォルダーの作成", "Create Folder": "フォルダーの作成",
"View as text": "テキストとして表示", "Set Folder Name": "名前を入力",
"View as text (unfinished)": "テキストとして表示 (未完成)", "View as text (unfinished)": "テキストとして表示 (未完成)",
"Set Archive Bit": "アーカイブビットの設定", "Empty...": "このフォルダーは空です",
"AppStore Options": "AppStore オプション", "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",
"AppStore Options": "AppStoreの設定",
"All": "全て", "All": "全て",
"Games": "ゲーム", "Games": "ゲーム",
"Emulators": "エミュレータ", "Emulators": "エミュレータ",
"Tools": "ツール", "Tools": "ツール",
"Advanced": "高度な",
"Themes": "テーマ", "Themes": "テーマ",
"Legacy": "遺産", "Legacy": "レガシー",
"Misc": "その他", "version: %s": "バージョン: %s",
"Downloads": "ダウンロード", "updated: %s": "更新日: %s",
"Filter": "フィルター", "category: %s": "カテゴリー: %s",
"Search": "検索", "extracted: %.2f MiB": "容量: %.2f MiB",
"Menu Options": "メニューオプション", "app_dls: %s": "ダウンロード: %s",
"Header": "ヘッダ", "More by Author": "ディベロッパーの他のアプリを見る",
"Theme": "テーマ", "Leave Feedback": "意見を残す",
"Network": "ネットワーク",
"Logging": "ロギング", "Irs": "Joy-Con IRカメラ",
"Enabled": "有効", "Ambient Noise Level: ": "ノイズレベル: ",
"Disabled": "無効", "Controller": "コントローラー",
"Replace hbmenu on exit": "終了時に hbmenu を置き換える", "Pad ": "Joy-Con ",
"Misc Options": "その他のオプション", " (Available)": " (利用可能)",
"Themezer": "テーマ設定者", " (Unsupported)": " (未対応)",
"Irs": "イルス",
"Web": "ウェブ",
"Download": "ダウンロード",
"Next Page": "次のページ",
"Prev Page": "前のページ",
"Pad ": "パッド ",
" (Unconnected)": " (未接続)", " (Unconnected)": " (未接続)",
"HandHeld": "ハンドヘルド", "HandHeld": "ハンドヘルド",
" (Available)": " (利用可能)", "Rotation": "回転",
"0 (Sideways)": "0(横)", "0 (Sideways)": "0 (横)",
"90 (Flat)": "90(フラット)", "90 (Flat)": "90 (フラット)",
"180 (-Sideways)": "180 (-横)", "180 (-Sideways)": "180 (-横)",
"270 (Upside down)": "270上下逆さま", "270 (Upside down)": "270 (上下逆さま)",
"Colour": "色",
"Grey": "グレー", "Grey": "グレー",
"Ironbow": "アイアンボウ", "Ironbow": "アイアンボウ",
"Green": "緑", "Green": "緑",
"Red": "赤", "Red": "赤",
"Blue": "青", "Blue": "青",
"Light Target": "ライトターゲット",
"All leds": "すべてのLED", "All leds": "すべてのLED",
"Bright group": "明るいグループ", "Bright group": "明るいグループ",
"Dim group": "薄暗いグループ", "Dim group": "薄暗いグループ",
"None": "なし", "None": "なし",
"Gain": "増幅",
"Negative Image": "ネガティブなイメージ",
"Normal image": "通常画像", "Normal image": "通常画像",
"Negative image": "ネガティブなイメージ", "Negative image": "ネガティブなイメージ",
"Format": "解像度",
"320x240": "320×240", "320x240": "320×240",
"160x120": "160×120", "160x120": "160×120",
"80x60": "80×60", "80x60": "80×60",
"40x30": "40×30", "40x30": "40×30",
"20x15": "20x15", "20x15": "20×15",
"Controller": "コントローラ", "Trimming Format": "トリミングされた解像度",
"Rotation": "回転",
"Colour": "色",
"Light Target": "ライトターゲット",
"Gain": "得",
"Negative Image": "ネガティブなイメージ",
"Format": "形式",
"Trimming Format": "トリミングフォーマット",
"External Light Filter": "外光フィルター", "External Light Filter": "外光フィルター",
"Load Default": "デフォルトをロード", "Load Default": "基本設定に戻す",
"No Internet": "インターネットなし",
"[Applet Mode]": "[アプレットモード]", "Themezer": "Themezer",
"Language": "言語" "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.": "このメッセージが繰り返し表示される場合は、問題を開いてください。"
} }

View File

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

View File

@@ -1,114 +1,238 @@
{ {
"Launch": "Launch", "[Applet Mode]": "[Applet-modus]",
"No Internet": "Geen internet",
"Files": "",
"Apps": "",
"Store": "",
"Menu": "Menu",
"Options": "Opties", "Options": "Opties",
"Homebrew Options": "Homebrew-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",
"Sort By": "Sorteer op", "Sort By": "Sorteer op",
"Sort Options": "Sorteeropties", "Sort Options": "Sorteeropties",
"Updated": "Bijgewerkt", "Filter": "Filter",
"Size": "Maat",
"Alphabetical": "Alfabetisch",
"Decending": "Aflopend",
"Ascending": "Oplopend",
"Sort": "Soort", "Sort": "Soort",
"Order": "Volgorde", "Order": "Volgorde",
"Info": "Info", "Search": "Zoekopdracht",
"Delete": "Verwijderen", "Updated": "Bijgewerkt",
"Hide Sphaira": "Verberg Sphaira", "Updated (Star)": "",
"Are you sure you want to delete ": "Weet u zeker dat u wilt verwijderen ", "Downloads": "Downloads",
"Install Forwarder": "Forwarder installeren", "Size": "Maat",
"WARNING: Installing forwarders will lead to a ban!": "WAARSCHUWING: Het installeren van forwarders leidt tot een ban!", "Size (Star)": "",
"Back": "Terug", "Alphabetical": "Alfabetisch",
"Install": "Installeren", "Alphabetical (Star)": "",
"Fs": "Fs", "Likes": "",
"App": "App", "ID": "",
"Menu": "Menu", "Decending": "Aflopend",
"Homebrew": "Zelf brouwen", "Descending (down)": "Aflopend",
"FileBrowser": "Bestandsbrowser", "Desc": "Aflopend",
"Open": "Open", "Ascending": "Oplopend",
"Ascending (Up)": "Oplopend",
"Asc": "Oplopend",
"Menu Options": "Menu-opties",
"Header": "Koptekst",
"Theme": "Thema",
"Theme Options": "Thema Opties", "Theme Options": "Thema Opties",
"Select Theme": "Selecteer Thema", "Select Theme": "Selecteer Thema",
"Shuffle": "Schudden", "Shuffle": "Schudden",
"Music": "Muziek", "Music": "Muziek",
"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", "Show Hidden": "Toon verborgen",
"Folders First": "Mappen eerst", "Folders First": "Mappen eerst",
"Hidden Last": "Verborgen laatste", "Hidden Last": "Verborgen laatste",
"Yes": "Ja",
"No": "Nee",
"Network Options": "Netwerkopties",
"Nxlink": "Nxlink",
"Check for update": "Controleer op update",
"File Options": "Bestandsopties",
"Cut": "Snee", "Cut": "Snee",
"Copy": "Kopiëren", "Copy": "Kopiëren",
"Paste": "",
"Paste ": "",
" file(s)?": "",
"Rename": "Hernoemen", "Rename": "Hernoemen",
"Set New File Name": "",
"Advanced": "Geavanceerd",
"Advanced Options": "Bestand maken", "Advanced Options": "Bestand maken",
"Create File": "Bestand maken", "Create File": "Bestand maken",
"Set File Name": "",
"Create Folder": "Map maken", "Create Folder": "Map maken",
"View as text": "Bekijk als tekst", "Set Folder Name": "",
"View as text (unfinished)": "Bekijk als tekst (onvoltooid)", "View as text (unfinished)": "Bekijk als tekst (onvoltooid)",
"Set Archive Bit": "Archiefbit instellen", "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",
"AppStore Options": "AppStore-opties", "AppStore Options": "AppStore-opties",
"All": "Alle", "All": "Alle",
"Games": "Spellen", "Games": "Spellen",
"Emulators": "Emulators", "Emulators": "Emulators",
"Tools": "Hulpmiddelen", "Tools": "Hulpmiddelen",
"Advanced": "Geavanceerd",
"Themes": "Thema's", "Themes": "Thema's",
"Legacy": "Nalatenschap", "Legacy": "Nalatenschap",
"Misc": "Diversen", "version: %s": "version: %s",
"Downloads": "Downloads", "updated: %s": "updated: %s",
"Filter": "Filter", "category: %s": "category: %s",
"Search": "Zoekopdracht", "extracted: %.2f MiB": "extracted: %.2f MiB",
"Menu Options": "Menu-opties", "app_dls: %s": "app_dls: %s",
"Header": "Koptekst", "More by Author": "",
"Theme": "Thema", "Leave Feedback": "",
"Network": "Netwerk",
"Logging": "Loggen",
"Enabled": "Ingeschakeld",
"Disabled": "Gehandicapt",
"Replace hbmenu on exit": "Vervang hbmenu bij afsluiten",
"Misc Options": "Diverse opties",
"Themezer": "Themamaker",
"Irs": "Ir", "Irs": "Ir",
"Web": "Web", "Ambient Noise Level: ": "",
"Download": "Downloaden", "Controller": "Controleur",
"Next Page": "Volgende pagina",
"Prev Page": "Vorige pagina",
"Pad ": "Pad ", "Pad ": "Pad ",
" (Available)": " (Beschikbaar)",
" (Unsupported)": "",
" (Unconnected)": " (Niet verbonden)", " (Unconnected)": " (Niet verbonden)",
"HandHeld": "Handbediende", "HandHeld": "Handbediende",
" (Available)": " (Beschikbaar)", "Rotation": "Rotatie",
"0 (Sideways)": "0 (zijwaarts)", "0 (Sideways)": "0 (zijwaarts)",
"90 (Flat)": "90 (plat)", "90 (Flat)": "90 (plat)",
"180 (-Sideways)": "180 (-zijwaarts)", "180 (-Sideways)": "180 (-zijwaarts)",
"270 (Upside down)": "270 (ondersteboven)", "270 (Upside down)": "270 (ondersteboven)",
"Colour": "Kleur",
"Grey": "Grijs", "Grey": "Grijs",
"Ironbow": "Ijzerboog", "Ironbow": "Ijzerboog",
"Green": "Groente", "Green": "Groente",
"Red": "Rood", "Red": "Rood",
"Blue": "Blauw", "Blue": "Blauw",
"Light Target": "Licht doel",
"All leds": "Alle leds", "All leds": "Alle leds",
"Bright group": "Heldere groep", "Bright group": "Heldere groep",
"Dim group": "Dim groep", "Dim group": "Dim groep",
"None": "Geen", "None": "Geen",
"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", "Gain": "Verdienen",
"Negative Image": "Negatief beeld", "Negative Image": "Negatief beeld",
"Normal image": "Normaal beeld",
"Negative image": "Negatief beeld",
"Format": "Formaat", "Format": "Formaat",
"320x240": "320×240",
"160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "Trimformaat", "Trimming Format": "Trimformaat",
"External Light Filter": "Extern lichtfilter", "External Light Filter": "Extern lichtfilter",
"Load Default": "Standaard laden", "Load Default": "Standaard laden",
"No Internet": "Geen internet",
"[Applet Mode]": "[Applet-modus]", "Themezer": "Themamaker",
"Language": "Taal" "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.": ""
} }

View File

@@ -1,114 +1,238 @@
{ {
"Launch": "Lançar", "[Applet Mode]": "[Modo Applet]",
"Options": "Opções", "No Internet": "Sem Internet",
"Homebrew Options": "Opções de fermentação caseira", "Files": "Arquivos",
"Sort By": "Ordenar por", "Apps": "Aplicativos",
"Sort Options": "Opções de classificação", "Store": "Loja",
"Updated": "Atualizado",
"Size": "Tamanho",
"Alphabetical": "Alfabético",
"Decending": "Decrescente",
"Ascending": "Ascendente",
"Sort": "Organizar",
"Order": "Ordem",
"Info": "Informações",
"Delete": "Excluir",
"Hide Sphaira": "Esconder Sphaira",
"Are you sure you want to delete ": "Tem certeza de que deseja excluir ",
"Install Forwarder": "Instalar encaminhador",
"WARNING: Installing forwarders will lead to a ban!": "AVISO: A instalação de encaminhadores levará ao banimento!",
"Back": "Voltar",
"Install": "Instalar",
"Fs": "Fs",
"App": "Aplicativo",
"Menu": "Menu", "Menu": "Menu",
"Homebrew": "Cerveja caseira", "Options": "Opções",
"FileBrowser": "Navegador de arquivos", "OK": "OK",
"Back": "Voltar",
"Select": "Selecionar",
"Open": "Abrir", "Open": "Abrir",
"Theme Options": "Opções de tema", "Launch": "Iniciar",
"Select Theme": "Selecione o tema", "Info": "Informações",
"Shuffle": "Embaralhar", "Install": "Instalar",
"Music": "Música", "Delete": "Excluir",
"Show Hidden": "Mostrar oculto", "Restart": "Reiniciar",
"Folders First": "Pastas primeiro", "Changelog": "Changelog",
"Hidden Last": "Oculto por último", "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", "Yes": "Sim",
"No": "Não", "No": "Não",
"Enabled": "Habilitado",
"Disabled": "Desabilitado",
"Sort By": "Ordenar por",
"Sort Options": "Opções de classificação",
"Filter": "Filtro",
"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",
"Theme Options": "Opções de tema",
"Select Theme": "Selecionar tema",
"Shuffle": "Embaralhar",
"Music": "Música",
"Network": "Rede",
"Network Options": "Opções de rede", "Network Options": "Opções de rede",
"Ftp": "FTP",
"Mtp": "MTP",
"Nxlink": "Nxlink", "Nxlink": "Nxlink",
"Check for update": "Verifique se há atualização", "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", "File Options": "Opções de arquivo",
"Cut": "Corte", "Show Hidden": "Mostrar ocultos",
"Copy": "Cópia", "Folders First": "Pastas primeiro",
"Hidden Last": "Ocultos por último",
"Cut": "Cortar",
"Copy": "Copiar",
"Paste": "Colar",
"Paste ": "Colar",
" file(s)?": " arquivo(s)?",
"Rename": "Renomear", "Rename": "Renomear",
"Advanced Options": "Criar arquivo", "Set New File Name": "Definir novo nome do arquivo",
"Advanced": "Avançado",
"Advanced Options": "Opções avançadas",
"Create File": "Criar arquivo", "Create File": "Criar arquivo",
"Set File Name": "Definir nome do arquivo",
"Create Folder": "Criar pasta", "Create Folder": "Criar pasta",
"View as text": "Ver como texto", "Set Folder Name": "Definir novo nome da pasta",
"View as text (unfinished)": "Ver como texto (inacabado)", "View as text (unfinished)": "Ver como texto (inacabado)",
"Set Archive Bit": "Definir bit de arquivo", "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",
"AppStore Options": "Opções da AppStore", "AppStore Options": "Opções da AppStore",
"All": "Todos", "All": "Todos",
"Games": "Jogos", "Games": "Jogos",
"Emulators": "Emuladores", "Emulators": "Emuladores",
"Tools": "Ferramentas", "Tools": "Ferramentas",
"Advanced": "Avançado",
"Themes": "Temas", "Themes": "Temas",
"Legacy": "Legado", "Legacy": "Legado",
"Misc": "Diversos", "version: %s": "versão: %s",
"Downloads": "Transferências", "updated: %s": "atualizado: %s",
"Filter": "Filtro", "category: %s": "categoria: %s",
"Search": "Procurar", "extracted: %.2f MiB": "tam. extraído: %.2f MiB",
"Menu Options": "Opções de cardápio", "app_dls: %s": "downloads: %s",
"Header": "Cabeçalho", "More by Author": "Mais do autor",
"Theme": "Tema", "Leave Feedback": "Deixar um feedback",
"Network": "Rede",
"Logging": "Registro", "Irs": "Irs",
"Enabled": "Habilitado", "Ambient Noise Level: ": "Nível de ruído ambiente",
"Disabled": "Desabilitado", "Controller": "Controle",
"Replace hbmenu on exit": "Substitua hbmenu ao sair", "Pad ": "Pad ",
"Misc Options": "Opções diversas", " (Available)": " (Disponível)",
"Themezer": "Temazer", " (Unsupported)": "(Não suportado)",
"Irs": "Receita Federal",
"Web": "Rede",
"Download": "Download",
"Next Page": "Próxima página",
"Prev Page": "Página anterior",
"Pad ": "Almofada ",
" (Unconnected)": " (Desconectado)", " (Unconnected)": " (Desconectado)",
"HandHeld": "Portátil", "HandHeld": "Portátil",
" (Available)": " (Disponível)", "Rotation": "Rotação",
"0 (Sideways)": "0 (lateralmente)", "0 (Sideways)": "0 (Lateralmente)",
"90 (Flat)": "90 (plano)", "90 (Flat)": "90 (plano)",
"180 (-Sideways)": "180 (-lateralmente)", "180 (-Sideways)": "180 (-Lateralmente)",
"270 (Upside down)": "270 (de cabeça para baixo)", "270 (Upside down)": "270 (De cabeça para baixo)",
"Colour": "Cor",
"Grey": "Cinza", "Grey": "Cinza",
"Ironbow": "Arco de Ferro", "Ironbow": "Arco de ferro",
"Green": "Verde", "Green": "Verde",
"Red": "Vermelho", "Red": "Vermelho",
"Blue": "Azul", "Blue": "Azul",
"Light Target": "Alvo leve",
"All leds": "Todos os LEDs", "All leds": "Todos os LEDs",
"Bright group": "Grupo brilhante", "Bright group": "Grupo claro",
"Dim group": "Grupo escuro", "Dim group": "Grupo escuro",
"None": "Nenhum", "None": "Nenhum",
"Normal image": "Imagem normal",
"Negative image": "Imagem negativa",
"320x240": "320x240",
"160x120": "160x120",
"80x60": "80x60",
"40x30": "40x30",
"20x15": "20x15",
"Controller": "Controlador",
"Rotation": "Rotação",
"Colour": "Cor",
"Light Target": "Alvo leve",
"Gain": "Ganho", "Gain": "Ganho",
"Negative Image": "Imagem negativa", "Negative Image": "Imagem negativa",
"Format": "Formatar", "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",
"Trimming Format": "Formato de corte", "Trimming Format": "Formato de corte",
"External Light Filter": "Filtro de luz externo", "External Light Filter": "Filtro de luz externa",
"Load Default": "Carregar padrão", "Load Default": "Carregar padrão",
"No Internet": "Sem Internet",
"[Applet Mode]": "[Modo miniaplicativo]", "Themezer": "Themezer",
"Language": "Idioma" "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."
} }

View File

@@ -1,114 +1,238 @@
{ {
"[Applet Mode]": "[Режим апплета]",
"No Internet": "Нет Интернета",
"Files": "",
"Apps": "",
"Store": "",
"Menu": "Меню",
"Options": "Параметры темы",
"OK": "",
"Back": "Назад",
"Select": "",
"Open": "Открыть",
"Launch": "Запуск", "Launch": "Запуск",
"Options": "Параметры", "Info": "Информация",
"Homebrew Options": "Варианты домашнего пивоварения", "Install": "Установить",
"Delete": "Удалить",
"Restart": "",
"Changelog": "",
"Details": "",
"Update": "",
"Remove": "",
"Download": "Скачать",
"Next Page": "Следующая страница",
"Prev Page": "Предыдущая страница",
"Unstar": "",
"Star": "",
"System memory": "",
"microSD card": "",
"Yes": "Да",
"No": "Нет",
"Enabled": "Включено",
"Disabled": "Отключено",
"Sort By": "Сортировать по", "Sort By": "Сортировать по",
"Sort Options": "Параметры сортировки", "Sort Options": "Параметры сортировки",
"Updated": "Обновлено", "Filter": "Фильтр",
"Size": "Размер",
"Alphabetical": "Алфавитный",
"Decending": "по убыванию",
"Ascending": "восходящий",
"Sort": "Сортировать", "Sort": "Сортировать",
"Order": "Заказ", "Order": "Порядок",
"Info": "Информация", "Search": "Поиск",
"Delete": "Удалить", "Updated": "Обновлено",
"Hide Sphaira": "Скрыть Сфаиру", "Updated (Star)": "",
"Are you sure you want to delete ": "Вы уверены, что хотите удалить ", "Downloads": "Загрузки",
"Install Forwarder": "Установить переадресатор", "Size": "Размер",
"WARNING: Installing forwarders will lead to a ban!": "ВНИМАНИЕ: Установка форвардеров приведет к бану!", "Size (Star)": "",
"Back": "Назад", "Alphabetical": "По наименованию",
"Install": "Установить", "Alphabetical (Star)": "",
"Fs": "Фс", "Likes": "",
"App": "Приложение", "ID": "",
"Menu": "Меню", "Decending": "По убыванию",
"Homebrew": "Домашнее пиво", "Descending (down)": "По убыванию",
"FileBrowser": "ФайлБраузер", "Desc": "По убыванию",
"Open": "Открыть", "Ascending": "По возрастанию",
"Ascending (Up)": "По возрастанию",
"Asc": "По возрастанию",
"Menu Options": "Параметры меню",
"Header": "Заголовок",
"Theme": "Тема",
"Theme Options": "Параметры темы", "Theme Options": "Параметры темы",
"Select Theme": "Выберите тему", "Select Theme": "Выберите тему",
"Shuffle": "Перетасовать", "Shuffle": "Перетасовать",
"Music": "Музыка", "Music": "Музыка",
"Show Hidden": "Показать скрытое", "Network": "Сеть",
"Folders First": "Папки в первую очередь",
"Hidden Last": "Скрытый последний",
"Yes": "Да",
"No": "Нет",
"Network Options": "Параметры сети", "Network Options": "Параметры сети",
"Nxlink": "Нкслинк", "Ftp": "FTP",
"Check for update": "Проверить наличие обновлений", "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": "Параметры файла", "File Options": "Параметры файла",
"Cut": "Резать", "Show Hidden": "Показать скрытые",
"Folders First": "Папки в первую очередь",
"Hidden Last": "Скрытые в последнюю очередь",
"Cut": "Вырезать",
"Copy": "Копировать", "Copy": "Копировать",
"Paste": "",
"Paste ": "",
" file(s)?": "",
"Rename": "Переименовать", "Rename": "Переименовать",
"Advanced Options": "Создать файл", "Set New File Name": "",
"Advanced": "Продвинутые",
"Advanced Options": "Расширенные параметры",
"Create File": "Создать файл", "Create File": "Создать файл",
"Set File Name": "",
"Create Folder": "Создать папку", "Create Folder": "Создать папку",
"View as text": "Посмотреть как текст", "Set Folder Name": "",
"View as text (unfinished)": "Посмотреть как текст (незакончено)", "View as text (unfinished)": "Посмотреть как текст (незакончено)",
"Set Archive Bit": "Установить бит архива", "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",
"AppStore Options": "Параметры магазина приложений", "AppStore Options": "Параметры магазина приложений",
"All": "Все", "All": "Все",
"Games": "Игры", "Games": "Игры",
"Emulators": "Эмуляторы", "Emulators": "Эмуляторы",
"Tools": "Инструменты", "Tools": "Инструменты",
"Advanced": "Передовой",
"Themes": "Темы", "Themes": "Темы",
"Legacy": "Наследие", "Legacy": "Легаси",
"Misc": "Разное", "version: %s": "version: %s",
"Downloads": "Загрузки", "updated: %s": "updated: %s",
"Filter": "Фильтр", "category: %s": "category: %s",
"Search": "Поиск", "extracted: %.2f MiB": "extracted: %.2f MiB",
"Menu Options": "Опции меню", "app_dls: %s": "app_dls: %s",
"Header": "Заголовок", "More by Author": "",
"Theme": "Тема", "Leave Feedback": "",
"Network": "Сеть",
"Logging": "Ведение журнала", "Irs": "Irs",
"Enabled": "Включено", "Ambient Noise Level: ": "",
"Disabled": "Неполноценный", "Controller": "Контроллер",
"Replace hbmenu on exit": "Заменить hbmenu при выходе", "Pad ": "Pad ",
"Misc Options": "Разные параметры", " (Available)": " (Доступно)",
"Themezer": "Темезер", " (Unsupported)": "",
"Irs": "IRS",
"Web": "Интернет",
"Download": "Скачать",
"Next Page": "Следующая страница",
"Prev Page": "Предыдущая страница",
"Pad ": "Подушка ",
" (Unconnected)": " (Не подключено)", " (Unconnected)": " (Не подключено)",
"HandHeld": "Ручной", "HandHeld": "Портативный",
" (Available)": " (Доступный)", "Rotation": "Вращение",
"0 (Sideways)": "0 (вбок)", "0 (Sideways)": "0 (набок)",
"90 (Flat)": "90 (квартира)", "90 (Flat)": "90 (ровно)",
"180 (-Sideways)": "180 (-вбок)", "180 (-Sideways)": "180 (-вбок)",
"270 (Upside down)": "270 (перевернутый)", "270 (Upside down)": "270 (перевернуто)",
"Colour": "Цвет",
"Grey": "Серый", "Grey": "Серый",
"Ironbow": "Железный лук", "Ironbow": "Стальной",
"Green": "Зеленый", "Green": "Зеленый",
"Red": "Красный", "Red": "Красный",
"Blue": "Синий", "Blue": "Синий",
"Light Target": "Световая мишень",
"All leds": "Все светодиоды", "All leds": "Все светодиоды",
"Bright group": "Яркая группа", "Bright group": "Яркая группа",
"Dim group": "Тусклая группа", "Dim group": "Тусклая группа",
"None": "Никто", "None": "Никто",
"Normal image": "Обычное изображение",
"Negative image": "Негативный имидж",
"320x240": "320x240",
"160x120": "160x120",
"80x60": "80х60",
"40x30": "40x30",
"20x15": "20x15",
"Controller": "Контроллер",
"Rotation": "Вращение",
"Colour": "Цвет",
"Light Target": "Легкая мишень",
"Gain": "Прирост", "Gain": "Прирост",
"Negative Image": "Негативное изображение", "Negative Image": "Негативное изображение",
"Normal 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": "Загрузить умолчания",
"No Internet": "Нет Интернета",
"[Applet Mode]": "[Режим апплета]", "Themezer": "Themezer",
"Language": "Язык" "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.": ""
} }

238
assets/romfs/i18n/se.json Normal file
View File

@@ -0,0 +1,238 @@
{
"[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,114 +1,238 @@
{ {
"Launch": "发射", "[Applet Mode]": "[小程序模式]",
"No Internet": "网络未连接",
"Files": "文件",
"Apps": "应用",
"Store": "商店",
"Menu": "菜单",
"Options": "选项", "Options": "选项",
"Homebrew Options": "自制选项", "OK": "确定",
"Back": "返回",
"Select": "选择",
"Open": "打开",
"Launch": "启动",
"Info": "信息",
"Install": "安装",
"Delete": "删除",
"Restart": "",
"Changelog": "更新日志",
"Details": "详情",
"Update": "更新",
"Remove": "删除",
"Download": "下载",
"Next Page": "下一页",
"Prev Page": "上一页",
"Unstar": "取消星标",
"Star": "星标",
"System memory": "主机内存",
"microSD card": "SD卡",
"Yes": "是",
"No": "否",
"Enabled": "启用",
"Disabled": "禁用",
"Sort By": "排序方式", "Sort By": "排序方式",
"Sort Options": "排序选项", "Sort Options": "排序选项",
"Updated": "已更新", "Filter": "筛选",
"Size": "尺寸", "Sort": "排序",
"Order": "顺序",
"Search": "搜索",
"Updated": "最近使用",
"Updated (Star)": "最近更新(星标优先)",
"Downloads": "下载",
"Size": "按大小",
"Size (Star)": "按大小(星标优先)",
"Alphabetical": "按字母顺序", "Alphabetical": "按字母顺序",
"Alphabetical (Star)": "按字母顺序(星标优先)",
"Likes": "点赞量",
"ID": "ID",
"Decending": "降序", "Decending": "降序",
"Descending (down)": "降序",
"Desc": "降序",
"Ascending": "升序", "Ascending": "升序",
"Sort": "种类", "Ascending (Up)": "升序",
"Order": "命令", "Asc": "升序",
"Info": "信息",
"Delete": "删除", "Menu Options": "菜单选项",
"Hide Sphaira": "隐藏斯菲拉", "Header": "标题",
"Are you sure you want to delete ": "您确定要删除吗 ", "Theme": "主题",
"Install Forwarder": "安装转发器",
"WARNING: Installing forwarders will lead to a ban!": "警告:安装转发器将导致禁止!",
"Back": "后退",
"Install": "安装",
"Fs": "FS",
"App": "应用程序",
"Menu": "菜单",
"Homebrew": "自制",
"FileBrowser": "文件浏览器",
"Open": "打开",
"Theme Options": "主题选项", "Theme Options": "主题选项",
"Select Theme": "选择主题", "Select Theme": "选择主题",
"Shuffle": "随机播放", "Shuffle": "随机播放",
"Music": "音乐", "Music": "音乐",
"Show Hidden": "显示隐藏", "Network": "网络",
"Folders First": "文件夹优先",
"Hidden Last": "隐藏最后",
"Yes": "是的",
"No": "不",
"Network Options": "网络选项", "Network Options": "网络选项",
"Nxlink": "恩克斯联", "Ftp": "FTP",
"Check for update": "检查更新", "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": "文件选项", "File Options": "文件选项",
"Cut": "", "Show Hidden": "显示隐藏项目",
"Folders First": "文件夹靠前",
"Hidden Last": "隐藏项目置后",
"Cut": "剪切",
"Copy": "复制", "Copy": "复制",
"Paste": "粘贴",
"Paste ": "粘贴 ",
" file(s)?": "个文件(夹)",
"Rename": "重命名", "Rename": "重命名",
"Advanced Options": "创建文件", "Set New File Name": "输入新命名",
"Create File": "创建文件", "Advanced": "高级",
"Create Folder": "创建文件夹", "Advanced Options": "高级选项",
"View as text": "以文本形式查看", "Create File": "新建文件",
"View as text (unfinished)": "以文本形式查看(未完成)", "Set File Name": "输入文件名",
"Set Archive Bit": "设置存档位", "Create Folder": "新建文件夹",
"Set Folder Name": "输入文件夹名",
"View as text (unfinished)": "以文本形式查看(未完善)",
"Empty...": "空...",
"Open with DayBreak?": "使用DayBreak打开",
"Launch ": "",
"Launch option for: ": "启动选项:",
"Select launcher for: ": "",
"Homebrew": "应用列表",
"Homebrew Options": "应用选项",
"Hide Sphaira": "在应用列表中隐藏Sphaira",
"Install Forwarder": "安装前端应用",
"WARNING: Installing forwarders will lead to a ban!": "警告安装前端应用可能导致ban机",
"Installing Forwarder": "正在生成前端应用",
"Creating Program": "正在创建程序",
"Creating Control": "正在创建控制器",
"Creating Meta": "正在创建元数据",
"Writing Nca": "正在写入Nca",
"Updating ncm databse": "正在更新ncm数据库",
"Pushing application record": "正在推送应用记录",
"Installed!": "安装完成!",
"Failed to install forwarder": "前端应用安装失败",
"Unstarred ": "",
"Starred ": "",
"AppStore": "应用商店",
"Filter: %s | Sort: %s | Order: %s": "筛选: %s | 排序: %s | 顺序: %s",
"AppStore Options": "应用商店选项", "AppStore Options": "应用商店选项",
"All": "全部", "All": "全部",
"Games": "游戏", "Games": "游戏",
"Emulators": "模拟器", "Emulators": "模拟器",
"Tools": "工具", "Tools": "工具",
"Advanced": "先进的",
"Themes": "主题", "Themes": "主题",
"Legacy": "遗产", "Legacy": "可更新",
"Misc": "杂项", "version: %s": "版本: %s",
"Downloads": "下载", "updated: %s": "更新时间: %s",
"Filter": "筛选", "category: %s": "分类: %s",
"Search": "搜索", "extracted: %.2f MiB": "应用大小: %.2f MiB",
"Menu Options": "菜单选项", "app_dls: %s": "下载量: %s",
"Header": "标头", "More by Author": "作者更多作品",
"Theme": "主题", "Leave Feedback": "留言反馈",
"Network": "网络",
"Logging": "记录", "Irs": "红外成像",
"Enabled": "启用", "Ambient Noise Level: ": "环境噪声等级:",
"Disabled": "残疾人",
"Replace hbmenu on exit": "退出时替换 hbmenu",
"Misc Options": "其他选项",
"Themezer": "主题器",
"Irs": "国税局",
"Web": "网络",
"Download": "下载",
"Next Page": "下一页",
"Prev Page": "上一页",
"Pad ": "软垫 ",
" (Unconnected)": " (未连接)",
"HandHeld": "手持式",
" (Available)": " (可用的)",
"0 (Sideways)": "0横向",
"90 (Flat)": "90",
"180 (-Sideways)": "180-横向)",
"270 (Upside down)": "270颠倒",
"Grey": "灰色的",
"Ironbow": "铁弓",
"Green": "绿色的",
"Red": "红色的",
"Blue": "蓝色的",
"All leds": "所有 LED",
"Bright group": "光明集团",
"Dim group": "昏暗组",
"None": "没有任何",
"Normal image": "正常图像",
"Negative image": "负像",
"320x240": "320x240",
"160x120": "160x120",
"80x60": "80x60",
"40x30": "40x30",
"20x15": "20x15",
"Controller": "控制器", "Controller": "控制器",
"Pad ": "手柄 ",
" (Available)": " (可用的)",
" (Unsupported)": "",
" (Unconnected)": " (未连接)",
"HandHeld": "掌机模式",
"Rotation": "旋转", "Rotation": "旋转",
"0 (Sideways)": "0度",
"90 (Flat)": "90度",
"180 (-Sideways)": "180度",
"270 (Upside down)": "270度",
"Colour": "颜色", "Colour": "颜色",
"Light Target": "光目标", "Grey": "灰色",
"Gain": "获得", "Ironbow": "紫黄",
"Negative Image": "负面形象", "Green": "绿色",
"Red": "红色",
"Blue": "蓝色",
"Light Target": "光源目标",
"All leds": "全部",
"Bright group": "亮色组",
"Dim group": "暗色组",
"None": "无",
"Gain": "增益",
"Negative Image": "负片图像",
"Normal image": "正常图像",
"Negative image": "负片图像",
"Format": "格式", "Format": "格式",
"Trimming Format": "修剪格式", "320x240": "320×240",
"External Light Filter": "外部滤光片", "160x120": "160×120",
"80x60": "80×60",
"40x30": "40×30",
"20x15": "20×15",
"Trimming Format": "裁剪格式",
"External Light Filter": "外部光滤镜",
"Load Default": "加载默认值", "Load Default": "加载默认值",
"No Internet": "没有互联网",
"[Applet Mode]": "[小程序模式]", "Themezer": "在线主题",
"Language": "语言" "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.": ""
} }

View File

@@ -1,18 +1,18 @@
[meta] [meta]
name="White not finished" name=OLED Black
author=TotalJustice author=iTotalJustice/Sanras
version=1.0.0 version=1.0.0
preview=romfs:/theme/preview.jpg preview=romfs:/theme/preview.jpg
[theme] [theme]
background=0xEBEBEBff background=0x000000ff
cursor=romfs:/theme/cursor.png cursor=romfs:/theme/cursor.png
cursor_drag=romfs:/theme/cursor_drag.png cursor_drag=romfs:/theme/cursor_drag.png
grid=0x46464630 grid=0x46464640
selected=0x464646ff selected=0x323232ff
selected_overlay=0x00ffc8ff selected_overlay=0x00ffc8ff
text=0x2D2D2Dff text=0xfbfbfbff
text_selected=0x3A50F0ff text_selected=0x00ffc8ff
icon_audio=romfs:/theme/icon_audio.png icon_audio=romfs:/theme/icon_audio.png
icon_video=romfs:/theme/icon_video.png icon_video=romfs:/theme/icon_video.png

View File

@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.13) cmake_minimum_required(VERSION 3.13)
set(sphaira_VERSION 0.3.0) set(sphaira_VERSION 0.5.0)
project(sphaira project(sphaira
VERSION ${sphaira_VERSION} VERSION ${sphaira_VERSION}
@@ -45,18 +45,18 @@ add_executable(sphaira
source/ui/menus/main_menu.cpp source/ui/menus/main_menu.cpp
source/ui/menus/menu_base.cpp source/ui/menus/menu_base.cpp
source/ui/menus/themezer.cpp source/ui/menus/themezer.cpp
source/ui/menus/ghdl.cpp
source/ui/error_box.cpp source/ui/error_box.cpp
source/ui/notification.cpp source/ui/notification.cpp
source/ui/nvg_util.cpp source/ui/nvg_util.cpp
source/ui/option_box.cpp source/ui/option_box.cpp
source/ui/option_list.cpp
source/ui/popup_list.cpp source/ui/popup_list.cpp
source/ui/progress_box.cpp source/ui/progress_box.cpp
source/ui/scrollable_text.cpp source/ui/scrollable_text.cpp
source/ui/scrollbar.cpp
source/ui/sidebar.cpp source/ui/sidebar.cpp
source/ui/widget.cpp source/ui/widget.cpp
source/ui/list.cpp
source/app.cpp source/app.cpp
source/download.cpp source/download.cpp
@@ -72,6 +72,7 @@ add_executable(sphaira
source/swkbd.cpp source/swkbd.cpp
source/web.cpp source/web.cpp
source/i18n.cpp source/i18n.cpp
source/ftpsrv_helper.cpp
) )
target_compile_definitions(sphaira PRIVATE target_compile_definitions(sphaira PRIVATE
@@ -82,6 +83,16 @@ target_compile_definitions(sphaira PRIVATE
include(FetchContent) include(FetchContent)
set(FETCHCONTENT_QUIET FALSE) 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 FetchContent_Declare(libpulsar
GIT_REPOSITORY https://github.com/ITotalJustice/switch-libpulsar.git GIT_REPOSITORY https://github.com/ITotalJustice/switch-libpulsar.git
GIT_TAG d729be3 GIT_TAG d729be3
@@ -102,12 +113,12 @@ FetchContent_Declare(yyjson
GIT_TAG 0.10.0 GIT_TAG 0.10.0
) )
FetchContent_Declare(minIni-sphaira FetchContent_Declare(minIni
GIT_REPOSITORY https://github.com/ITotalJustice/minIni-nx.git GIT_REPOSITORY https://github.com/ITotalJustice/minIni-nx.git
GIT_TAG 63ec295 GIT_TAG 63ec295
) )
set(MININI_LIB_NAME minIni-sphaira) set(MININI_LIB_NAME minIni)
set(MININI_USE_STDIO ON) set(MININI_USE_STDIO ON)
set(MININI_USE_NX ON) set(MININI_USE_NX ON)
set(MININI_USE_FLOAT OFF) set(MININI_USE_FLOAT OFF)
@@ -128,7 +139,7 @@ set(NANOVG_STBI_STATIC OFF)
set(NANOVG_STBTT_STATIC ON) set(NANOVG_STBTT_STATIC ON)
set(YYJSON_DISABLE_READER OFF) set(YYJSON_DISABLE_READER OFF)
set(YYJSON_DISABLE_WRITER ON) set(YYJSON_DISABLE_WRITER OFF)
set(YYJSON_DISABLE_UTILS ON) set(YYJSON_DISABLE_UTILS ON)
set(YYJSON_DISABLE_FAST_FP_CONV ON) set(YYJSON_DISABLE_FAST_FP_CONV ON)
set(YYJSON_DISABLE_NON_STANDARD ON) set(YYJSON_DISABLE_NON_STANDARD ON)
@@ -136,13 +147,85 @@ set(YYJSON_DISABLE_UTF8_VALIDATION ON)
set(YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS OFF) set(YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS OFF)
FetchContent_MakeAvailable( FetchContent_MakeAvailable(
# ftpsrv
libhaze
libpulsar libpulsar
nanovg nanovg
stb stb
minIni-sphaira minIni
yyjson yyjson
) )
FetchContent_GetProperties(ftpsrv)
if (NOT ftpsrv_POPULATED)
FetchContent_Populate(ftpsrv)
endif()
set(FTPSRV_LIB_BUILD TRUE)
set(FTPSRV_LIB_SOCK_UNISTD TRUE)
set(FTPSRV_LIB_VFS_CUSTOM ${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs_nx.h)
set(FTPSRV_LIB_PATH_SIZE 0x301)
set(FTPSRV_LIB_SESSIONS 32)
set(FTPSRV_LIB_BUF_SIZE 1024*64)
# workaround until a64 container has latest libnx release.
if (NOT DEFINED USE_VFS_GC)
set(USE_VFS_GC TRUE)
endif()
set(FTPSRV_LIB_CUSTOM_DEFINES
USE_VFS_SAVE=$<BOOL:TRUE>
USE_VFS_STORAGE=$<BOOL:TRUE>
USE_VFS_GC=$<BOOL:${USE_VFS_GC}>
VFS_NX_BUFFER_IO=$<BOOL:TRUE>
)
add_subdirectory(${ftpsrv_SOURCE_DIR} binary_dir)
add_library(ftpsrv_helper
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs_nx.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_none.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_root.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_fs.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_save.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_storage.c
${ftpsrv_SOURCE_DIR}/src/platform/nx/utils.c
)
target_link_libraries(ftpsrv_helper PUBLIC ftpsrv)
target_include_directories(ftpsrv_helper PUBLIC ${ftpsrv_SOURCE_DIR}/src/platform)
if (USE_VFS_GC)
target_sources(ftpsrv_helper PRIVATE
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_gc.c
)
endif()
# todo: upstream cmake
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 # todo: upstream cmake
add_library(libpulsar add_library(libpulsar
${libpulsar_SOURCE_DIR}/src/archive/archive_file.c ${libpulsar_SOURCE_DIR}/src/archive/archive_file.c
@@ -195,8 +278,10 @@ set_target_properties(sphaira PROPERTIES
) )
target_link_libraries(sphaira PRIVATE target_link_libraries(sphaira PRIVATE
ftpsrv_helper
libhaze
libpulsar libpulsar
minIni-sphaira minIni
nanovg nanovg
stb stb
yyjson yyjson

View File

@@ -33,7 +33,9 @@ enum class LaunchType {
Forwader_Sphaira, Forwader_Sphaira,
}; };
// todo: why is this global???
void DrawElement(float x, float y, float w, float h, ThemeEntryID id); void DrawElement(float x, float y, float w, float h, ThemeEntryID id);
void DrawElement(const Vec4&, ThemeEntryID id);
class App { class App {
public: public:
@@ -42,36 +44,50 @@ public:
void Loop(); void Loop();
static void Exit(); static void Exit();
static void ExitRestart();
static auto GetVg() -> NVGcontext*; static auto GetVg() -> NVGcontext*;
static void Push(std::shared_ptr<ui::Widget>); static void Push(std::shared_ptr<ui::Widget>);
// pops all widgets above a menu
static void PopToMenu();
// this is thread safe (todo: make it thread safe) // this is thread safe
static void Notify(std::string text, ui::NotifEntry::Side side = ui::NotifEntry::Side::RIGHT); static void Notify(std::string text, ui::NotifEntry::Side side = ui::NotifEntry::Side::RIGHT);
static void Notify(ui::NotifEntry entry); static void Notify(ui::NotifEntry entry);
static void NotifyPop(ui::NotifEntry::Side side = ui::NotifEntry::Side::RIGHT); static void NotifyPop(ui::NotifEntry::Side side = ui::NotifEntry::Side::RIGHT);
static void NotifyClear(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 auto GetThemeMetaList() -> std::span<ThemeMeta>;
static void SetTheme(u64 theme_index); static void SetTheme(s64 theme_index);
static auto GetThemeIndex() -> u64; static auto GetThemeIndex() -> s64;
static auto GetDefaultImage(int* w = nullptr, int* h = nullptr) -> int;
// returns argv[0] // returns argv[0]
static auto GetExePath() -> fs::FsPath; static auto GetExePath() -> fs::FsPath;
// returns true if we are hbmenu. // returns true if we are hbmenu.
static auto IsHbmenu() -> bool; static auto IsHbmenu() -> bool;
static auto GetMtpEnable() -> bool;
static auto GetFtpEnable() -> bool;
static auto GetNxlinkEnable() -> bool; static auto GetNxlinkEnable() -> bool;
static auto GetLogEnable() -> bool; static auto GetLogEnable() -> bool;
static auto GetReplaceHbmenuEnable() -> bool; static auto GetReplaceHbmenuEnable() -> bool;
static auto GetInstallEnable() -> bool;
static auto GetInstallSdEnable() -> bool; static auto GetInstallSdEnable() -> bool;
static auto GetInstallPrompt() -> bool;
static auto GetThemeShuffleEnable() -> bool; static auto GetThemeShuffleEnable() -> bool;
static auto GetThemeMusicEnable() -> bool; static auto GetThemeMusicEnable() -> bool;
static auto GetLanguage() -> long; static auto GetLanguage() -> long;
static void SetMtpEnable(bool enable);
static void SetFtpEnable(bool enable);
static void SetNxlinkEnable(bool enable); static void SetNxlinkEnable(bool enable);
static void SetLogEnable(bool enable); static void SetLogEnable(bool enable);
static void SetReplaceHbmenuEnable(bool enable); static void SetReplaceHbmenuEnable(bool enable);
static void SetInstallEnable(bool enable);
static void SetInstallSdEnable(bool enable); static void SetInstallSdEnable(bool enable);
static void SetInstallPrompt(bool enable);
static void SetThemeShuffleEnable(bool enable); static void SetThemeShuffleEnable(bool enable);
static void SetThemeMusicEnable(bool enable); static void SetThemeMusicEnable(bool enable);
static void SetLanguage(long index); static void SetLanguage(long index);
@@ -85,9 +101,6 @@ public:
void Update(); void Update();
void Poll(); void Poll();
void DrawBackground();
void DrawTouch();
// void DrawElement(float x, float y, float w, float h, ui::ThemeEntryID id); // void DrawElement(float x, float y, float w, float h, ui::ThemeEntryID id);
auto LoadElementImage(std::string_view value) -> ElementEntry; auto LoadElementImage(std::string_view value) -> ElementEntry;
auto LoadElementColour(std::string_view value) -> ElementEntry; auto LoadElementColour(std::string_view value) -> ElementEntry;
@@ -112,6 +125,7 @@ public:
u64 m_start_timestamp{}; u64 m_start_timestamp{};
u64 m_prev_timestamp{}; u64 m_prev_timestamp{};
fs::FsPath m_prev_last_launch{}; fs::FsPath m_prev_last_launch{};
int m_default_image{};
bool m_is_launched_via_sphaira_forwader{}; bool m_is_launched_via_sphaira_forwader{};
@@ -131,14 +145,18 @@ public:
Theme m_theme{}; Theme m_theme{};
fs::FsPath theme_path{}; fs::FsPath theme_path{};
std::size_t m_theme_index{}; s64 m_theme_index{};
bool m_quit{}; bool m_quit{};
option::OptionBool m_nxlink_enabled{INI_SECTION, "nxlink_enabled", true}; 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_log_enabled{INI_SECTION, "log_enabled", false};
option::OptionBool m_replace_hbmenu{INI_SECTION, "replace_hbmenu", 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::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_shuffle{INI_SECTION, "theme_shuffle", false};
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true}; option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto option::OptionLong m_language{INI_SECTION, "language", 0}; // auto

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,8 @@ namespace sphaira::i18n {
bool init(long index); bool init(long index);
void exit(); void exit();
std::string get(const char* str);
} // namespace sphaira::i18n } // namespace sphaira::i18n
inline namespace literals { inline namespace literals {

View File

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

View File

@@ -4,6 +4,7 @@
#include <vector> #include <vector>
#include <string> #include <string>
#include <span> #include <span>
#include <optional>
#include "fs.hpp" #include "fs.hpp"
namespace sphaira { namespace sphaira {
@@ -14,20 +15,20 @@ struct Hbini {
}; };
struct NroEntry { struct NroEntry {
fs::FsPath path; fs::FsPath path{};
s64 size; s64 size{};
NacpStruct nacp; NacpStruct nacp{};
std::vector<u8> icon; u64 icon_size{};
u64 icon_size; u64 icon_offset{};
u64 icon_offset;
FsTimeStampRaw timestamp; FsTimeStampRaw timestamp{};
Hbini hbini; Hbini hbini{};
int image; // nvg image int image{}; // nvg image
int x,y,w,h; // image int x,y,w,h{}; // image
bool is_nacp_valid; bool is_nacp_valid{};
std::optional<bool> has_star{std::nullopt};
auto GetName() const -> const char* { auto GetName() const -> const char* {
return nacp.lang[0].name; return nacp.lang[0].name;
@@ -74,4 +75,10 @@ auto nro_add_arg_file(std::string arg) -> std::string;
// strips sdmc: // strips sdmc:
auto nro_normalise_path(const std::string& p) -> std::string; auto nro_normalise_path(const std::string& p) -> std::string;
// helpers to find nro entry, will be made methods soon once i convert vector into a struct.
auto nro_find(std::span<const NroEntry> array, std::string_view name, std::string_view author, const fs::FsPath& path) -> std::optional<NroEntry>;
auto nro_find_name(std::span<const NroEntry> array, std::string_view name) -> std::optional<NroEntry>;
auto nro_find_author(std::span<const NroEntry> array, std::string_view author) -> std::optional<NroEntry>;
auto nro_find_path(std::span<const NroEntry> array, const fs::FsPath& path) -> std::optional<NroEntry>;
} // namespace sphaira } // namespace sphaira

View File

@@ -5,7 +5,7 @@
namespace sphaira::swkbd { namespace sphaira::swkbd {
Result ShowText(std::string& out, const char* guide = nullptr, s64 len_min = -1, s64 len_max = -1); Result ShowText(std::string& out, const char* guide = nullptr, const char* initial = nullptr, s64 len_min = -1, s64 len_max = FS_MAX_PATH);
Result ShowNumPad(s64& out, const char* guide = nullptr, s64 len_min = -1, s64 len_max = -1); Result ShowNumPad(s64& out, const char* guide = nullptr, const char* initial = nullptr, s64 len_min = -1, s64 len_max = FS_MAX_PATH);
} // namespace sphaira::swkbd } // namespace sphaira::swkbd

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "ui/menus/menu_base.hpp" #include "ui/menus/menu_base.hpp"
#include "ui/list.hpp"
#include "nro.hpp" #include "nro.hpp"
#include "fs.hpp" #include "fs.hpp"
#include "option.hpp" #include "option.hpp"
@@ -9,8 +10,11 @@ namespace sphaira::ui::menu::homebrew {
enum SortType { enum SortType {
SortType_Updated, SortType_Updated,
SortType_Size,
SortType_Alphabetical, SortType_Alphabetical,
SortType_Size,
SortType_UpdatedStar,
SortType_AlphabeticalStar,
SortType_SizeStar,
}; };
enum OrderType { enum OrderType {
@@ -26,7 +30,7 @@ struct Menu final : MenuBase {
void Draw(NVGcontext* vg, Theme* theme) override; void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override; void OnFocusGained() override;
void SetIndex(std::size_t index); void SetIndex(s64 index);
void InstallHomebrew(); void InstallHomebrew();
void ScanHomebrew(); void ScanHomebrew();
void Sort(); void Sort();
@@ -36,16 +40,23 @@ struct Menu final : MenuBase {
return m_entries; 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);
private: private:
static constexpr inline const char* INI_SECTION = "homebrew"; static constexpr inline const char* INI_SECTION = "homebrew";
std::vector<NroEntry> m_entries; std::vector<NroEntry> m_entries;
std::size_t m_start{}; s64 m_index{}; // where i am in the array
std::size_t m_index{}; // where i am in the array std::unique_ptr<List> m_list;
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Updated}; option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_AlphabeticalStar};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Decending}; option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Decending};
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 } // namespace sphaira::ui::menu::homebrew

View File

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

View File

@@ -7,6 +7,17 @@
namespace sphaira::ui::menu::main { namespace sphaira::ui::menu::main {
enum class UpdateState {
// still downloading json from github
Pending,
// no update available.
None,
// update available!
Update,
// there was an error whilst checking for updates.
Error,
};
// this holds 2 menus and allows for switching between them // this holds 2 menus and allows for switching between them
struct MainMenu final : Widget { struct MainMenu final : Widget {
MainMenu(); MainMenu();
@@ -17,10 +28,13 @@ struct MainMenu final : Widget {
void OnFocusGained() override; void OnFocusGained() override;
void OnFocusLost() override; void OnFocusLost() override;
auto IsMenu() const -> bool override {
return true;
}
private: private:
void OnLRPress(std::shared_ptr<MenuBase> menu, Button b); void OnLRPress(std::shared_ptr<MenuBase> menu, Button b);
void AddOnLPress(); void AddOnLRPress();
void AddOnRPress();
private: private:
std::shared_ptr<homebrew::Menu> m_homebrew_menu{}; std::shared_ptr<homebrew::Menu> m_homebrew_menu{};
@@ -31,7 +45,7 @@ private:
std::string m_update_url{}; std::string m_update_url{};
std::string m_update_version{}; std::string m_update_version{};
std::string m_update_description{}; std::string m_update_description{};
bool m_update_avaliable{}; UpdateState m_update_state{UpdateState::Pending};
}; };
} // namespace sphaira::ui::menu::main } // namespace sphaira::ui::menu::main

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -114,15 +114,34 @@ struct [[nodiscard]] Vec4 {
struct TimeStamp { struct TimeStamp {
TimeStamp() { TimeStamp() {
Update();
}
void Update() {
start = armGetSystemTick(); start = armGetSystemTick();
} }
auto GetNs() -> u64 { auto GetNs() const -> u64 {
const auto end_ticks = armGetSystemTick(); const auto end_ticks = armGetSystemTick();
return armTicksToNs(end_ticks) - armTicksToNs(start); return armTicksToNs(end_ticks) - armTicksToNs(start);
} }
auto GetSeconds() -> double { auto GetMs() const -> u64 {
const auto ns = GetNs();
return ns/1000/1000;
}
auto GetSeconds() const -> u64 {
const auto ns = GetNs();
return ns/1000/1000/1000;
}
auto GetMsD() const -> double {
const double ns = GetNs();
return ns/1000.0/1000.0;
}
auto GetSecondsD() const -> double {
const double ns = GetNs(); const double ns = GetNs();
return ns/1000.0/1000.0/1000.0; return ns/1000.0/1000.0/1000.0;
} }
@@ -144,10 +163,8 @@ struct ElementEntry {
enum ThemeEntryID { enum ThemeEntryID {
ThemeEntryID_BACKGROUND, ThemeEntryID_BACKGROUND,
ThemeEntryID_LOGO,
ThemeEntryID_GRID, ThemeEntryID_GRID,
ThemeEntryID_GRID_HOVER,
ThemeEntryID_SELECTED, ThemeEntryID_SELECTED,
ThemeEntryID_SELECTED_OVERLAY, ThemeEntryID_SELECTED_OVERLAY,
ThemeEntryID_TEXT, ThemeEntryID_TEXT,
@@ -179,39 +196,31 @@ struct Theme {
fs::FsPath path; fs::FsPath path;
PLSR_BFSTM music; PLSR_BFSTM music;
ElementEntry elements[ThemeEntryID_MAX]; ElementEntry elements[ThemeEntryID_MAX];
// NVGcolor background; // bg
// NVGcolor lines; // grid lines
// NVGcolor spacer; // lines in popup box
// NVGcolor text; // text colour
// NVGcolor text_info; // description text
NVGcolor selected; // selected colours
// NVGcolor overlay; // popup overlay colour
// void DrawElement(float x, float y, float w, float h, ThemeEntryID id);
}; };
enum class TouchState { // enum class TouchGesture {
Start, // set when touch has started // None,
Touching, // set when touch is held longer than 1 frame // Tap,
Stop, // set after touch is released // Scroll,
None, // set when there is no touch // };
};
struct TouchInfo { struct TouchInfo {
s32 initial_x; HidTouchState initial;
s32 initial_y; HidTouchState cur;
s32 cur_x; auto in_range(const Vec4& v) const -> bool {
s32 cur_y; return cur.x >= v.x && cur.x <= v.x + v.w && cur.y >= v.y && cur.y <= v.y + v.h;
}
s32 prev_x; auto in_range(s32 x, s32 y, s32 w, s32 h) const -> bool {
s32 prev_y; return in_range(Vec4(x, y, w, h));
}
u32 finger_id;
bool is_touching; bool is_touching;
bool is_tap; bool is_tap;
bool is_scroll;
bool is_clicked;
bool is_end;
}; };
enum class Button : u64 { enum class Button : u64 {
@@ -342,7 +351,7 @@ struct Controller {
m_kup = 0; m_kup = 0;
} }
void UpdateButtonHeld(HidNpadButton buttons) { void UpdateButtonHeld(u64 buttons) {
if (m_kdown & buttons) { if (m_kdown & buttons) {
m_step = 50; m_step = 50;
m_counter = 0; m_counter = 0;
@@ -350,7 +359,7 @@ struct Controller {
m_counter += m_step; m_counter += m_step;
if (m_counter >= m_MAX) { if (m_counter >= m_MAX) {
m_kdown |= buttons; m_kdown |= m_kheld & buttons;
m_counter = 0; m_counter = 0;
m_step = std::min(m_step + 50, m_MAX_STEP); m_step = std::min(m_step + 50, m_MAX_STEP);
} }

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
#include "ui/menus/main_menu.hpp" #include "ui/menus/main_menu.hpp"
#include "ui/error_box.hpp" #include "ui/error_box.hpp"
#include "ui/option_box.hpp"
#include "app.hpp" #include "app.hpp"
#include "log.hpp" #include "log.hpp"
@@ -12,10 +13,12 @@
#include "fs.hpp" #include "fs.hpp"
#include "defines.hpp" #include "defines.hpp"
#include "i18n.hpp" #include "i18n.hpp"
#include "ftpsrv_helper.hpp"
#include <nanovg_dk.h> #include <nanovg_dk.h>
#include <minIni.h> #include <minIni.h>
#include <pulsar.h> #include <pulsar.h>
#include <haze.h>
#include <algorithm> #include <algorithm>
#include <cassert> #include <cassert>
#include <cstring> #include <cstring>
@@ -107,12 +110,12 @@ void on_applet_operation_mode(App* app) {
switch (appletGetOperationMode()) { switch (appletGetOperationMode()) {
case AppletOperationMode_Handheld: case AppletOperationMode_Handheld:
log_write("[APPLET] AppletOperationMode_Handheld\n"); log_write("[APPLET] AppletOperationMode_Handheld\n");
App::Notify("Switch-Handheld!"); App::Notify("Switch-Handheld!"_i18n);
break; break;
case AppletOperationMode_Console: case AppletOperationMode_Console:
log_write("[APPLET] AppletOperationMode_Console\n"); log_write("[APPLET] AppletOperationMode_Console\n");
App::Notify("Switch-Docked!"); App::Notify("Switch-Docked!"_i18n);
break; break;
} }
} }
@@ -200,7 +203,13 @@ auto GetNroIcon(const std::vector<u8>& nro_icon) -> std::vector<u8> {
return nro_icon; return nro_icon;
} }
void haze_callback(const HazeCallbackData *data) {
App::NotifyFlashLed();
evman::push(*data, false);
}
void nxlink_callback(const NxlinkCallbackData *data) { void nxlink_callback(const NxlinkCallbackData *data) {
App::NotifyFlashLed();
evman::push(*data, false); evman::push(*data, false);
} }
@@ -220,9 +229,22 @@ void App::Loop() {
ui::gfx::updateHighlightAnimation(); ui::gfx::updateHighlightAnimation();
auto events = evman::popall(); // fire all events in in a 3ms timeslice
// while (auto e = evman::pop()) { TimeStamp ts_event;
for (auto& e : events) { const u64 event_timeout = 3;
// limit events to a max per frame in order to not block for too long.
while (true) {
if (ts_event.GetMs() >= event_timeout) {
log_write("event loop timed-out\n");
break;
}
auto event = evman::pop();
if (!event.has_value()) {
break;
}
std::visit([this](auto&& arg){ std::visit([this](auto&& arg){
using T = std::decay_t<decltype(arg)>; using T = std::decay_t<decltype(arg)>;
if constexpr(std::is_same_v<T, evman::LaunchNroEventData>) { if constexpr(std::is_same_v<T, evman::LaunchNroEventData>) {
@@ -247,31 +269,34 @@ void App::Loop() {
} else if constexpr(std::is_same_v<T, evman::ExitEventData>) { } else if constexpr(std::is_same_v<T, evman::ExitEventData>) {
log_write("[ExitEventData] got event\n"); log_write("[ExitEventData] got event\n");
m_quit = true; 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>) { } else if constexpr(std::is_same_v<T, NxlinkCallbackData>) {
switch (arg.type) { switch (arg.type) {
case NxlinkCallbackType_Connected: case NxlinkCallbackType_Connected:
log_write("[NxlinkCallbackType_Connected]\n"); log_write("[NxlinkCallbackType_Connected]\n");
App::Notify("Nxlink Connected"); App::Notify("Nxlink Connected"_i18n);
break; break;
case NxlinkCallbackType_WriteBegin: case NxlinkCallbackType_WriteBegin:
log_write("[NxlinkCallbackType_WriteBegin] %s\n", arg.file.filename); log_write("[NxlinkCallbackType_WriteBegin] %s\n", arg.file.filename);
App::Notify("Nxlink Upload"); App::Notify("Nxlink Upload"_i18n);
break; break;
case NxlinkCallbackType_WriteProgress: case NxlinkCallbackType_WriteProgress:
// log_write("[NxlinkCallbackType_WriteProgress]\n"); // log_write("[NxlinkCallbackType_WriteProgress]\n");
break; break;
case NxlinkCallbackType_WriteEnd: case NxlinkCallbackType_WriteEnd:
log_write("[NxlinkCallbackType_WriteEnd] %s\n", arg.file.filename); log_write("[NxlinkCallbackType_WriteEnd] %s\n", arg.file.filename);
App::Notify("Nxlink Finished"); App::Notify("Nxlink Finished"_i18n);
break; break;
} }
} else if constexpr(std::is_same_v<T, DownloadEventData>) { } else if constexpr(std::is_same_v<T, curl::DownloadEventData>) {
log_write("[DownloadEventData] got event\n"); log_write("[DownloadEventData] got event\n");
arg.callback(arg.data, arg.result); arg.callback(arg.result);
} else { } else {
static_assert(false, "non-exhaustive visitor!"); static_assert(false, "non-exhaustive visitor!");
} }
}, e); }, event.value());
} }
u32 w{},h{}; u32 w{},h{};
@@ -314,6 +339,16 @@ auto App::Push(std::shared_ptr<ui::Widget> widget) -> void {
log_write("did it\n"); log_write("did it\n");
} }
auto App::PopToMenu() -> void {
for (auto it = g_app->m_widgets.rbegin(); it != g_app->m_widgets.rend(); it++) {
const auto& p = *it;
if (p->IsMenu()) {
break;
}
p->SetPop();
}
}
void App::Notify(std::string text, ui::NotifEntry::Side side) { void App::Notify(std::string text, ui::NotifEntry::Side side) {
g_app->m_notif_manager.Push({text, side}); g_app->m_notif_manager.Push({text, side});
} }
@@ -330,19 +365,45 @@ void App::NotifyClear(ui::NotifEntry::Side side) {
g_app->m_notif_manager.Clear(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> { auto App::GetThemeMetaList() -> std::span<ThemeMeta> {
return g_app->m_theme_meta_entries; return g_app->m_theme_meta_entries;
} }
void App::SetTheme(u64 theme_index) { void App::SetTheme(s64 theme_index) {
g_app->LoadTheme(g_app->m_theme_meta_entries[theme_index].ini_path.c_str()); g_app->LoadTheme(g_app->m_theme_meta_entries[theme_index].ini_path.c_str());
g_app->m_theme_index = theme_index; g_app->m_theme_index = theme_index;
} }
auto App::GetThemeIndex() -> u64 { auto App::GetThemeIndex() -> s64 {
return g_app->m_theme_index; return g_app->m_theme_index;
} }
auto App::GetDefaultImage(int* w, int* h) -> int {
return g_app->m_default_image;
}
auto App::GetExePath() -> fs::FsPath { auto App::GetExePath() -> fs::FsPath {
return g_app->m_app_path; return g_app->m_app_path;
} }
@@ -363,10 +424,18 @@ auto App::GetReplaceHbmenuEnable() -> bool {
return g_app->m_replace_hbmenu.Get(); return g_app->m_replace_hbmenu.Get();
} }
auto App::GetInstallEnable() -> bool {
return g_app->m_install.Get();
}
auto App::GetInstallSdEnable() -> bool { auto App::GetInstallSdEnable() -> bool {
return g_app->m_install_sd.Get(); return g_app->m_install_sd.Get();
} }
auto App::GetInstallPrompt() -> bool {
return g_app->m_install_prompt.Get();
}
auto App::GetThemeShuffleEnable() -> bool { auto App::GetThemeShuffleEnable() -> bool {
return g_app->m_theme_shuffle.Get(); return g_app->m_theme_shuffle.Get();
} }
@@ -375,6 +444,14 @@ auto App::GetThemeMusicEnable() -> bool {
return g_app->m_theme_music.Get(); 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 { auto App::GetLanguage() -> long {
return g_app->m_language.Get(); return g_app->m_language.Get();
} }
@@ -402,13 +479,118 @@ void App::SetLogEnable(bool enable) {
} }
void App::SetReplaceHbmenuEnable(bool enable) { void App::SetReplaceHbmenuEnable(bool enable) {
g_app->m_replace_hbmenu.Set(enable); if (App::GetReplaceHbmenuEnable() != enable) {
g_app->m_replace_hbmenu.Set(enable);
if (!enable) {
// check we have already replaced hbmenu with sphaira
NacpStruct hbmenu_nacp;
if (R_FAILED(nro_get_nacp("/hbmenu.nro", hbmenu_nacp))) {
return;
}
if (std::strcmp(hbmenu_nacp.lang[0].name, "sphaira")) {
return;
}
// ask user if they want to restore hbmenu
App::Push(std::make_shared<ui::OptionBox>(
"Restore hbmenu?"_i18n,
"Back"_i18n, "Restore"_i18n, 1, [hbmenu_nacp](auto op_index){
if (!op_index || *op_index == 0) {
return;
}
NacpStruct actual_hbmenu_nacp;
if (R_FAILED(nro_get_nacp("/switch/hbmenu.nro", actual_hbmenu_nacp))) {
App::Push(std::make_shared<ui::OptionBox>(
"Failed to find /switch/hbmenu.nro\n"
"Use the Appstore to re-install hbmenu"_i18n,
"OK"_i18n
));
return;
}
// NOTE: do NOT use rename anywhere here as it's possible
// to have a race condition with another app that opens hbmenu as a file
// in between the delete + rename.
// this would require a sys-module to open hbmenu.nro, such as an ftp server.
// a copy means that it opens the file handle, if successfull, then
// the full read/write will succeed.
fs::FsNativeSd fs;
NacpStruct sphaira_nacp;
fs::FsPath sphaira_path = "/switch/sphaira/sphaira.nro";
Result rc;
// first, try and backup sphaira, its not super important if this fails.
rc = nro_get_nacp(sphaira_path, sphaira_nacp);
if (R_FAILED(rc) || std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
sphaira_path = "/switch/sphaira.nro";
rc = nro_get_nacp(sphaira_path, sphaira_nacp);
}
if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
if (std::strcmp(sphaira_nacp.display_version, hbmenu_nacp.display_version) < 0) {
if (R_FAILED(rc = fs.copy_entire_file(sphaira_path, "/hbmenu.nro"))) {
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path, rc, R_MODULE(rc), R_DESCRIPTION(rc));
} else {
log_write("success with updating hbmenu!\n");
}
}
} else {
// sphaira doesn't yet exist, create a new file.
sphaira_path = "/switch/sphaira/sphaira.nro";
fs.CreateDirectoryRecursively("/switch/sphaira/");
fs.copy_entire_file(sphaira_path, "/hbmenu.nro");
}
// this should never fail, if it does, well then the sd card is fucked.
if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", "/switch/hbmenu.nro"))) {
// try and restore sphaira in a last ditch effort.
if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", sphaira_path))) {
App::Push(std::make_shared<ui::ErrorBox>(rc,
"Failed to restore hbmenu, please re-download hbmenu"_i18n
));
} else {
App::Push(std::make_shared<ui::OptionBox>(
"Failed to restore hbmenu, using sphaira instead"_i18n,
"OK"_i18n
));
}
return;
}
// don't need this any more.
fs.DeleteFile("/switch/hbmenu.nro");
// if we were hbmenu, exit now (as romfs is gone).
if (IsHbmenu()) {
App::Push(std::make_shared<ui::OptionBox>(
"Restored hbmenu, closing sphaira"_i18n,
"OK"_i18n, [](auto) {
App::Exit();
}
));
} else {
App::Notify("Restored hbmenu"_i18n);
}
}
));
}
}
}
void App::SetInstallEnable(bool enable) {
g_app->m_install.Set(enable);
} }
void App::SetInstallSdEnable(bool enable) { void App::SetInstallSdEnable(bool enable) {
g_app->m_install_sd.Set(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) { void App::SetThemeShuffleEnable(bool enable) {
g_app->m_theme_shuffle.Set(enable); g_app->m_theme_shuffle.Set(enable);
} }
@@ -418,6 +600,28 @@ void App::SetThemeMusicEnable(bool enable) {
PlaySoundEffect(SoundEffect::SoundEffect_Music); 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) { void App::SetLanguage(long index) {
if (App::GetLanguage() != index) { if (App::GetLanguage() != index) {
g_app->m_language.Set(index); g_app->m_language.Set(index);
@@ -446,10 +650,10 @@ auto App::Install(OwoConfig& config) -> Result {
if (R_FAILED(rc)) { if (R_FAILED(rc)) {
App::PlaySoundEffect(SoundEffect_Error); App::PlaySoundEffect(SoundEffect_Error);
App::Push(std::make_shared<ui::ErrorBox>(rc, "Failed to install forwarder")); App::Push(std::make_shared<ui::ErrorBox>(rc, "Failed to install forwarder"_i18n));
} else { } else {
App::PlaySoundEffect(SoundEffect_Install); App::PlaySoundEffect(SoundEffect_Install);
App::Notify("Installed!"); App::Notify("Installed!"_i18n);
} }
return rc; return rc;
@@ -476,10 +680,10 @@ auto App::Install(ui::ProgressBox* pbox, OwoConfig& config) -> Result {
if (R_FAILED(rc)) { if (R_FAILED(rc)) {
App::PlaySoundEffect(SoundEffect_Error); App::PlaySoundEffect(SoundEffect_Error);
App::Push(std::make_shared<ui::ErrorBox>(rc, "Failed to install forwarder")); App::Push(std::make_shared<ui::ErrorBox>(rc, "Failed to install forwarder"_i18n));
} else { } else {
App::PlaySoundEffect(SoundEffect_Install); App::PlaySoundEffect(SoundEffect_Install);
App::Notify("Installed!"); App::Notify("Installed!"_i18n);
} }
return rc; return rc;
@@ -489,62 +693,49 @@ void App::Exit() {
g_app->m_quit = true; g_app->m_quit = true;
} }
void App::ExitRestart() {
nro_launch(GetExePath());
Exit();
}
void App::Poll() { void App::Poll() {
m_controller.Reset(); m_controller.Reset();
padUpdate(&m_pad); HidTouchScreenState state{};
m_controller.m_kdown = padGetButtonsDown(&m_pad); hidGetTouchScreenStates(&state, 1);
m_controller.m_kheld = padGetButtons(&m_pad); m_touch_info.is_clicked = false;
m_controller.m_kup = padGetButtonsUp(&m_pad);
// dpad if (state.count == 1 && !m_touch_info.is_touching) {
m_controller.UpdateButtonHeld(HidNpadButton_Left); m_touch_info.initial = m_touch_info.cur = state.touches[0];
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_touching = true;
m_touch_info.is_tap = true; m_touch_info.is_tap = true;
PlaySoundEffect(SoundEffect_Limit); } else if (state.count >= 1 && m_touch_info.is_touching) {
} 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.cur = state.touches[0];
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 && if (m_touch_info.is_tap &&
(std::abs(m_touch_info.initial_x - m_touch_info.cur_x) > 20 || (std::abs((s32)m_touch_info.initial.x - (s32)m_touch_info.cur.x) > 20 ||
std::abs(m_touch_info.initial_y - m_touch_info.cur_y) > 20)) { std::abs((s32)m_touch_info.initial.y - (s32)m_touch_info.cur.y) > 20)) {
m_touch_info.is_tap = false; m_touch_info.is_tap = false;
m_touch_info.is_scroll = true;
} }
} else if (m_touch_info.is_touching) { } else if (m_touch_info.is_touching) {
m_touch_info.is_touching = false; m_touch_info.is_touching = false;
m_touch_info.is_scroll = false;
// check if we clicked on anything, if so, handle it
if (m_touch_info.is_tap) { if (m_touch_info.is_tap) {
// todo: m_touch_info.is_clicked = true;
} else {
m_touch_info.is_end = true;
} }
} }
// todo: better implement this to match hos
if (!m_touch_info.is_touching && !m_touch_info.is_clicked) {
padUpdate(&m_pad);
m_controller.m_kdown = padGetButtonsDown(&m_pad);
m_controller.m_kheld = padGetButtons(&m_pad);
m_controller.m_kup = padGetButtonsUp(&m_pad);
m_controller.UpdateButtonHeld(static_cast<u64>(Button::ANY_DIRECTION));
}
} }
void App::Update() { void App::Update() {
@@ -579,10 +770,29 @@ void App::Draw() {
nvgBeginFrame(this->vg, s_width, s_height, 1.f); nvgBeginFrame(this->vg, s_width, s_height, 1.f);
nvgScale(vg, m_scale.x, m_scale.y); nvgScale(vg, m_scale.x, m_scale.y);
// NOTE: widgets should never pop themselves from drawing! // find the last menu in the list, start drawing from there
for (auto& p : m_widgets) { auto menu_it = m_widgets.rend();
if (!p->IsHidden()) { for (auto it = m_widgets.rbegin(); it != m_widgets.rend(); it++) {
p->Draw(vg, &m_theme); const auto& p = *it;
if (!p->IsHidden() && p->IsMenu()) {
menu_it = it;
break;
}
}
// reverse itr so loop backwards to go forwarders.
if (menu_it != m_widgets.rend()) {
for (auto it = menu_it; ; it--) {
const auto& p = *it;
// draw everything not hidden on top of the menu.
if (!p->IsHidden()) {
p->Draw(vg, &m_theme);
}
if (it == m_widgets.rbegin()) {
break;
}
} }
} }
@@ -597,51 +807,22 @@ auto App::GetVg() -> NVGcontext* {
return g_app->vg; return g_app->vg;
} }
#if 0
void App::UpdateList() {
const auto index_copy = this->index;
const auto start_copy = this->start;
else if (controller.down) {
// todo: replace with actual focus
PlaySoundEffect(SoundEffect_Limit);
}
if (controller.any_direction()) {
if (index_copy != this->index) {
PlaySoundEffect(SoundEffect_Focus);
if (start_copy != this->start) {
// float r = randomGet64() % 100;
// float pitch = r / 100.0;
// plsrPlayerSetPitch(m_sound_ids[SoundEffect_Scroll], pitch);
PlaySoundEffect(SoundEffect_Scroll);
}
if (this->index == 0 || this->index == this->nro_entries.size() || ((controller.down & HidNpadButton_AnyLeft) && this->index && this->index % 3 == 0) || ((controller.down & HidNpadButton_AnyRight) && this->index && (this->index + 1) % 3 == 0)) {
PlaySoundEffect(SoundEffect_Limit);
}
} else {
const auto mask = HidNpadButton_AnyDown | HidNpadButton_AnyUp | HidNpadButton_AnyLeft | HidNpadButton_AnyRight;
if (controller.down & mask) {
PlaySoundEffect(SoundEffect_Limit);
}
}
}
}
#endif
void DrawElement(float x, float y, float w, float h, ThemeEntryID id) { void DrawElement(float x, float y, float w, float h, ThemeEntryID id) {
DrawElement({x, y, w, h}, id);
}
void DrawElement(const Vec4& v, ThemeEntryID id) {
const auto& e = g_app->m_theme.elements[id]; const auto& e = g_app->m_theme.elements[id];
switch (e.type) { switch (e.type) {
case ElementType::None: { case ElementType::None: {
} break; } break;
case ElementType::Texture: { case ElementType::Texture: {
const auto paint = nvgImagePattern(g_app->vg, x, y, w, h, 0, e.texture, 1.f); const auto paint = nvgImagePattern(g_app->vg, v.x, v.y, v.w, v.h, 0, e.texture, 1.f);
ui::gfx::drawRect(g_app->vg, x, y, w, h, paint); ui::gfx::drawRect(g_app->vg, v, paint);
} break; } break;
case ElementType::Colour: { case ElementType::Colour: {
ui::gfx::drawRect(g_app->vg, x, y, w, h, e.colour); ui::gfx::drawRect(g_app->vg, v, e.colour);
} break; } break;
} }
} }
@@ -738,12 +919,8 @@ void App::LoadTheme(const fs::FsPath& path) {
app->PlaySoundEffect(SoundEffect_Music); app->PlaySoundEffect(SoundEffect_Music);
} }
} }
} else if (key == "logo") {
theme.elements[ThemeEntryID_LOGO] = app->LoadElement(value);
} else if (key == "grid") { } else if (key == "grid") {
theme.elements[ThemeEntryID_GRID] = app->LoadElement(value); theme.elements[ThemeEntryID_GRID] = app->LoadElement(value);
} else if (key == "grid_hover") {
theme.elements[ThemeEntryID_GRID_HOVER] = app->LoadElement(value);
} else if (key == "selected") { } else if (key == "selected") {
theme.elements[ThemeEntryID_SELECTED] = app->LoadElement(value); theme.elements[ThemeEntryID_SELECTED] = app->LoadElement(value);
} else if (key == "selected_overlay") { } else if (key == "selected_overlay") {
@@ -798,6 +975,10 @@ void App::ScanThemes(const std::string& path) {
continue; continue;
} }
if (d->d_type != DT_REG) {
continue;
}
const std::string name = d->d_name; const std::string name = d->d_name;
if (!name.ends_with(".ini")) { if (!name.ends_with(".ini")) {
continue; continue;
@@ -862,25 +1043,35 @@ App::App(const char* argv0) {
m_app_path = argv0; m_app_path = argv0;
} }
// set pop-back if applet and we are hbmenu // set if we are hbmenu
if (!IsApplication() && IsHbmenu()) { if (IsHbmenu()) {
__nx_applet_exit_mode = 1; __nx_applet_exit_mode = 1;
} }
fs::FsNativeSd fs; fs::FsNativeSd fs;
fs.CreateDirectoryRecursively("/config/sphaira/assoc"); fs.CreateDirectoryRecursively("/config/sphaira/assoc");
fs.CreateDirectoryRecursively("/config/sphaira/themes"); fs.CreateDirectoryRecursively("/config/sphaira/themes");
fs.CreateDirectoryRecursively("/config/sphaira/github");
fs.CreateDirectoryRecursively("/config/sphaira/i18n");
if (App::GetLogEnable()) { if (App::GetLogEnable()) {
log_file_init(); log_file_init();
log_write("hello world\n"); log_write("hello world\n");
} }
if (App::GetMtpEnable()) {
hazeInitialize(haze_callback);
}
if (App::GetFtpEnable()) {
ftpsrv::Init();
}
if (App::GetNxlinkEnable()) { if (App::GetNxlinkEnable()) {
nxlinkInitialize(nxlink_callback); nxlinkInitialize(nxlink_callback);
} }
DownloadInit(); curl::Init();
// Create the deko3d device // Create the deko3d device
this->device = dk::DeviceMaker{} this->device = dk::DeviceMaker{}
@@ -1014,6 +1205,15 @@ App::App(const char* argv0) {
log_write("sd_total_space: %zd\n", sd_total_space); log_write("sd_total_space: %zd\n", sd_total_space);
} }
// load default image
if (R_SUCCEEDED(romfsInit())) {
ON_SCOPE_EXIT(romfsExit());
const auto image = ImageLoadFromFile("romfs:/default.png");
if (!image.data.empty()) {
m_default_image = nvgCreateImageRGBA(vg, image.w, image.h, 0, image.data.data());
}
}
App::Push(std::make_shared<ui::menu::main::MainMenu>()); App::Push(std::make_shared<ui::menu::main::MainMenu>());
log_write("finished app constructor\n"); log_write("finished app constructor\n");
} }
@@ -1035,11 +1235,12 @@ App::~App() {
log_write("starting to exit\n"); log_write("starting to exit\n");
i18n::exit(); i18n::exit();
DownloadExit(); curl::Exit();
// this has to be called before any cleanup to ensure the lifetime of // this has to be called before any cleanup to ensure the lifetime of
// nvg is still active as some widgets may need to free images. // nvg is still active as some widgets may need to free images.
m_widgets.clear(); m_widgets.clear();
nvgDeleteImage(vg, m_default_image);
appletUnhook(&m_appletHookCookie); appletUnhook(&m_appletHookCookie);
@@ -1066,23 +1267,64 @@ App::~App() {
// backup hbmenu if it is not sphaira // backup hbmenu if it is not sphaira
if (App::GetReplaceHbmenuEnable() && !IsHbmenu()) { if (App::GetReplaceHbmenuEnable() && !IsHbmenu()) {
NacpStruct nacp; NacpStruct hbmenu_nacp;
fs::FsNativeSd fs; fs::FsNativeSd fs;
if (R_SUCCEEDED(nro_get_nacp("/hbmenu.nro", nacp)) && std::strcmp(nacp.lang[0].name, "sphaira")) { Result rc;
log_write("backing up hbmenu\n");
if (R_FAILED(fs.copy_entire_file("/switch/hbmenu.nro", "/hbmenu.nro", true))) { if (R_SUCCEEDED(rc = nro_get_nacp("/hbmenu.nro", hbmenu_nacp)) && std::strcmp(hbmenu_nacp.lang[0].name, "sphaira")) {
log_write("failed to copy sphaire.nro to hbmenu.nro\n"); 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");
} }
} else { } else {
log_write("not backing up\n"); log_write("not backing up\n");
} }
Result rc; if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", GetExePath()))) {
if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", GetExePath(), true))) {
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", GetExePath(), rc, R_MODULE(rc), R_DESCRIPTION(rc)); log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", GetExePath(), rc, R_MODULE(rc), R_DESCRIPTION(rc));
} else { } else {
log_write("success with copying over root file!\n"); log_write("success with copying over root file!\n");
} }
} else if (IsHbmenu()) {
// 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";
rc = nro_get_nacp(sphaira_path, sphaira_nacp);
if (R_FAILED(rc) || std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
sphaira_path = "/switch/sphaira.nro";
rc = nro_get_nacp(sphaira_path, sphaira_nacp);
}
// found sphaira, now lets get compare version
if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
if (std::strcmp(hbmenu_nacp.display_version, sphaira_nacp.display_version) < 0) {
if (R_FAILED(rc = fs.copy_entire_file(GetExePath(), sphaira_path))) {
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path, rc, R_MODULE(rc), R_DESCRIPTION(rc));
} else {
log_write("success with updating hbmenu!\n");
}
}
}
} else {
log_write("no longer hbmenu!\n");
}
}
if (App::GetMtpEnable()) {
log_write("closing mtp\n");
hazeExit();
}
if (App::GetFtpEnable()) {
log_write("closing ftp\n");
ftpsrv::Exit();
} }
if (App::GetNxlinkEnable()) { if (App::GetNxlinkEnable()) {

View File

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

View File

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

View File

@@ -0,0 +1,141 @@
#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,6 +3,7 @@
#include "log.hpp" #include "log.hpp"
#include <yyjson.h> #include <yyjson.h>
#include <vector> #include <vector>
#include <unordered_map>
namespace sphaira::i18n { namespace sphaira::i18n {
namespace { namespace {
@@ -10,37 +11,52 @@ namespace {
std::vector<u8> g_i18n_data; std::vector<u8> g_i18n_data;
yyjson_doc* json; yyjson_doc* json;
yyjson_val* root; 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;
std::string get(const char* str, size_t len) {
if (!json || !root) { if (!json || !root) {
log_write("no json or root\n"); log_write("no json or root\n");
return str; return kkey;
} }
auto key = yyjson_obj_getn(root, str, len); auto key = yyjson_obj_getn(root, str, len);
if (!key) { if (!key) {
log_write("\tfailed to find key: [%.*s]\n", len, str); log_write("\tfailed to find key: [%s]\n", kkey.c_str());
return str; return kkey;
} }
auto val = yyjson_get_str(key); auto val = yyjson_get_str(key);
auto val_len = yyjson_get_len(key); auto val_len = yyjson_get_len(key);
if (!val || !val_len) { if (!val || !val_len) {
log_write("\tfailed to get value: [%.*s]\n", len, str); log_write("\tfailed to get value: [%s]\n", kkey.c_str());
return str; return kkey;
} }
return {val, val_len}; // update entry in cache
const std::string ret = {val, val_len};
g_tr_cache.insert_or_assign(it, kkey, ret);
return ret;
} }
} // namespace } // namespace
bool init(long index) { bool init(long index) {
g_tr_cache.clear();
R_TRY_RESULT(romfsInit(), false); R_TRY_RESULT(romfsInit(), false);
ON_SCOPE_EXIT( romfsExit() ); ON_SCOPE_EXIT( romfsExit() );
u64 languageCode; u64 languageCode;
SetLanguage setLanguage = SetLanguage_ENGB; SetLanguage setLanguage = SetLanguage_ENGB;
std::string lang_name = "en";
switch (index) { switch (index) {
case 0: // auto case 0: // auto
@@ -60,9 +76,9 @@ bool init(long index) {
case 9: setLanguage = SetLanguage_NL; break; // "Dutch" case 9: setLanguage = SetLanguage_NL; break; // "Dutch"
case 10: setLanguage = SetLanguage_PT; break; // "Portuguese" case 10: setLanguage = SetLanguage_PT; break; // "Portuguese"
case 11: setLanguage = SetLanguage_RU; break; // "Russian" case 11: setLanguage = SetLanguage_RU; break; // "Russian"
case 12: lang_name = "se"; break; // "Swedish"
} }
std::string lang_name;
switch (setLanguage) { switch (setLanguage) {
case SetLanguage_JA: lang_name = "ja"; break; case SetLanguage_JA: lang_name = "ja"; break;
case SetLanguage_FR: lang_name = "fr"; break; case SetLanguage_FR: lang_name = "fr"; break;
@@ -75,11 +91,20 @@ bool init(long index) {
case SetLanguage_PT: lang_name = "pt"; break; case SetLanguage_PT: lang_name = "pt"; break;
case SetLanguage_RU: lang_name = "ru"; break; case SetLanguage_RU: lang_name = "ru"; break;
case SetLanguage_ZHTW: lang_name = "zh"; break; case SetLanguage_ZHTW: lang_name = "zh"; break;
default: lang_name = "en"; break;
} }
const fs::FsPath path = "romfs:/i18n/" + lang_name + ".json"; const fs::FsPath sdmc_path = "/config/sphaira/i18n/" + lang_name + ".json";
if (R_SUCCEEDED(fs::FsStdio().read_entire_file(path, g_i18n_data))) { const fs::FsPath romfs_path = "romfs:/i18n/" + lang_name + ".json";
fs::FsPath path = sdmc_path;
// try and load override translation first
Result rc = fs::FsNativeSd().read_entire_file(path, g_i18n_data);
if (R_FAILED(rc)) {
path = romfs_path;
rc = fs::FsStdio().read_entire_file(path, g_i18n_data);
}
if (R_SUCCEEDED(rc)) {
json = yyjson_read((const char*)g_i18n_data.data(), g_i18n_data.size(), YYJSON_READ_ALLOW_TRAILING_COMMAS|YYJSON_READ_ALLOW_COMMENTS|YYJSON_READ_ALLOW_INVALID_UNICODE); json = yyjson_read((const char*)g_i18n_data.data(), g_i18n_data.size(), YYJSON_READ_ALLOW_TRAILING_COMMAS|YYJSON_READ_ALLOW_COMMENTS|YYJSON_READ_ALLOW_INVALID_UNICODE);
if (json) { if (json) {
root = yyjson_doc_get_root(json); root = yyjson_doc_get_root(json);
@@ -107,12 +132,16 @@ void exit() {
g_i18n_data.clear(); g_i18n_data.clear();
} }
std::string get(const char* str) {
return get_internal(str, std::strlen(str));
}
} // namespace sphaira::i18n } // namespace sphaira::i18n
namespace literals { namespace literals {
std::string operator"" _i18n(const char* str, size_t len) { std::string operator"" _i18n(const char* str, size_t len) {
return sphaira::i18n::get(str, len); return sphaira::i18n::get_internal(str, len);
} }
} // namespace literals } // namespace literals

View File

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

View File

@@ -59,6 +59,12 @@ void userAppInit(void) {
diagAbortWithResult(rc); diagAbortWithResult(rc);
if (R_FAILED(rc = accountInitialize(is_application ? AccountServiceType_Application : AccountServiceType_System))) if (R_FAILED(rc = accountInitialize(is_application ? AccountServiceType_Application : AccountServiceType_System)))
diagAbortWithResult(rc); 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(); log_nxlink_init();
} }
@@ -66,6 +72,9 @@ void userAppInit(void) {
void userAppExit(void) { void userAppExit(void) {
log_nxlink_exit(); log_nxlink_exit();
ncmExit();
hidsysExit();
setExit();
accountExit(); accountExit();
nifmExit(); nifmExit();
psmExit(); psmExit();

View File

@@ -71,7 +71,6 @@ auto nro_parse_internal(fs::FsNative& fs, const fs::FsPath& path, NroEntry& entr
} else { } else {
R_TRY(fsFileRead(&f, data.header.size + asset.nacp.offset, &entry.nacp, sizeof(entry.nacp), FsReadOption_None, &bytes_read)); R_TRY(fsFileRead(&f, data.header.size + asset.nacp.offset, &entry.nacp, sizeof(entry.nacp), FsReadOption_None, &bytes_read));
entry.is_nacp_valid = true; entry.is_nacp_valid = true;
log_write("got nacp\n");
} }
// lazy load the icons // lazy load the icons
@@ -241,7 +240,7 @@ auto nro_get_icon(const fs::FsPath& path) -> std::vector<u8> {
R_TRY_RESULT(fsFileRead(&f, data.header.size, &asset, sizeof(asset), FsReadOption_None, &bytes_read), {}); R_TRY_RESULT(fsFileRead(&f, data.header.size, &asset, sizeof(asset), FsReadOption_None, &bytes_read), {});
R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, {}); R_UNLESS(asset.magic == NROASSETHEADER_MAGIC, {});
return nro_get_icon_internal(&f, asset.icon.size, asset.icon.offset); return nro_get_icon_internal(&f, asset.icon.size, data.header.size + asset.icon.offset);
} }
auto nro_get_nacp(const fs::FsPath& path, NacpStruct& nacp) -> Result { auto nro_get_nacp(const fs::FsPath& path, NacpStruct& nacp) -> Result {
@@ -310,4 +309,37 @@ auto nro_normalise_path(const std::string& p) -> std::string {
return p; return p;
} }
auto nro_find(std::span<const NroEntry> array, std::string_view name, std::string_view author, const fs::FsPath& path) -> std::optional<NroEntry> {
const auto it = std::find_if(array.cbegin(), array.cend(), [name, author, path](auto& e){
if (!name.empty() && !author.empty() && !path.empty()) {
return e.GetName() == name && e.GetAuthor() == author && e.path == path;
} else if (!name.empty()) {
return e.GetName() == name;
} else if (!author.empty()) {
return e.GetAuthor() == author;
} else if (!path.empty()) {
return e.path == path;
}
return false;
});
if (it == array.cend()) {
return std::nullopt;
}
return *it;
}
auto nro_find_name(std::span<const NroEntry> array, std::string_view name) -> std::optional<NroEntry> {
return nro_find(array, name, {}, {});
}
auto nro_find_author(std::span<const NroEntry> array, std::string_view author) -> std::optional<NroEntry> {
return nro_find(array, {}, author, {});
}
auto nro_find_path(std::span<const NroEntry> array, const fs::FsPath& path) -> std::optional<NroEntry> {
return nro_find(array, {}, {}, path);
}
} // namespace sphaira } // namespace sphaira

View File

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

View File

@@ -11,6 +11,8 @@
#include "defines.hpp" #include "defines.hpp"
#include "app.hpp" #include "app.hpp"
#include "ui/progress_box.hpp" #include "ui/progress_box.hpp"
#include "i18n.hpp"
#include "log.hpp"
namespace sphaira { namespace sphaira {
namespace { namespace {
@@ -190,6 +192,40 @@ struct NpdmMeta {
u32 acid_size; u32 acid_size;
}; };
struct NpdmAcid {
u8 rsa_sig[0x100];
u8 rsa_pub[0x100];
u32 magic; // "ACID"
u32 size;
u8 version;
u8 _0x209[0x1];
u8 _0x20A[0x2];
u32 flags;
u64 program_id_min;
u64 program_id_max;
u32 fac_offset;
u32 fac_size;
u32 sac_offset;
u32 sac_size;
u32 kac_offset;
u32 kac_size;
u8 _0x238[0x8];
};
struct NpdmAci0 {
u32 magic; // "ACI0"
u8 _0x4[0xC];
u64 program_id;
u8 _0x18[0x8];
u32 fac_offset;
u32 fac_size;
u32 sac_offset;
u32 sac_size;
u32 kac_offset;
u32 kac_size;
u8 _0x38[0x8];
};
struct NpdmPatch { struct NpdmPatch {
char title_name[0x10]{"Application"}; char title_name[0x10]{"Application"};
char product_code[0x10]{}; char product_code[0x10]{};
@@ -577,17 +613,56 @@ auto romfs_build(const FileEntries& entries, u64 *out_size) -> std::vector<u8> {
return buf.buf; return buf.buf;
} }
auto npdm_patch_kc(std::vector<u8>& npdm, u32 off, u32 size, u32 bitmask, u32 value) -> bool {
const u32 pattern = BIT(bitmask) - 1;
const u32 mask = BIT(bitmask) | pattern;
for (u32 i = 0; i < size; i += 4) {
u32 cup;
std::memcpy(&cup, npdm.data() + off + i, sizeof(cup));
if ((cup & mask) == pattern) {
cup = value | pattern;
std::memcpy(npdm.data() + off + i, &cup, sizeof(cup));
return true;
}
}
return false;
}
// todo: manually build npdm // todo: manually build npdm
void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) { void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
NpdmMeta meta{}; NpdmMeta meta{};
NpdmAci0 aci0{};
NpdmAcid acid{};
std::memcpy(&meta, npdm.data(), sizeof(meta)); std::memcpy(&meta, npdm.data(), sizeof(meta));
std::memcpy(&aci0, npdm.data() + meta.aci0_offset, sizeof(aci0));
std::memcpy(&acid, npdm.data() + meta.acid_offset, sizeof(acid));
// apply patch // apply patch
std::memcpy(npdm.data() + 0x20, &patch.title_name, sizeof(patch.title_name)); std::memcpy(meta.title_name, &patch.title_name, sizeof(meta.title_name));
std::memcpy(npdm.data() + 0x30, &patch.product_code, sizeof(patch.product_code)); std::memcpy(meta.product_code, &patch.product_code, sizeof(patch.product_code));
std::memcpy(npdm.data() + meta.aci0_offset + 0x10, &patch.tid, sizeof(patch.tid)); aci0.program_id = patch.tid;
std::memcpy(npdm.data() + meta.acid_offset + 0x210, &patch.tid, sizeof(patch.tid)); acid.program_id_min = patch.tid;
std::memcpy(npdm.data() + meta.acid_offset + 0x218, &patch.tid, sizeof(patch.tid)); acid.program_id_max = patch.tid;
// patch debug flags based on ams version
// SEE: https://github.com/ITotalJustice/sphaira/issues/67
u64 ver{};
splInitialize();
ON_SCOPE_EXIT(splExit());
const auto SplConfigItem_ExosphereVersion = (SplConfigItem)65000;
splGetConfig(SplConfigItem_ExosphereVersion, &ver);
ver >>= 40;
if (ver >= MAKEHOSVERSION(1,7,1)) {
npdm_patch_kc(npdm, meta.aci0_offset + aci0.kac_offset, aci0.kac_size, 16, BIT(19));
npdm_patch_kc(npdm, meta.acid_offset + acid.kac_offset, acid.kac_size, 16, BIT(19));
}
std::memcpy(npdm.data(), &meta, sizeof(meta));
std::memcpy(npdm.data() + meta.aci0_offset, &aci0, sizeof(aci0));
std::memcpy(npdm.data() + meta.acid_offset, &acid, sizeof(acid));
} }
void patch_nacp(NacpStruct& nacp, const NcapPatch& patch) { void patch_nacp(NacpStruct& nacp, const NcapPatch& patch) {
@@ -1027,7 +1102,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
R_UNLESS(!config.main.empty(), OwoError_BadArgs); R_UNLESS(!config.main.empty(), OwoError_BadArgs);
R_UNLESS(!config.npdm.empty(), OwoError_BadArgs); R_UNLESS(!config.npdm.empty(), OwoError_BadArgs);
pbox->NewTransfer("Creating Program").UpdateTransfer(0, 8); pbox->NewTransfer("Creating Program"_i18n).UpdateTransfer(0, 8);
FileEntries exefs; FileEntries exefs;
add_file_entry(exefs, "main", config.main); add_file_entry(exefs, "main", config.main);
add_file_entry(exefs, "main.npdm", config.npdm); add_file_entry(exefs, "main.npdm", config.npdm);
@@ -1059,7 +1134,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
// create control // create control
{ {
pbox->NewTransfer("Creating Control").UpdateTransfer(1, 8); pbox->NewTransfer("Creating Control"_i18n).UpdateTransfer(1, 8);
// patch nacp // patch nacp
NcapPatch nacp_patch{}; NcapPatch nacp_patch{};
nacp_patch.tid = tid; nacp_patch.tid = tid;
@@ -1082,7 +1157,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
NcmContentStorageRecord content_storage_record; NcmContentStorageRecord content_storage_record;
NcmContentMetaData content_meta_data; NcmContentMetaData content_meta_data;
{ {
pbox->NewTransfer("Creating Meta").UpdateTransfer(2, 8); pbox->NewTransfer("Creating Meta"_i18n).UpdateTransfer(2, 8);
const auto meta_entry = create_meta_nca(tid, key, storage_id, nca_entries); const auto meta_entry = create_meta_nca(tid, key, storage_id, nca_entries);
nca_entries.emplace_back(meta_entry.nca_entry); nca_entries.emplace_back(meta_entry.nca_entry);
@@ -1099,7 +1174,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
ON_SCOPE_EXIT(ncmContentStorageClose(&cs)); ON_SCOPE_EXIT(ncmContentStorageClose(&cs));
for (const auto& nca : nca_entries) { for (const auto& nca : nca_entries) {
pbox->NewTransfer("Writing Nca").UpdateTransfer(3, 8); pbox->NewTransfer("Writing Nca"_i18n).UpdateTransfer(3, 8);
NcmContentId content_id; NcmContentId content_id;
NcmPlaceHolderId placeholder_id; NcmPlaceHolderId placeholder_id;
std::memcpy(&content_id, nca.hash, sizeof(content_id)); std::memcpy(&content_id, nca.hash, sizeof(content_id));
@@ -1114,7 +1189,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
// setup database // setup database
{ {
pbox->NewTransfer("Updating ncm databse").UpdateTransfer(4, 8); pbox->NewTransfer("Updating ncm databse"_i18n).UpdateTransfer(4, 8);
NcmContentMetaDatabase db; NcmContentMetaDatabase db;
R_TRY(ncmOpenContentMetaDatabase(&db, storage_id)); R_TRY(ncmOpenContentMetaDatabase(&db, storage_id));
ON_SCOPE_EXIT(ncmContentMetaDatabaseClose(&db)); ON_SCOPE_EXIT(ncmContentMetaDatabaseClose(&db));
@@ -1125,7 +1200,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
// push record // push record
{ {
pbox->NewTransfer("Pushing application record").UpdateTransfer(5, 8); pbox->NewTransfer("Pushing application record"_i18n).UpdateTransfer(5, 8);
Service srv{}, *srv_ptr = &srv; Service srv{}, *srv_ptr = &srv;
bool already_installed{}; bool already_installed{};
@@ -1166,7 +1241,7 @@ auto install_forwarder(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId st
} }
auto install_forwarder(OwoConfig& config, NcmStorageId storage_id) -> Result { auto install_forwarder(OwoConfig& config, NcmStorageId storage_id) -> Result {
App::Push(std::make_shared<ui::ProgressBox>("Installing Forwarder", [config, storage_id](auto pbox) mutable -> bool { App::Push(std::make_shared<ui::ProgressBox>("Installing Forwarder"_i18n, [config, storage_id](auto pbox) mutable -> bool {
return R_SUCCEEDED(install_forwarder(pbox, config, storage_id)); return R_SUCCEEDED(install_forwarder(pbox, config, storage_id));
})); }));
R_SUCCEED(); R_SUCCEED();

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