Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad4dd48026 | ||
|
|
0474566576 | ||
|
|
09e8e50e69 | ||
|
|
db72e84c5b | ||
|
|
d47bb0a41d | ||
|
|
3b741f3180 | ||
|
|
a8965c0d4a | ||
|
|
87c277470e | ||
|
|
c1b6287437 | ||
|
|
4540dd0112 | ||
|
|
dbb0501420 | ||
|
|
28411fcdd1 | ||
|
|
7f2d0e72f2 | ||
|
|
c9552f9785 | ||
|
|
444ff3e2d1 | ||
|
|
7d56c8a381 | ||
|
|
da051f8d8f | ||
|
|
81e6bc5833 | ||
|
|
ca5ea827b2 | ||
|
|
b700fff9ac | ||
|
|
81741364a7 | ||
|
|
faebc42f0d | ||
|
|
63e11ca377 | ||
|
|
54a2215e04 | ||
|
|
5edc3869cd | ||
|
|
a772d660f3 | ||
|
|
3c504cc85d | ||
|
|
0a2c16db0c | ||
|
|
2bd84c8d5a | ||
|
|
7cd668efb7 | ||
|
|
a6265c3089 | ||
|
|
a2300c1a96 | ||
|
|
3dae3f9173 | ||
|
|
63c420d5d8 | ||
|
|
a94c6bb581 | ||
|
|
9fe0044a65 | ||
|
|
c05ce5eff4 | ||
|
|
a019103ed5 | ||
|
|
50e55f4fca | ||
|
|
0706683690 | ||
|
|
9cdb77bafa | ||
|
|
b476c54825 | ||
|
|
8b2e541b1d | ||
|
|
931531e799 | ||
|
|
1695d69aa3 | ||
|
|
217bd3bed3 | ||
|
|
384e8794bf | ||
|
|
61b398a89a | ||
|
|
ba78fd0dc5 | ||
|
|
43969a773e | ||
|
|
6e1eabbe0f | ||
|
|
b99d1e5dea | ||
|
|
6ce566aea5 | ||
|
|
a4209961e2 | ||
|
|
181ff3f2bf | ||
|
|
b85b522643 | ||
|
|
5158e264c0 | ||
|
|
fd67da0527 | ||
|
|
7bdec8457f | ||
|
|
bc75c9a89f |
8
.github/workflows/build_presets.yml
vendored
8
.github/workflows/build_presets.yml
vendored
@@ -1,6 +1,12 @@
|
||||
name: build
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- dev
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
4
.github/workflows/python-usb-export.yml
vendored
4
.github/workflows/python-usb-export.yml
vendored
@@ -3,7 +3,7 @@ name: USB Export Python Tests
|
||||
on:
|
||||
push:
|
||||
paths: &python_usb_export_paths
|
||||
- 'tools/test_usb_export.py'
|
||||
- 'tools/tests/test_usb_export.py'
|
||||
- 'tools/usb_export.py'
|
||||
- 'tools/usb_common.py'
|
||||
- 'tools/requirements.txt'
|
||||
@@ -30,4 +30,4 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
python3 tools/test_usb_export.py
|
||||
python3 tools/tests/test_usb_export.py
|
||||
|
||||
4
.github/workflows/python-usb-install.yml
vendored
4
.github/workflows/python-usb-install.yml
vendored
@@ -3,7 +3,7 @@ name: USB Install Python Tests
|
||||
on:
|
||||
push:
|
||||
paths: &python_usb_install_paths
|
||||
- 'tools/test_usb_install.py'
|
||||
- 'tools/tests/test_usb_install.py'
|
||||
- 'tools/usb_install.py'
|
||||
- 'tools/usb_common.py'
|
||||
- 'tools/requirements.txt'
|
||||
@@ -30,4 +30,4 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
python3 tools/test_usb_install.py
|
||||
python3 tools/tests/test_usb_install.py
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -27,3 +27,6 @@ out
|
||||
usb_test/
|
||||
__pycache__
|
||||
usb_*.spec
|
||||
|
||||
CMakeUserPresets.json
|
||||
build_patreon.sh
|
||||
|
||||
@@ -21,6 +21,35 @@
|
||||
"LTO": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Lite",
|
||||
"displayName": "Lite",
|
||||
"inherits":["core"],
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "MinSizeRel",
|
||||
"LTO": true,
|
||||
|
||||
"ENABLE_NVJPG": false,
|
||||
"ENABLE_NSZ": false,
|
||||
|
||||
"ENABLE_LIBUSBHSFS": false,
|
||||
"ENABLE_LIBUSBDVD": false,
|
||||
"ENABLE_FTPSRV": false,
|
||||
"ENABLE_LIBHAZE": false,
|
||||
|
||||
"ENABLE_AUDIO_MP3": false,
|
||||
"ENABLE_AUDIO_OGG": false,
|
||||
"ENABLE_AUDIO_WAV": false,
|
||||
"ENABLE_AUDIO_FLAC": false,
|
||||
|
||||
"ENABLE_DEVOPTAB_HTTP": false,
|
||||
"ENABLE_DEVOPTAB_NFS": false,
|
||||
"ENABLE_DEVOPTAB_SMB2": false,
|
||||
"ENABLE_DEVOPTAB_FTP": false,
|
||||
"ENABLE_DEVOPTAB_SFTP": false,
|
||||
"ENABLE_DEVOPTAB_WEBDAV": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Dev",
|
||||
"displayName": "Dev",
|
||||
@@ -38,6 +67,11 @@
|
||||
"configurePreset": "Release",
|
||||
"jobs": 16
|
||||
},
|
||||
{
|
||||
"name": "Lite",
|
||||
"configurePreset": "Lite",
|
||||
"jobs": 16
|
||||
},
|
||||
{
|
||||
"name": "Dev",
|
||||
"configurePreset": "Dev",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
918
assets/romfs/i18n/zh-CN.json
Normal file
918
assets/romfs/i18n/zh-CN.json
Normal file
@@ -0,0 +1,918 @@
|
||||
{
|
||||
"Homebrew": "自制软件",
|
||||
"FileBrowser": "文件浏览器",
|
||||
"Appstore": "应用商店",
|
||||
"Corrupted": "已损坏",
|
||||
"enable_install_info": [
|
||||
"安装功能已禁用.\n",
|
||||
"可以通过选择 菜单 (Y) -> 高级选项 -> 安装选项 -> 启用"
|
||||
],
|
||||
"On": "开启",
|
||||
"Off": "关闭",
|
||||
"Entry": "条目",
|
||||
"Store": "商店",
|
||||
"File": "文件",
|
||||
"Files": "文件",
|
||||
"FTP": "FTP",
|
||||
"Games": "游戏",
|
||||
"Meta": "元数据",
|
||||
"Nca": "NCA",
|
||||
"GC": "GC",
|
||||
"GitHub": "GitHub",
|
||||
"Apps": "应用",
|
||||
"IRS": "IRS",
|
||||
"MTP": "MTP",
|
||||
"Saves": "存档",
|
||||
"USB": "USB",
|
||||
"Level 0 (no compression)": "",
|
||||
"Level 1": "",
|
||||
"Level 2": "",
|
||||
"Level 3 (default)": "",
|
||||
"Level 4": "",
|
||||
"Level 5": "",
|
||||
"Level 6": "",
|
||||
"Level 7": "",
|
||||
"Level 8": "",
|
||||
"0 (single threaded)": "",
|
||||
"1": "",
|
||||
"2": "",
|
||||
"3 (default)": "",
|
||||
"4": "",
|
||||
"16 KB": "",
|
||||
"32 KB": "",
|
||||
"64 KB": "",
|
||||
"128 KB": "",
|
||||
"256 KB": "",
|
||||
"512 KB": "",
|
||||
"1 MB (default)": "",
|
||||
"2 MB": "",
|
||||
"4 MB": "",
|
||||
"8 MB": "",
|
||||
"16 MB": "",
|
||||
"Switch-Handheld!": "切换至掌机模式!",
|
||||
"Switch-Docked!": "切换至底座模式!",
|
||||
"Nxlink Connected": "Nxlink已连接",
|
||||
"Nxlink Upload": "Nxlink上传中",
|
||||
"Nxlink Finished": "Nxlink已结束",
|
||||
"Restore hbmenu?": "恢复 hbmenu?",
|
||||
"Back": "返回",
|
||||
"Restore": "恢复",
|
||||
"missing_hbmenu_info": [
|
||||
"未找到 /switch/hbmenu.nro",
|
||||
"请使用应用商店重新安装 hbmenu"
|
||||
],
|
||||
"OK": "确定",
|
||||
"Failed to restore hbmenu, please re-download hbmenu": "恢复 hbmenu 失败,请重新下载 hbmenu",
|
||||
"Failed to restore hbmenu, using sphaira instead": "恢复 hbmenu 失败,改用 Sphaira",
|
||||
"Restored hbmenu, closing sphaira": "已恢复 hbmenu,正在关闭 Sphaira",
|
||||
"Restored hbmenu": "已恢复 hbmenu",
|
||||
"Restart Sphaira?": "重启 Sphaira?",
|
||||
"Restart": "重启",
|
||||
"Installing Forwarder": "正在生成前端应用",
|
||||
"Failed to install forwarder": "前端应用安装失败",
|
||||
"Installed!": "安装完成!",
|
||||
"Warning! Logs are enabled, Sphaira will run slowly!": "警告!日志已启用,将导致Sphaira运行缓慢!",
|
||||
"Audio disabled due to suspended game": "由于游戏暂停,音频已禁用",
|
||||
"Theme Options": "主题选项",
|
||||
"Select Theme": "选择主题",
|
||||
"Customise the look of Sphaira by changing the theme": "通过更换主题来自定义Sphaira的外观",
|
||||
"Music": "音乐",
|
||||
"bgm_enable_info": [
|
||||
"启用背景音乐\n",
|
||||
"每个主题设置独立的音乐文件。",
|
||||
"如果主题未设置音乐文件,则加载默认音乐(如果存在)。"
|
||||
],
|
||||
"Show IP address": "显示 IP 地址",
|
||||
"display_ip_info": [
|
||||
"在所有菜单中显示IP地址,包括WiFi信号栏。\n",
|
||||
"注意:在小程序模式下,IP地址将被隐藏,因为小程序警告会显示在其位置。"
|
||||
],
|
||||
"Remove Background Music": "移除背景音乐",
|
||||
"Removes the background music file": "删除背景音乐文件",
|
||||
"No background music file is set": "未设置背景音乐文件",
|
||||
"Menus": "菜单",
|
||||
"Web": "网络",
|
||||
"Enter custom URL": "输入自定义链接",
|
||||
"Select URL": "选择链接",
|
||||
"Enter URL": "输入网址",
|
||||
"web_browser_info": [
|
||||
"启动内置浏览器\n",
|
||||
"注意:浏览器功能受限,某些网站可能无法加载,并且浏览器会在30分钟超时后关闭"
|
||||
],
|
||||
"Advanced Options": "高级选项",
|
||||
"Slow": "慢",
|
||||
"Normal": "正常",
|
||||
"Fast": "快",
|
||||
"Logging": "日志",
|
||||
"Logs to /config/sphaira/log.txt": "日志输出到 /config/sphaira/log.txt",
|
||||
"Replace hbmenu on exit": "退出后用Sphaira替换hbmenu",
|
||||
"hbmenu_replace_info": [
|
||||
"启用后,将用 Sphaira 替换 /hbmenu.nro,并将 hbmenu 备份到 /switch/hbmenu.nro \n",
|
||||
"禁用后将为您提供还原 hbmenu 的选项。"
|
||||
],
|
||||
"Add / modify mounts": "添加/修改挂载点",
|
||||
"mount_options_info": [
|
||||
"创建、修改、删除网络挂载 (HTTP, FTP, SFTP, SMB, NFS)\n",
|
||||
"挂载选项只需要设置 URL 和名称,其他字段(如端口、用户、密码等)为可选\n",
|
||||
"任何更改都需要重启 Sphaira 后生效"
|
||||
],
|
||||
"Boost CPU during transfer": "传输时提升CPU频率",
|
||||
"transfer_boost_info": [
|
||||
"在传输期间启用加速模式,可提高传输速度。 ",
|
||||
"这会将 CPU 设置为 1785MHz,并将 GPU 降至 76MHz"
|
||||
],
|
||||
"Text scroll speed": "文本滚动速度",
|
||||
"Change how fast the scrolling text updates": "更改滚动文本的更新速度",
|
||||
"Set center menu": "设置中间菜单",
|
||||
"Press OK to restart Sphaira": "按OK键以重启shphaira菜单",
|
||||
"Set the menu that appears on the center tab.": "设置显示在中间选项卡上的菜单。",
|
||||
"Set left-side menu": "设置左侧菜单",
|
||||
"Set the menu that appears on the left tab.": "设置显示在左侧选项卡上的菜单。",
|
||||
"Set right-side menu": "设置右侧菜单",
|
||||
"Set the menu that appears on the right tab.": "设置显示在右侧选项卡上的菜单。",
|
||||
"Install options": "安装选项",
|
||||
"install_options_info": [
|
||||
"更改安装选项。\n",
|
||||
"您可以在此启用安装功能"
|
||||
],
|
||||
"Export options": "导出选项",
|
||||
"Change the export options.": "更改导出选项。",
|
||||
"Install Options": "安装选项",
|
||||
"System memory": "主机内存",
|
||||
"microSD card": "SD卡",
|
||||
"Enable sysmmc": "启用sysmmc",
|
||||
"Enables installing whilst in sysMMC mode.": "允许在 sysMMC 模式下安装。",
|
||||
"Enable emummc": "启用emummc",
|
||||
"Enables installing whilst in emuMMC mode.": "允许在 emuMMC 模式下安装。",
|
||||
"Install location": "安装位置",
|
||||
"Allow downgrade": "允许降级",
|
||||
"Allows for installing title updates that are lower than the currently installed update.": "允许安装低于当前已安装版本的游戏更新。",
|
||||
"Skip if already installed": "若已安装则跳过",
|
||||
"Skips installing titles / ncas if they're already installed.": "跳过已安装的游戏/NCA",
|
||||
"Ticket only": "仅安装票据",
|
||||
"Installs tickets only, useful if the title was already installed however the tickets were missing or corrupted.": "仅安装 tickets, 适用于已安装但损坏的游戏",
|
||||
"Skip base": "跳过基础部分",
|
||||
"Skips installing the base application.": "跳过安装基础应用程序。",
|
||||
"Skip patch": "跳过更新补丁(UPD)",
|
||||
"Skips installing updates.": "跳过安装更新。",
|
||||
"Skip dlc": "跳过追加内容(DLC)",
|
||||
"Skips installing DLC.": "跳过安装 DLC。",
|
||||
"Skip data patch": "跳过数据补丁",
|
||||
"Skips installing DLC update (data patch).": "跳过安装 DLC 更新(数据补丁)。",
|
||||
"Skip ticket": "跳过票据",
|
||||
"Skips installing tickets, not recommended.": "跳过安装票据,不推荐。",
|
||||
"Skip NCA hash verify": "跳过 NCA 哈希验证",
|
||||
"skip_nca_info": [
|
||||
"启用跳过完整 NCA 的 sha256 验证。",
|
||||
"用于验证 NCA 是否有效/未损坏。",
|
||||
"当安装损坏的游戏时,您可能看到过\"检查损坏数据\"的选项",
|
||||
"该检查会执行包括对 NCA 在内的各种哈希校验\n",
|
||||
"建议保持禁用状态。"
|
||||
],
|
||||
"Skip RSA header verify": "跳过 RSA 头部验证",
|
||||
"nca_verify_info": [
|
||||
"启用跳过 RSA NCA固定密钥验证。",
|
||||
"这是对NCA头的哈希值。它用于验证头部是否未被修改。 ",
|
||||
"头部由任天堂签名,因此无法伪造,可以可靠地检测修改过的NCA头(如NSP/XCI转换)。\n",
|
||||
"建议保持禁用状态,除非您需要安装nsp/xci转换文件。"
|
||||
],
|
||||
"Skip RSA NPDM verify": "跳过 RSA NPDM 验证",
|
||||
"npdm_verify_info": [
|
||||
"启用跳过RSA NPDM固定密钥验证\n",
|
||||
"目前此选项未实现。"
|
||||
],
|
||||
"Ignore distribution bit": "忽略发行位",
|
||||
"nca_distbit_info": [
|
||||
"如果设置,将忽略 NCA 头部中的发行位",
|
||||
"发行位用于标识NCA是来自Eshop还是游戏卡。",
|
||||
"您通常无法启动将发行位设置为游戏卡的已安装游戏。\n",
|
||||
"建议保持禁用状态。"
|
||||
],
|
||||
"Convert to common ticket": "转换为公共票据",
|
||||
"ticket_convert_info": [
|
||||
"[需要密钥] 将个人票据转换为公共(虚拟)票据。\n",
|
||||
"建议保持启用状态。"
|
||||
],
|
||||
"Convert to standard crypto": "转换为标准加密方式",
|
||||
"titlekey_crypto_info": [
|
||||
"[需要密钥] 将标题密钥转换为标准加密,也称为\"无票据\"。\n",
|
||||
"建议保持禁用状态。"
|
||||
],
|
||||
"Lower master key": "降低主密钥",
|
||||
"keyarea_crypto_info": [
|
||||
"[需要密钥] 使用主密钥0加密keak(密钥区域密钥),允许游戏在任何固件上启动。 ",
|
||||
"隐式执行标准加密。\n",
|
||||
"请注意,仅仅因为游戏可以在任何固件上启动(因为它可以被解密),并不意味着它就能正常工作。强烈建议更新您的固件和Atmosphere版本以玩游戏,而不是启用此选项。\n",
|
||||
"建议保持禁用状态。"
|
||||
],
|
||||
"Lower system version": "降低系统版本",
|
||||
"cnmt_fw_info": [
|
||||
"将cnmt扩展头中的system_firmware字段设置为0。",
|
||||
"注意:如果主密钥高于固件版本,游戏仍然无法启动,因为固件没有解密keak的密钥(见上文)。\n",
|
||||
"建议保持禁用状态。"
|
||||
],
|
||||
"Export Options": "导出选项",
|
||||
"Created nested folder": "已创建嵌套文件夹",
|
||||
"game_folder_info": [
|
||||
"使用游戏名称创建文件夹。\n",
|
||||
"例如:/name/name.xci",
|
||||
"禁用后将使用 /name.xci"
|
||||
],
|
||||
"Append folder with .xci": "用.xci附加文件夹",
|
||||
"xci_folder_info": [
|
||||
"XCI转储将使用.xci扩展名命名文件夹。\n",
|
||||
"例如:/name.xci/name.xci\n",
|
||||
"某些设备仅在xci文件夹名称与xci文件完全相同时才能正常工作。"
|
||||
],
|
||||
"Trim XCI": "裁剪 XCI",
|
||||
"Removes the unused data at the end of the XCI, making the output smaller.": "移除 XCI 末尾未使用的数据,使输出更小。",
|
||||
"Label trimmed XCI": "标记已裁剪的XCI",
|
||||
"Names the trimmed xci.\nFor example, /name/name (trimmed).xci": "为修剪后的xci命名。\n例如:/name/name (trimmed).xci",
|
||||
"Converts personalised ticket to a fake common ticket.": "将个人票据转换为虚拟公共票据。",
|
||||
"NSZ level": "NSZ 压缩级别",
|
||||
"compress_level_info": [
|
||||
"设置导出NSZ时使用的压缩级别。\n",
|
||||
"注意:Switch的CPU速度不是很快,将值设置得过高会导致导出耗时非常长,但文件大小仅有很小的改善。\n",
|
||||
"建议将此值设置为3。"
|
||||
],
|
||||
"NSZ threads": "NSZ 线程数",
|
||||
"compress_threads_info": [
|
||||
"设置压缩NCA时使用的线程数。\n",
|
||||
"小于3的值允许另一个线程自由运行,例如读/写线程。但在我的测试中,值为3时通常性能最佳。\n",
|
||||
"值为0将不使用线程,仅应用于测试,因为它总是更慢。\n",
|
||||
"建议将此值设置在1-3之间。"
|
||||
],
|
||||
"NSZ long distance mode": "NSZ 远距离模式",
|
||||
"Enables \"Long Distance Mode\" which can reduce the output size at the cost of speed.": "启用\"远距离模式\",可以减少输出大小,但会降低速度。",
|
||||
"NSZ block compression": "NSZ 块压缩",
|
||||
"block_compress_info": [
|
||||
"启用块压缩,将NCA压缩成块(代价是压缩率降低) ",
|
||||
"允许随机访问,使NCZ可以作为文件系统挂载。\n",
|
||||
"注意:Sphaira目前还不支持将NCZ作为文件系统挂载,但最终会添加此功能。"
|
||||
],
|
||||
"NSZ block size": "NSZ 块大小",
|
||||
"Sets the size of each block. The smaller the size, the faster the random access is at the cost of compression ratio.": "设置每个块的大小。块越小,随机访问速度越快,但压缩率会降低。",
|
||||
"NSZ block compression is disabled.": "NSZ 块压缩已禁用。",
|
||||
"FTP Options": "FTP选项",
|
||||
"Restarting FTP server...": "正在重启 FTP 服务器...",
|
||||
"Enable": "启用",
|
||||
"Enable FTP server to run in the background.": "启用 FTP 服务器后台运行。",
|
||||
"Port": "端口号",
|
||||
"Opens the FTP server on this port.": "在此端口上打开 FTP 服务器。",
|
||||
"Anon": "匿名登录",
|
||||
"login_require_info": [
|
||||
"允许您无需设置用户名和密码即可登录。\n",
|
||||
"如果禁用,您必须在下方设置用户名和密码!"
|
||||
],
|
||||
"User": "用户",
|
||||
"Sets the username, must be set if anon is disabled.": "设置用户名,如果禁用匿名登录则必须设置。",
|
||||
"Pass": "密码",
|
||||
"Sets the password, must be set if anon is disabled.": "设置密码,如果禁用匿名登录则必须设置。",
|
||||
"Show album": "显示相册",
|
||||
"Shows the microSD card album folder.": "显示 microSD 卡的相册文件夹。",
|
||||
"Show Atmosphere contents": "显示 Atmosphere 内容",
|
||||
"Shows the shortcut for the /atmosphere/contents folder.": "显示 /atmosphere/contents 文件夹的快捷方式。",
|
||||
"Show bis storage": "显示 bis 存储",
|
||||
"bis_contents_info": [
|
||||
"显示包含以下文件的bis文件夹:",
|
||||
"- BootPartition1Root.bin",
|
||||
"- BootPartition2Root.bin",
|
||||
"- UserDataRoot.bin",
|
||||
"- BootConfigAndPackage2Part1.bin",
|
||||
"- BootConfigAndPackage2Part2.bin",
|
||||
"- BootConfigAndPackage2Part3.bin",
|
||||
"- BootConfigAndPackage2Part4.bin",
|
||||
"- BootConfigAndPackage2Part5.bin",
|
||||
"- BootConfigAndPackage2Part6.bin",
|
||||
"- CalibrationFile.bin",
|
||||
"- SafeMode.bin",
|
||||
"- User.bin",
|
||||
"- System.bin",
|
||||
"- SystemProperEncryption.bin"
|
||||
],
|
||||
"Show bis file systems": "显示 bis 文件系统",
|
||||
"bis_fs_info": [
|
||||
"显示以下bis文件系统:",
|
||||
"- bis_calibration_file",
|
||||
"- bis_safe_mode",
|
||||
"- bis_user",
|
||||
"- bis_system"
|
||||
],
|
||||
"Show system contents": "显示系统内容",
|
||||
"Shows the system contents folder.": "显示系统内容文件夹。",
|
||||
"Show user contents": "显示用户内容",
|
||||
"Shows the user contents folder.": "显示用户内容文件夹。",
|
||||
"Show microSD contents": "显示 microSD 卡内容",
|
||||
"microsd_contents_info": [
|
||||
"显示microSD contents文件夹。\n",
|
||||
"注意:这不是普通的microSD卡存储,而是NCA存储的位置。",
|
||||
"普通microSD卡始终是挂载的。"
|
||||
],
|
||||
"Show games": "显示游戏",
|
||||
"games_ftp_info": [
|
||||
"显示games文件夹。\n",
|
||||
"此文件夹包含您所有已安装的游戏,允许您通过FTP创建备份!\n",
|
||||
"注意:此文件夹是只读的。您无法通过FTP删除游戏。"
|
||||
],
|
||||
"Show install": "显示安装文件夹",
|
||||
"install_ftp_info": [
|
||||
"显示install文件夹。\n",
|
||||
"此文件夹用于通过FTP安装游戏。\n",
|
||||
"注意:尝试安装游戏时必须打开\"FTP安装\"菜单!"
|
||||
],
|
||||
"Show mounts": "显示挂载点",
|
||||
"mounts_ftp_info": [
|
||||
"显示mounts文件夹。\n",
|
||||
"此文件夹包含添加到Sphaira的所有挂载点,允许您通过FTP访问它们!",
|
||||
"例如,您可以通过FTP访问SMB、WebDav或其他FTP挂载点。"
|
||||
],
|
||||
"Show switch": "显示 Switch 目录",
|
||||
"homebrew_folder_info": [
|
||||
"显示/switch文件夹的快捷方式。",
|
||||
"这是包含所有自制软件(NRO)的文件夹。"
|
||||
],
|
||||
"MTP Options": "MTP选项",
|
||||
"Restarting MTP server...": "正在重启 MTP 服务器...",
|
||||
"Enable MTP server to run in the background.": "启用 MTP 服务器后台运行。",
|
||||
"Pre-allocate file": "预分配文件",
|
||||
"prealloc_info": [
|
||||
"在写入前启用文件大小预分配。",
|
||||
"这会加快文件写入速度,但如果同时满足以下所有条件,可能会导致超时:",
|
||||
"- 使用 Windows 系统",
|
||||
"- 使用 emuMMC",
|
||||
"- 传输大文件(>1GB)\n",
|
||||
"建议保持此选项启用,但如果您使用上述配置并遇到超时, ",
|
||||
"请在禁用此选项后重试。"
|
||||
],
|
||||
"games_mtp_info": [
|
||||
"显示games文件夹。\n",
|
||||
"此文件夹包含您所有已安装的游戏,允许您通过MTP创建备份!\n",
|
||||
"注意:此文件夹是只读的。您无法通过MTP删除游戏。"
|
||||
],
|
||||
"install_mtp_info": [
|
||||
"显示install文件夹。\n",
|
||||
"此文件夹用于通过MTP安装游戏。\n",
|
||||
"注意:尝试安装游戏时必须打开\"MTP安装\"菜单!"
|
||||
],
|
||||
"mounts_mtp_info": [
|
||||
"显示mounts文件夹。\n",
|
||||
"此文件夹包含添加到Sphaira的所有挂载点,允许您通过MTP访问它们!",
|
||||
"例如,您可以通过MTP访问SMB、WebDav和FTP挂载点。"
|
||||
],
|
||||
"Show DevNull": "显示 DevNull",
|
||||
"usb_benchmark_info": [
|
||||
"显示DevNull(速度测试)文件夹。\n",
|
||||
"此文件夹用于USB上传的基准测试。",
|
||||
"这是一个虚拟文件夹,实际上不会向磁盘写入任何内容。"
|
||||
],
|
||||
"HDD Options": "硬盘选项",
|
||||
"mount_hdd_info": [
|
||||
"启用已连接的USB/HDD设备挂载。 ",
|
||||
"已连接的设备可在文件浏览器中使用,也可作为转储游戏和存档时的备份位置。"
|
||||
],
|
||||
"HDD write protect": "硬盘写保护",
|
||||
"Makes the connected HDD read-only.": "将已连接的硬盘设置为只读。",
|
||||
"Installing is disabled, enable now?": "安装功能已禁用,现在启用吗?",
|
||||
"WARNING: Installing apps will lead to a ban!": "警告:安装应用程序将导致封禁!",
|
||||
"Installing enabled!": "已启用安装!",
|
||||
"microSD card (/dumps/)": "",
|
||||
"USB export to PC (usb_export.py)": "",
|
||||
"USB transfer (Switch 2 Switch)": "USB传输(Switch对Switch)",
|
||||
"/dev/null (Speed Test)": "/dev/null(速度测试)",
|
||||
"Waiting for USB connection...": "",
|
||||
"USB connected, sending file list": "USB已连接,正在发送文件列表",
|
||||
"Sent file list, waiting for command...": "已发送文件列表,正在等待命令...",
|
||||
"waiting for usb connection...": "等待USB连接...",
|
||||
"Exporting": "正在导出",
|
||||
"Export failed!": "导出失败!",
|
||||
"Export successfull!": "导出成功!",
|
||||
"Select export location": "选择导出位置",
|
||||
"Please launch MTP install menu before trying to install": "请在尝试安装前启动 MTP 安装菜单",
|
||||
"Album": "专辑",
|
||||
"Install (NSP, XCI, NSZ, XCZ)": "安装 (NSP, XCI, NSZ, XCZ)",
|
||||
" (Read Only)": " (只读)",
|
||||
"Unknown": "未知",
|
||||
"Application": "",
|
||||
"Creating Program": "正在创建程序",
|
||||
"Creating Control": "正在创建控制器",
|
||||
"Creating Meta": "正在创建元数据",
|
||||
"Writing Nca": "正在写入Nca",
|
||||
"Updating ncm database": "正在更新ncm数据库",
|
||||
"Pushing application record": "正在推送应用记录",
|
||||
"Failed to remove old forwarder, please manually remove it!": "删除旧前端应用失败,请手动删除!",
|
||||
"An error occurred": "发生错误",
|
||||
"If this message appears repeatedly, please open an issue.": "若此消息反复出现,请提交问题报告。",
|
||||
"Failed to load music": "音乐加载失败",
|
||||
"Artist: Unknown": "艺术家: 未知",
|
||||
"Album: Unknown": "专辑: 未知",
|
||||
"Select": "选择",
|
||||
"Are you sure you wish to cancel?": "您确定要取消吗?",
|
||||
"No": "否",
|
||||
"Yes": "是",
|
||||
"%zu hours %zu minutes remaining": "剩余 %zu 小时 %zu 分钟",
|
||||
"%zu minutes %zu seconds remaining": "剩余 %zu 分钟 %zu 秒",
|
||||
"%zu seconds remaining": "剩余 %zu 秒",
|
||||
"Closing empty sidebar": "关闭空侧边栏",
|
||||
"All": "全部",
|
||||
"Emulators": "模拟器",
|
||||
"Tools": "工具",
|
||||
"Advanced": "高级",
|
||||
"Themes": "主题",
|
||||
"Legacy": "旧版",
|
||||
"Misc": "拓展",
|
||||
"Updated": "最近使用",
|
||||
"Downloads": "下载",
|
||||
"Size": "按大小",
|
||||
"Alphabetical": "按字母顺序",
|
||||
"Desc": "降序",
|
||||
"Asc": "升序",
|
||||
"Removing ": "正在移除 ",
|
||||
"Downloading ": "正在下载 ",
|
||||
"Checking MD5": "正在校验 MD5",
|
||||
"Options": "选项",
|
||||
"More by Author": "作者更多作品",
|
||||
"Leave Feedback": "留言反馈",
|
||||
"Visit Website": "访问网站",
|
||||
"version: %s": "版本: %s",
|
||||
"updated: %s": "更新时间: %s",
|
||||
"category: %s": "分类: %s",
|
||||
"extracted: %s": "已解压:%s",
|
||||
"app_dls: %s": "下载量: %s",
|
||||
"Loading...": "加载中...",
|
||||
"Failed to download manifest": "下载清单失败",
|
||||
"Details": "详情",
|
||||
"Changelog": "更新日志",
|
||||
"Downloaded ": "已下载 ",
|
||||
"Uninstalling ": "正在卸载 ",
|
||||
"Removed ": "已移除 ",
|
||||
"Install": "安装",
|
||||
"Update": "更新",
|
||||
"Launch": "启动",
|
||||
"Remove": "删除",
|
||||
"Completely remove ": "彻底删除 ",
|
||||
"AppStore": "应用商店",
|
||||
"Info": "信息",
|
||||
"AppStore Options": "应用商店选项",
|
||||
"Descending": "降序",
|
||||
"Ascending": "升序",
|
||||
"List": "列表",
|
||||
"Icon": "图标",
|
||||
"Grid": "网格",
|
||||
"Filter": "筛选",
|
||||
"Sort": "排序",
|
||||
"Order": "顺序",
|
||||
"Layout": "布局",
|
||||
"Search": "搜索",
|
||||
"Empty!": "空空如也!",
|
||||
"Filter: %s | Sort: %s | Order: %s": "筛选: %s | 排序: %s | 顺序: %s",
|
||||
"File Picker": "文件选择器",
|
||||
"Failed to parse nro": "解析 nro 失败",
|
||||
"Name": "名称",
|
||||
"Set the name of the application": "设置应用程序的名称",
|
||||
"Author": "作者",
|
||||
"Set the author of the application": "设置应用程序的作者",
|
||||
"Version": "版本",
|
||||
"Set the display version of the application": "设置应用程序的显示版本",
|
||||
"Set the path to the icon for the forwarder": "设置前端启动器图标的路径",
|
||||
"Create": "创建",
|
||||
"Failed to load icon": "加载图标失败",
|
||||
"Create the forwarder.": "创建前端启动器。",
|
||||
"All fields must be non-empty!": "所有字段不能为空!",
|
||||
"Open": "打开",
|
||||
"Empty...": "空...",
|
||||
"%zd files": "%zd 个文件",
|
||||
"%zd dirs": "%zd 个文件夹",
|
||||
"Open with DayBreak?": "使用DayBreak打开?",
|
||||
"Launch ": "启动 ",
|
||||
"Launch option for: ": "启动选项:",
|
||||
"Select launcher for: ": "选择启动器用于:",
|
||||
"Install Selected files?": "安装所选文件?",
|
||||
"Installing ": "正在安装 ",
|
||||
"Installed ": "已安装",
|
||||
"File install failed!": "文件安装失败!",
|
||||
"Extracting ": "正在解压",
|
||||
"Extract failed!": "解压失败!",
|
||||
"Extract success!": "解压成功!",
|
||||
"Compressing ": "正在压缩 ",
|
||||
"Compress failed!": "压缩失败",
|
||||
"Compress success!": "压缩成功!",
|
||||
"Failed to delete directory": "删除目录失败",
|
||||
"Failed to delete file": "删除文件失败",
|
||||
"Deleting": "正在删除",
|
||||
"Scanning ": "正在扫描 ",
|
||||
"Pasting": "正在粘贴",
|
||||
"Pasting ": "正在粘贴 ",
|
||||
"Creating ": "正在创建 ",
|
||||
"Copying ": "正在复制 ",
|
||||
"Deleting ": "正在删除 ",
|
||||
"Hashing": "正在计算文件哈希",
|
||||
"Failed to hash file...": "计算文件哈希失败...",
|
||||
"File Options": "文件选项",
|
||||
"Mount": "挂载",
|
||||
"Sort By": "排序方式",
|
||||
"Sort Options": "排序选项",
|
||||
"Show Hidden": "显示隐藏项目",
|
||||
"Folders First": "文件夹靠前",
|
||||
"Hidden Last": "隐藏项目置后",
|
||||
"Cut": "剪切",
|
||||
"Copy": "复制",
|
||||
"Paste": "粘贴",
|
||||
"Paste file(s)?": "粘贴 个文件(夹)?",
|
||||
"Rename": "重命名",
|
||||
"Set new name": "设置新名称",
|
||||
"Failed to rename file: ": "重命名文件失败: ",
|
||||
"Delete": "删除",
|
||||
"Delete Selected files?": "删除选中的文件?",
|
||||
"Install Forwarder": "安装前端应用",
|
||||
"Extract zip": "解压 zip",
|
||||
"Extract Options": "解压选项",
|
||||
"Extract here": "解压到这里",
|
||||
"Extract to root": "解压到根目录",
|
||||
"Are you sure you want to extract to root?": "您确定要解压到根目录吗?",
|
||||
"Extract to...": "解压到...",
|
||||
"Enter the path to the folder to extract into": "输入要解压到的文件夹的路径",
|
||||
"Compress to zip": "压缩到zip",
|
||||
"Compress Options": "压缩选项",
|
||||
"Compress": "压缩",
|
||||
"Compress to...": "压缩到...",
|
||||
"Enter the path to the folder to compress into": "输入要压缩的文件夹路径",
|
||||
"Create File": "新建文件",
|
||||
"Set File Name": "输入文件名",
|
||||
"Create Folder": "新建文件夹",
|
||||
"Set Folder Name": "输入文件夹名",
|
||||
"View as text (unfinished)": "以文本形式查看(未完善)",
|
||||
"Set as background music": "设置为背景音乐",
|
||||
"Failed to set default music path": "设置默认音乐路径失败",
|
||||
"Hash": "哈希",
|
||||
"Hash Options": "哈希选项",
|
||||
"Ignore read only": "忽略只读",
|
||||
"Failed to mount FS.": "文件系统挂载失败。",
|
||||
"Close FileBrowser?": "是否关闭文件浏览器?",
|
||||
"Split": "拆分",
|
||||
"Close": "关闭",
|
||||
"FTP Install": "通过 FTP 安装",
|
||||
"Connection Type: WiFi | Strength: ": "连接类型:WiFi |强度:",
|
||||
"Connection Type: Ethernet": "连接类型:以太网",
|
||||
"Connection Type: None": "连接类型:无",
|
||||
"Host:": "主机:",
|
||||
"Port:": "端口:",
|
||||
"Username:": "用户名:",
|
||||
"Password:": "密码:",
|
||||
"SSID:": "网络名称:",
|
||||
"Passphrase:": "密码:",
|
||||
"Success": "成功",
|
||||
"Failed to launch application": "",
|
||||
"Game Options": "游戏选项",
|
||||
"Hide forwarders": "隐藏前端启动",
|
||||
"View application content": "查看应用程序内容",
|
||||
"Launch random game": "启动随机游戏",
|
||||
"Export NSP": "导出 NSP",
|
||||
"Not supported in Applet Mode": "小程序模式不支持",
|
||||
"Export NSZ": "导出 NSZ",
|
||||
"Exports to NSZ (compressed NSP)": "导出为 NSZ(压缩的 NSP)",
|
||||
"Are you sure you want to delete ": "您确定要删除吗 ",
|
||||
"Advanced options": "高级选项",
|
||||
"Refresh": "刷新",
|
||||
"Create contents folder": "创建内容文件夹",
|
||||
"Folder create failed!": "文件夹创建失败!",
|
||||
"Folder created!": "文件夹创建成功!",
|
||||
"Create save": "创建存档",
|
||||
"Select user to create save for": "选择要为其创建存档的用户",
|
||||
"Delete title cache": "删除游戏缓存",
|
||||
"Are you sure you want to delete the title cache?": "您确定要删除标题缓存吗?",
|
||||
"Updating application record list": "更新应用程序记录列表",
|
||||
"Delete failed!": "删除失败!",
|
||||
"Delete successfull!": "删除成功!",
|
||||
"Select content to export": "选择要导出的内容",
|
||||
"Export All": "导出全部",
|
||||
"Export Application": "导出应用程序",
|
||||
"Export Patch": "导出补丁",
|
||||
"Export AddOnContent": "导出追加内容(DLC)",
|
||||
"Export DataPatch": "导出数据补丁",
|
||||
"Creating": "正在创建",
|
||||
"Save create failed!": "存档创建失败!",
|
||||
"Save create successfull!": "存档创建成功!",
|
||||
"Failed to delete meta entry": "删除元数据失败",
|
||||
"None": "无",
|
||||
"Common": "公共",
|
||||
"Personalised": "个性化",
|
||||
"Missing": "丢失",
|
||||
"View Content": "查看内容",
|
||||
"Content Options": "内容选项",
|
||||
"Are you sure you want to delete the selected entries?": "您确定要删除选定的条目吗?",
|
||||
"Reset required system version": "重置所需系统版本",
|
||||
"Are you sure you want to reset required system version?": "您确定要重置所需的系统版本吗?",
|
||||
"Reset": "重置",
|
||||
"Failed to reset required system version": "重置所需系统版本失败",
|
||||
"Required System Version: %u.%u.%u": "",
|
||||
"Required Application Version: v%u": "",
|
||||
"Content Count: %u (%u missing)": "",
|
||||
"Content Count: %u": "",
|
||||
"Ticket: %s": "",
|
||||
"Key Generation: %u (%s)": "",
|
||||
"Display Version: %s": "",
|
||||
"Mount Fs": "挂载文件系统",
|
||||
"Failed to mount NCA": "NCA 挂载失败",
|
||||
"NCA Options": "NCA选项",
|
||||
"Export NCA": "导出NCA",
|
||||
"Export NCA decrypted": "导出已解密的 NCA",
|
||||
"Exports the NCA with all fs sections decrypted (NCA header is still encrypted).": "导出所有文件系统分区已解密的 NCA(NCA 头仍为加密)。",
|
||||
"Verify NCA 256 hash": "验证 NCA 256 哈希",
|
||||
"NCA hash missmatch!": "NCA 哈希值不匹配!",
|
||||
"NCA hash valid.": "NCA 哈希值有效。",
|
||||
"nca_validate_info": [
|
||||
"对NCA执行sha256哈希以检查其是否有效。\n",
|
||||
"注意:这仅检测哈希是否不匹配,并不验证内容是否已被修改。"
|
||||
],
|
||||
"Verify NCA fixed key": "验证NCA固定密钥",
|
||||
"NCA fixed key is invalid!": "NCA固定密钥无效!",
|
||||
"NCA fixed key is valid.": "NCA固定密钥有效。",
|
||||
"nca_fixedkey_info": [
|
||||
"执行RSA NCA固定密钥验证。",
|
||||
"这是对NCA头部的哈希值。它用于验证头部是否未被修改。 ",
|
||||
"头部由任天堂签名,因此无法伪造,可以可靠地检测修改过的NCA头(如NSP/XCI转换)。"
|
||||
],
|
||||
"Failed to decrypt NCA": "解密 NCA 失败",
|
||||
"Application Type: %s": "",
|
||||
"Content Type: %s": "",
|
||||
"Distribution Type: %s": "",
|
||||
"Program ID: %016lX": "",
|
||||
"SDK Version: %u.%u.%u.%u": "",
|
||||
"Export XCI (Gamecard)": "",
|
||||
"Export XCZ (Compressed XCI)": "",
|
||||
"GameCard": "游戏卡",
|
||||
"Gc install failed!": "游戏安装失败。",
|
||||
"Gc install success!": "游戏安装成功。",
|
||||
"Select content to dump": "选择要转储的内容",
|
||||
"Export All Bins": "导出所有 Bin 文件",
|
||||
"Export XCI": "导出 XCI",
|
||||
"Export Card ID Set": "导出游戏卡ID集",
|
||||
"Export Card UID": "导出游戏卡UID",
|
||||
"Export Certificate": "导出证书",
|
||||
"Export Initial Data": "导出初始数据",
|
||||
"Failed to mount GameCard filesystem": "游戏卡文件系统挂载失败",
|
||||
"System memory %.1f GB": "主机内存 %.1f GB",
|
||||
"microSD card %.1f GB": "microSD卡 %.1f GB",
|
||||
"App-ID: 0%lX": "",
|
||||
"Key-Gen: %u (%s)": "",
|
||||
"Size: %.2f GB": "",
|
||||
"Base: %zu Patch: %zu Addon: %zu Data: %zu": "",
|
||||
"Prev": "上一项",
|
||||
"Next": "下一项",
|
||||
"No GameCard inserted": "未插入游戏卡",
|
||||
"WARNING: GameCard is already trimmed!": "警告:游戏卡已被修剪!",
|
||||
"Continue": "继续",
|
||||
"GameCard is trimmed, full dump is not possible!": "游戏卡已被裁剪,无法进行完整转储!",
|
||||
"Downloading json": "正在下载JSON",
|
||||
"Download": "下载",
|
||||
"Failed to download json": "下载JSON失败",
|
||||
"Select release to download for ": "选择要下载的版本:",
|
||||
"Select asset to download for ": "选择要下载的资源:",
|
||||
"Failed to download app!": "下载应用失败!",
|
||||
"Unstar": "取消星标",
|
||||
"Unstarred ": "取消星标 ",
|
||||
"Star": "星标",
|
||||
"Starred ": "已星标 ",
|
||||
"Homebrew Options": "自制软件选项",
|
||||
"Updated (Star)": "最近更新(星标优先)",
|
||||
"Alphabetical (Star)": "按字母顺序(星标优先)",
|
||||
"Size (Star)": "按大小(星标优先)",
|
||||
"Display entries in Ascending or Descending order.": "以升序或降序显示条目。",
|
||||
"Change the layout to List, Icon and Grid.": "切换布局:列表、图标和网格。",
|
||||
"Show hidden": "显示隐藏项目",
|
||||
"Shows all hidden homebrew.": "显示所有隐藏的自制软件。",
|
||||
"Test": "测试",
|
||||
"Hide": "隐藏",
|
||||
"Hides the selected homebrew.\nTo unhide homebrew, enable \"Show hidden\" in the sort options.": "隐藏所选自制软件。\n要取消隐藏自制软件,请在排序选项中启用\"显示隐藏项目\"。",
|
||||
"Mount NRO Fs": "挂载 NRO 文件系统",
|
||||
"Failed to mount NRO FileSystem": "NRO 文件系统挂载失败",
|
||||
"Mounts the NRO FileSystem (icon, nacp and RomFS).": "挂载 NRO 文件系统(图标、nacp 和 RomFS)。",
|
||||
"hb_remove_info": [
|
||||
"永久删除所选自制软件。\n",
|
||||
"自制软件创建的文件和文件夹仍将保留。 ",
|
||||
"请使用文件浏览器删除它们。"
|
||||
],
|
||||
"Failed to load image": "图片加载失败",
|
||||
"Install failed!": "通过 FTP 安装失败。",
|
||||
"Install success!": "通过 FTP 安装成功。",
|
||||
"Drag'n'Drop (NSP, XCI, NSZ, XCZ) to the install folder": "拖放文件(NSP, XCI, NSZ, XCZ)至安装文件夹",
|
||||
"Failed to install, press B to exit...": "通过 FTP 安装失败,按 B 键退出...",
|
||||
"Irs": "红外成像",
|
||||
"0 (Sideways)": "0度",
|
||||
"90 (Flat)": "90度",
|
||||
"180 (-Sideways)": "180度",
|
||||
"270 (Upside down)": "270度",
|
||||
"Grey": "灰色",
|
||||
"Ironbow": "紫黄",
|
||||
"Green": "绿色",
|
||||
"Red": "红色",
|
||||
"Blue": "蓝色",
|
||||
"All leds": "全部",
|
||||
"Bright group": "亮色组",
|
||||
"Dim group": "暗色组",
|
||||
"Normal image": "正常图像",
|
||||
"Negative image": "负片图像",
|
||||
"Controller": "控制器",
|
||||
"Rotation": "旋转",
|
||||
"Colour": "颜色",
|
||||
"Light Target": "光源目标",
|
||||
"Gain": "曝光",
|
||||
"Negative Image": "负片图像",
|
||||
"Format": "格式",
|
||||
"Trimming Format": "修剪格式",
|
||||
"External Light Filter": "外部光滤镜",
|
||||
"Load Default": "加载默认值",
|
||||
"Ambient Noise Level: ": "环境噪声等级:",
|
||||
"Pad ": "手柄 ",
|
||||
"HandHeld": "掌机模式",
|
||||
" (Available)": " (可用)",
|
||||
" (Unsupported)": " (不支持)",
|
||||
" (Unconnected)": " (未连接)",
|
||||
"The homebrew menu.\n\nAllows you to launch, delete and mount homebrew!": "自制软件菜单。\n\n允许您启动、删除和挂载自制软件!",
|
||||
"Download and update apps.\n\nInternet connection required.": "下载和更新应用。\n\n需要互联网连接。",
|
||||
"View all installed games. In this menu you can launch, backup, create savedata and much more.": "查看所有已安装的游戏。在此菜单中,您可以启动、备份、创建存档等操作。",
|
||||
"Browse files on you SD Card. You can move, copy, delete, extract zip, create zip, upload and much more.\n\nA connected USB/HDD can be opened by mounting it in the advanced options.": "浏览您的SD卡文件。您可以移动、复制、删除、解压zip、创建zip、上传等更多操作。\n\n连接的USB/HDD可以通过在高级选项中挂载来打开。",
|
||||
"View save data for each user. You can backup and restore saves.\n\nExperimental support for backing up system saves is possible.": "查看每个用户的存档数据。您可以备份和还原存档。\n\n可能支持备份系统存档(实验性功能)。",
|
||||
"Download releases directly from GitHub. Custom entries can be added to /config/sphaira/github": "直接从GitHub下载发布版本。自定义条目可以添加到 /config/sphaira/github",
|
||||
"Install apps via FTP.": "通过FTP安装应用。",
|
||||
"MTP Install": "MTP安装",
|
||||
"Install apps via MTP.": "通过MTP安装应用。",
|
||||
"USB Install": "通过 USB 安装",
|
||||
"Install apps via USB.\n\nA USB client is required on PC.": "通过USB安装应用。\n\n需要在PC上安装USB客户端。",
|
||||
"View info on the inserted Game Card (GC). You can backup and install the inserted GC. To swap GC's, simply remove the old GC and insert the new one. You do not need to exit the menu.": "查看已插入游戏卡(GC)的信息。您可以备份和安装已插入的游戏卡。要更换游戏卡,只需取出旧游戏卡并插入新的。无需退出菜单。",
|
||||
"IRS (Infrared Joycon Camera)": "IRS(Joycon红外摄像头)",
|
||||
"InfraRed Sensor (IRS) is the small camera found on right JoyCon.": "红外传感器(IRS)是右侧JoyCon上的小型摄像头。",
|
||||
"Update avaliable: ": "有可用更新!",
|
||||
"Download via the Network options!": "通过网络选项下载!",
|
||||
"Menu": "菜单",
|
||||
"Menu Options": "菜单选项",
|
||||
"Auto": "自动",
|
||||
"English": "English",
|
||||
"Japanese": "日本語",
|
||||
"French": "Français",
|
||||
"German": "Deutsch",
|
||||
"Italian": "Italiano",
|
||||
"Spanish": "Español",
|
||||
"Chinese (Simplified)": "简体中文",
|
||||
"Korean": "한국어",
|
||||
"Dutch": "Dutch",
|
||||
"Portuguese": "Português",
|
||||
"Russian": "Русский",
|
||||
"Chinese (Traditional)": "繁體中文",
|
||||
"Swedish": "Svenska",
|
||||
"Vietnamese": "Vietnamese",
|
||||
"Ukrainian": "Українська",
|
||||
"Launch one of Sphaira's menus:\n": "启动Sphaira的一个菜单:\n",
|
||||
"\nYou can change the left/right menu in the Advanced Options.": "\n您可以在高级选项中更改左/右菜单。",
|
||||
"Network": "网络",
|
||||
"Network Options": "网络选项",
|
||||
"Download update: ": "下载更新:",
|
||||
"Failed to download update": "更新下载失败",
|
||||
"Updated to ": "更新至 ",
|
||||
"ftp_settings_info": [
|
||||
"启用/修改FTP服务器设置,如端口、用户/密码以及显示的文件夹。\n",
|
||||
"注意:更改任何选项将在退出选项菜单时自动重启FTP服务器。"
|
||||
],
|
||||
"mtp_settings_info": [
|
||||
"启用/修改MTP响应器设置,如显示的文件夹。\n",
|
||||
"注意:更改任何选项将在退出选项菜单时自动重启MTP服务器。"
|
||||
],
|
||||
"HDD": "硬盘",
|
||||
"Enable / modify the HDD mount options.": "启用/修改 HDD 挂载选项。",
|
||||
"NXlink": "NXlink",
|
||||
"nxlink_enable_info": [
|
||||
"启用NXlink服务器后台运行",
|
||||
"NXlink用于从DC向Switch发送.nro文件\n",
|
||||
"如果您不是开发者,可以禁用此选项。"
|
||||
],
|
||||
"nxlink_toggle_info": [
|
||||
"切换FTP、MTP、HDD和NXlink\n",
|
||||
"如果Sphaira有可用更新,您可以从此菜单下载"
|
||||
],
|
||||
"Theme": "主题",
|
||||
"Language": "语言",
|
||||
"translation_info": [
|
||||
"更改语言\n",
|
||||
"如果找不到您的语言或翻译缺失,请考虑在 github.com/ITotalJustice/sphaira 提交PR"
|
||||
],
|
||||
"advanced_options_info": [
|
||||
"更改高级选项。",
|
||||
"请查看信息框以更好地理解每个选项。"
|
||||
],
|
||||
"System %.1f GB": "系统 %.1f GB",
|
||||
"microSD %.1f GB": "microSD %.1f GB",
|
||||
"[Applet Mode]": "[小程序模式]",
|
||||
"Ethernet": "以太网",
|
||||
"No Internet": "网络未连接",
|
||||
"State: %s | Speed: %s": "状态: %s | 速度: %s",
|
||||
"Failed to mount save filesystem": "存档文件系统挂载失败",
|
||||
"Save Options": "存档选项",
|
||||
"System": "系统",
|
||||
"Account": "账户",
|
||||
"BCAT": "BCAT数据",
|
||||
"Device": "设备",
|
||||
"Temporary": "临时",
|
||||
"Cache": "缓存",
|
||||
"System BCAT": "系统备份",
|
||||
"Data Type": "数据类型",
|
||||
"Backup": "备份",
|
||||
"Backup the selected save(s) to a location of your choice.": "将选中的存档备份到您选择的位置。",
|
||||
"Backup to...": "备份到...",
|
||||
"Backup the selected save(s) to a location of your choice, and set the name of the backup.": "将选定的存档备份到您选择的位置,并设置备份名称。",
|
||||
"save_backuprestore_info": [
|
||||
"还原当前游戏的存档。",
|
||||
"如果启用了\"自动备份\",存档将首先被备份然后还原。 ",
|
||||
"自动备份的存档名称中将包含\"Auto\"。"
|
||||
],
|
||||
"Auto backup on restore": "还原时自动备份",
|
||||
"If enabled, when restoring a save, the current save will first be backed up.": "如果启用,当还原存档时,当前存档将首先被备份。",
|
||||
"Compress backup": "压缩备份",
|
||||
"save_backup_compress_info": [
|
||||
"如果启用,备份将被压缩为zip文件。\n",
|
||||
"注意:禁用此选项不会禁用zip文件,它只会禁用zip中存储文件的压缩。",
|
||||
"禁用将使备份速度更快,但代价是文件大小增加。"
|
||||
],
|
||||
"Select backup location": "选择备份位置",
|
||||
"Backup failed!": "备份失败!",
|
||||
"Backup successfull!": "已备份成功!",
|
||||
"Select restore location": "选择还原位置",
|
||||
"Invalid location type!": "位置类型无效!",
|
||||
"No saves found in ": "未找到存档 ",
|
||||
"Restore save for: ": "还原备份文件: ",
|
||||
"Are you sure you want to restore ": "您确定要还原已备份文件吗 ",
|
||||
"Auto backup": "自动备份",
|
||||
"Restore failed!": "还原失败!",
|
||||
"Restore successfull!": "已还原成功!",
|
||||
"Set name for ": "设置名称为:",
|
||||
"Set backup name": "设备备份名称",
|
||||
"Flushing zip to file": "将压缩包写入文件",
|
||||
"Disable MTP for usb install": "暂时禁用 USB 安装的 MTP 功能",
|
||||
"Re-enabled MTP": "重新启用 MTP",
|
||||
"Installed via usb": "通过 USB 安装",
|
||||
"USB install failed!": "USB 安装失败!",
|
||||
"Usb install success!": "USB安装成功!",
|
||||
"Waiting for connection...": "等待连接中...",
|
||||
"Connected, waiting for file list...": "已连接,正在等待文件列表...",
|
||||
"Connected, starting transfer...": "已连接,开始传输...",
|
||||
"Transferring data...": "正在传输数据...",
|
||||
"Press B to exit...": "按 B 键退出...",
|
||||
"Failed to init usb, press B to exit...": "USB 初始化失败,按 B 键退出...",
|
||||
"Detached": "已断开",
|
||||
"Attached": "已连接",
|
||||
"Powered": "已供电",
|
||||
"Default": "默认",
|
||||
"Address": "地址",
|
||||
"Configured": "已配置",
|
||||
"Suspended": "已挂起",
|
||||
"USB 1.0 Low Speed": "USB 1.0 低速",
|
||||
"USB 1.1 Full Speed": "USB 1.1 全速",
|
||||
"USB 2.0 High Speed": "USB 2.0 高速",
|
||||
"USB 3.0 Super Speed": "USB 3.0 超高速",
|
||||
"Mount Creator": "创建挂载",
|
||||
"Type": "类型",
|
||||
"Select the type of the forwarder.": "选择前端启动器的类型。",
|
||||
"URL": "",
|
||||
"Set the URL of the application": "设置应用程序的 URL",
|
||||
"Optional: Set the port of the server. If left empty, the default port for the protocol will be used.": "可选:设置服务器端口。如果留空,将使用协议的默认端口。",
|
||||
"Timeout": "超时",
|
||||
"Timeout in milliseconds": "超时(毫秒)",
|
||||
"Optional: Set the timeout in seconds.": "可选:设置超时时间(秒)。",
|
||||
"Optional: Set the username of the application": "可选:设置应用程序的用户名",
|
||||
"Optional: Set the password of the application": "可选:设置应用程序的密码",
|
||||
"Dump path": "转储路径",
|
||||
"Optional: Set the dump path used when exporting games and saves.": "可选:设置导出游戏和存档时使用的转储路径。",
|
||||
"Read only": "只读",
|
||||
"mount_readonly_info": [
|
||||
"将文件系统挂载为只读。\n",
|
||||
"设置此选项还会隐藏挂载点,使其不显示为导出选项。"
|
||||
],
|
||||
"No stat file": "",
|
||||
"filecheck_disable_info": [
|
||||
"启用后将阻止文件浏览器检查每个文件的大小和时间戳。",
|
||||
"这可以提高浏览性能。"
|
||||
],
|
||||
"No stat dir": "",
|
||||
"dircheck_disable_info": [
|
||||
"启用后,文件浏览器将停止检查文件夹中有多少文件和文件夹。",
|
||||
"这可以提高浏览性能,尤其是对于目录列表速度慢的服务器。"
|
||||
],
|
||||
"FS hidden": "隐藏挂载点",
|
||||
"Hide the mount from being visible in the file browser.": "隐藏挂载点,不在文件浏览器中显示。",
|
||||
"Export hidden": "隐藏到处",
|
||||
"Hide the mount from being visible as a export option for games and saves.": "隐藏挂载点,不显示为游戏和存档的导出选项。",
|
||||
"Save": "保存",
|
||||
"Mount entry saved. Restart Sphaira to apply changes.": "挂载条目已保存。重启 Sphaira 以应用更改。",
|
||||
"Saves the mount entry.\n\nNOTE: You must restart Sphaira for changes to take effect!": "保存挂载条目。\n\n注意:您必须重启Sphaira才能使更改生效!",
|
||||
"Name and URL must be set!": "必须设置名称和URL!",
|
||||
"Devoptab Options": "Devoptab选项",
|
||||
"Create New Entry": "创建新条目",
|
||||
"Creates a new mount option.\n\nNOTE: You must restart Sphaira for changes to take effect!": "创建新的挂载选项。\n\n注意:您必须重启Sphaira才能使更改生效!",
|
||||
"Modify Existing Entry": "修改现有条目",
|
||||
"No mount entries found.": "未找到挂载条目。",
|
||||
"Modify Entry": "修改条目",
|
||||
"Modify an existing mount option.\n\nNOTE: You must restart Sphaira for changes to take effect!": "修改现有的挂载选项。\n\n注意:您必须重启Sphaira才能使更改生效!",
|
||||
"Delete Existing Entry": "删除现有条目",
|
||||
"Delete Entry": "删除条目",
|
||||
"Delete an existing mount option.\n\nNOTE: You must restart Sphaira for changes to take effect!": "删除现有的挂载选项。\n\n注意:您必须重启Sphaira才能使更改生效!",
|
||||
"Section #": ""
|
||||
}
|
||||
923
assets/romfs/i18n/zh-TW.json
Normal file
923
assets/romfs/i18n/zh-TW.json
Normal file
@@ -0,0 +1,923 @@
|
||||
{
|
||||
"Homebrew": "自製軟體",
|
||||
"FileBrowser": "檔案瀏覽器",
|
||||
"Appstore": "程式商店",
|
||||
"Corrupted": "已損毀",
|
||||
"enable_install_info": [
|
||||
"安裝功能已停用。\n",
|
||||
"請在 選單 (Y) -> 進階 -> 安裝選項 -> 啟用 中開啟。"
|
||||
],
|
||||
"On": "開",
|
||||
"Off": "關",
|
||||
"Entry": "入口",
|
||||
"Store": "商店",
|
||||
"File": "檔案",
|
||||
"Files": "檔案",
|
||||
"FTP": "FTP",
|
||||
"Games": "遊戲",
|
||||
"Meta": "Meta",
|
||||
"Nca": "NCA",
|
||||
"GC": "卡匣",
|
||||
"GitHub": "GitHub",
|
||||
"Apps": "應用程式",
|
||||
"IRS": "IRS",
|
||||
"MTP": "MTP",
|
||||
"Saves": "存檔",
|
||||
"USB": "USB",
|
||||
"Level 0 (no compression)": "等級 0 (無壓縮)",
|
||||
"Level 1": "等級 1",
|
||||
"Level 2": "等級 2",
|
||||
"Level 3 (default)": "等級 3 (預設)",
|
||||
"Level 4": "等級 4",
|
||||
"Level 5": "等級 5",
|
||||
"Level 6": "等級 6",
|
||||
"Level 7": "等級 7",
|
||||
"Level 8": "等級 8",
|
||||
"0 (single threaded)": "0 (單執行緒)",
|
||||
"1": "1",
|
||||
"2": "2",
|
||||
"3 (default)": "3 (預設)",
|
||||
"4": "4",
|
||||
"16 KB": "16 KB",
|
||||
"32 KB": "32 KB",
|
||||
"64 KB": "64 KB",
|
||||
"128 KB": "128 KB",
|
||||
"256 KB": "256 KB",
|
||||
"512 KB": "512 KB",
|
||||
"1 MB (default)": "1 MB (預設)",
|
||||
"2 MB": "2 MB",
|
||||
"4 MB": "4 MB",
|
||||
"8 MB": "8 MB",
|
||||
"16 MB": "16 MB",
|
||||
"Switch-Handheld!": "切換到手提模式!",
|
||||
"Switch-Docked!": "切換到底座模式!",
|
||||
"Nxlink Connected": "Nxlink 已連線",
|
||||
"Nxlink Upload": "Nxlink 上傳",
|
||||
"Nxlink Finished": "Nxlink 完成",
|
||||
"Restore hbmenu?": "還原 hbmenu?",
|
||||
"Back": "返回",
|
||||
"Restore": "還原",
|
||||
"missing_hbmenu_info": [
|
||||
"找不到 /switch/hbmenu.nro",
|
||||
"請使用程式商店重新安裝 hbmenu"
|
||||
],
|
||||
"OK": "確定",
|
||||
"Failed to restore hbmenu, please re-download hbmenu": "還原 hbmenu 失敗,請重新下載 hbmenu",
|
||||
"Failed to restore hbmenu, using sphaira instead": "還原 hbmenu 失敗,改用 Sphaira",
|
||||
"Restored hbmenu, closing sphaira": "已還原 hbmenu,正在關閉 Sphaira",
|
||||
"Restored hbmenu": "已還原 hbmenu",
|
||||
"Restart Sphaira?": "重新啟動 Sphaira?",
|
||||
"Restart": "重新啟動",
|
||||
"Installing Forwarder": "正在安裝前導程式",
|
||||
"Failed to install forwarder": "安裝前導程式失敗",
|
||||
"Installed!": "已安裝!",
|
||||
"Warning! Logs are enabled, Sphaira will run slowly!": "警告!記錄檔已啟用,Sphaira 執行速度將變慢!",
|
||||
"Audio disabled due to suspended game": "因遊戲暫停而停用音訊",
|
||||
"Theme Options": "佈景主題選項",
|
||||
"Select Theme": "選擇佈景主題",
|
||||
"Customise the look of Sphaira by changing the theme": "更換佈景主題以自訂 Sphaira 的外觀",
|
||||
"Music": "音樂",
|
||||
"bgm_enable_info": [
|
||||
"啟用背景音樂。",
|
||||
"每個佈景主題都可以有自己的音樂檔案。",
|
||||
"如果佈景主題未設定音樂檔案,則會載入預設音樂 (若存在)。"
|
||||
],
|
||||
"Show IP address": "顯示 IP 位址",
|
||||
"display_ip_info": [
|
||||
"在所有選單中顯示 IP 位址 (包含 WiFi 強度)。\n",
|
||||
"注意:在小程式模式下,由於該位置會顯示警告訊息,IP 位址將被隱藏。"
|
||||
],
|
||||
"Remove Background Music": "移除背景音樂",
|
||||
"Removes the background music file": "移除背景音樂檔案",
|
||||
"No background music file is set": "未設定背景音樂檔案",
|
||||
"Menus": "選單",
|
||||
"Web": "網頁",
|
||||
"Enter custom URL": "輸入自訂網址",
|
||||
"Select URL": "選擇網址",
|
||||
"Enter URL": "輸入網址",
|
||||
"web_browser_info": [
|
||||
"啟動內建網頁瀏覽器。\n",
|
||||
"注意:此瀏覽器功能非常有限,部分網站可能無法載入,且有 30 分鐘逾時限制,超時後將關閉瀏覽器。"
|
||||
],
|
||||
"Advanced Options": "進階選項",
|
||||
"Slow": "慢",
|
||||
"Normal": "正常",
|
||||
"Fast": "快",
|
||||
"Logging": "記錄檔",
|
||||
"Logs to /config/sphaira/log.txt": "記錄至 /config/sphaira/log.txt",
|
||||
"Replace hbmenu on exit": "離開時取代 hbmenu",
|
||||
"hbmenu_replace_info": [
|
||||
"啟用後,會將 /hbmenu.nro 替換為 Sphaira,並將 hbmenu 備份至 /switch/hbmenu.nro\n",
|
||||
"停用此選項將可選擇還原 hbmenu。"
|
||||
],
|
||||
"Add / modify mounts": "新增 / 修改掛載點",
|
||||
"mount_options_info": [
|
||||
"建立、修改、刪除網路掛載 (HTTP, FTP, SFTP, SMB, NFS)。",
|
||||
"掛載選項僅需設定網址和名稱,其他欄位 (如連接埠、使用者、密碼等) 為選填。\n",
|
||||
"任何變更都需要重新啟動 Sphaira 才能生效。"
|
||||
],
|
||||
"Boost CPU during transfer": "傳輸時加速 CPU",
|
||||
"transfer_boost_info": [
|
||||
"在傳輸期間啟用加速模式,可提高傳輸速度。",
|
||||
"這會將 CPU 設定為 1785mhz 並將 GPU 降低至 76mhz。"
|
||||
],
|
||||
"Text scroll speed": "文字捲動速度",
|
||||
"Change how fast the scrolling text updates": "更改捲動文字的更新速度",
|
||||
"Set center menu": "設定中央選單",
|
||||
"Press OK to restart Sphaira": "按 OK 重新啟動 Sphaira",
|
||||
"Set the menu that appears on the center tab.": "設定顯示在中央分頁的選單。",
|
||||
"Set left-side menu": "設定左側選單",
|
||||
"Set the menu that appears on the left tab.": "設定顯示在左側分頁的選單。",
|
||||
"Set right-side menu": "設定右側選單",
|
||||
"Set the menu that appears on the right tab.": "設定顯示在右側分頁的選單。",
|
||||
"Install options": "安裝選項",
|
||||
"install_options_info": [
|
||||
"更改安裝選項。",
|
||||
"您可以從此處啟用安裝功能。"
|
||||
],
|
||||
"Export options": "匯出選項",
|
||||
"Change the export options.": "更改匯出選項。",
|
||||
"Install Options": "安裝選項",
|
||||
"System memory": "系統記憶體",
|
||||
"microSD card": "microSD 卡",
|
||||
"Enable sysmmc": "啟用真實系統 (SysMMC)",
|
||||
"Enables installing whilst in sysMMC mode.": "啟用在真實系統模式下進行安裝。",
|
||||
"Enable emummc": "啟用虛擬系統 (EmuMMC)",
|
||||
"Enables installing whilst in emuMMC mode.": "啟用在虛擬系統模式下進行安裝。",
|
||||
"Install location": "安裝位置",
|
||||
"Allow downgrade": "允許降級",
|
||||
"Allows for installing title updates that are lower than the currently installed update.": "允許安裝比目前已安裝版本更舊的更新檔。",
|
||||
"Skip if already installed": "若已安裝則跳過",
|
||||
"Skips installing titles / ncas if they're already installed.": "若已安裝該 Title / NCA,則跳過安裝。",
|
||||
"Ticket only": "僅安裝 Ticket",
|
||||
"Installs tickets only, useful if the title was already installed however the tickets were missing or corrupted.": "僅安裝 Ticket,適用於遊戲已安裝但 Ticket 遺失或損毀的情況。",
|
||||
"Skip base": "跳過本體",
|
||||
"Skips installing the base application.": "跳過安裝應用程式本體。",
|
||||
"Skip patch": "跳過更新檔 (UPD)",
|
||||
"Skips installing updates.": "跳過安裝更新檔。",
|
||||
"Skip dlc": "跳過追加內容 (DLC)",
|
||||
"Skips installing DLC.": "跳過安裝 DLC。",
|
||||
"Skip data patch": "跳過資料更新",
|
||||
"Skips installing DLC update (data patch).": "跳過安裝 DLC 更新 (資料更新)。",
|
||||
"Skip ticket": "跳過 Ticket",
|
||||
"Skips installing tickets, not recommended.": "跳過安裝 Ticket,不建議使用。",
|
||||
"Skip NCA hash verify": "跳過 NCA 雜湊驗證",
|
||||
"skip_nca_info": [
|
||||
"啟用此選項以跳過 sha256 驗證。這是針對整個 NCA 的雜湊檢查。",
|
||||
"用於驗證 NCA 是否有效且未損毀。",
|
||||
"您可能在安裝損毀的遊戲時看過「正在檢查損毀資料」的選項。",
|
||||
"該檢查執行各種雜湊檢查,包括針對 NCA 的雜湊。\n",
|
||||
"建議保持停用此選項。"
|
||||
],
|
||||
"Skip RSA header verify": "跳過 RSA 標頭驗證",
|
||||
"nca_verify_info": [
|
||||
"啟用此選項以跳過 RSA NCA 固定金鑰驗證。",
|
||||
"這是針對 NCA 標頭的雜湊檢查,用於驗證標頭是否被修改。",
|
||||
"標頭由任天堂簽署,因此無法偽造,能可靠地檢測修改過的 NCA 標頭 (如 NSP/XCI 轉換檔)。\n",
|
||||
"建議保持停用此選項,除非您需要安裝 NSP/XCI 轉換檔。"
|
||||
],
|
||||
"Skip RSA NPDM verify": "跳過 RSA NPDM 驗證",
|
||||
"npdm_verify_info": [
|
||||
"啟用此選項以跳過 RSA NPDM 固定金鑰驗證。\n",
|
||||
"目前此選項尚未實作 (Stubbed)。"
|
||||
],
|
||||
"Ignore distribution bit": "忽略分發位元",
|
||||
"nca_distbit_info": [
|
||||
"若設定,將忽略 NCA 標頭中的分發位元 (Distribution bit)。",
|
||||
"分發位元用於標示 NCA 是 Eshop 版還是卡匣 (GameCard) 版。",
|
||||
"您通常無法啟動安裝後且分發位元設為卡匣版的遊戲。\n",
|
||||
"建議保持停用此選項。"
|
||||
],
|
||||
"Convert to common ticket": "轉換為通用 Ticket",
|
||||
"ticket_convert_info": [
|
||||
"[需要金鑰] 將個人化 Ticket 轉換為通用 (偽造) Ticket。\n",
|
||||
"建議保持啟用此選項。"
|
||||
],
|
||||
"Convert to standard crypto": "轉換為標準加密",
|
||||
"titlekey_crypto_info": [
|
||||
"[需要金鑰] 將 TitleKey 轉換為標準加密,也稱為「無 Ticket (Ticketless)」。\n",
|
||||
"建議保持停用此選項。"
|
||||
],
|
||||
"Lower master key": "降低主金鑰 (Master Key)",
|
||||
"keyarea_crypto_info": [
|
||||
"[需要金鑰] 使用主金鑰 0 加密 Keak (金鑰區域金鑰),允許遊戲在所有韌體版本上啟動。",
|
||||
"隱含執行標準加密。\n",
|
||||
"請注意,僅因遊戲可解密而在任何韌體上啟動,並不代表它能正常運作。強烈建議更新您的韌體和 Atmosphere 版本來遊玩遊戲,而不是啟用此選項。\n",
|
||||
"建議保持停用此選項。"
|
||||
],
|
||||
"Lower system version": "降低系統版本要求",
|
||||
"cnmt_fw_info": [
|
||||
"將 CNMT 擴充標頭中的 system_firmware 欄位設定為 0。",
|
||||
"注意:如果主金鑰高於韌體版本,遊戲仍無法啟動,因為韌體沒有解密 Keak 的金鑰 (見上文)。\n",
|
||||
"建議保持停用此選項。"
|
||||
],
|
||||
"Export Options": "匯出選項",
|
||||
"Created nested folder": "已建立巢狀資料夾",
|
||||
"game_folder_info": [
|
||||
"使用遊戲名稱建立資料夾。",
|
||||
"例如:/名稱/名稱.xci",
|
||||
"停用此選項將使用 /名稱.xci"
|
||||
],
|
||||
"Append folder with .xci": "資料夾名稱加上 .xci",
|
||||
"xci_folder_info": [
|
||||
"XCI 提取時會在資料夾名稱加上 .xci 副檔名。",
|
||||
"例如:/名稱.xci/名稱.xci\n",
|
||||
"某些裝置僅在 XCI 資料夾名稱與 XCI 檔案名稱完全相同時才能運作。"
|
||||
],
|
||||
"Trim XCI": "裁剪 XCI (瘦身)",
|
||||
"Removes the unused data at the end of the XCI, making the output smaller.": "移除 XCI 尾端未使用的資料,讓輸出的檔案更小。",
|
||||
"Label trimmed XCI": "標記已裁剪的 XCI",
|
||||
"Names the trimmed xci.\nFor example, /name/name (trimmed).xci": "命名已裁剪的 XCI。\n例如:/名稱/名稱 (trimmed).xci",
|
||||
"Converts personalised ticket to a fake common ticket.": "將個人化 Ticket 轉換為偽造的通用 Ticket。",
|
||||
"NSZ level": "NSZ 壓縮等級",
|
||||
"compress_level_info": [
|
||||
"設定匯出為 NSZ 時使用的壓縮等級。\n",
|
||||
"注意:Switch CPU 速度不快,設定太高的值會導致",
|
||||
"匯出時間非常長,但檔案大小縮減有限。\n",
|
||||
"建議將此值設定為 3。"
|
||||
],
|
||||
"NSZ threads": "NSZ 執行緒",
|
||||
"compress_threads_info": [
|
||||
"設定壓縮 NCA 時使用的執行緒數量。\n",
|
||||
"小於 3 的值允許另一個執行緒自由運作,例如讀/寫執行緒。",
|
||||
"但在測試中,值為 3 通常效能最佳。",
|
||||
"值為 0 將不使用執行緒,僅應於測試時使用,因為速度總是較慢。\n",
|
||||
"建議將此值設定在 1-3 之間。"
|
||||
],
|
||||
"NSZ long distance mode": "NSZ 長距離模式",
|
||||
"Enables \"Long Distance Mode\" which can reduce the output size at the cost of speed.": "啟用「長距離模式」,可縮小輸出檔案大小,但會降低速度。",
|
||||
"NSZ block compression": "NSZ 區塊壓縮",
|
||||
"block_compress_info": [
|
||||
"啟用區塊壓縮,將 NCA 壓縮成區塊 (代價是壓縮率較差),",
|
||||
"這允許隨機存取,讓 NCZ 可以像檔案系統一樣被掛載。\n",
|
||||
"注意:Sphaira 尚不支援將 NCZ 掛載為檔案系統,但最終將會加入此功能。"
|
||||
],
|
||||
"NSZ block size": "NSZ 區塊大小",
|
||||
"Sets the size of each block. The smaller the size, the faster the random access is at the cost of compression ratio.": "設定每個區塊的大小。區塊越小,隨機存取速度越快,但壓縮率較低。",
|
||||
"NSZ block compression is disabled.": "NSZ 區塊壓縮已停用。",
|
||||
"FTP Options": "FTP 選項",
|
||||
"Restarting FTP server...": "正在重新啟動 FTP 伺服器...",
|
||||
"Enable": "啟用",
|
||||
"Enable FTP server to run in the background.": "啟用 FTP 伺服器在背景執行。",
|
||||
"Port": "連接埠",
|
||||
"Opens the FTP server on this port.": "在此連接埠開啟 FTP 伺服器。",
|
||||
"Anon": "匿名",
|
||||
"login_require_info": [
|
||||
"允許您在未設定使用者名稱和密碼的情況下登入。",
|
||||
"如果停用,您必須在下方設定使用者名稱和密碼!"
|
||||
],
|
||||
"User": "使用者",
|
||||
"Sets the username, must be set if anon is disabled.": "設定使用者名稱,若停用匿名則必須設定。",
|
||||
"Pass": "密碼",
|
||||
"Sets the password, must be set if anon is disabled.": "設定密碼,若停用匿名則必須設定。",
|
||||
"Show album": "顯示相簿",
|
||||
"Shows the microSD card album folder.": "顯示 microSD 卡的相簿資料夾。",
|
||||
"Show Atmosphere contents": "顯示 Atmosphere contents",
|
||||
"Shows the shortcut for the /atmosphere/contents folder.": "顯示 /atmosphere/contents 資料夾的捷徑。",
|
||||
"Show bis storage": "顯示 BIS 儲存區",
|
||||
"bis_contents_info": [
|
||||
"顯示 BIS 資料夾,包含以下內容:",
|
||||
"- BootPartition1Root.bin",
|
||||
"- BootPartition2Root.bin",
|
||||
"- UserDataRoot.bin",
|
||||
"- BootConfigAndPackage2Part1.bin",
|
||||
"- BootConfigAndPackage2Part2.bin",
|
||||
"- BootConfigAndPackage2Part3.bin",
|
||||
"- BootConfigAndPackage2Part4.bin",
|
||||
"- BootConfigAndPackage2Part5.bin",
|
||||
"- BootConfigAndPackage2Part6.bin",
|
||||
"- CalibrationFile.bin",
|
||||
"- SafeMode.bin",
|
||||
"- User.bin",
|
||||
"- System.bin",
|
||||
"- SystemProperEncryption.bin"
|
||||
],
|
||||
"Show bis file systems": "顯示 BIS 檔案系統",
|
||||
"bis_fs_info": [
|
||||
"顯示以下 BIS 檔案系統:",
|
||||
"- bis_calibration_file",
|
||||
"- bis_safe_mode",
|
||||
"- bis_user",
|
||||
"- bis_system"
|
||||
],
|
||||
"Show system contents": "顯示系統 contents",
|
||||
"Shows the system contents folder.": "顯示系統 contents 資料夾。",
|
||||
"Show user contents": "顯示使用者 contents",
|
||||
"Shows the user contents folder.": "顯示使用者 contents 資料夾。",
|
||||
"Show microSD contents": "顯示 microSD contents",
|
||||
"microsd_contents_info": [
|
||||
"顯示 microSD contents 資料夾。\n",
|
||||
"注意:這不是一般的 microSD 卡儲存空間,而是",
|
||||
"儲存 NCA 的位置。一般的 microSD 卡總是會被掛載。"
|
||||
],
|
||||
"Show games": "顯示遊戲",
|
||||
"games_ftp_info": [
|
||||
"顯示遊戲資料夾。\n",
|
||||
"此資料夾包含所有您已安裝的遊戲,允許您透過 FTP 建立備份!\n",
|
||||
"注意:此資料夾為唯讀。您無法透過 FTP 刪除遊戲。"
|
||||
],
|
||||
"Show install": "顯示安裝目錄",
|
||||
"install_ftp_info": [
|
||||
"顯示安裝資料夾。\n",
|
||||
"此資料夾用於透過 FTP 安裝遊戲。\n",
|
||||
"注意:嘗試安裝遊戲時,您必須開啟「FTP 安裝」選單!"
|
||||
],
|
||||
"Show mounts": "顯示掛載點",
|
||||
"mounts_ftp_info": [
|
||||
"顯示掛載資料夾。\n",
|
||||
"此資料夾包含所有新增至 Sphaira 的掛載點,允許您透過 FTP 存取它們!",
|
||||
"例如,您可以透過 FTP 存取您的 SMB、WebDav 或其他 FTP 掛載。"
|
||||
],
|
||||
"Show switch": "顯示 Switch 目錄",
|
||||
"homebrew_folder_info": [
|
||||
"顯示 /switch 資料夾的捷徑。",
|
||||
"這是包含您所有自製軟體 (NRO) 的資料夾。"
|
||||
],
|
||||
"MTP Options": "MTP 選項",
|
||||
"Restarting MTP server...": "正在重新啟動 MTP 伺服器...",
|
||||
"Enable MTP server to run in the background.": "啟用 MTP 伺服器在背景執行。",
|
||||
"Pre-allocate file": "預先配置檔案",
|
||||
"prealloc_info": [
|
||||
"啟用在寫入前預先配置檔案大小。",
|
||||
"這能加快檔案寫入速度,但如果符合以下所有條件,可能會導致逾時:",
|
||||
"- 使用 Windows",
|
||||
"- 使用虛擬系統 (emuMMC)",
|
||||
"- 傳輸大型檔案 (>1GB)\n",
|
||||
"此選項應保持啟用,但如果您符合上述條件並遇到逾時,",
|
||||
"請嘗試停用此選項後重試。"
|
||||
],
|
||||
"games_mtp_info": [
|
||||
"顯示遊戲資料夾。\n",
|
||||
"此資料夾包含所有您已安裝的遊戲,允許您透過 MTP 建立備份!\n",
|
||||
"注意:此資料夾為唯讀。您無法透過 MTP 刪除遊戲。"
|
||||
],
|
||||
"install_mtp_info": [
|
||||
"顯示安裝資料夾。\n",
|
||||
"此資料夾用於透過 MTP 安裝遊戲。\n",
|
||||
"注意:嘗試安裝遊戲時,您必須開啟「MTP 安裝」選單!"
|
||||
],
|
||||
"mounts_mtp_info": [
|
||||
"顯示掛載資料夾。\n",
|
||||
"此資料夾包含所有新增至 Sphaira 的掛載點,允許您透過 MTP 存取它們!",
|
||||
"例如,您可以透過 MTP 存取您的 SMB、WebDav 和 FTP 掛載。"
|
||||
],
|
||||
"Show DevNull": "顯示 DevNull",
|
||||
"usb_benchmark_info": [
|
||||
"顯示 DevNull (速度測試) 資料夾。\n",
|
||||
"此資料夾用於測試 USB 上傳效能。",
|
||||
"這是一個虛擬資料夾,實際上不會寫入任何資料到磁碟。"
|
||||
],
|
||||
"HDD Options": "硬碟選項",
|
||||
"mount_hdd_info": [
|
||||
"啟用掛載已連接的 USB/HDD 硬碟裝置。",
|
||||
"連接的裝置可用於檔案瀏覽器,也可作為提取遊戲和存檔時的備份位置。"
|
||||
],
|
||||
"HDD write protect": "硬碟寫入保護",
|
||||
"Makes the connected HDD read-only.": "將連接的硬碟設為唯讀。",
|
||||
"Installing is disabled, enable now?": "安裝功能已停用,要現在啟用嗎?",
|
||||
"WARNING: Installing apps will lead to a ban!": "警告:安裝未授權的應用程式會導致主機被 Ban!",
|
||||
"Installing enabled!": "安裝功能已啟用!",
|
||||
"microSD card (/dumps/)": "microSD 卡 (/dumps/)",
|
||||
"USB export to PC (usb_export.py)": "USB 匯出至電腦 (usb_export.py)",
|
||||
"USB transfer (Switch 2 Switch)": "USB 傳輸 (Switch 對 Switch)",
|
||||
"/dev/null (Speed Test)": "/dev/null (速度測試)",
|
||||
"Waiting for USB connection...": "等待 USB 連線中...",
|
||||
"USB connected, sending file list": "USB 已連線,正在傳送檔案列表",
|
||||
"Sent file list, waiting for command...": "已傳送檔案列表,等待指令中...",
|
||||
"waiting for usb connection...": "等待 USB 連線中...",
|
||||
"Exporting": "正在匯出",
|
||||
"Export failed!": "匯出失敗!",
|
||||
"Export successfull!": "匯出成功!",
|
||||
"Select export location": "選擇匯出位置",
|
||||
"Please launch MTP install menu before trying to install": "嘗試安裝前請先開啟 MTP 安裝選單",
|
||||
"Album": "相簿",
|
||||
"Install (NSP, XCI, NSZ, XCZ)": "安裝 (NSP、XCI、NSZ、XCZ)",
|
||||
" (Read Only)": " (唯讀)",
|
||||
"Unknown": "未知",
|
||||
"Application": "應用程式",
|
||||
"Creating Program": "正在建立程式",
|
||||
"Creating Control": "正在建立控制項",
|
||||
"Creating Meta": "正在建立 Meta",
|
||||
"Writing Nca": "正在寫入 Nca",
|
||||
"Updating ncm database": "正在更新 ncm 資料庫",
|
||||
"Pushing application record": "正在推送應用程式記錄",
|
||||
"Failed to remove old forwarder, please manually remove it!": "移除舊的前導程式失敗,請手動移除!",
|
||||
"An error occurred": "發生錯誤",
|
||||
"If this message appears repeatedly, please open an issue.": "如果此訊息重複出現,請回報問題 (Issue)。",
|
||||
"Failed to load music": "無法載入音樂",
|
||||
"Artist: Unknown": "演出者:未知",
|
||||
"Album: Unknown": "專輯:未知",
|
||||
"Select": "選擇",
|
||||
"Are you sure you wish to cancel?": "您確定要取消嗎?",
|
||||
"No": "否",
|
||||
"Yes": "是",
|
||||
"%zu hours %zu minutes remaining": "剩餘 %zu 時 %zu 分",
|
||||
"%zu minutes %zu seconds remaining": "剩餘 %zu 分 %zu 秒",
|
||||
"%zu seconds remaining": "剩餘 %zu 秒",
|
||||
"Closing empty sidebar": "關閉空的側邊欄",
|
||||
"All": "全部",
|
||||
"Emulators": "模擬器",
|
||||
"Tools": "工具",
|
||||
"Advanced": "進階",
|
||||
"Themes": "佈景主題",
|
||||
"Legacy": "舊版相容",
|
||||
"Misc": "雜項",
|
||||
"Updated": "更新時間",
|
||||
"Downloads": "下載次數",
|
||||
"Size": "大小",
|
||||
"Alphabetical": "按字母順序",
|
||||
"Desc": "遞減",
|
||||
"Asc": "遞增",
|
||||
"Removing ": "正在移除 ",
|
||||
"Downloading ": "正在下載 ",
|
||||
"Checking MD5": "正在檢查 MD5",
|
||||
"Options": "選項",
|
||||
"More by Author": "作者的其他作品",
|
||||
"Leave Feedback": "留下回饋",
|
||||
"Visit Website": "造訪網站",
|
||||
"version: %s": "版本:%s",
|
||||
"updated: %s": "更新日期:%s",
|
||||
"category: %s": "分類:%s",
|
||||
"extracted: %s": "已解壓:%s",
|
||||
"app_dls: %s": "下載次數:%s",
|
||||
"Loading...": "載入中...",
|
||||
"Failed to download manifest": "無法下載資訊清單",
|
||||
"Details": "詳細資料",
|
||||
"Changelog": "更新日誌",
|
||||
"Downloaded ": "已下載 ",
|
||||
"Uninstalling ": "正在解除安裝 ",
|
||||
"Removed ": "已移除 ",
|
||||
"Install": "安裝",
|
||||
"Update": "更新",
|
||||
"Launch": "啟動",
|
||||
"Remove": "移除",
|
||||
"Completely remove ": "完全移除 ",
|
||||
"AppStore": "程式商店",
|
||||
"Info": "資訊",
|
||||
"AppStore Options": "程式商店選項",
|
||||
"Descending": "遞減",
|
||||
"Ascending": "遞增",
|
||||
"List": "列表",
|
||||
"Icon": "圖示",
|
||||
"Grid": "格狀",
|
||||
"Filter": "篩選",
|
||||
"Sort": "排序",
|
||||
"Order": "順序",
|
||||
"Layout": "版面配置",
|
||||
"Search": "搜尋",
|
||||
"Empty!": "空的!",
|
||||
"Filter: %s | Sort: %s | Order: %s": "篩選:%s | 排序:%s | 順序:%s",
|
||||
"File Picker": "檔案選擇器",
|
||||
"Failed to parse nro": "解析 NRO 失敗",
|
||||
"Name": "名稱",
|
||||
"Set the name of the application": "設定應用程式名稱",
|
||||
"Author": "作者",
|
||||
"Set the author of the application": "設定應用程式作者",
|
||||
"Version": "版本",
|
||||
"Set the display version of the application": "設定應用程式的顯示版本",
|
||||
"Set the path to the icon for the forwarder": "設定前導程式的圖示路徑",
|
||||
"Create": "建立",
|
||||
"Failed to load icon": "載入圖示失敗",
|
||||
"Create the forwarder.": "建立前導程式。",
|
||||
"All fields must be non-empty!": "所有欄位都不能留空!",
|
||||
"Open": "開啟",
|
||||
"Empty...": "空的...",
|
||||
"%zd files": "%zd 個檔案",
|
||||
"%zd dirs": "%zd 個目錄",
|
||||
"Open with DayBreak?": "用 DayBreak 開啟?",
|
||||
"Launch ": "啟動 ",
|
||||
"Launch option for: ": "啟動選項:",
|
||||
"Select launcher for: ": "選擇啟動器:",
|
||||
"Install Selected files?": "安裝選取的檔案?",
|
||||
"Installing ": "正在安裝 ",
|
||||
"Installed ": "已安裝 ",
|
||||
"File install failed!": "檔案安裝失敗!",
|
||||
"Extracting ": "正在解壓縮 ",
|
||||
"Extract failed!": "解壓縮失敗!",
|
||||
"Extract success!": "解壓縮成功!",
|
||||
"Compressing ": "正在壓縮 ",
|
||||
"Compress failed!": "壓縮失敗!",
|
||||
"Compress success!": "壓縮成功!",
|
||||
"Failed to delete directory": "無法刪除資料夾",
|
||||
"Failed to delete file": "無法刪除檔案",
|
||||
"Deleting": "正在刪除",
|
||||
"Scanning ": "正在掃描 ",
|
||||
"Pasting": "正在貼上",
|
||||
"Pasting ": "正在貼上 ",
|
||||
"Creating ": "正在建立 ",
|
||||
"Copying ": "正在複製 ",
|
||||
"Deleting ": "正在刪除 ",
|
||||
"Hashing": "正在計算雜湊",
|
||||
"Failed to hash file...": "計算檔案雜湊失敗...",
|
||||
"File Options": "檔案選項",
|
||||
"Mount": "掛載",
|
||||
"Sort By": "排序依據",
|
||||
"Sort Options": "排序選項",
|
||||
"Show Hidden": "顯示隱藏檔",
|
||||
"Folders First": "資料夾優先",
|
||||
"Hidden Last": "隱藏檔置後",
|
||||
"Cut": "剪下",
|
||||
"Copy": "複製",
|
||||
"Paste": "貼上",
|
||||
"Paste file(s)?": "貼上檔案?",
|
||||
"Rename": "重新命名",
|
||||
"Set new name": "設定新名稱",
|
||||
"Failed to rename file: ": "重新命名檔案失敗:",
|
||||
"Delete": "刪除",
|
||||
"Delete Selected files?": "刪除選取的檔案?",
|
||||
"Install Forwarder": "安裝前導程式",
|
||||
"Extract zip": "解壓縮 zip",
|
||||
"Extract Options": "解壓縮選項",
|
||||
"Extract here": "解壓縮至此",
|
||||
"Extract to root": "解壓縮至根目錄",
|
||||
"Are you sure you want to extract to root?": "確定要解壓縮至根目錄嗎?",
|
||||
"Extract to...": "解壓縮至...",
|
||||
"Enter the path to the folder to extract into": "輸入要解壓縮到的資料夾路徑",
|
||||
"Compress to zip": "壓縮為 zip",
|
||||
"Compress Options": "壓縮選項",
|
||||
"Compress": "壓縮",
|
||||
"Compress to...": "壓縮至...",
|
||||
"Enter the path to the folder to compress into": "輸入要壓縮到的資料夾路徑",
|
||||
"Create File": "建立檔案",
|
||||
"Set File Name": "設定檔案名稱",
|
||||
"Create Folder": "建立資料夾",
|
||||
"Set Folder Name": "設定資料夾名稱",
|
||||
"View as text (unfinished)": "以文字檢視 (未完成)",
|
||||
"Set as background music": "設為背景音樂",
|
||||
"Failed to set default music path": "設定預設音樂路徑失敗",
|
||||
"Hash": "雜湊",
|
||||
"Hash Options": "雜湊選項",
|
||||
"Ignore read only": "忽略唯讀屬性",
|
||||
"Failed to mount FS.": "掛載檔案系統失敗。",
|
||||
"Close FileBrowser?": "關閉檔案瀏覽器?",
|
||||
"Split": "分割",
|
||||
"Close": "關閉",
|
||||
"FTP Install": "FTP 安裝",
|
||||
"Connection Type: WiFi | Strength: ": "連線類型:WiFi | 強度:",
|
||||
"Connection Type: Ethernet": "連線類型:乙太網路",
|
||||
"Connection Type: None": "連線類型:無",
|
||||
"Host:": "主機:",
|
||||
"Port:": "連接埠:",
|
||||
"Username:": "使用者名稱:",
|
||||
"Password:": "密碼:",
|
||||
"SSID:": "SSID:",
|
||||
"Passphrase:": "密碼:",
|
||||
"Success": "成功",
|
||||
"Failed to launch application": "啟動應用程式失敗",
|
||||
"Game Options": "遊戲選項",
|
||||
"Hide forwarders": "隱藏前導程式",
|
||||
"View application content": "檢視應用程式內容",
|
||||
"Launch random game": "隨機啟動遊戲",
|
||||
"Export NSP": "匯出 NSP",
|
||||
"Not supported in Applet Mode": "不支援小程式模式",
|
||||
"Export NSZ": "匯出 NSZ",
|
||||
"Exports to NSZ (compressed NSP)": "匯出為 NSZ (壓縮的 NSP)",
|
||||
"Are you sure you want to delete ": "確定要刪除 ",
|
||||
"Advanced options": "進階選項",
|
||||
"Refresh": "重新整理",
|
||||
"Create contents folder": "建立 contents 資料夾",
|
||||
"Folder create failed!": "建立資料夾失敗!",
|
||||
"Folder created!": "資料夾已建立!",
|
||||
"Create save": "建立存檔",
|
||||
"Select user to create save for": "選擇要建立存檔的使用者",
|
||||
"Delete title cache": "刪除 Title 快取",
|
||||
"Are you sure you want to delete the title cache?": "您確定要刪除 Title 快取嗎?",
|
||||
"Updating application record list": "正在更新應用程式記錄列表",
|
||||
"Delete failed!": "刪除失敗!",
|
||||
"Delete successfull!": "刪除成功!",
|
||||
"Select content to export": "選擇要匯出的內容",
|
||||
"Export All": "匯出全部",
|
||||
"Export Application": "匯出應用程式",
|
||||
"Export Patch": "匯出更新檔 (Patch)",
|
||||
"Export AddOnContent": "匯出追加內容 (DLC)",
|
||||
"Export DataPatch": "匯出資料更新 (DataPatch)",
|
||||
"Creating": "正在建立",
|
||||
"Save create failed!": "建立存檔失敗!",
|
||||
"Save create successfull!": "建立存檔成功!",
|
||||
"Failed to delete meta entry": "刪除 Meta 項目失敗",
|
||||
"None": "無",
|
||||
"Common": "通用",
|
||||
"Personalised": "個人化",
|
||||
"Missing": "遺失",
|
||||
"View Content": "檢視內容",
|
||||
"Content Options": "內容選項",
|
||||
"Are you sure you want to delete the selected entries?": "您確定要刪除選取的項目嗎?",
|
||||
"Reset required system version": "重設系統版本要求",
|
||||
"Are you sure you want to reset required system version?": "您確定要重設系統版本要求嗎?",
|
||||
"Reset": "重設",
|
||||
"Failed to reset required system version": "重設系統版本要求失敗",
|
||||
"Required System Version: %u.%u.%u": "系統版本要求:%u.%u.%u",
|
||||
"Required Application Version: v%u": "應用程式版本要求:v%u",
|
||||
"Content Count: %u (%u missing)": "內容數量:%u (遺失 %u)",
|
||||
"Content Count: %u": "內容數量:%u",
|
||||
"Ticket: %s": "Ticket:%s",
|
||||
"Key Generation: %u (%s)": "金鑰世代:%u (%s)",
|
||||
"Display Version: %s": "顯示版本:%s",
|
||||
"Mount Fs": "掛載檔案系統",
|
||||
"Failed to mount NCA": "掛載 NCA 失敗",
|
||||
"NCA Options": "NCA 選項",
|
||||
"Export NCA": "匯出 NCA",
|
||||
"Export NCA decrypted": "匯出已解密 NCA",
|
||||
"Exports the NCA with all fs sections decrypted (NCA header is still encrypted).": "匯出所有檔案系統區段皆已解密的 NCA (NCA 標頭仍為加密狀態)。",
|
||||
"Verify NCA 256 hash": "驗證 NCA 256 雜湊",
|
||||
"NCA hash missmatch!": "NCA 雜湊不符!",
|
||||
"NCA hash valid.": "NCA 雜湊有效。",
|
||||
"nca_validate_info": [
|
||||
"對 NCA 執行 sha256 雜湊運算以檢查其是否有效。\n",
|
||||
"注意:這只能檢測雜湊是否不符,無法驗證",
|
||||
"內容是否經過修改。"
|
||||
],
|
||||
"Verify NCA fixed key": "驗證 NCA 固定金鑰",
|
||||
"NCA fixed key is invalid!": "NCA 固定金鑰無效!",
|
||||
"NCA fixed key is valid.": "NCA 固定金鑰有效。",
|
||||
"nca_fixedkey_info": [
|
||||
"執行 RSA NCA 固定金鑰驗證。",
|
||||
"這是針對 NCA 標頭的雜湊檢查,用於驗證標頭是否被修改。",
|
||||
"標頭由任天堂簽署,因此無法偽造,能可靠地檢測修改過的 NCA 標頭 (如 NSP/XCI 轉換檔)。"
|
||||
],
|
||||
"Failed to decrypt NCA": "解密 NCA 失敗",
|
||||
"Application Type: %s": "應用程式類型:%s",
|
||||
"Content Type: %s": "內容類型:%s",
|
||||
"Distribution Type: %s": "分發類型:%s",
|
||||
"Program ID: %016lX": "程式 ID:%016lX",
|
||||
"SDK Version: %u.%u.%u.%u": "SDK 版本:%u.%u.%u.%u",
|
||||
"Export XCI (Gamecard)": "匯出 XCI (遊戲卡匣)",
|
||||
"Export XCZ (Compressed XCI)": "匯出 XCZ (壓縮的 XCI)",
|
||||
"GameCard": "遊戲卡匣",
|
||||
"Gc install failed!": "卡匣安裝失敗!",
|
||||
"Gc install success!": "卡匣安裝成功!",
|
||||
"Select content to dump": "選擇要提取的內容",
|
||||
"Export All Bins": "匯出所有 Bin",
|
||||
"Export XCI": "匯出 XCI",
|
||||
"Export Card ID Set": "匯出卡匣 ID Set",
|
||||
"Export Card UID": "匯出卡匣 UID",
|
||||
"Export Certificate": "匯出憑證",
|
||||
"Export Initial Data": "匯出初始資料",
|
||||
"Failed to mount GameCard filesystem": "掛載遊戲卡匣檔案系統失敗",
|
||||
"System memory %.1f GB": "系統記憶體 %.1f GB",
|
||||
"microSD card %.1f GB": "microSD 卡 %.1f GB",
|
||||
"App-ID: 0%lX": "App-ID:0%lX",
|
||||
"Key-Gen: %u (%s)": "金鑰世代:%u (%s)",
|
||||
"Size: %.2f GB": "大小:%.2f GB",
|
||||
"Base: %zu Patch: %zu Addon: %zu Data: %zu": "本體:%zu 更新:%zu DLC:%zu 資料:%zu",
|
||||
"Prev": "上一個",
|
||||
"Next": "下一個",
|
||||
"No GameCard inserted": "未插入遊戲卡匣",
|
||||
"WARNING: GameCard is already trimmed!": "警告:遊戲卡匣已經過裁剪!",
|
||||
"Continue": "繼續",
|
||||
"GameCard is trimmed, full dump is not possible!": "遊戲卡匣已經過裁剪,無法完整提取!",
|
||||
"Downloading json": "正在下載 json",
|
||||
"Download": "下載",
|
||||
"Failed to download json": "下載 json 失敗",
|
||||
"Select release to download for ": "選擇要下載的版本:",
|
||||
"Select asset to download for ": "選擇要下載的資源檔:",
|
||||
"Failed to download app!": "下載應用程式失敗!",
|
||||
"Unstar": "取消星號",
|
||||
"Unstarred ": "已取消星號 ",
|
||||
"Star": "加上星號",
|
||||
"Starred ": "已加星號 ",
|
||||
"Homebrew Options": "自製軟體選項",
|
||||
"Updated (Star)": "更新時間 (星號)",
|
||||
"Alphabetical (Star)": "按字母順序 (星號)",
|
||||
"Size (Star)": "大小 (星號)",
|
||||
"Display entries in Ascending or Descending order.": "以遞增或遞減順序顯示項目。",
|
||||
"Change the layout to List, Icon and Grid.": "將版面配置更改為列表、圖示和格狀。",
|
||||
"Show hidden": "顯示隱藏檔",
|
||||
"Shows all hidden homebrew.": "顯示所有隱藏的自製軟體。",
|
||||
"Test": "測試",
|
||||
"Hide": "隱藏",
|
||||
"Hides the selected homebrew.\n\nTo unhide homebrew, enable \"Show hidden\" in the sort options.": "隱藏選取的自製軟體。\n\n若要取消隱藏,請在排序選項中啟用「顯示隱藏檔」。",
|
||||
"Mount NRO Fs": "掛載 NRO 檔案系統",
|
||||
"Failed to mount NRO FileSystem": "掛載 NRO 檔案系統失敗",
|
||||
"Mounts the NRO FileSystem (icon, nacp and RomFS).": "掛載 NRO 檔案系統 (圖示、NACP 和 RomFS)。",
|
||||
"hb_remove_info": [
|
||||
"永久刪除選取的自製軟體。\n",
|
||||
"該自製軟體建立的檔案和資料夾仍會保留。",
|
||||
"請使用檔案瀏覽器刪除它們。"
|
||||
],
|
||||
"Failed to load image": "載入圖片失敗",
|
||||
"Install failed!": "安裝失敗!",
|
||||
"Install success!": "安裝成功!",
|
||||
"Drag'n'Drop (NSP, XCI, NSZ, XCZ) to the install folder": "拖放檔案 (NSP、XCI、NSZ、XCZ) 至 install 資料夾",
|
||||
"Failed to install, press B to exit...": "安裝失敗,請按 離開...",
|
||||
"Irs": "紅外成像",
|
||||
"0 (Sideways)": "0 (橫向)",
|
||||
"90 (Flat)": "90 (平放)",
|
||||
"180 (-Sideways)": "180 (反向橫向)",
|
||||
"270 (Upside down)": "270 (倒置)",
|
||||
"Grey": "灰色",
|
||||
"Ironbow": "彩虹熱圖 (Ironbow)",
|
||||
"Green": "綠色",
|
||||
"Red": "紅色",
|
||||
"Blue": "藍色",
|
||||
"All leds": "所有 LED",
|
||||
"Bright group": "高亮度群組",
|
||||
"Dim group": "低亮度群組",
|
||||
"Normal image": "正常影像",
|
||||
"Negative image": "負片影像",
|
||||
"Controller": "控制器",
|
||||
"Rotation": "旋轉",
|
||||
"Colour": "色彩",
|
||||
"Light Target": "照明目標",
|
||||
"Gain": "增益",
|
||||
"Negative Image": "負片影像",
|
||||
"Format": "格式",
|
||||
"Trimming Format": "裁剪格式",
|
||||
"External Light Filter": "外部濾光片",
|
||||
"Load Default": "載入預設值",
|
||||
"Ambient Noise Level: ": "環境噪音等級:",
|
||||
"Pad ": "控制器 ",
|
||||
"HandHeld": "手提模式",
|
||||
" (Available)": " (可用)",
|
||||
" (Unsupported)": " (不支援)",
|
||||
" (Unconnected)": " (未連線)",
|
||||
"The homebrew menu.\n\nAllows you to launch, delete and mount homebrew!": "自製軟體選單。\n\n允許您啟動、刪除和掛載自製軟體!",
|
||||
"Download and update apps.\n\nInternet connection required.": "下載並更新應用程式。\n\n需要網際網路連線。",
|
||||
"View all installed games. In this menu you can launch, backup, create savedata and much more.": "檢視所有已安裝的遊戲。您可以在此選單中啟動、備份、建立存檔等。",
|
||||
"Browse files on you SD Card. You can move, copy, delete, extract zip, create zip, upload and much more.\n\nA connected USB/HDD can be opened by mounting it in the advanced options.": "瀏覽 SD 卡上的檔案。您可以移動、複製、刪除、解壓縮 zip、建立 zip、上傳等。\n\n連接的 USB/HDD 可透過進階選項中的掛載功能開啟。",
|
||||
"View save data for each user. You can backup and restore saves.\n\nExperimental support for backing up system saves is possible.": "檢視每位使用者的存檔資料。您可以備份和還原存檔。\n\n支援實驗性的系統存檔備份功能。",
|
||||
"Download releases directly from GitHub. Custom entries can be added to /config/sphaira/github": "直接從 GitHub 下載發行版本。可將自訂項目新增至 /config/sphaira/github",
|
||||
"Install apps via FTP.": "透過 FTP 安裝應用程式。",
|
||||
"MTP Install": "MTP 安裝",
|
||||
"Install apps via MTP.": "透過 MTP 安裝應用程式。",
|
||||
"USB Install": "USB 安裝",
|
||||
"Install apps via USB.\n\nA USB client is required on PC.": "透過 USB 安裝應用程式。\n\n電腦端需要 USB 用戶端軟體。",
|
||||
"View info on the inserted Game Card (GC). You can backup and install the inserted GC. To swap GC's, simply remove the old GC and insert the new one. You do not need to exit the menu.": "檢視已插入遊戲卡匣的資訊。您可以備份和安裝已插入的卡匣。如要更換卡匣,只需取出舊卡匣並插入新卡匣即可,無需離開選單。",
|
||||
"IRS (Infrared Joycon Camera)": "IRS (紅外線 Joy-Con 攝影機)",
|
||||
"InfraRed Sensor (IRS) is the small camera found on right JoyCon.": "紅外線感應器 (IRS) 是位於右側 Joy-Con 上的小型攝影機。",
|
||||
"Update avaliable: ": "有可用更新:",
|
||||
"Download via the Network options!": "請透過網路選項下載!",
|
||||
"Menu": "選單",
|
||||
"Menu Options": "選單選項",
|
||||
"Auto": "自動",
|
||||
"English": "English",
|
||||
"Japanese": "日本語",
|
||||
"French": "Français",
|
||||
"German": "Deutsch",
|
||||
"Italian": "Italiano",
|
||||
"Spanish": "Español",
|
||||
"Chinese (Simplified)": "简体中文",
|
||||
"Korean": "한국어",
|
||||
"Dutch": "Dutch",
|
||||
"Portuguese": "Português",
|
||||
"Russian": "Русский",
|
||||
"Chinese (Traditional)": "繁體中文",
|
||||
"Swedish": "Svenska",
|
||||
"Vietnamese": "Vietnamese",
|
||||
"Ukrainian": "Українська",
|
||||
"Launch one of Sphaira's menus:\n": "啟動 Sphaira 的其中一個選單:\n",
|
||||
"\nYou can change the left/right menu in the Advanced Options.": " \n您可以在進階選項中更改左側/右側選單。",
|
||||
"Network": "網路",
|
||||
"Network Options": "網路選項",
|
||||
"Download update: ": "下載更新:",
|
||||
"Failed to download update": "下載更新失敗",
|
||||
"Updated to ": "已更新至 ",
|
||||
"ftp_settings_info": [
|
||||
"啟用 / 修改 FTP 伺服器設定,例如連接埠、使用者/密碼和顯示的資料夾。\n",
|
||||
"注意:更改任何選項,會在離開選項選單時自動重新啟動 FTP 伺服器。"
|
||||
],
|
||||
"mtp_settings_info": [
|
||||
"啟用 / 修改 MTP 回應端設定,例如顯示的資料夾。\n",
|
||||
"注意:更改任何選項,會在離開選項選單時自動重新啟動 MTP 伺服器。"
|
||||
],
|
||||
"HDD": "HDD 硬碟",
|
||||
"Enable / modify the HDD mount options.": "啟用 / 修改硬碟掛載選項。",
|
||||
"NXlink": "NXlink",
|
||||
"nxlink_enable_info": [
|
||||
"啟用 NXlink 伺服器在背景執行。",
|
||||
"NXlink 用於將 .nro 從電腦傳送到 Switch。\n",
|
||||
"如果您不是開發者,可以停用此選項。"
|
||||
],
|
||||
"nxlink_toggle_info": [
|
||||
"切換 FTP、MTP、硬碟 和 NXlink\n",
|
||||
"如果 Sphaira 有可用更新,您可以從此選單下載。"
|
||||
],
|
||||
"Theme": "佈景主題",
|
||||
"Language": "語言",
|
||||
"translation_info": [
|
||||
"更換語言。\n",
|
||||
"如果找不到您的語言,或缺少翻譯,請考慮在 github.com/ITotalJustice/sphaira 提交 PR (Pull Request)。",
|
||||
""
|
||||
],
|
||||
"advanced_options_info": [
|
||||
"更改進階選項。",
|
||||
"請查看資訊框以更了解每個選項。"
|
||||
],
|
||||
"System %.1f GB": "系統 %.1f GB",
|
||||
"microSD %.1f GB": "microSD %.1f GB",
|
||||
"[Applet Mode]": "[小程式模式]",
|
||||
"Ethernet": "乙太網路",
|
||||
"No Internet": "無網際網路連線",
|
||||
"State: %s | Speed: %s": "狀態:%s | 速度:%s",
|
||||
"Failed to mount save filesystem": "掛載存檔檔案系統失敗",
|
||||
"Save Options": "存檔選項",
|
||||
"System": "系統",
|
||||
"Account": "帳號",
|
||||
"BCAT": "BCAT",
|
||||
"Device": "裝置",
|
||||
"Temporary": "暫存",
|
||||
"Cache": "快取",
|
||||
"System BCAT": "系統 BCAT",
|
||||
"Data Type": "資料類型",
|
||||
"Backup": "備份",
|
||||
"Backup the selected save(s) to a location of your choice.": "將選取的存檔備份至您選擇的位置。",
|
||||
"Backup to...": "備份至...",
|
||||
"Backup the selected save(s) to a location of your choice, and set the name of the backup.": "將選取的存檔備份至您選擇的位置,並設定備份名稱。",
|
||||
"save_backuprestore_info": [
|
||||
"還原目前 Title 的存檔。",
|
||||
"如果啟用「自動備份」,存檔將先被備份然後才還原。",
|
||||
"自動備份的存檔名稱中將包含「Auto」。"
|
||||
],
|
||||
"Auto backup on restore": "還原時自動備份",
|
||||
"If enabled, when restoring a save, the current save will first be backed up.": "啟用時,還原存檔前會先備份目前的存檔。",
|
||||
"Compress backup": "壓縮備份",
|
||||
"save_backup_compress_info": [
|
||||
"如果啟用,備份將被壓縮為 zip 檔案。\n",
|
||||
"注意:停用此選項不會停用 zip 檔案格式,只會停用壓縮",
|
||||
"儲存在 zip 中的檔案。",
|
||||
"停用壓縮將使備份速度更快,但檔案較大。"
|
||||
],
|
||||
"Select backup location": "選擇備份位置",
|
||||
"Backup failed!": "備份失敗!",
|
||||
"Backup successfull!": "備份成功!",
|
||||
"Select restore location": "選擇還原位置",
|
||||
"Invalid location type!": "無效的位置類型!",
|
||||
"No saves found in ": "找不到存檔於 ",
|
||||
"Restore save for: ": "還原存檔給:",
|
||||
"Are you sure you want to restore ": "您確定要還原 ",
|
||||
"Auto backup": "自動備份",
|
||||
"Restore failed!": "還原失敗!",
|
||||
"Restore successfull!": "還原成功!",
|
||||
"Set name for ": "設定名稱給 ",
|
||||
"Set backup name": "設定備份名稱",
|
||||
"Flushing zip to file": "正在將 zip 寫入檔案",
|
||||
"Disable MTP for usb install": "為 USB 安裝停用 MTP",
|
||||
"Re-enabled MTP": "已重新啟用 MTP",
|
||||
"Installed via usb": "已透過 USB 安裝",
|
||||
"USB install failed!": "USB 安裝失敗!",
|
||||
"Usb install success!": "USB 安裝成功!",
|
||||
"Waiting for connection...": "等待連線中...",
|
||||
"Connected, waiting for file list...": "已連線,等待檔案列表中...",
|
||||
"Connected, starting transfer...": "已連線,開始傳輸...",
|
||||
"Transferring data...": "資料傳輸中...",
|
||||
"Press B to exit...": "請按 離開...",
|
||||
"Failed to init usb, press B to exit...": "USB 初始化失敗,請按 離開...",
|
||||
"Detached": "已斷開",
|
||||
"Attached": "已連接",
|
||||
"Powered": "已供電",
|
||||
"Default": "預設",
|
||||
"Address": "位址",
|
||||
"Configured": "已設定",
|
||||
"Suspended": "已暫停",
|
||||
"USB 1.0 Low Speed": "USB 1.0 低速",
|
||||
"USB 1.1 Full Speed": "USB 1.1 全速",
|
||||
"USB 2.0 High Speed": "USB 2.0 高速",
|
||||
"USB 3.0 Super Speed": "USB 3.0 超高速",
|
||||
"Mount Creator": "掛載建立器",
|
||||
"Type": "類型",
|
||||
"Select the type of the forwarder.": "選擇前導程式的類型。",
|
||||
"URL": "網址",
|
||||
"Set the URL of the application": "設定應用程式的網址",
|
||||
"Optional: Set the port of the server. If left empty, the default port for the protocol will be used.": "選填:設定伺服器的連接埠。若留空,將使用通訊協定的預設連接埠。",
|
||||
"Timeout": "逾時",
|
||||
"Timeout in milliseconds": "逾時 (毫秒)",
|
||||
"Optional: Set the timeout in seconds.": "選填:設定逾時秒數。",
|
||||
"Optional: Set the username of the application": "選填:設定應用程式的使用者名稱",
|
||||
"Optional: Set the password of the application": "選填:設定應用程式的密碼",
|
||||
"Dump path": "提取路徑",
|
||||
"Optional: Set the dump path used when exporting games and saves.": "選填:設定匯出遊戲和存檔時使用的提取路徑。",
|
||||
"Read only": "唯讀",
|
||||
"mount_readonly_info": [
|
||||
"將檔案系統掛載為唯讀。\n",
|
||||
"設定此選項也會隱藏該掛載點,使其不顯示為匯出選項。"
|
||||
],
|
||||
"No stat file": "不檢查檔案狀態",
|
||||
"filecheck_disable_info": [
|
||||
"啟用此選項可停止檔案瀏覽器檢查每個檔案的大小和時間戳記。",
|
||||
"這能改善瀏覽效能。"
|
||||
],
|
||||
"No stat dir": "不檢查目錄狀態",
|
||||
"dircheck_disable_info": [
|
||||
"啟用此選項可停止檔案瀏覽器檢查資料夾中的檔案和資料夾數量。",
|
||||
"這能改善瀏覽效能,特別是對於目錄列表速度較慢的伺服器。"
|
||||
],
|
||||
"FS hidden": "隱藏檔案系統",
|
||||
"Hide the mount from being visible in the file browser.": "隱藏掛載點,使其不顯示於檔案瀏覽器中。",
|
||||
"Export hidden": "隱藏匯出",
|
||||
"Hide the mount from being visible as a export option for games and saves.": "隱藏掛載點,使其不顯示於遊戲和存檔的匯出選項中。",
|
||||
"Save": "儲存",
|
||||
"Mount entry saved. Restart Sphaira to apply changes.": "掛載項目已儲存。請重新啟動 Sphaira 以套用變更。",
|
||||
"Saves the mount entry.\n\nNOTE: You must restart Sphaira for changes to take effect!": "儲存掛載項目。\n\n注意:您必須重新啟動 Sphaira 才能使變更生效!",
|
||||
"Name and URL must be set!": "名稱和網址必須設定!",
|
||||
"Devoptab Options": "Devoptab 選項",
|
||||
"Create New Entry": "建立新項目",
|
||||
"Creates a new mount option.\n\nNOTE: You must restart Sphaira for changes to take effect!": "建立新的掛載選項。\n\n注意:您必須重新啟動 Sphaira 才能使變更生效!",
|
||||
"Modify Existing Entry": "修改現有項目",
|
||||
"No mount entries found.": "找不到掛載項目。",
|
||||
"Modify Entry": "修改項目",
|
||||
"Modify an existing mount option.\n\nNOTE: You must restart Sphaira for changes to take effect!": "修改現有的掛載選項。\n\n注意:您必須重新啟動 Sphaira 才能使變更生效!",
|
||||
"Delete Existing Entry": "刪除現有項目",
|
||||
"Delete Entry": "刪除項目",
|
||||
"Delete an existing mount option.\n\nNOTE: You must restart Sphaira for changes to take effect!": "刪除現有的掛載選項。\n\n注意:您必須重新啟動 Sphaira 才能使變更生效!",
|
||||
"Section #": "區段 #"
|
||||
}
|
||||
@@ -1,494 +0,0 @@
|
||||
{
|
||||
"[Applet Mode]": "[小程序模式]",
|
||||
"No Internet": "网络未连接",
|
||||
"Switch-Handheld!": "切换至掌机模式!",
|
||||
"Switch-Docked!": "切换至底座模式!",
|
||||
"Warning! Logs are enabled, Sphaira will run slowly!": "警告!日志已启用,将导致Sphaira运行缓慢!",
|
||||
"Audio disabled due to suspended game": "由于游戏暂停,音频已禁用",
|
||||
"Are you sure you wish to cancel?": "您确定要取消吗?",
|
||||
"An error occurred": "发生错误",
|
||||
"If this message appears repeatedly, please open an issue.": "若此消息反复出现,请提交问题报告。",
|
||||
|
||||
"Menu Options": "菜单选项",
|
||||
"Menu": "菜单",
|
||||
"Theme": "主题",
|
||||
"Theme Options": "主题选项",
|
||||
"Select Theme": "选择主题",
|
||||
"Music": "音乐",
|
||||
"12 Hour Time": "12小时制时间",
|
||||
"Download Default Music": "下载默认音乐",
|
||||
"Failed to download default_music.bfstm, please try again": "下载default_music.bfstm失败,请重试",
|
||||
"Overwrite current default music?": "覆盖当前默认音乐?",
|
||||
|
||||
"Network": "网络",
|
||||
"Network Options": "网络选项",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"MTP Install": "MTP安装",
|
||||
"Nxlink": "Nxlink上传",
|
||||
"Nxlink Connected": "Nxlink已连接",
|
||||
"Nxlink Upload": "Nxlink上传中",
|
||||
"Nxlink Finished": "Nxlink已结束",
|
||||
"Hdd": "HDD",
|
||||
"Hdd write protect": "HDD写保护",
|
||||
|
||||
"Language": "语言",
|
||||
"Auto": "自动",
|
||||
"English": "English",
|
||||
"Japanese": "日本語",
|
||||
"French": "Français",
|
||||
"German": "Deutsch",
|
||||
"Italian": "Italiano",
|
||||
"Spanish": "Español",
|
||||
"Chinese": "中文",
|
||||
"Korean": "한국어",
|
||||
"Dutch": "Dutch",
|
||||
"Portuguese": "Português",
|
||||
"Russian": "Русский",
|
||||
"Swedish": "Svenska",
|
||||
"Vietnamese": "Vietnamese",
|
||||
"Ukrainian": "Українська",
|
||||
|
||||
"Misc": "拓展",
|
||||
"Misc Options": "拓展选项",
|
||||
"Games": "游戏",
|
||||
"Game Options": "游戏选项",
|
||||
"Hide forwarders": "隐藏前端启动",
|
||||
"Launch random game": "启动随机游戏",
|
||||
"List meta records": "列出元数据记录",
|
||||
"Entries": "条目",
|
||||
"Failed to list application meta entries": "未能列出应用程序元条目",
|
||||
"No meta entries found...\n": "未找到元条目...\n",
|
||||
"Updating application record list": "更新应用程序记录列表",
|
||||
"Dump": "转储",
|
||||
"Select content to dump": "选择要转储的内容",
|
||||
"Dump All": "转储全部内容",
|
||||
"Dump Application": "转储应用程序(本体)",
|
||||
"Dump Patch": "转储更新补丁(UPD)",
|
||||
"Dump AddOnContent": "转储追加内容(DLC)",
|
||||
"Dump DataPatch": "转储数据补丁",
|
||||
"Dump All Bins": "转储全部文件",
|
||||
"Dump XCI": "转储.xci",
|
||||
"Dump Card ID Set": "转储游戏卡ID",
|
||||
"Dump Card UID": "转储游戏卡UID",
|
||||
"Dump Certificate": "转储游戏证书",
|
||||
"Dump Initial Data": "转储初始数据",
|
||||
"Select dump location": "选择转储位置",
|
||||
"microSD card (/dumps/NSP/)": "microSD卡(/dumps/NSP/)",
|
||||
"USB transfer (Switch 2 Switch)": "USB传输(Switch对Switch)",
|
||||
"/dev/null (Speed Test)": "/dev/null(速度测试)",
|
||||
"Dumping": "正在转储",
|
||||
"Dump successfull!": "转储成功!",
|
||||
"Dump failed!": "转储失败!",
|
||||
"Success": "成功",
|
||||
"Delete successfull!": "删除成功!",
|
||||
"Delete failed!": "删除失败!",
|
||||
"Title cache": "标题缓存",
|
||||
|
||||
"Saves": "存档",
|
||||
"Save Options": "存档选项",
|
||||
"Account": "账户",
|
||||
"Backup": "备份",
|
||||
"Auto backup": "自动备份",
|
||||
"Auto backup on restore": "还原时自动备份",
|
||||
"Compress backup": "压缩备份",
|
||||
"Are you sure you want to backup save(s)?": "您确定要备份存档吗?",
|
||||
"No saves found in ": "未找到存档 ",
|
||||
"Backed up to ": "已备份到 ",
|
||||
"Backup successfull!": "已备份成功!",
|
||||
"Backup failed!": "备份失败!",
|
||||
"Restore save for: ": "还原备份文件: ",
|
||||
"Are you sure you want to restore ": "您确定要还原已备份文件吗 ",
|
||||
"Restore successfull!": "已还原成功!",
|
||||
"Restore failed!": "还原失败!",
|
||||
"Data Type": "数据类型",
|
||||
"System": "系统",
|
||||
"BCAT": "BCAT数据",
|
||||
"Device": "设备",
|
||||
"Temporary": "临时",
|
||||
"Cache": "缓存",
|
||||
"System BCAT": "系统备份",
|
||||
"Select backup location": "选择备份位置",
|
||||
"Select restore location": "选择还原位置",
|
||||
|
||||
"Themezer": "在线主题",
|
||||
"Themezer Options": "在线主题选项",
|
||||
"Nsfw": "公共场合不宜的主题",
|
||||
"Page": "页面",
|
||||
"Page %zu / %zu": "页面 %zu / %zu",
|
||||
"Enter Page Number": "输入跳转的页码",
|
||||
"Bad Page": "错误的页面",
|
||||
"Download theme?": "下载该主题?",
|
||||
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "正在下载 json",
|
||||
"Select asset to download for ": "选择要下载的资源用于 ",
|
||||
"Failed to download json": "下载json失败",
|
||||
"Failed to download app!": "下载应用程序失败!",
|
||||
|
||||
"FTP Install": "通过 FTP 安装",
|
||||
"Connection Type: WiFi | Strength: ": "连接类型:WiFi |强度:",
|
||||
"Connection Type: Ethernet": "连接类型:以太网",
|
||||
"Connection Type: None": "连接类型:无",
|
||||
"Host:": "主机:",
|
||||
"Port:": "端口:",
|
||||
"Username:": "用户名:",
|
||||
"Password:": "密码:",
|
||||
"SSID:": "网络名称:",
|
||||
"Passphrase:": "密码:",
|
||||
"Failed to install, press B to exit...": "通过 FTP 安装失败,按 B 键退出...",
|
||||
"Install success!": "通过 FTP 安装成功。",
|
||||
"Install failed!": "通过 FTP 安装失败。",
|
||||
"USB Install": "通过 USB 安装",
|
||||
"USB": "USB",
|
||||
"Connected, waiting for file list...": "已连接,正在等待文件列表...",
|
||||
"Connected, starting transfer...": "已连接,开始传输...",
|
||||
"Failed to init usb, press B to exit...": "USB 初始化失败,按 B 键退出...",
|
||||
"Waiting for connection...": "等待连接中...",
|
||||
"Transferring data...": "正在传输数据...",
|
||||
"USB connected, sending file list": "USB已连接,正在发送文件列表",
|
||||
"Sent file list, waiting for command...": "已发送文件列表,正在等待命令...",
|
||||
"waiting for usb connection...": "等待USB连接...",
|
||||
"Disable MTP for usb install": "暂时禁用 USB 安装的 MTP 功能",
|
||||
"Re-enabled MTP": "重新启用 MTP",
|
||||
"Installed via usb": "通过 USB 安装",
|
||||
"Usb install success!": "USB安装成功!",
|
||||
"Usb install failed!": "USB安装失败!",
|
||||
"State: %s | Speed: %s": "状态: %s | 速度: %s",
|
||||
"Detached": "已断开",
|
||||
"Attached": "已连接",
|
||||
"Powered": "已供电",
|
||||
"Default": "默认",
|
||||
"Address": "地址",
|
||||
"Configured": "已配置",
|
||||
"Suspended": "已挂起",
|
||||
"USB 1.0 Low Speed": "USB 1.0 低速",
|
||||
"USB 1.1 Full Speed": "USB 1.1 全速",
|
||||
"USB 2.0 High Speed": "USB 2.0 高速",
|
||||
"USB 3.0 Super Speed": "USB 3.0 超高速",
|
||||
"Drag'n'Drop (NSP, XCI, NSZ, XCZ) to the install folder": "拖放文件(NSP, XCI, NSZ, XCZ)至安装文件夹",
|
||||
"Failed to install via MTP, press B to exit...": "MTP安装失败,请按B键退出...",
|
||||
"MTP install success!": "MTP安装成功!",
|
||||
"MTP install failed!": "MTP安装失败!",
|
||||
"Press B to exit...": "按 B 键退出...",
|
||||
"GameCard Install": "游戏卡安装",
|
||||
"GameCard": "游戏卡",
|
||||
"GC": "GC",
|
||||
"System memory %.1f GB": "主机内存 %.1f GB",
|
||||
"microSD card %.1f GB": "microSD卡 %.1f GB",
|
||||
"Exit": "退出",
|
||||
"Install disabled...\nPlease enable installing via the install options.": "安装已禁用...\n请通过安装选项启用安装。",
|
||||
"No GameCard inserted": "未插入游戏卡",
|
||||
"GameCard is already trimmed!": "游戏卡已被修剪!",
|
||||
"WARNING: GameCard is already trimmed!": "警告:游戏卡已被修剪!",
|
||||
"Continue": "继续",
|
||||
"Gc install success!": "游戏安装成功。",
|
||||
"Gc install failed!": "游戏安装失败。",
|
||||
|
||||
"IRS (Infrared Joycon Camera)": "IRS(Joycon红外摄像头)",
|
||||
"IRS": "IRS",
|
||||
"Irs": "红外成像",
|
||||
"Ambient Noise Level: ": "环境噪声等级:",
|
||||
"Controller": "控制器",
|
||||
"Pad ": "手柄 ",
|
||||
"HandHeld": "掌机模式",
|
||||
" (Available)": " (可用)",
|
||||
" (Unsupported)": " (不支持)",
|
||||
" (Unconnected)": " (未连接)",
|
||||
"Rotation": "旋转",
|
||||
"0 (Sideways)": "0度",
|
||||
"90 (Flat)": "90度",
|
||||
"180 (-Sideways)": "180度",
|
||||
"270 (Upside down)": "270度",
|
||||
"Colour": "颜色",
|
||||
"Grey": "灰色",
|
||||
"Ironbow": "紫黄",
|
||||
"Green": "绿色",
|
||||
"Red": "红色",
|
||||
"Blue": "蓝色",
|
||||
"Light Target": "光源目标",
|
||||
"All leds": "全部",
|
||||
"Bright group": "亮色组",
|
||||
"Dim group": "暗色组",
|
||||
"None": "无",
|
||||
"Gain": "曝光",
|
||||
"Negative Image": "负片图像",
|
||||
"Normal image": "正常图像",
|
||||
"Negative image": "负片图像",
|
||||
"Format": "格式",
|
||||
"Trimming Format": "修剪格式",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"External Light Filter": "外部光滤镜",
|
||||
"Load Default": "加载默认值",
|
||||
|
||||
"Web": "网络",
|
||||
"Select URL": "选择链接",
|
||||
"Enter custom URL": "输入自定义链接",
|
||||
"Enter URL": "输入网址",
|
||||
|
||||
"Advanced": "高级",
|
||||
"Advanced Options": "高级选项",
|
||||
"Logging": "日志",
|
||||
"Replace hbmenu on exit": "退出后用Sphaira替换hbmenu",
|
||||
"Restore hbmenu?": "恢复 hbmenu?",
|
||||
"Restore": "恢复",
|
||||
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "未能找到 /switch/hbmenu.nro\n请使用应用商店重新安装 hbmenu",
|
||||
"Failed to restore hbmenu, please re-download hbmenu": "恢复 hbmenu 失败,请重新下载 hbmenu",
|
||||
"Failed to restore hbmenu, using sphaira instead": "恢复 hbmenu 失败,改用 Sphaira",
|
||||
"Restored hbmenu, closing sphaira": "已恢复 hbmenu,正在关闭 Sphaira",
|
||||
"Restored hbmenu": "已恢复 hbmenu",
|
||||
"Restart Sphaira?": "重启 Sphaira?",
|
||||
"Press OK to restart Sphaira": "按OK键以重启shphaira菜单",
|
||||
"Boost CPU during transfer": "传输时提升CPU频率",
|
||||
"Text scroll speed": "文本滚动速度",
|
||||
"Slow": "慢",
|
||||
"Normal": "正常",
|
||||
"Fast": "快",
|
||||
"Set left-side menu": "设置左侧菜单",
|
||||
"Set right-side menu": "设置右侧菜单",
|
||||
"Install options": "安装选项",
|
||||
"Install Options": "安装选项",
|
||||
"Enable sysmmc": "启用sysmmc",
|
||||
"Enable emummc": "启用emummc",
|
||||
"Show install warning": "显示安装警告",
|
||||
"Install location": "安装位置",
|
||||
"System memory": "主机内存",
|
||||
"microSD card": "SD卡",
|
||||
"Boost CPU clock": "提升 CPU 频率",
|
||||
"Allow downgrade": "允许降级",
|
||||
"Skip if already installed": "若已安装则跳过",
|
||||
"Ticket only": "仅安装票据",
|
||||
"Skip base": "跳过基础部分",
|
||||
"Skip patch": "跳过更新补丁(UPD)",
|
||||
"Skip dlc": "跳过追加内容(DLC)",
|
||||
"Skip data patch": "跳过数据补丁",
|
||||
"Skip ticket": "跳过票据",
|
||||
"Skip NCA hash verify": "跳过 NCA 哈希验证",
|
||||
"Skip RSA header verify": "跳过 RSA 头部验证",
|
||||
"Skip RSA NPDM verify": "跳过 RSA NPDM 验证",
|
||||
"Ignore distribution bit": "忽略分布位",
|
||||
"Convert to standard crypto": "转换为标准加密方式",
|
||||
"Lower master key": "降低主密钥",
|
||||
"Lower system version": "降低系统版本",
|
||||
"Dump options": "转储选项",
|
||||
"Dump Options": "转储选项",
|
||||
"Created nested folder": "已创建嵌套文件夹",
|
||||
"Append folder with .xci": "用.xci附加文件夹",
|
||||
"Trim XCI": "裁剪 XCI",
|
||||
"Label trimmed XCI": "标记已裁剪的XCI",
|
||||
"Multi-threaded USB transfer": "多线程USB传输",
|
||||
|
||||
"Homebrew": "自制软件",
|
||||
"Apps": "应用",
|
||||
"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 database": "正在更新ncm数据库",
|
||||
"Pushing application record": "正在推送应用记录",
|
||||
"Failed to install forwarder": "前端应用安装失败",
|
||||
"Unstar": "取消星标",
|
||||
"Star": "星标",
|
||||
"Unstarred ": "取消星标 ",
|
||||
"Starred ": "已星标 ",
|
||||
"Failed to remove old forwarder, please manually remove it!": "删除旧前端应用失败,请手动删除!",
|
||||
|
||||
"AppStore": "应用商店",
|
||||
"Appstore": "应用商店",
|
||||
"Store": "商店",
|
||||
"Filter: %s | Sort: %s | Order: %s": "筛选: %s | 排序: %s | 顺序: %s",
|
||||
"AppStore Options": "应用商店选项",
|
||||
"Info": "信息",
|
||||
"Changelog": "更新日志",
|
||||
"Details": "详情",
|
||||
"version: %s": "版本: %s",
|
||||
"updated: %s": "更新时间: %s",
|
||||
"category: %s": "分类: %s",
|
||||
"extracted: %.2f MiB": "应用大小: %.2f MiB",
|
||||
"app_dls: %s": "下载量: %s",
|
||||
"More by Author": "作者更多作品",
|
||||
"Leave Feedback": "留言反馈",
|
||||
|
||||
"FileBrowser": "文件浏览器",
|
||||
"Files": "文件",
|
||||
"%zd files": "%zd 个文件",
|
||||
"%zd dirs": "%zd 个文件夹",
|
||||
"File Options": "文件选项",
|
||||
"Show Hidden": "显示隐藏项目",
|
||||
"Folders First": "文件夹靠前",
|
||||
"Hidden Last": "隐藏项目置后",
|
||||
"Cut": "剪切",
|
||||
"Copy": "复制",
|
||||
"Copying ": "正在复制 ",
|
||||
"Paste": "粘贴",
|
||||
"Paste file(s)?": "粘贴 个文件(夹)?",
|
||||
"Pasting ": "正在粘贴 ",
|
||||
"Pasting": "正在粘贴",
|
||||
"Rename": "重命名",
|
||||
"Set New File Name": "输入新命名",
|
||||
"Failed to delete directory": "删除目录失败",
|
||||
"Failed to delete file": "删除文件失败",
|
||||
"Extract zip": "解压 zip",
|
||||
"Extract Options": "解压选项",
|
||||
"Extract here": "解压到这里",
|
||||
"Extract to root": "解压到根目录",
|
||||
"Are you sure you want to extract to root?": "您确定要解压到根目录吗?",
|
||||
"Extract to...": "解压到...",
|
||||
"Enter the path to the folder to extract into": "输入要解压到的文件夹的路径",
|
||||
"Extracting ": "正在解压",
|
||||
"Extract success!": "解压成功!",
|
||||
"Extract failed!": "解压失败!",
|
||||
"Compress to zip": "压缩到zip",
|
||||
"Compress Options": "压缩选项",
|
||||
"Compress": "压缩",
|
||||
"Compress to...": "压缩到...",
|
||||
"Compressing ": "正在压缩 ",
|
||||
"Compress success!": "压缩成功!",
|
||||
"Compress failed!": "压缩失败",
|
||||
"Create File": "新建文件",
|
||||
"Set File Name": "输入文件名",
|
||||
"Create Folder": "新建文件夹",
|
||||
"Set Folder Name": "输入文件夹名",
|
||||
"Creating ": "正在创建 ",
|
||||
"View as text (unfinished)": "以文本形式查看(未完善)",
|
||||
"Upload": "上传",
|
||||
"Select upload location": "选择上传位置",
|
||||
"No upload locations set!": "未设置上传位置",
|
||||
"Uploading": "正在上传",
|
||||
"Upload successfull!": "上传成功!",
|
||||
"Upload failed!": "上传失败!",
|
||||
"Hash": "哈希",
|
||||
"Hash Options": "哈希选项",
|
||||
"Hashing": "正在计算文件哈希",
|
||||
"Failed to hash file...": "计算文件哈希失败...",
|
||||
"Ignore read only": "忽略只读",
|
||||
"Mount": "挂载",
|
||||
"Sd": "SD卡",
|
||||
"Image System memory": "主机内存图像",
|
||||
"Image microSD card": "SD卡图像",
|
||||
"Empty...": "空...",
|
||||
"Open with DayBreak?": "使用DayBreak打开?",
|
||||
"Launch ": "启动 ",
|
||||
"Launch option for: ": "启动选项:",
|
||||
"Select launcher for: ": "选择启动器用于:",
|
||||
"Close FileBrowser?": "是否关闭文件浏览器?",
|
||||
|
||||
"Sort By": "排序方式",
|
||||
"Sort Options": "排序选项",
|
||||
"Filter": "筛选",
|
||||
"All": "全部",
|
||||
"Emulators": "模拟器",
|
||||
"Tools": "工具",
|
||||
"Themes": "主题",
|
||||
"Legacy": "旧版",
|
||||
"Sort": "排序",
|
||||
"Size": "按大小",
|
||||
"Size (Star)": "按大小(星标优先)",
|
||||
"Alphabetical": "按字母顺序",
|
||||
"Alphabetical (Star)": "按字母顺序(星标优先)",
|
||||
"Updated": "最近使用",
|
||||
"Updated (Star)": "最近更新(星标优先)",
|
||||
"Downloads": "下载",
|
||||
"Likes": "点赞量",
|
||||
"ID": "ID",
|
||||
"Order": "顺序",
|
||||
"Descending": "降序",
|
||||
"Descending (down)": "降序",
|
||||
"Desc": "降序",
|
||||
"Ascending": "升序",
|
||||
"Ascending (Up)": "升序",
|
||||
"Asc": "升序",
|
||||
"Layout": "布局",
|
||||
"List": "列表",
|
||||
"Icon": "图标",
|
||||
"Grid": "网格",
|
||||
"Search": "搜索",
|
||||
|
||||
"Options": "选项",
|
||||
"Split": "拆分",
|
||||
"OK": "确定",
|
||||
"Back": "返回",
|
||||
"Select": "选择",
|
||||
"Open": "打开",
|
||||
"Close": "关闭",
|
||||
"Launch": "启动",
|
||||
"Restart": "重启",
|
||||
"Next": "下一项",
|
||||
"Prev": "上一项",
|
||||
"Yes": "是",
|
||||
"No": "否",
|
||||
"On": "开启",
|
||||
"Off": "关闭",
|
||||
|
||||
"Install": "安装",
|
||||
"Install Selected files?": "安装所选文件?",
|
||||
"Installing ": "正在安装 ",
|
||||
"Installed ": "已安装",
|
||||
"Installed!": "安装完成!",
|
||||
"Trying to load ": "尝试加载 ",
|
||||
"Checking MD5": "正在校验 MD5",
|
||||
|
||||
"Delete": "删除",
|
||||
"Delete Selected files?": "删除选中的文件?",
|
||||
"Are you sure you want to delete ": "您确定要删除吗 ",
|
||||
"Scanning ": "正在扫描 ",
|
||||
"Deleting ": "正在删除 ",
|
||||
"Deleting": "正在删除",
|
||||
"Remove": "删除",
|
||||
"Completely remove ": "彻底删除 ",
|
||||
"Removing ": "正在移除 ",
|
||||
"Uninstalling ": "正在卸载 ",
|
||||
"Removed ": "已移除 ",
|
||||
|
||||
"Download": "下载",
|
||||
"Downloading ": "正在下载 ",
|
||||
"Downloaded ": "已下载 ",
|
||||
"Download via the Network options!": "通过网络选项下载!",
|
||||
|
||||
"Update": "更新",
|
||||
"Update avaliable: ": "有可用更新!",
|
||||
"Download update: ": "下载更新:",
|
||||
"Updated to ": "更新至 ",
|
||||
"Failed to download update": "更新下载失败",
|
||||
|
||||
"%zu hours %zu minutes remaining": "剩余 %zu 小时 %zu 分钟",
|
||||
"%zu minutes %zu seconds remaining": "剩余 %zu 分钟 %zu 秒",
|
||||
"%zu seconds remaining": "剩余 %zu 秒",
|
||||
|
||||
"Loading...": "加载中...",
|
||||
"Loading": "加载中",
|
||||
"Empty!": "空空如也!",
|
||||
"Not Ready...": "尚未准备好...",
|
||||
"Error loading page!": "页面加载失败!",
|
||||
"USB Speed Status": "USB速度状态",
|
||||
"Data Type": "数据类型",
|
||||
"Save Location Selection": "存档位置选择",
|
||||
"MTP Transfer": "MTP传输",
|
||||
"USB Installation": "USB安装",
|
||||
"Archive Data": "存档数据",
|
||||
"Game Card Data": "游戏卡数据",
|
||||
"Storage Path": "存储路径",
|
||||
"MTP Connection": "MTP连接",
|
||||
"USB Device": "USB设备",
|
||||
"Data Category": "数据类别",
|
||||
"Save Management": "存档管理",
|
||||
"Transfer Mode": "传输模式",
|
||||
"Installation Method": "安装方式",
|
||||
"File Type": "文件类型",
|
||||
"Storage Device": "存储设备",
|
||||
"Data Transfer": "数据传输",
|
||||
"Save Data Type": "存档数据类型",
|
||||
"USB Connection": "USB连接",
|
||||
"MTP Settings": "MTP设置",
|
||||
"USB Configuration": "USB配置",
|
||||
"Data Storage": "数据存储"
|
||||
}
|
||||
@@ -1,6 +1,34 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
set(sphaira_VERSION 0.13.3)
|
||||
# generic options.
|
||||
option(ENABLE_NVJPG "" OFF)
|
||||
option(ENABLE_NSZ "enables exporting to nsz" ON)
|
||||
|
||||
# lib options.
|
||||
option(ENABLE_LIBUSBHSFS "enables FAT/exFAT hdd mounting" ON)
|
||||
option(ENABLE_LIBUSBDVD "enables cd/dvd/iso/cue mounting" ON)
|
||||
option(ENABLE_FTPSRV "enables MTP server support" ON)
|
||||
option(ENABLE_LIBHAZE "enables MTP server support" ON)
|
||||
|
||||
# audio options.
|
||||
option(ENABLE_AUDIO_MP3 "" ON)
|
||||
option(ENABLE_AUDIO_OGG "" ON)
|
||||
option(ENABLE_AUDIO_WAV "" ON)
|
||||
option(ENABLE_AUDIO_FLAC "" ON)
|
||||
|
||||
# devoptab options.
|
||||
option(ENABLE_DEVOPTAB_HTTP "" ON)
|
||||
option(ENABLE_DEVOPTAB_NFS "" ON)
|
||||
option(ENABLE_DEVOPTAB_SMB2 "" ON)
|
||||
option(ENABLE_DEVOPTAB_FTP "" ON)
|
||||
option(ENABLE_DEVOPTAB_WEBDAV "" ON)
|
||||
# disable by default because we are CPU bound for upload/download.
|
||||
# max speed is 8MiB/s, which is fine for wifi, but awful for ethernet.
|
||||
# other clients get 36-40MiB/s.
|
||||
# it also adds 230k to binary size, and i don't think anyone will use it.
|
||||
option(ENABLE_DEVOPTAB_SFTP "" OFF)
|
||||
|
||||
set(sphaira_VERSION 1.0.0)
|
||||
|
||||
project(sphaira
|
||||
VERSION ${sphaira_VERSION}
|
||||
@@ -30,7 +58,11 @@ execute_process(
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
|
||||
set(sphaira_VERSION_HASH "${sphaira_VERSION} [${GIT_COMMIT}]")
|
||||
if (DEFINED sphaira_VERSION_OVERRIDE)
|
||||
set(sphaira_DISPLAY_VERSION "${sphaira_VERSION_OVERRIDE} [${GIT_COMMIT}]")
|
||||
else()
|
||||
set(sphaira_DISPLAY_VERSION "${sphaira_VERSION} [${GIT_COMMIT}]")
|
||||
endif()
|
||||
|
||||
add_executable(sphaira
|
||||
source/ui/menus/appstore.cpp
|
||||
@@ -46,8 +78,6 @@ add_executable(sphaira
|
||||
source/ui/menus/themezer.cpp
|
||||
source/ui/menus/ghdl.cpp
|
||||
source/ui/menus/usb_menu.cpp
|
||||
source/ui/menus/ftp_menu.cpp
|
||||
source/ui/menus/mtp_menu.cpp
|
||||
source/ui/menus/gc_menu.cpp
|
||||
source/ui/menus/game_menu.cpp
|
||||
source/ui/menus/game_meta_menu.cpp
|
||||
@@ -85,17 +115,12 @@ add_executable(sphaira
|
||||
source/web.cpp
|
||||
source/hasher.cpp
|
||||
source/i18n.cpp
|
||||
source/ftpsrv_helper.cpp
|
||||
source/haze_helper.cpp
|
||||
source/threaded_file_transfer.cpp
|
||||
source/title_info.cpp
|
||||
source/minizip_helper.cpp
|
||||
source/fatfs.cpp
|
||||
source/usbdvd.cpp
|
||||
|
||||
source/utils/utils.cpp
|
||||
source/utils/audio.cpp
|
||||
source/utils/nsz_dumper.cpp
|
||||
source/utils/devoptab_common.cpp
|
||||
source/utils/devoptab_romfs.cpp
|
||||
source/utils/devoptab_save.cpp
|
||||
@@ -105,6 +130,11 @@ add_executable(sphaira
|
||||
source/utils/devoptab_xci.cpp
|
||||
source/utils/devoptab_zip.cpp
|
||||
source/utils/devoptab_bfsar.cpp
|
||||
source/utils/devoptab_vfs.cpp
|
||||
source/utils/devoptab_fatfs.cpp
|
||||
source/utils/devoptab_game.cpp
|
||||
source/utils/devoptab_mounts.cpp
|
||||
source/utils/devoptab.cpp
|
||||
|
||||
source/usb/base.cpp
|
||||
source/usb/usbds.cpp
|
||||
@@ -133,7 +163,7 @@ add_executable(sphaira
|
||||
|
||||
target_compile_definitions(sphaira PRIVATE
|
||||
-DAPP_VERSION="${sphaira_VERSION}"
|
||||
-DAPP_VERSION_HASH="${sphaira_VERSION_HASH}"
|
||||
-DAPP_DISPLAY_VERSION="${sphaira_DISPLAY_VERSION}"
|
||||
-DCURL_NO_OLDIES=1
|
||||
-DDEV_BUILD=$<BOOL:${DEV_BUILD}>
|
||||
-DZSTD_STATIC_LINKING_ONLY=1
|
||||
@@ -182,17 +212,6 @@ target_compile_options(sphaira PRIVATE
|
||||
include(FetchContent)
|
||||
set(FETCHCONTENT_QUIET FALSE)
|
||||
|
||||
FetchContent_Declare(ftpsrv
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
|
||||
GIT_TAG 85b3cf0
|
||||
SOURCE_SUBDIR NONE
|
||||
)
|
||||
|
||||
FetchContent_Declare(libhaze
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git
|
||||
GIT_TAG f0b2a14
|
||||
)
|
||||
|
||||
FetchContent_Declare(libpulsar
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/switch-libpulsar.git
|
||||
GIT_TAG ac7bc97
|
||||
@@ -224,11 +243,6 @@ FetchContent_Declare(zstd
|
||||
SOURCE_SUBDIR build/cmake
|
||||
)
|
||||
|
||||
FetchContent_Declare(libusbhsfs
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/libusbhsfs.git
|
||||
GIT_TAG 625269b
|
||||
)
|
||||
|
||||
FetchContent_Declare(libnxtc
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/libnxtc.git
|
||||
GIT_TAG 88ce3d8
|
||||
@@ -245,35 +259,279 @@ FetchContent_Declare(dr_libs
|
||||
SOURCE_SUBDIR NONE
|
||||
)
|
||||
|
||||
FetchContent_Declare(id3v2lib
|
||||
GIT_REPOSITORY https://github.com/larsbs/id3v2lib.git
|
||||
GIT_TAG 141ffb8
|
||||
)
|
||||
if (ENABLE_NVJPG)
|
||||
FetchContent_Declare(nvjpg
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/oss-nvjpg.git
|
||||
GIT_TAG 45680e7
|
||||
)
|
||||
|
||||
FetchContent_Declare(libusbdvd
|
||||
GIT_REPOSITORY https://github.com/proconsule/libusbdvd.git
|
||||
GIT_TAG 3cb0613
|
||||
)
|
||||
FetchContent_MakeAvailable(nvjpg)
|
||||
|
||||
set(USE_NEW_ZSTD ON)
|
||||
# has issues with some homebrew and game icons (oxenfree, overwatch2).
|
||||
set(USE_NVJPG OFF)
|
||||
add_library(nvjpg
|
||||
${nvjpg_SOURCE_DIR}/lib/decoder.cpp
|
||||
${nvjpg_SOURCE_DIR}/lib/image.cpp
|
||||
${nvjpg_SOURCE_DIR}/lib/surface.cpp
|
||||
)
|
||||
|
||||
target_include_directories(nvjpg PUBLIC ${nvjpg_SOURCE_DIR}/include)
|
||||
set_target_properties(nvjpg PROPERTIES CXX_STANDARD 26)
|
||||
|
||||
target_link_libraries(nvjpg PRIVATE nvjpg)
|
||||
target_compile_definitions(sphaira PRIVATE ENABLE_NVJPG)
|
||||
endif()
|
||||
|
||||
if (ENABLE_NSZ)
|
||||
target_sources(sphaira PRIVATE source/utils/nsz_dumper.cpp)
|
||||
target_compile_definitions(sphaira PRIVATE ENABLE_NSZ)
|
||||
endif()
|
||||
|
||||
if (ENABLE_LIBUSBHSFS)
|
||||
# enable this if you want ntfs and ext4 support, at the cost of a huge final binary size.
|
||||
set(USBHSFS_GPL OFF)
|
||||
set(USBHSFS_SXOS_DISABLE ON)
|
||||
|
||||
FetchContent_Declare(libusbhsfs
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/libusbhsfs.git
|
||||
GIT_TAG 625269b
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(libusbhsfs)
|
||||
|
||||
target_compile_definitions(sphaira PRIVATE ENABLE_LIBUSBHSFS)
|
||||
target_link_libraries(sphaira PRIVATE libusbhsfs)
|
||||
else()
|
||||
target_sources(sphaira PRIVATE source/ff16/ffunicode.c)
|
||||
endif()
|
||||
|
||||
if (ENABLE_LIBUSBDVD)
|
||||
FetchContent_Declare(libusbdvd
|
||||
GIT_REPOSITORY https://github.com/proconsule/libusbdvd.git
|
||||
GIT_TAG 3cb0613
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(libusbdvd)
|
||||
|
||||
add_library(libusbdvd
|
||||
${libusbdvd_SOURCE_DIR}/source/usbdvd.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/usbdvd_scsi.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/usbdvd_utils.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/usbdvd_datadisc.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/audiocdfs/audiocdfs.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/audiocdfs/cdaudio_devoptab.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/iso9660/usbdvd_iso9660.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/iso9660/iso9660_devoptab.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/udf/usbdvd_udf.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/udf/udf_devoptab.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/os/switch/switch_usb.cpp
|
||||
|
||||
)
|
||||
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/)
|
||||
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/os/switch)
|
||||
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs)
|
||||
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs/audiocdfs)
|
||||
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs/iso9660)
|
||||
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs/udf)
|
||||
target_include_directories(libusbdvd PUBLIC ${libusbdvd_SOURCE_DIR}/include)
|
||||
|
||||
target_compile_definitions(sphaira PRIVATE ENABLE_LIBUSBDVD)
|
||||
target_link_libraries(sphaira PRIVATE libusbdvd)
|
||||
target_sources(sphaira PRIVATE source/usbdvd.cpp)
|
||||
endif()
|
||||
|
||||
if (ENABLE_FTPSRV)
|
||||
FetchContent_Declare(ftpsrv
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
|
||||
GIT_TAG 7c82402
|
||||
SOURCE_SUBDIR NONE
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(ftpsrv)
|
||||
|
||||
set(FTPSRV_LIB_BUILD 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 16)
|
||||
set(FTPSRV_LIB_BUF_SIZE 1024*64)
|
||||
|
||||
set(FTPSRV_LIB_CUSTOM_DEFINES
|
||||
USE_VFS_SAVE=$<BOOL:FALSE>
|
||||
USE_VFS_STORAGE=$<BOOL:TRUE>
|
||||
# disabled as it may conflict with the gamecard menu.
|
||||
USE_VFS_GC=$<BOOL:FALSE>
|
||||
USE_VFS_USBHSFS=$<BOOL:FALSE>
|
||||
VFS_NX_BUFFER_IO=$<BOOL:TRUE>
|
||||
# let sphaira handle init / closing of the hdd.
|
||||
USE_VFS_USBHSFS_INIT=$<BOOL:FALSE>
|
||||
# disable romfs mounting as otherwise we cannot write / modify sphaira.nro
|
||||
USE_VFS_ROMFS=$<BOOL:FALSE>
|
||||
FTP_SOCKET_HEADER="${ftpsrv_SOURCE_DIR}/src/platform/nx/socket_nx.h"
|
||||
)
|
||||
|
||||
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_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)
|
||||
|
||||
target_compile_definitions(sphaira PRIVATE ENABLE_FTPSRV)
|
||||
target_link_libraries(sphaira PRIVATE ftpsrv_helper)
|
||||
|
||||
target_sources(sphaira PRIVATE
|
||||
source/ftpsrv_helper.cpp
|
||||
source/ui/menus/ftp_menu.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (ENABLE_LIBHAZE)
|
||||
FetchContent_Declare(libhaze
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git
|
||||
GIT_TAG 81154c1
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(libhaze)
|
||||
target_compile_definitions(sphaira PRIVATE ENABLE_LIBHAZE)
|
||||
target_link_libraries(sphaira PRIVATE libhaze)
|
||||
|
||||
target_sources(sphaira PRIVATE
|
||||
source/haze_helper.cpp
|
||||
source/ui/menus/mtp_menu.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (ENABLE_DEVOPTAB_HTTP)
|
||||
target_compile_definitions(sphaira PRIVATE ENABLE_DEVOPTAB_HTTP)
|
||||
target_sources(sphaira PRIVATE source/utils/devoptab_http.cpp)
|
||||
endif()
|
||||
|
||||
if (ENABLE_DEVOPTAB_NFS)
|
||||
FetchContent_Declare(libnfs
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/libnfs.git
|
||||
GIT_TAG 65f3e11
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(libnfs)
|
||||
target_compile_definitions(sphaira PRIVATE ENABLE_DEVOPTAB_NFS)
|
||||
target_link_libraries(sphaira PRIVATE nfs)
|
||||
target_sources(sphaira PRIVATE source/utils/devoptab_nfs.cpp)
|
||||
|
||||
# todo: fix this upstream as nfs should export these folders.
|
||||
target_include_directories(sphaira PRIVATE
|
||||
${libnfs_SOURCE_DIR}/include
|
||||
${libnfs_SOURCE_DIR}/include/nfsc
|
||||
${libnfs_SOURCE_DIR}/nfs
|
||||
)
|
||||
endif()
|
||||
|
||||
if (ENABLE_DEVOPTAB_SMB2)
|
||||
FetchContent_Declare(libsmb2
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/libsmb2.git
|
||||
GIT_TAG 867beea
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(libsmb2)
|
||||
target_compile_definitions(sphaira PRIVATE ENABLE_DEVOPTAB_SMB2)
|
||||
target_link_libraries(sphaira PRIVATE smb2)
|
||||
target_sources(sphaira PRIVATE source/utils/devoptab_smb2.cpp)
|
||||
endif()
|
||||
|
||||
if (ENABLE_DEVOPTAB_FTP)
|
||||
target_compile_definitions(sphaira PRIVATE ENABLE_DEVOPTAB_FTP)
|
||||
target_sources(sphaira PRIVATE source/utils/devoptab_ftp.cpp)
|
||||
endif()
|
||||
|
||||
if (ENABLE_DEVOPTAB_SFTP)
|
||||
# set to build from source, otherwise it will link against the older dkp libssh2.
|
||||
if (1)
|
||||
set(CRYPTO_BACKEND mbedTLS)
|
||||
set(ENABLE_ZLIB_COMPRESSION ON)
|
||||
set(ENABLE_DEBUG_LOGGING OFF)
|
||||
set(BUILD_EXAMPLES OFF)
|
||||
set(BUILD_TESTING OFF)
|
||||
set(LINT OFF)
|
||||
|
||||
FetchContent_Declare(libssh2
|
||||
GIT_REPOSITORY https://github.com/libssh2/libssh2.git
|
||||
# GIT_TAG a0dafb3 # latest commit, works fine, but i'll stick to main release.
|
||||
GIT_TAG libssh2-1.11.1
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(libssh2)
|
||||
target_link_libraries(sphaira PRIVATE libssh2::libssh2)
|
||||
else()
|
||||
include(FindPkgConfig)
|
||||
pkg_check_modules(LIBSSH2 libssh2 REQUIRED)
|
||||
target_include_directories(sphaira PRIVATE ${LIBSSH2_INCLUDE_DIRS})
|
||||
target_link_libraries(sphaira PRIVATE ${LIBSSH2_LIBRARIES})
|
||||
endif()
|
||||
|
||||
target_compile_definitions(sphaira PRIVATE ENABLE_DEVOPTAB_SFTP)
|
||||
target_sources(sphaira PRIVATE source/utils/devoptab_sftp.cpp)
|
||||
endif()
|
||||
|
||||
if (ENABLE_DEVOPTAB_WEBDAV)
|
||||
set(PUGIXML_NO_EXCEPTIONS ON)
|
||||
set(PUGIXML_WCHAR_MODE OFF)
|
||||
|
||||
FetchContent_Declare(pugixml
|
||||
GIT_REPOSITORY https://github.com/zeux/pugixml.git
|
||||
GIT_TAG v1.15
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(pugixml)
|
||||
target_compile_definitions(sphaira PRIVATE ENABLE_DEVOPTAB_WEBDAV)
|
||||
target_link_libraries(sphaira PRIVATE pugixml)
|
||||
target_sources(sphaira PRIVATE source/utils/devoptab_webdav.cpp)
|
||||
endif()
|
||||
|
||||
if (ENABLE_AUDIO_MP3)
|
||||
FetchContent_Declare(id3v2lib
|
||||
GIT_REPOSITORY https://github.com/larsbs/id3v2lib.git
|
||||
GIT_TAG 141ffb8
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(id3v2lib)
|
||||
target_link_libraries(sphaira PRIVATE id3v2lib)
|
||||
target_compile_definitions(sphaira PRIVATE ENABLE_AUDIO_MP3)
|
||||
endif()
|
||||
|
||||
if (ENABLE_AUDIO_OGG)
|
||||
target_compile_definitions(sphaira PRIVATE ENABLE_AUDIO_OGG)
|
||||
endif()
|
||||
|
||||
if (ENABLE_AUDIO_WAV)
|
||||
target_compile_definitions(sphaira PRIVATE ENABLE_AUDIO_WAV)
|
||||
endif()
|
||||
|
||||
if (ENABLE_AUDIO_FLAC)
|
||||
target_compile_definitions(sphaira PRIVATE ENABLE_AUDIO_FLAC)
|
||||
endif()
|
||||
|
||||
# ztsd
|
||||
set(ZSTD_BUILD_STATIC ON)
|
||||
set(ZSTD_BUILD_SHARED OFF)
|
||||
set(ZSTD_BUILD_COMPRESSION ON)
|
||||
set(ZSTD_MULTITHREAD_SUPPORT ON)
|
||||
set(ZSTD_BUILD_COMPRESSION ${ENABLE_NSZ})
|
||||
set(ZSTD_MULTITHREAD_SUPPORT ${ENABLE_NSZ})
|
||||
set(ZSTD_BUILD_DECOMPRESSION ON)
|
||||
set(ZSTD_BUILD_DICTBUILDER OFF)
|
||||
set(ZSTD_LEGACY_SUPPORT OFF)
|
||||
set(ZSTD_BUILD_PROGRAMS OFF)
|
||||
set(ZSTD_BUILD_TESTS OFF)
|
||||
|
||||
# minini
|
||||
set(MININI_LIB_NAME minIni)
|
||||
set(MININI_USE_STDIO ON)
|
||||
set(MININI_USE_NX OFF)
|
||||
set(MININI_USE_FLOAT ON)
|
||||
|
||||
# nanovg
|
||||
if (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
|
||||
set(NANOVG_DEBUG ON)
|
||||
endif()
|
||||
@@ -287,6 +545,7 @@ set(NANOVG_NO_HDR ON)
|
||||
set(NANOVG_NO_PIC ON)
|
||||
set(NANOVG_NO_PNM ON)
|
||||
|
||||
# yyjson
|
||||
set(YYJSON_INSTALL OFF)
|
||||
set(YYJSON_DISABLE_READER OFF)
|
||||
set(YYJSON_DISABLE_WRITER OFF)
|
||||
@@ -296,63 +555,17 @@ set(YYJSON_DISABLE_NON_STANDARD ON)
|
||||
set(YYJSON_DISABLE_UTF8_VALIDATION ON)
|
||||
set(YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS OFF)
|
||||
|
||||
# enable this if you want ntfs and ext4 support, at the cost of a huge final binary size.
|
||||
set(USBHSFS_GPL OFF)
|
||||
set(USBHSFS_SXOS_DISABLE ON)
|
||||
|
||||
FetchContent_MakeAvailable(
|
||||
ftpsrv
|
||||
libhaze
|
||||
libpulsar
|
||||
nanovg
|
||||
stb
|
||||
minIni
|
||||
yyjson
|
||||
zstd
|
||||
libusbhsfs
|
||||
libnxtc
|
||||
nvjpg
|
||||
dr_libs
|
||||
id3v2lib
|
||||
libusbdvd
|
||||
)
|
||||
|
||||
set(FTPSRV_LIB_BUILD 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 16)
|
||||
set(FTPSRV_LIB_BUF_SIZE 1024*64)
|
||||
|
||||
set(FTPSRV_LIB_CUSTOM_DEFINES
|
||||
USE_VFS_SAVE=$<BOOL:FALSE>
|
||||
USE_VFS_STORAGE=$<BOOL:TRUE>
|
||||
# disabled as it may conflict with the gamecard menu.
|
||||
USE_VFS_GC=$<BOOL:FALSE>
|
||||
USE_VFS_USBHSFS=$<BOOL:TRUE>
|
||||
VFS_NX_BUFFER_IO=$<BOOL:TRUE>
|
||||
# let sphaira handle init / closing of the hdd.
|
||||
USE_VFS_USBHSFS_INIT=$<BOOL:FALSE>
|
||||
# disable romfs mounting as otherwise we cannot write / modify sphaira.nro
|
||||
USE_VFS_ROMFS=$<BOOL:FALSE>
|
||||
FTP_SOCKET_HEADER="${ftpsrv_SOURCE_DIR}/src/platform/nx/socket_nx.h"
|
||||
)
|
||||
|
||||
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_storage.c
|
||||
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_stdio.c
|
||||
${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs/vfs_nx_hdd.c
|
||||
${ftpsrv_SOURCE_DIR}/src/platform/nx/utils.c
|
||||
)
|
||||
|
||||
target_link_libraries(ftpsrv_helper PUBLIC ftpsrv libusbhsfs)
|
||||
target_include_directories(ftpsrv_helper PUBLIC ${ftpsrv_SOURCE_DIR}/src/platform)
|
||||
|
||||
add_library(stb INTERFACE)
|
||||
target_include_directories(stb INTERFACE ${stb_SOURCE_DIR})
|
||||
|
||||
@@ -366,38 +579,6 @@ add_library(libnxtc
|
||||
)
|
||||
target_include_directories(libnxtc PUBLIC ${libnxtc_SOURCE_DIR}/include)
|
||||
|
||||
add_library(libusbdvd
|
||||
${libusbdvd_SOURCE_DIR}/source/usbdvd.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/usbdvd_scsi.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/usbdvd_utils.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/usbdvd_datadisc.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/audiocdfs/audiocdfs.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/audiocdfs/cdaudio_devoptab.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/iso9660/usbdvd_iso9660.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/iso9660/iso9660_devoptab.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/udf/usbdvd_udf.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/fs/udf/udf_devoptab.cpp
|
||||
${libusbdvd_SOURCE_DIR}/source/os/switch/switch_usb.cpp
|
||||
|
||||
)
|
||||
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/)
|
||||
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/os/switch)
|
||||
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs)
|
||||
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs/audiocdfs)
|
||||
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs/iso9660)
|
||||
target_include_directories(libusbdvd PRIVATE ${libusbdvd_SOURCE_DIR}/source/fs/udf)
|
||||
target_include_directories(libusbdvd PUBLIC ${libusbdvd_SOURCE_DIR}/include)
|
||||
|
||||
if (USE_NVJPG)
|
||||
add_library(nvjpg
|
||||
${nvjpg_SOURCE_DIR}/lib/decoder.cpp
|
||||
${nvjpg_SOURCE_DIR}/lib/image.cpp
|
||||
${nvjpg_SOURCE_DIR}/lib/surface.cpp
|
||||
)
|
||||
target_include_directories(nvjpg PUBLIC ${nvjpg_SOURCE_DIR}/include)
|
||||
set_target_properties(nvjpg PROPERTIES CXX_STANDARD 26)
|
||||
endif()
|
||||
|
||||
find_package(ZLIB REQUIRED)
|
||||
find_library(minizip_lib minizip REQUIRED)
|
||||
find_path(minizip_inc minizip REQUIRED)
|
||||
@@ -406,11 +587,6 @@ find_package(CURL REQUIRED)
|
||||
find_path(mbedtls_inc mbedtls REQUIRED)
|
||||
find_library(mbedcrypto_lib mbedcrypto REQUIRED)
|
||||
|
||||
if (NOT USE_NEW_ZSTD)
|
||||
find_path(zstd_inc zstd.h REQUIRED)
|
||||
find_library(zstd_lib zstd REQUIRED)
|
||||
endif()
|
||||
|
||||
add_library(fatfs
|
||||
source/ff16/diskio.c
|
||||
source/ff16/ff.c
|
||||
@@ -426,19 +602,15 @@ set_target_properties(sphaira PROPERTIES
|
||||
)
|
||||
|
||||
target_link_libraries(sphaira PRIVATE
|
||||
ftpsrv_helper
|
||||
libhaze
|
||||
libpulsar
|
||||
minIni
|
||||
nanovg
|
||||
stb
|
||||
yyjson
|
||||
# libusbhsfs
|
||||
libnxtc
|
||||
fatfs
|
||||
dr_libs
|
||||
id3v2lib
|
||||
libusbdvd
|
||||
libzstd_static
|
||||
|
||||
${minizip_lib}
|
||||
ZLIB::ZLIB
|
||||
@@ -446,20 +618,6 @@ target_link_libraries(sphaira PRIVATE
|
||||
${mbedcrypto_lib}
|
||||
)
|
||||
|
||||
if (USE_NEW_ZSTD)
|
||||
message(STATUS "USING UPSTREAM ZSTD")
|
||||
target_link_libraries(sphaira PRIVATE libzstd_static)
|
||||
else()
|
||||
message(STATUS "USING LOCAL ZSTD")
|
||||
target_link_libraries(sphaira PRIVATE ${zstd_lib})
|
||||
target_include_directories(sphaira PRIVATE ${zstd_inc})
|
||||
endif()
|
||||
|
||||
if (USE_NVJPG)
|
||||
target_link_libraries(sphaira PRIVATE nvjpg)
|
||||
target_compile_definitions(sphaira PRIVATE USE_NVJPG)
|
||||
endif()
|
||||
|
||||
target_include_directories(sphaira PRIVATE
|
||||
include
|
||||
${minizip_inc}
|
||||
|
||||
@@ -95,7 +95,6 @@ public:
|
||||
static auto GetInstallEmummcEnable() -> bool;
|
||||
static auto GetInstallSdEnable() -> bool;
|
||||
static auto GetThemeMusicEnable() -> bool;
|
||||
static auto Get12HourTimeEnable() -> bool;
|
||||
static auto GetLanguage() -> long;
|
||||
static auto GetTextScrollSpeed() -> long;
|
||||
|
||||
@@ -127,10 +126,13 @@ public:
|
||||
static void DisplayThemeOptions(bool left_side = true);
|
||||
// todo:
|
||||
static void DisplayNetworkOptions(bool left_side = true);
|
||||
static void DisplayMiscOptions(bool left_side = true);
|
||||
static void DisplayMenuOptions(bool left_side = true);
|
||||
static void DisplayAdvancedOptions(bool left_side = true);
|
||||
static void DisplayInstallOptions(bool left_side = true);
|
||||
static void DisplayDumpOptions(bool left_side = true);
|
||||
static void DisplayFtpOptions(bool left_side = true);
|
||||
static void DisplayMtpOptions(bool left_side = true);
|
||||
static void DisplayHddOptions(bool left_side = true);
|
||||
|
||||
// helper for sidebar options to toggle install on/off
|
||||
static void ShowEnableInstallPromptOption(option::OptionBool& option, bool& enable);
|
||||
@@ -155,6 +157,9 @@ public:
|
||||
static Result SetDefaultBackgroundMusic(fs::Fs* fs, const fs::FsPath& path);
|
||||
static void SetBackgroundMusicPause(bool pause);
|
||||
|
||||
static Result GetSdSize(s64* free, s64* total);
|
||||
static Result GetEmmcSize(s64* free, s64* total);
|
||||
|
||||
// helper that converts 1.2.3 to a u32 used for comparisons.
|
||||
static auto GetVersionFromString(const char* str) -> u32;
|
||||
static auto IsVersionNewer(const char* current, const char* new_version) -> u32;
|
||||
@@ -267,6 +272,7 @@ public:
|
||||
PadState m_pad{};
|
||||
TouchInfo m_touch_info{};
|
||||
Controller m_controller{};
|
||||
KeyboardState m_keyboard{};
|
||||
std::vector<ThemeMeta> m_theme_meta_entries;
|
||||
|
||||
Vec2 m_scale{1, 1};
|
||||
@@ -296,8 +302,9 @@ public:
|
||||
option::OptionString m_default_music{INI_SECTION, "default_music", "/config/sphaira/themes/default_music.bfstm"};
|
||||
option::OptionString m_theme_path{INI_SECTION, "theme", DEFAULT_THEME_PATH};
|
||||
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
|
||||
option::OptionBool m_12hour_time{INI_SECTION, "12hour_time", false};
|
||||
option::OptionBool m_show_ip_addr{INI_SECTION, "show_ip_addr", true};
|
||||
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto
|
||||
option::OptionString m_center_menu{INI_SECTION, "center_side_menu", "Homebrew"};
|
||||
option::OptionString m_left_menu{INI_SECTION, "left_side_menu", "FileBrowser"};
|
||||
option::OptionString m_right_menu{INI_SECTION, "right_side_menu", "Appstore"};
|
||||
option::OptionBool m_progress_boost_mode{INI_SECTION, "progress_boost_mode", true};
|
||||
@@ -338,6 +345,39 @@ public:
|
||||
// todo: move this into it's own menu
|
||||
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal
|
||||
|
||||
// ftp options.
|
||||
option::OptionLong m_ftp_port{"ftp", "port", 5000};
|
||||
option::OptionBool m_ftp_anon{"ftp", "anon", true};
|
||||
option::OptionString m_ftp_user{"ftp", "user", ""};
|
||||
option::OptionString m_ftp_pass{"ftp", "pass", ""};
|
||||
option::OptionBool m_ftp_show_album{"ftp", "show_album", true};
|
||||
option::OptionBool m_ftp_show_ams_contents{"ftp", "show_ams_contents", false};
|
||||
option::OptionBool m_ftp_show_bis_storage{"ftp", "show_bis_storage", false};
|
||||
option::OptionBool m_ftp_show_bis_fs{"ftp", "show_bis_fs", false};
|
||||
option::OptionBool m_ftp_show_content_system{"ftp", "show_content_system", false};
|
||||
option::OptionBool m_ftp_show_content_user{"ftp", "show_content_user", false};
|
||||
option::OptionBool m_ftp_show_content_sd{"ftp", "show_content_sd", false};
|
||||
// option::OptionBool m_ftp_show_content_sd0{"ftp", "show_content_sd0", false};
|
||||
// option::OptionBool m_ftp_show_custom_system{"ftp", "show_custom_system", false};
|
||||
// option::OptionBool m_ftp_show_custom_sd{"ftp", "show_custom_sd", false};
|
||||
option::OptionBool m_ftp_show_games{"ftp", "show_games", true};
|
||||
option::OptionBool m_ftp_show_install{"ftp", "show_install", true};
|
||||
option::OptionBool m_ftp_show_mounts{"ftp", "show_mounts", false};
|
||||
option::OptionBool m_ftp_show_switch{"ftp", "show_switch", false};
|
||||
|
||||
// mtp options.
|
||||
option::OptionLong m_mtp_vid{"mtp", "vid", 0x057e}; // nintendo (hidden from ui)
|
||||
option::OptionLong m_mtp_pid{"mtp", "pid", 0x201d}; // switch (hidden from ui)
|
||||
option::OptionBool m_mtp_allocate_file{"mtp", "allocate_file", true};
|
||||
option::OptionBool m_mtp_show_album{"mtp", "show_album", true};
|
||||
option::OptionBool m_mtp_show_content_sd{"mtp", "show_content_sd", false};
|
||||
option::OptionBool m_mtp_show_content_system{"mtp", "show_content_system", false};
|
||||
option::OptionBool m_mtp_show_content_user{"mtp", "show_content_user", false};
|
||||
option::OptionBool m_mtp_show_games{"mtp", "show_games", true};
|
||||
option::OptionBool m_mtp_show_install{"mtp", "show_install", true};
|
||||
option::OptionBool m_mtp_show_mounts{"mtp", "show_mounts", false};
|
||||
option::OptionBool m_mtp_show_speedtest{"mtp", "show_speedtest", false};
|
||||
|
||||
std::shared_ptr<fs::FsNativeSd> m_fs{};
|
||||
audio::SongID m_background_music{};
|
||||
|
||||
|
||||
@@ -511,7 +511,22 @@ enum class SphairaResult : Result {
|
||||
FsNewPathEmpty,
|
||||
FsLoadingCancelled,
|
||||
FsBrokenRoot,
|
||||
|
||||
FsUnknownStdioError,
|
||||
FsStdioFailedToSeek,
|
||||
FsStdioFailedToRead,
|
||||
FsStdioFailedToWrite,
|
||||
FsStdioFailedToOpenFile,
|
||||
FsStdioFailedToCreate,
|
||||
FsStdioFailedToTruncate,
|
||||
FsStdioFailedToFlush,
|
||||
FsStdioFailedToCreateDirectory,
|
||||
FsStdioFailedToDeleteFile,
|
||||
FsStdioFailedToDeleteDirectory,
|
||||
FsStdioFailedToOpenDirectory,
|
||||
FsStdioFailedToRename,
|
||||
FsStdioFailedToStat,
|
||||
|
||||
FsReadOnly,
|
||||
FsNotActive,
|
||||
FsFailedStdioStat,
|
||||
@@ -680,6 +695,19 @@ enum : Result {
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsLoadingCancelled),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsBrokenRoot),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsUnknownStdioError),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToSeek),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToRead),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToWrite),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToOpenFile),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToCreate),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToTruncate),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToFlush),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToCreateDirectory),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToDeleteFile),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToDeleteDirectory),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToOpenDirectory),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToRename),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsStdioFailedToStat),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsReadOnly),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsNotActive),
|
||||
MAKE_SPHAIRA_RESULT_ENUM(FsFailedStdioStat),
|
||||
@@ -823,11 +851,83 @@ enum : Result {
|
||||
#define CONCATENATE(s1, s2) CONCATENATE_IMPL(s1, s2)
|
||||
#define ANONYMOUS_VARIABLE(pref) CONCATENATE(pref, __COUNTER__)
|
||||
|
||||
#define ON_SCOPE_EXIT(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { _f; }};
|
||||
template<typename Function>
|
||||
struct ScopeGuard {
|
||||
ScopeGuard(Function&& function) : m_function(std::forward<Function>(function)) {
|
||||
|
||||
}
|
||||
~ScopeGuard() {
|
||||
m_function();
|
||||
}
|
||||
|
||||
ScopeGuard(const ScopeGuard&) = delete;
|
||||
void operator=(const ScopeGuard&) = delete;
|
||||
|
||||
private:
|
||||
const Function m_function;
|
||||
};
|
||||
|
||||
struct ScopedMutex {
|
||||
ScopedMutex(Mutex* mutex) : m_mutex{mutex} {
|
||||
mutexLock(m_mutex);
|
||||
}
|
||||
~ScopedMutex() {
|
||||
mutexUnlock(m_mutex);
|
||||
}
|
||||
|
||||
ScopedMutex(const ScopedMutex&) = delete;
|
||||
void operator=(const ScopedMutex&) = delete;
|
||||
|
||||
private:
|
||||
Mutex* const m_mutex;
|
||||
};
|
||||
|
||||
struct ScopedRMutex {
|
||||
ScopedRMutex(RMutex* _mutex) : mutex{_mutex} {
|
||||
rmutexLock(mutex);
|
||||
}
|
||||
|
||||
~ScopedRMutex() {
|
||||
rmutexUnlock(mutex);
|
||||
}
|
||||
|
||||
ScopedRMutex(const ScopedRMutex&) = delete;
|
||||
void operator=(const ScopedRMutex&) = delete;
|
||||
|
||||
private:
|
||||
RMutex* const mutex;
|
||||
};
|
||||
|
||||
struct ScopedRwLock {
|
||||
ScopedRwLock(RwLock* _lock, bool _write) : lock{_lock}, write{_write} {
|
||||
if (write) {
|
||||
rwlockWriteLock(lock);
|
||||
} else {
|
||||
rwlockReadLock(lock);
|
||||
}
|
||||
}
|
||||
|
||||
~ScopedRwLock() {
|
||||
if (write) {
|
||||
rwlockWriteUnlock(lock);
|
||||
} else {
|
||||
rwlockReadUnlock(lock);
|
||||
}
|
||||
}
|
||||
|
||||
ScopedRwLock(const ScopedRwLock&) = delete;
|
||||
void operator=(const ScopedRwLock&) = delete;
|
||||
|
||||
private:
|
||||
RwLock* const lock;
|
||||
bool const write;
|
||||
};
|
||||
|
||||
// #define ON_SCOPE_EXIT(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { _f; }};
|
||||
#define ON_SCOPE_EXIT(_f) ScopeGuard ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { _f; }};
|
||||
#define SCOPED_MUTEX(_m) ScopedMutex ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){_m}
|
||||
#define SCOPED_RMUTEX(_m) ScopedRMutex ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){_m}
|
||||
#define SCOPED_RWLOCK(_m, _write) ScopedRwLock ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){_m, _write}
|
||||
|
||||
// #define ON_SCOPE_FAIL(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_FAILED(rc)) { _f; } }};
|
||||
// #define ON_SCOPE_SUCCESS(_f) std::experimental::scope_exit ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE_){[&] { if (R_SUCCEEDED(rc)) { _f; } }};
|
||||
|
||||
// mutex helpers.
|
||||
#define SCOPED_MUTEX(mutex) \
|
||||
mutexLock(mutex); \
|
||||
ON_SCOPE_EXIT(mutexUnlock(mutex))
|
||||
|
||||
@@ -23,8 +23,6 @@ enum DumpLocationType {
|
||||
DumpLocationType_DevNull,
|
||||
// dump to stdio, ideal for custom mount points using devoptab, such as hdd.
|
||||
DumpLocationType_Stdio,
|
||||
// dump to custom locations found in locations.ini.
|
||||
DumpLocationType_Network,
|
||||
};
|
||||
|
||||
enum DumpLocationFlag {
|
||||
@@ -33,8 +31,7 @@ enum DumpLocationFlag {
|
||||
DumpLocationFlag_UsbS2S = 1 << DumpLocationType_UsbS2S,
|
||||
DumpLocationFlag_DevNull = 1 << DumpLocationType_DevNull,
|
||||
DumpLocationFlag_Stdio = 1 << DumpLocationType_Stdio,
|
||||
DumpLocationFlag_Network = 1 << DumpLocationType_Network,
|
||||
DumpLocationFlag_All = DumpLocationFlag_SdCard | DumpLocationFlag_Usb | DumpLocationFlag_UsbS2S | DumpLocationFlag_DevNull | DumpLocationFlag_Stdio | DumpLocationFlag_Network,
|
||||
DumpLocationFlag_All = DumpLocationFlag_SdCard | DumpLocationFlag_Usb | DumpLocationFlag_UsbS2S | DumpLocationFlag_DevNull | DumpLocationFlag_Stdio,
|
||||
};
|
||||
|
||||
struct DumpEntry {
|
||||
@@ -44,7 +41,6 @@ struct DumpEntry {
|
||||
|
||||
struct DumpLocation {
|
||||
DumpEntry entry{};
|
||||
location::Entries network{};
|
||||
location::StdioEntries stdio{};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::fatfs {
|
||||
|
||||
Result MountAll();
|
||||
void UnmountAll();
|
||||
|
||||
} // namespace sphaira::fatfs
|
||||
@@ -4,12 +4,26 @@
|
||||
#include <dirent.h>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <sys/syslimits.h>
|
||||
#include "defines.hpp"
|
||||
|
||||
namespace fs {
|
||||
|
||||
enum OpenMode : u32 {
|
||||
OpenMode_Read = FsOpenMode_Read,
|
||||
OpenMode_Write = FsOpenMode_Write,
|
||||
OpenMode_Append = FsOpenMode_Append,
|
||||
|
||||
// enables buffering for stdio based files.
|
||||
OpenMode_EnableBuffer = 1 << 16,
|
||||
OpenMode_ReadBuffered = OpenMode_Read | OpenMode_EnableBuffer,
|
||||
OpenMode_WriteBuffered = OpenMode_Write | OpenMode_EnableBuffer,
|
||||
OpenMode_AppendBuffered = OpenMode_Append | OpenMode_EnableBuffer,
|
||||
};
|
||||
|
||||
struct FsPath {
|
||||
FsPath() = default;
|
||||
|
||||
@@ -138,20 +152,24 @@ struct FsPath {
|
||||
return *this;
|
||||
}
|
||||
|
||||
static constexpr bool path_equal(std::string_view a, std::string_view b) {
|
||||
return a.length() == b.length() && !strncasecmp(a.data(), b.data(), a.length());
|
||||
}
|
||||
|
||||
constexpr bool operator==(const FsPath& v) const noexcept {
|
||||
return !strcasecmp(*this, v);
|
||||
return path_equal(*this, v);
|
||||
}
|
||||
|
||||
constexpr bool operator==(const char* v) const noexcept {
|
||||
return !strcasecmp(*this, v);
|
||||
return path_equal(*this, v);
|
||||
}
|
||||
|
||||
constexpr bool operator==(const std::string& v) const noexcept {
|
||||
return !strncasecmp(*this, v.data(), v.length());
|
||||
return path_equal(*this, v);
|
||||
}
|
||||
|
||||
constexpr bool operator==(const std::string_view v) const noexcept {
|
||||
return !strncasecmp(*this, v.data(), v.length());
|
||||
return path_equal(*this, v);
|
||||
}
|
||||
|
||||
static consteval bool Test(const auto& str) {
|
||||
@@ -164,7 +182,7 @@ struct FsPath {
|
||||
return path[0] == str[0];
|
||||
}
|
||||
|
||||
char s[FS_MAX_PATH]{};
|
||||
char s[PATH_MAX]{};
|
||||
};
|
||||
|
||||
inline FsPath operator+(const char* v, const FsPath& fp) {
|
||||
@@ -183,8 +201,12 @@ inline FsPath operator+(const std::string_view& v, const FsPath& fp) {
|
||||
}
|
||||
|
||||
// Fs seems to be limted to file paths of 255 characters.
|
||||
// i've disabled this as network mounts will often have very long paths
|
||||
// that do not have this limit.
|
||||
// a proper fix would be to return an error if the path is too long and the path
|
||||
// is native.
|
||||
struct FsPathReal {
|
||||
static constexpr inline size_t FS_REAL_MAX_LENGTH = 255;
|
||||
static constexpr inline size_t FS_REAL_MAX_LENGTH = PATH_MAX;
|
||||
|
||||
constexpr FsPathReal(const FsPath& str) : FsPathReal{str.s} { }
|
||||
explicit constexpr FsPathReal(const char* str) {
|
||||
@@ -211,7 +233,7 @@ struct FsPathReal {
|
||||
constexpr operator const char*() const { return s; }
|
||||
constexpr operator std::string_view() const { return s; }
|
||||
|
||||
char s[FS_MAX_PATH];
|
||||
char s[PATH_MAX];
|
||||
};
|
||||
|
||||
// fwd
|
||||
@@ -229,7 +251,6 @@ struct File {
|
||||
fs::Fs* m_fs{};
|
||||
FsFile m_native{};
|
||||
std::FILE* m_stdio{};
|
||||
s64 m_stdio_off{};
|
||||
u32 m_mode{};
|
||||
};
|
||||
|
||||
@@ -299,7 +320,7 @@ Result IsDirEmpty(fs::Fs* m_fs, const fs::FsPath& path, bool* out);
|
||||
|
||||
// helpers.
|
||||
Result read_entire_file(Fs* fs, const FsPath& path, std::vector<u8>& out);
|
||||
Result write_entire_file(Fs* fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only = true);
|
||||
Result write_entire_file(Fs* fs, const FsPath& path, std::span<const u8> in, bool ignore_read_only = true);
|
||||
Result copy_entire_file(Fs* fs, const FsPath& dst, const FsPath& src, bool ignore_read_only = true);
|
||||
|
||||
struct Fs {
|
||||
@@ -346,7 +367,7 @@ struct Fs {
|
||||
Result read_entire_file(const FsPath& path, std::vector<u8>& out) {
|
||||
return fs::read_entire_file(this, path, out);
|
||||
}
|
||||
Result write_entire_file(const FsPath& path, const std::vector<u8>& in) {
|
||||
Result write_entire_file(const FsPath& path, std::span<const u8> in) {
|
||||
return fs::write_entire_file(this, path, in, m_ignore_read_only);
|
||||
}
|
||||
Result copy_entire_file(const FsPath& dst, const FsPath& src) {
|
||||
|
||||
@@ -15,8 +15,6 @@ enum class Type {
|
||||
Sha1,
|
||||
Sha256,
|
||||
Null,
|
||||
Deflate,
|
||||
Zstd,
|
||||
};
|
||||
|
||||
struct BaseSource {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace sphaira::haze {
|
||||
namespace sphaira::libhaze {
|
||||
|
||||
bool Init();
|
||||
bool IsInit();
|
||||
@@ -15,4 +15,4 @@ using OnInstallClose = std::function<void()>;
|
||||
void InitInstallMode(const OnInstallStart& on_start, const OnInstallWrite& on_write, const OnInstallClose& on_close);
|
||||
void DisableInstallMode();
|
||||
|
||||
} // namespace sphaira::haze
|
||||
} // namespace sphaira::libhaze
|
||||
|
||||
@@ -5,10 +5,21 @@
|
||||
|
||||
namespace sphaira::i18n {
|
||||
|
||||
enum class WordOrder {
|
||||
PhraseName, // default: SVO (English, French, German, etc.)
|
||||
NamePhrase // SOV (Japanese, Korean)
|
||||
};
|
||||
|
||||
bool init(long index);
|
||||
void exit();
|
||||
|
||||
std::string get(std::string_view str);
|
||||
std::string get(std::string_view str, std::string_view fallback);
|
||||
|
||||
WordOrder GetWordOrder();
|
||||
bool WordOrderLocale();
|
||||
|
||||
std::string Reorder(std::string_view phrase, std::string_view name);
|
||||
|
||||
} // namespace sphaira::i18n
|
||||
|
||||
|
||||
@@ -3,23 +3,13 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <switch.h>
|
||||
// to import FsEntryFlags.
|
||||
// todo: this should be part of a smaller header, such as filesystem_types.hpp
|
||||
#include "ui/menus/filebrowser.hpp"
|
||||
|
||||
namespace sphaira::location {
|
||||
|
||||
struct Entry {
|
||||
std::string name{};
|
||||
std::string url{};
|
||||
std::string user{};
|
||||
std::string pass{};
|
||||
std::string bearer{};
|
||||
std::string pub_key{};
|
||||
std::string priv_key{};
|
||||
u16 port{};
|
||||
};
|
||||
using Entries = std::vector<Entry>;
|
||||
|
||||
auto Load() -> Entries;
|
||||
void Add(const Entry& e);
|
||||
using FsEntryFlag = ui::menu::filebrowser::FsEntryFlag;
|
||||
|
||||
// helper for hdd devices.
|
||||
// this doesn't really belong in this header, however
|
||||
@@ -29,14 +19,19 @@ struct StdioEntry {
|
||||
std::string mount{};
|
||||
// ums0: (USB Flash Disk)
|
||||
std::string name{};
|
||||
// set if read-only.
|
||||
bool write_protect;
|
||||
// FsEntryFlag
|
||||
u32 flags{};
|
||||
// optional dump path inside the mount point.
|
||||
std::string dump_path{};
|
||||
// set to hide for filebrowser.
|
||||
bool fs_hidden{};
|
||||
// set to hide in dump list.
|
||||
bool dump_hidden{};
|
||||
};
|
||||
|
||||
using StdioEntries = std::vector<StdioEntry>;
|
||||
|
||||
// set write=true to filter out write protected devices.
|
||||
auto GetStdio(bool write) -> StdioEntries;
|
||||
auto GetFat() -> StdioEntries;
|
||||
|
||||
} // namespace sphaira::location
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
#include <switch.h>
|
||||
#include <string>
|
||||
#include <sys/syslimits.h>
|
||||
|
||||
namespace sphaira::swkbd {
|
||||
|
||||
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, const char* initial = nullptr, s64 len_min = -1, s64 len_max = FS_MAX_PATH);
|
||||
Result ShowText(std::string& out, const char* header = nullptr, const char* guide = nullptr, const char* initial = nullptr, s64 len_min = -1, s64 len_max = PATH_MAX);
|
||||
Result ShowNumPad(s64& out, const char* header = nullptr, const char* guide = nullptr, const char* initial = nullptr, s64 len_min = -1, s64 len_max = PATH_MAX);
|
||||
|
||||
} // namespace sphaira::swkbd
|
||||
|
||||
@@ -59,6 +59,7 @@ void Clear();
|
||||
|
||||
// adds new entry to queue.
|
||||
void PushAsync(u64 app_id);
|
||||
void PushAsync(const std::span<const NsApplicationRecord> app_ids);
|
||||
// gets entry without removing it from the queue.
|
||||
auto GetAsync(u64 app_id) -> ThreadResultData*;
|
||||
// single threaded title info fetch.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "fs.hpp"
|
||||
#include "option.hpp"
|
||||
#include "hasher.hpp"
|
||||
#include "nro.hpp"
|
||||
#include <span>
|
||||
|
||||
namespace sphaira::ui::menu::filebrowser {
|
||||
@@ -16,18 +17,16 @@ enum FsOption : u32 {
|
||||
|
||||
// can split screen.
|
||||
FsOption_CanSplit = BIT(0),
|
||||
// can upload files.
|
||||
FsOption_CanUpload = BIT(1),
|
||||
// can selected multiple files.
|
||||
FsOption_CanSelect = BIT(2),
|
||||
FsOption_CanSelect = BIT(1),
|
||||
// shows the option to install.
|
||||
FsOption_CanInstall = BIT(3),
|
||||
FsOption_CanInstall = BIT(2),
|
||||
// loads file assoc.
|
||||
FsOption_LoadAssoc = BIT(4),
|
||||
FsOption_LoadAssoc = BIT(3),
|
||||
// do not prompt on exit even if not tabbed.
|
||||
FsOption_DoNotPrompt = BIT(5),
|
||||
FsOption_DoNotPrompt = BIT(4),
|
||||
|
||||
FsOption_Normal = FsOption_LoadAssoc | FsOption_CanInstall | FsOption_CanSplit | FsOption_CanUpload | FsOption_CanSelect,
|
||||
FsOption_Normal = FsOption_LoadAssoc | FsOption_CanInstall | FsOption_CanSplit | FsOption_CanSelect,
|
||||
FsOption_All = FsOption_DoNotPrompt | FsOption_Normal,
|
||||
FsOption_Picker = FsOption_NONE,
|
||||
};
|
||||
@@ -39,7 +38,12 @@ enum FsEntryFlag {
|
||||
// supports file assoc.
|
||||
FsEntryFlag_Assoc = 1 << 1,
|
||||
// this is an sd card, files can be launched from here.
|
||||
FsEntryFlag_IsSd = 1 << 2,
|
||||
FsEntryFlag_IsSd = 1 << 2, // todo: remove this.
|
||||
// do not stat files in this entry (faster for network mount).
|
||||
FsEntryFlag_NoStatFile = 1 << 3,
|
||||
FsEntryFlag_NoStatDir = 1 << 4,
|
||||
FsEntryFlag_NoRandomReads = 1 << 5,
|
||||
FsEntryFlag_NoRandomWrites = 1 << 6,
|
||||
};
|
||||
|
||||
enum class FsType {
|
||||
@@ -90,6 +94,22 @@ struct FsEntry {
|
||||
return flags & FsEntryFlag_IsSd;
|
||||
}
|
||||
|
||||
auto IsNoStatFile() const -> bool {
|
||||
return flags & FsEntryFlag_NoStatFile;
|
||||
}
|
||||
|
||||
auto IsNoStatDir() const -> bool {
|
||||
return flags & FsEntryFlag_NoStatDir;
|
||||
}
|
||||
|
||||
auto IsNoRandomReads() const -> bool {
|
||||
return flags & FsEntryFlag_NoRandomReads;
|
||||
}
|
||||
|
||||
auto IsNoRandomWrites() const -> bool {
|
||||
return flags & FsEntryFlag_NoRandomWrites;
|
||||
}
|
||||
|
||||
auto IsSame(const FsEntry& e) const {
|
||||
return root == e.root && type == e.type;
|
||||
}
|
||||
@@ -460,6 +480,15 @@ protected:
|
||||
|
||||
std::vector<std::string> m_filter{};
|
||||
|
||||
// local copy of nro entries that is loaded in LoadAssocEntriesPath()
|
||||
// if homebrew::GetNroEntries() returns nothing, usually due to
|
||||
// the menu not being loaded.
|
||||
// this is a bit of a hack to support replacing the homebrew menu tab,
|
||||
// sphaira wasn't really designed for this.
|
||||
// however this will work for now, until i add support for additional
|
||||
// nro scan mounts, at which point this won't scale.
|
||||
std::vector<NroEntry> m_nro_entries{};
|
||||
|
||||
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Alphabetical};
|
||||
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
|
||||
option::OptionBool m_show_hidden{INI_SECTION, "show_hidden", false};
|
||||
|
||||
@@ -43,6 +43,8 @@ enum OrderType {
|
||||
|
||||
using LayoutType = grid::LayoutType;
|
||||
|
||||
void SignalChange();
|
||||
|
||||
struct Menu final : grid::Menu {
|
||||
Menu(u32 flags);
|
||||
~Menu();
|
||||
|
||||
@@ -34,7 +34,7 @@ auto GetNroEntries() -> std::span<const NroEntry>;
|
||||
void SignalChange();
|
||||
|
||||
struct Menu final : grid::Menu {
|
||||
Menu();
|
||||
Menu(u32 flags);
|
||||
~Menu();
|
||||
|
||||
auto GetShortTitle() const -> const char* override { return "Apps"; };
|
||||
|
||||
@@ -42,7 +42,7 @@ struct MiscMenuEntry {
|
||||
}
|
||||
};
|
||||
|
||||
auto GetMiscMenuEntries() -> std::span<const MiscMenuEntry>;
|
||||
auto GetMenuMenuEntries() -> std::span<const MiscMenuEntry>;
|
||||
|
||||
// this holds 2 menus and allows for switching between them
|
||||
struct MainMenu final : Widget {
|
||||
|
||||
@@ -13,12 +13,14 @@ enum MenuFlag {
|
||||
|
||||
struct PolledData {
|
||||
struct tm tm{};
|
||||
u32 battery_percetange{};
|
||||
PsmChargerType charger_type{};
|
||||
NifmInternetConnectionType type{};
|
||||
NifmInternetConnectionStatus status{};
|
||||
u32 strength{};
|
||||
u32 ip{};
|
||||
s64 sd_free{1};
|
||||
s64 sd_total{1};
|
||||
s64 emmc_free{1};
|
||||
s64 emmc_total{1};
|
||||
};
|
||||
|
||||
struct MenuBase : Widget {
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
|
||||
namespace sphaira::ui::menu::save {
|
||||
|
||||
enum BackupFlag {
|
||||
BackupFlag_None = 0,
|
||||
// option to allow the user to set the save file name.
|
||||
BackupFlag_SetName = 1 << 0,
|
||||
// set if this is a auto backup (on restore).
|
||||
BackupFlag_IsAuto = 1 << 1,
|
||||
};
|
||||
|
||||
struct Entry final : FsSaveDataInfo {
|
||||
NacpLanguageEntry lang{};
|
||||
int image{};
|
||||
@@ -82,13 +90,13 @@ private:
|
||||
|
||||
void DisplayOptions();
|
||||
|
||||
void BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries);
|
||||
void BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries, u32 flags);
|
||||
void RestoreSave();
|
||||
|
||||
auto BuildSavePath(const Entry& e, bool is_auto) const -> fs::FsPath;
|
||||
Result RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath& path) const;
|
||||
Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, Entry& e, bool compressed, bool is_auto = false) const;
|
||||
Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, std::span<const std::reference_wrapper<Entry>> entries, bool compressed, bool is_auto = false) const;
|
||||
auto BuildSavePath(const Entry& e, u32 flags) const -> fs::FsPath;
|
||||
Result RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath& path);
|
||||
Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, Entry& e, u32 flags);
|
||||
Result BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, std::span<const std::reference_wrapper<Entry>> entries, u32 flags);
|
||||
|
||||
Result MountSaveFs();
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ void drawScrollbar2(NVGcontext*, const Theme*, s64 index_off, s64 count, s64 row
|
||||
|
||||
void drawAppLable(NVGcontext* vg, const Theme*, ScrollingText& st, float x, float y, float w, const char* name);
|
||||
|
||||
void drawSpinner(NVGcontext* vg, const Theme*, float cx, float cy, float r, float t);
|
||||
|
||||
void updateHighlightAnimation();
|
||||
void getHighlightAnimation(float* gradientX, float* gradientY, float* color);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace sphaira::ui {
|
||||
struct ProgressBox;
|
||||
using ProgressBoxCallback = std::function<Result(ProgressBox*)>;
|
||||
using ProgressBoxDoneCallback = std::function<void(Result rc)>;
|
||||
// using CancelCallback = std::function<void()>;
|
||||
|
||||
struct ProgressBox final : Widget {
|
||||
ProgressBox(
|
||||
@@ -39,6 +40,9 @@ struct ProgressBox final : Widget {
|
||||
auto ShouldExit() -> bool;
|
||||
auto ShouldExitResult() -> Result;
|
||||
|
||||
void AddCancelEvent(UEvent* event);
|
||||
void RemoveCancelEvent(const UEvent* event);
|
||||
|
||||
// helper functions
|
||||
auto CopyFile(fs::Fs* fs_src, fs::Fs* fs_dst, const fs::FsPath& src, const fs::FsPath& dst, bool single_threaded = false) -> Result;
|
||||
auto CopyFile(fs::Fs* fs, const fs::FsPath& src, const fs::FsPath& dst, bool single_threaded = false) -> Result;
|
||||
@@ -82,6 +86,7 @@ private:
|
||||
Thread m_thread{};
|
||||
ThreadData m_thread_data{};
|
||||
ProgressBoxDoneCallback m_done{};
|
||||
std::vector<UEvent*> m_cancel_events{};
|
||||
|
||||
// shared data start.
|
||||
std::string m_action{};
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <memory>
|
||||
#include <concepts>
|
||||
#include <utility>
|
||||
#include <sys/syslimits.h>
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
@@ -43,6 +44,14 @@ public:
|
||||
m_depends_click = depends_click;
|
||||
}
|
||||
|
||||
void SetDirty(bool dirty = true) {
|
||||
m_dirty = dirty;
|
||||
}
|
||||
|
||||
auto IsDirty() const -> bool {
|
||||
return m_dirty;
|
||||
}
|
||||
|
||||
protected:
|
||||
auto IsEnabled() const -> bool {
|
||||
if (m_depends_callback) {
|
||||
@@ -68,6 +77,7 @@ private:
|
||||
DependsClickCallback m_depends_click{};
|
||||
ScrollingText m_scolling_title{};
|
||||
ScrollingText m_scolling_value{};
|
||||
bool m_dirty{};
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
@@ -174,12 +184,27 @@ private:
|
||||
|
||||
class SidebarEntryTextInput final : public SidebarEntryTextBase {
|
||||
public:
|
||||
explicit SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& guide = {}, s64 len_min = -1, s64 len_max = FS_MAX_PATH, const std::string& info = "");
|
||||
using Callback = std::function<void(SidebarEntryTextInput* input)>;
|
||||
|
||||
public:
|
||||
// uses normal keyboard.
|
||||
explicit SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& header = {}, const std::string& guide = {}, s64 len_min = -1, s64 len_max = PATH_MAX, const std::string& info = "", const Callback& callback = nullptr);
|
||||
// uses numpad.
|
||||
explicit SidebarEntryTextInput(const std::string& title, s64 value, const std::string& header = {}, const std::string& guide = {}, s64 len_min = -1, s64 len_max = PATH_MAX, const std::string& info = "", const Callback& callback = nullptr);
|
||||
|
||||
auto GetNumValue() const -> s64 {
|
||||
return std::stoul(GetValue());
|
||||
}
|
||||
|
||||
void SetNumValue(s64 value) {
|
||||
SetValue(std::to_string(value));
|
||||
}
|
||||
private:
|
||||
const std::string m_header;
|
||||
const std::string m_guide;
|
||||
const s64 m_len_min;
|
||||
const s64 m_len_max;
|
||||
const Callback m_callback;
|
||||
};
|
||||
|
||||
class SidebarEntryFilePicker final : public SidebarEntryTextBase {
|
||||
@@ -199,12 +224,12 @@ class Sidebar : public Widget {
|
||||
public:
|
||||
enum class Side { LEFT, RIGHT };
|
||||
using Items = std::vector<std::unique_ptr<SidebarEntryBase>>;
|
||||
using OnExitWhenChangedCallback = std::function<void()>;
|
||||
|
||||
public:
|
||||
explicit Sidebar(const std::string& title, Side side, Items&& items);
|
||||
explicit Sidebar(const std::string& title, Side side);
|
||||
explicit Sidebar(const std::string& title, const std::string& sub, Side side, Items&& items);
|
||||
explicit Sidebar(const std::string& title, const std::string& sub, Side side);
|
||||
explicit Sidebar(const std::string& title, Side side, float width = 450.f);
|
||||
explicit Sidebar(const std::string& title, const std::string& sub, Side side, float width = 450.f);
|
||||
~Sidebar();
|
||||
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
@@ -218,6 +243,12 @@ public:
|
||||
return (T*)Add(std::make_unique<T>(std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
// sets a callback that is called on exit when the any options were changed.
|
||||
// the change detection isn't perfect, it just checks if the A button was pressed...
|
||||
void SetOnExitWhenChanged(const OnExitWhenChangedCallback& cb) {
|
||||
m_on_exit_when_changed = cb;
|
||||
}
|
||||
|
||||
private:
|
||||
void SetIndex(s64 index);
|
||||
void SetupButtons();
|
||||
@@ -226,19 +257,28 @@ private:
|
||||
const std::string m_title;
|
||||
const std::string m_sub;
|
||||
const Side m_side;
|
||||
Items m_items;
|
||||
Items m_items{};
|
||||
s64 m_index{};
|
||||
|
||||
std::unique_ptr<List> m_list;
|
||||
std::unique_ptr<List> m_list{};
|
||||
|
||||
Vec4 m_top_bar{};
|
||||
Vec4 m_bottom_bar{};
|
||||
Vec2 m_title_pos{};
|
||||
Vec4 m_base_pos{};
|
||||
|
||||
OnExitWhenChangedCallback m_on_exit_when_changed{};
|
||||
|
||||
static constexpr float m_title_size{28.f};
|
||||
// static constexpr Vec2 box_size{380.f, 70.f};
|
||||
static constexpr Vec2 m_box_size{400.f, 70.f};
|
||||
};
|
||||
|
||||
class FormSidebar : public Sidebar {
|
||||
public:
|
||||
explicit FormSidebar(const std::string& title) : Sidebar{title, Side::LEFT, 540.f} {
|
||||
// explicit FormSidebar(const std::string& title) : Sidebar{title, Side::LEFT} {
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui
|
||||
|
||||
@@ -366,6 +366,81 @@ struct Action final {
|
||||
std::string m_hint{};
|
||||
};
|
||||
|
||||
struct GenericHidState {
|
||||
GenericHidState() {
|
||||
Reset();
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
buttons_cur = 0;
|
||||
buttons_old = 0;
|
||||
}
|
||||
|
||||
u64 GetButtons() const {
|
||||
return buttons_cur;
|
||||
}
|
||||
|
||||
u64 GetButtonsDown() const {
|
||||
return buttons_cur & ~buttons_old;
|
||||
}
|
||||
|
||||
u64 GetButtonsUp() const {
|
||||
return ~buttons_cur & buttons_old;
|
||||
}
|
||||
|
||||
virtual void Update() = 0;
|
||||
|
||||
protected:
|
||||
u64 buttons_cur;
|
||||
u64 buttons_old;
|
||||
};
|
||||
|
||||
struct KeyboardState final : GenericHidState {
|
||||
struct MapEntry {
|
||||
HidKeyboardKey key;
|
||||
u64 button;
|
||||
};
|
||||
using Map = std::span<const MapEntry>;
|
||||
|
||||
void Init(Map map) {
|
||||
m_map = map;
|
||||
Reset();
|
||||
}
|
||||
|
||||
void Update() override {
|
||||
buttons_old = buttons_cur;
|
||||
buttons_cur = 0;
|
||||
|
||||
if (!hidGetKeyboardStates(&m_state, 1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto ctrl = m_state.modifiers & HidKeyboardModifier_Control;
|
||||
const auto shift = m_state.modifiers & HidKeyboardModifier_Shift;
|
||||
|
||||
for (const auto& map : m_map) {
|
||||
if (hidKeyboardStateGetKey(&m_state, map.key)) {
|
||||
if (shift && map.button == static_cast<u64>(Button::L)) {
|
||||
buttons_cur |= static_cast<u64>(Button::L2);
|
||||
} else if (shift && map.button == static_cast<u64>(Button::R)) {
|
||||
buttons_cur |= static_cast<u64>(Button::R2);
|
||||
} else if (ctrl && map.button == static_cast<u64>(Button::L)) {
|
||||
buttons_cur |= static_cast<u64>(Button::L3);
|
||||
} else if (ctrl && map.button == static_cast<u64>(Button::R)) {
|
||||
buttons_cur |= static_cast<u64>(Button::R3);
|
||||
} else {
|
||||
buttons_cur |= map.button;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Map m_map{};
|
||||
HidKeyboardState m_state{};
|
||||
};
|
||||
|
||||
struct Controller {
|
||||
u64 m_kdown{};
|
||||
u64 m_kheld{};
|
||||
@@ -399,15 +474,29 @@ struct Controller {
|
||||
|
||||
void UpdateButtonHeld(u64 buttons, double delta) {
|
||||
if (m_kdown & buttons) {
|
||||
m_step = 50;
|
||||
m_step_max = m_MAX_STEP;
|
||||
m_step = m_INC_STEP;
|
||||
m_counter = 0;
|
||||
m_step_max_counter = 0;
|
||||
} else if (m_kheld & buttons) {
|
||||
m_counter += m_step * delta;
|
||||
|
||||
// if we are at the max, ignore the delta and go as fast as the frame rate.
|
||||
if (m_step_max == m_MAX) {
|
||||
m_counter = m_MAX;
|
||||
}
|
||||
|
||||
if (m_counter >= m_MAX) {
|
||||
m_kdown |= m_kheld & buttons;
|
||||
m_counter = 0;
|
||||
m_step = std::min(m_step + 50, m_MAX_STEP);
|
||||
m_step = std::min(m_step + m_INC_STEP, m_step_max);
|
||||
|
||||
// slowly speed up until we reach 1 button down per frame.
|
||||
m_step_max_counter++;
|
||||
if (m_step_max_counter >= 5) {
|
||||
m_step_max_counter = 0;
|
||||
m_step_max = std::min(m_step_max + m_INC_STEP, m_MAX);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -415,8 +504,12 @@ struct Controller {
|
||||
private:
|
||||
static constexpr double m_MAX = 1000;
|
||||
static constexpr double m_MAX_STEP = 250;
|
||||
double m_step = 50;
|
||||
static constexpr double m_INC_STEP = 50;
|
||||
|
||||
double m_step_max = m_MAX_STEP;
|
||||
double m_step = m_INC_STEP;
|
||||
double m_counter = 0;
|
||||
int m_step_max_counter = 0;
|
||||
};
|
||||
|
||||
} // namespace sphaira
|
||||
|
||||
@@ -24,6 +24,14 @@ struct Usb {
|
||||
// Result OpenFile(u32 index, s64& file_size);
|
||||
Result CloseFile();
|
||||
|
||||
auto GetOpenResult() const {
|
||||
return m_open_result;
|
||||
}
|
||||
|
||||
auto GetCancelEvent() {
|
||||
return m_usb->GetCancelEvent();
|
||||
}
|
||||
|
||||
private:
|
||||
Result SendAndVerify(const void* data, u32 size, u64 timeout, api::ResultPacket* out = nullptr);
|
||||
Result SendAndVerify(const void* data, u32 size, api::ResultPacket* out = nullptr);
|
||||
|
||||
@@ -25,6 +25,14 @@ struct Usb {
|
||||
Result OpenFile(u32 index, s64& file_size);
|
||||
Result CloseFile();
|
||||
|
||||
auto GetOpenResult() const {
|
||||
return m_open_result;
|
||||
}
|
||||
|
||||
auto GetCancelEvent() {
|
||||
return m_usb->GetCancelEvent();
|
||||
}
|
||||
|
||||
private:
|
||||
Result SendAndVerify(const void* data, u32 size, u64 timeout, api::ResultPacket* out = nullptr);
|
||||
Result SendAndVerify(const void* data, u32 size, api::ResultPacket* out = nullptr);
|
||||
|
||||
@@ -29,6 +29,14 @@ struct Usb {
|
||||
|
||||
Result file_transfer_loop();
|
||||
|
||||
auto GetOpenResult() const {
|
||||
return m_open_result;
|
||||
}
|
||||
|
||||
auto GetCancelEvent() {
|
||||
return m_usb->GetCancelEvent();
|
||||
}
|
||||
|
||||
private:
|
||||
Result SendResult(u32 result, u32 arg3 = 0, u32 arg4 = 0);
|
||||
|
||||
|
||||
@@ -2,34 +2,42 @@
|
||||
|
||||
#include "fs.hpp"
|
||||
#include "yati/source/base.hpp"
|
||||
#include "location.hpp"
|
||||
|
||||
#include <switch.h>
|
||||
#include <memory>
|
||||
|
||||
namespace sphaira::devoptab {
|
||||
|
||||
// mounts to "lower_case_hex_id:/"
|
||||
Result MountSaveSystem(u64 id, fs::FsPath& out_path);
|
||||
void UnmountSave(u64 id);
|
||||
|
||||
Result MountZip(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
|
||||
void UmountZip(const fs::FsPath& mount);
|
||||
|
||||
Result MountNsp(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
|
||||
void UmountNsp(const fs::FsPath& mount);
|
||||
|
||||
Result MountXci(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
|
||||
Result MountXciSource(const std::shared_ptr<sphaira::yati::source::Base>& source, s64 size, const fs::FsPath& path, fs::FsPath& out_path);
|
||||
void UmountXci(const fs::FsPath& mount);
|
||||
|
||||
Result MountNca(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
|
||||
Result MountNcaNcm(NcmContentStorage* cs, const NcmContentId* id, fs::FsPath& out_path);
|
||||
void UmountNca(const fs::FsPath& mount);
|
||||
|
||||
Result MountBfsar(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
|
||||
void UmountBfsar(const fs::FsPath& mount);
|
||||
|
||||
Result MountNro(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
|
||||
void UmountNro(const fs::FsPath& mount);
|
||||
|
||||
Result MountVfsAll();
|
||||
Result MountWebdavAll();
|
||||
Result MountHttpAll();
|
||||
Result MountFtpAll();
|
||||
Result MountSftpAll();
|
||||
Result MountNfsAll();
|
||||
Result MountSmb2All();
|
||||
Result MountFatfsAll();
|
||||
Result MountGameAll();
|
||||
Result MountInternalMounts();
|
||||
|
||||
Result GetNetworkDevices(location::StdioEntries& out);
|
||||
void UmountAllNeworkDevices();
|
||||
void UmountNeworkDevice(const fs::FsPath& mount);
|
||||
|
||||
// manually set the array so that we can avoid nullptr access.
|
||||
// SEE: https://github.com/devkitPro/newlib/issues/35
|
||||
void FixDkpBug();
|
||||
|
||||
void DisplayDevoptabSideBar();
|
||||
|
||||
} // namespace sphaira::devoptab
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
|
||||
#include "yati/source/file.hpp"
|
||||
#include "utils/lru.hpp"
|
||||
#include "location.hpp"
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <curl/curl.h>
|
||||
|
||||
namespace sphaira::devoptab::common {
|
||||
|
||||
@@ -81,6 +86,142 @@ private:
|
||||
std::vector<BufferedFileData> buffered_large{}; // 1MiB
|
||||
};
|
||||
|
||||
bool fix_path(const char* str, char* out);
|
||||
bool fix_path(const char* str, char* out, bool strip_leading_slash = false);
|
||||
|
||||
void update_devoptab_for_read_only(devoptab_t* devoptab, bool read_only);
|
||||
|
||||
struct PushPullThreadData {
|
||||
static constexpr size_t MAX_BUFFER_SIZE = 1024 * 64; // 64KB max buffer
|
||||
|
||||
explicit PushPullThreadData(CURL* _curl);
|
||||
virtual ~PushPullThreadData();
|
||||
|
||||
Result CreateAndStart();
|
||||
void Cancel();
|
||||
bool IsRunning();
|
||||
|
||||
// only set curl=true if called from a curl callback.
|
||||
size_t PullData(char* data, size_t total_size, bool curl = false);
|
||||
size_t PushData(const char* data, size_t total_size, bool curl = false);
|
||||
|
||||
static size_t progress_callback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow);
|
||||
|
||||
private:
|
||||
static void thread_func(void* arg);
|
||||
|
||||
public:
|
||||
CURL* const curl{};
|
||||
std::vector<char> buffer{};
|
||||
Mutex mutex{};
|
||||
CondVar can_push{};
|
||||
CondVar can_pull{};
|
||||
|
||||
long code{};
|
||||
bool error{};
|
||||
bool finished{};
|
||||
bool started{};
|
||||
|
||||
private:
|
||||
Thread thread{};
|
||||
};
|
||||
|
||||
struct MountConfig {
|
||||
std::string name{};
|
||||
std::string url{};
|
||||
std::string user{};
|
||||
std::string pass{};
|
||||
std::string dump_path{};
|
||||
long port{};
|
||||
long timeout{};
|
||||
bool read_only{};
|
||||
bool no_stat_file{true};
|
||||
bool no_stat_dir{true};
|
||||
bool fs_hidden{};
|
||||
bool dump_hidden{};
|
||||
|
||||
std::unordered_map<std::string, std::string> extra{};
|
||||
};
|
||||
using MountConfigs = std::vector<MountConfig>;
|
||||
|
||||
struct PullThreadData final : PushPullThreadData {
|
||||
using PushPullThreadData::PushPullThreadData;
|
||||
static size_t pull_thread_callback(char *ptr, size_t size, size_t nmemb, void *userdata);
|
||||
};
|
||||
|
||||
struct PushThreadData final : PushPullThreadData {
|
||||
using PushPullThreadData::PushPullThreadData;
|
||||
static size_t push_thread_callback(const char *ptr, size_t size, size_t nmemb, void *userdata);
|
||||
};
|
||||
|
||||
struct MountDevice {
|
||||
MountDevice(const MountConfig& _config) : config{_config} {}
|
||||
virtual ~MountDevice() = default;
|
||||
|
||||
virtual bool fix_path(const char* str, char* out, bool strip_leading_slash = false) {
|
||||
return common::fix_path(str, out, strip_leading_slash);
|
||||
}
|
||||
|
||||
virtual bool Mount() = 0;
|
||||
virtual int devoptab_open(void *fileStruct, const char *path, int flags, int mode) { return -EIO; }
|
||||
virtual int devoptab_close(void *fd) { return -EIO; }
|
||||
virtual ssize_t devoptab_read(void *fd, char *ptr, size_t len) { return -EIO; }
|
||||
virtual ssize_t devoptab_write(void *fd, const char *ptr, size_t len) { return -EIO; }
|
||||
virtual ssize_t devoptab_seek(void *fd, off_t pos, int dir) { return 0; }
|
||||
virtual int devoptab_fstat(void *fd, struct stat *st) { return -EIO; }
|
||||
virtual int devoptab_unlink(const char *path) { return -EIO; }
|
||||
virtual int devoptab_rename(const char *oldName, const char *newName) { return -EIO; }
|
||||
virtual int devoptab_mkdir(const char *path, int mode) { return -EIO; }
|
||||
virtual int devoptab_rmdir(const char *path) { return -EIO; }
|
||||
virtual int devoptab_diropen(void* fd, const char *path) { return -EIO; }
|
||||
virtual int devoptab_dirreset(void* fd) { return -EIO; }
|
||||
virtual int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) { return -EIO; }
|
||||
virtual int devoptab_dirclose(void* fd) { return -EIO; }
|
||||
virtual int devoptab_lstat(const char *path, struct stat *st) { return -EIO; }
|
||||
virtual int devoptab_ftruncate(void *fd, off_t len) { return -EIO; }
|
||||
virtual int devoptab_statvfs(const char *_path, struct statvfs *buf) { return -EIO; }
|
||||
virtual int devoptab_fsync(void *fd) { return -EIO; }
|
||||
virtual int devoptab_utimes(const char *_path, const struct timeval times[2]) { return -EIO; }
|
||||
|
||||
const MountConfig config;
|
||||
};
|
||||
|
||||
struct MountCurlDevice : MountDevice {
|
||||
using MountDevice::MountDevice;
|
||||
virtual ~MountCurlDevice();
|
||||
|
||||
PushThreadData* CreatePushData(CURL* curl, const std::string& url, size_t offset);
|
||||
PullThreadData* CreatePullData(CURL* curl, const std::string& url, bool append = false);
|
||||
|
||||
virtual bool Mount();
|
||||
virtual void curl_set_common_options(CURL* curl, const std::string& url);
|
||||
static size_t write_memory_callback(char *ptr, size_t size, size_t nmemb, void *userdata);
|
||||
static size_t write_data_callback(char *ptr, size_t size, size_t nmemb, void *userdata);
|
||||
static size_t read_data_callback(char *ptr, size_t size, size_t nmemb, void *userdata);
|
||||
static std::string html_decode(const std::string_view& str);
|
||||
static std::string url_decode(const std::string& str);
|
||||
std::string build_url(const std::string& path, bool is_dir);
|
||||
|
||||
protected:
|
||||
CURL* curl{};
|
||||
CURL* transfer_curl{};
|
||||
|
||||
private:
|
||||
// path extracted from the url.
|
||||
std::string m_url_path{};
|
||||
CURLU* curlu{};
|
||||
CURLSH* m_curl_share{};
|
||||
RwLock m_rwlocks[CURL_LOCK_DATA_LAST]{};
|
||||
bool m_mounted{};
|
||||
};
|
||||
|
||||
void LoadConfigsFromIni(const fs::FsPath& path, MountConfigs& out_configs);
|
||||
|
||||
using CreateDeviceCallback = std::function<std::unique_ptr<MountDevice>(const MountConfig& config)>;
|
||||
Result MountNetworkDevice(const CreateDeviceCallback& create_device, size_t file_size, size_t dir_size, const char* name, bool force_read_only = false);
|
||||
|
||||
// same as above but takes in the device and expects the mount name to be set.
|
||||
bool MountNetworkDevice2(std::unique_ptr<MountDevice>&& device, const MountConfig& config, size_t file_size, size_t dir_size, const char* name, const char* mount_name);
|
||||
|
||||
bool MountReadOnlyIndexDevice(const CreateDeviceCallback& create_device, size_t file_size, size_t dir_size, const char* name, fs::FsPath& out_path);
|
||||
|
||||
} // namespace sphaira::devoptab::common
|
||||
|
||||
@@ -22,4 +22,11 @@ constexpr inline T AlignDown(T value, T align) {
|
||||
return value &~ (align - 1);
|
||||
}
|
||||
|
||||
// formats size to 1.23 MB in 1024 base.
|
||||
// only uses 32 bytes so its SSO optimised, not need to cache.
|
||||
std::string formatSizeStorage(u64 size);
|
||||
|
||||
// formats size to 1.23 MB in 1000 base (used for progress bars).
|
||||
std::string formatSizeNetwork(u64 size);
|
||||
|
||||
} // namespace sphaira::utils
|
||||
|
||||
@@ -43,6 +43,14 @@ struct Usb final : Base {
|
||||
return m_usb->CloseFile();
|
||||
}
|
||||
|
||||
auto GetOpenResult() const {
|
||||
return m_usb->GetOpenResult();
|
||||
}
|
||||
|
||||
auto GetCancelEvent() {
|
||||
return m_usb->GetCancelEvent();
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<usb::install::Usb> m_usb{};
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,8 +33,6 @@ namespace {
|
||||
constexpr auto API_AGENT = "TotalJustice";
|
||||
constexpr u64 CHUNK_SIZE = 1024*1024;
|
||||
constexpr auto MAX_THREADS = 4;
|
||||
constexpr int THREAD_PRIO = 0x2F;
|
||||
constexpr int THREAD_CORE = 2;
|
||||
|
||||
std::atomic_bool g_running{};
|
||||
CURLSH* g_curl_share{};
|
||||
@@ -62,11 +60,6 @@ struct SeekCustomData {
|
||||
s64 size{};
|
||||
};
|
||||
|
||||
// helper for creating webdav folders as libcurl does not have built-in
|
||||
// support for it.
|
||||
// only creates the folders if they don't exist.
|
||||
auto WebdavCreateFolder(CURL* curl, const Api& e) -> bool;
|
||||
|
||||
auto generate_key_from_path(const fs::FsPath& path) -> std::string {
|
||||
const auto key = crc32Calculate(path.s, path.size());
|
||||
return std::to_string(key);
|
||||
@@ -595,22 +588,16 @@ auto EscapeString(CURL* curl, const std::string& str) -> std::string {
|
||||
return result;
|
||||
}
|
||||
|
||||
auto EncodeUrl(std::string url) -> std::string {
|
||||
auto EncodeUrl(const std::string& url) -> std::string {
|
||||
log_write("[CURL] encoding url\n");
|
||||
|
||||
if (url.starts_with("webdav://")) {
|
||||
log_write("[CURL] updating host\n");
|
||||
url.replace(0, std::strlen("webdav"), "https");
|
||||
log_write("[CURL] updated host: %s\n", url.c_str());
|
||||
}
|
||||
|
||||
auto clu = curl_url();
|
||||
R_UNLESS(clu, url);
|
||||
ON_SCOPE_EXIT(curl_url_cleanup(clu));
|
||||
|
||||
log_write("[CURL] setting url\n");
|
||||
CURLUcode clu_code;
|
||||
clu_code = curl_url_set(clu, CURLUPART_URL, url.c_str(), CURLU_URLENCODE);
|
||||
clu_code = curl_url_set(clu, CURLUPART_URL, url.c_str(), CURLU_DEFAULT_SCHEME | CURLU_URLENCODE);
|
||||
R_UNLESS(clu_code == CURLUE_OK, url);
|
||||
log_write("[CURL] set url success\n");
|
||||
|
||||
@@ -834,13 +821,6 @@ auto UploadInternal(CURL* curl, const Api& e) -> ApiResult {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (e.GetUrl().starts_with("webdav://")) {
|
||||
if (!WebdavCreateFolder(curl, e)) {
|
||||
log_write("[CURL] failed to create webdav folder, aborting\n");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const auto& info = e.GetUploadInfo();
|
||||
const auto url = e.GetUrl() + "/" + info.m_name;
|
||||
const auto encoded_url = EncodeUrl(url);
|
||||
@@ -960,76 +940,6 @@ auto UploadInternal(CURL* curl, const Api& e) -> ApiResult {
|
||||
return {success, http_code, header_out, chunk_out.data};
|
||||
}
|
||||
|
||||
auto WebdavCreateFolder(CURL* curl, const Api& e) -> bool {
|
||||
// if using webdav, extract the file path and create the directories.
|
||||
// https://github.com/WebDAVDevs/webdav-request-samples/blob/master/webdav_curl.md
|
||||
if (e.GetUrl().starts_with("webdav://")) {
|
||||
log_write("[CURL] found webdav url\n");
|
||||
|
||||
const auto info = e.GetUploadInfo();
|
||||
if (info.m_name.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto& file_path = info.m_name;
|
||||
log_write("got file path: %s\n", file_path.c_str());
|
||||
|
||||
const auto file_loc = file_path.find_last_of('/');
|
||||
if (file_loc == file_path.npos) {
|
||||
log_write("failed to find last slash\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto path_view = file_path.substr(0, file_loc);
|
||||
log_write("got folder path: %s\n", path_view.c_str());
|
||||
|
||||
auto e2 = e;
|
||||
e2.SetOption(Path{});
|
||||
e2.SetOption(Url{e.GetUrl() + "/" + path_view});
|
||||
e2.SetOption(Flags{e.GetFlags() | Flag_NoBody});
|
||||
e2.SetOption(CustomRequest{"PROPFIND"});
|
||||
e2.SetOption(Header{
|
||||
{ "Depth", "0" },
|
||||
});
|
||||
|
||||
// test to see if the directory exists first.
|
||||
const auto exist_result = DownloadInternal(curl, e2);
|
||||
if (exist_result.success) {
|
||||
log_write("[CURL] folder already exist: %s\n", path_view.c_str());
|
||||
return true;
|
||||
} else {
|
||||
log_write("[CURL] folder does NOT exist, manually creating: %s\n", path_view.c_str());
|
||||
}
|
||||
|
||||
// make the request to create the folder.
|
||||
std::string folder;
|
||||
for (const auto dir : std::views::split(path_view, '/')) {
|
||||
if (dir.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
folder += "/" + std::string{dir.data(), dir.size()};
|
||||
e2.SetOption(Url{e.GetUrl() + folder});
|
||||
e2.SetOption(Header{});
|
||||
e2.SetOption(CustomRequest{"MKCOL"});
|
||||
|
||||
const auto result = DownloadInternal(curl, e2);
|
||||
if (result.code == 201) {
|
||||
log_write("[CURL] created webdav directory\n");
|
||||
} else if (result.code == 405) {
|
||||
log_write("[CURL] webdav directory already exists: %ld\n", result.code);
|
||||
} else {
|
||||
log_write("[CURL] failed to create webdav directory: %ld\n", result.code);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log_write("[CURL] not a webdav url: %s\n", e.GetUrl().c_str());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void my_lock(CURL *handle, curl_lock_data data, curl_lock_access laccess, void *useptr) {
|
||||
mutexLock(&g_mutex_share[data]);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "i18n.hpp"
|
||||
#include "location.hpp"
|
||||
#include "threaded_file_transfer.hpp"
|
||||
#include "haze_helper.hpp"
|
||||
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/error_box.hpp"
|
||||
@@ -133,9 +132,9 @@ struct WriteNullSource final : WriteSource {
|
||||
struct WriteUsbSource final : WriteSource {
|
||||
WriteUsbSource(u64 transfer_timeout) {
|
||||
// disable mtp if enabled.
|
||||
m_was_mtp_enabled = haze::IsInit();
|
||||
m_was_mtp_enabled = App::GetMtpEnable();
|
||||
if (m_was_mtp_enabled) {
|
||||
haze::Exit();
|
||||
App::SetMtpEnable(false);
|
||||
}
|
||||
|
||||
m_usb = std::make_unique<usb::dump::Usb>(transfer_timeout);
|
||||
@@ -145,7 +144,7 @@ struct WriteUsbSource final : WriteSource {
|
||||
m_usb.reset();
|
||||
|
||||
if (m_was_mtp_enabled) {
|
||||
haze::Init();
|
||||
App::SetMtpEnable(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +164,14 @@ struct WriteUsbSource final : WriteSource {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
auto GetOpenResult() const {
|
||||
return m_usb->GetOpenResult();
|
||||
}
|
||||
|
||||
auto GetCancelEvent() {
|
||||
return m_usb->GetCancelEvent();
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<usb::dump::Usb> m_usb{};
|
||||
bool m_was_mtp_enabled{};
|
||||
@@ -178,8 +185,8 @@ constexpr DumpLocationEntry DUMP_LOCATIONS[]{
|
||||
};
|
||||
|
||||
struct UsbTest final : usb::upload::Usb, yati::source::Stream {
|
||||
UsbTest(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths)
|
||||
: Usb{UINT64_MAX}
|
||||
UsbTest(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths, u64 timeout)
|
||||
: Usb{timeout}
|
||||
, m_pbox{pbox}
|
||||
, m_source{source}
|
||||
, m_paths{paths} {
|
||||
@@ -248,6 +255,10 @@ struct UsbTest final : usb::upload::Usb, yati::source::Stream {
|
||||
return m_pull_offset;
|
||||
}
|
||||
|
||||
auto GetOpenResult() const {
|
||||
return Usb::GetOpenResult();
|
||||
}
|
||||
|
||||
private:
|
||||
ui::ProgressBox* m_pbox{};
|
||||
BaseSource* m_source{};
|
||||
@@ -261,19 +272,26 @@ private:
|
||||
};
|
||||
|
||||
Result DumpToUsb(ui::ProgressBox* pbox, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
|
||||
auto write_source = std::make_unique<WriteUsbSource>(3e+9);
|
||||
// create write source and verify that it opened.
|
||||
constexpr u64 timeout = UINT64_MAX;
|
||||
auto write_source = std::make_unique<WriteUsbSource>(timeout);
|
||||
R_TRY(write_source->GetOpenResult());
|
||||
|
||||
// add cancel event.
|
||||
pbox->AddCancelEvent(write_source->GetCancelEvent());
|
||||
ON_SCOPE_EXIT(pbox->RemoveCancelEvent(write_source->GetCancelEvent()));
|
||||
|
||||
for (const auto& path : paths) {
|
||||
const auto file_size = source->GetSize(path);
|
||||
pbox->SetImage(source->GetIcon(path));
|
||||
pbox->SetTitle(source->GetName(path));
|
||||
pbox->NewTransfer("Waiting for USB connection...");
|
||||
pbox->NewTransfer("Waiting for USB connection..."_i18n);
|
||||
|
||||
// wait until usb is ready.
|
||||
while (true) {
|
||||
R_TRY(pbox->ShouldExitResult());
|
||||
|
||||
const auto rc = write_source->WaitForConnection(path, 3e+9);
|
||||
const auto rc = write_source->WaitForConnection(path, timeout);
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
break;
|
||||
}
|
||||
@@ -318,7 +336,7 @@ Result DumpToFile(ui::ProgressBox* pbox, fs::Fs* fs, const fs::FsPath& root, Bas
|
||||
|
||||
{
|
||||
fs::File file;
|
||||
R_TRY(fs->OpenFile(temp_path, FsOpenMode_Write, &file));
|
||||
R_TRY(fs->OpenFile(temp_path, FsOpenMode_Write|FsOpenMode_Append, &file));
|
||||
auto write_source = std::make_unique<WriteFileSource>(&file);
|
||||
|
||||
if (custom_transfer) {
|
||||
@@ -353,7 +371,8 @@ Result DumpToFileNative(ui::ProgressBox* pbox, BaseSource* source, std::span<con
|
||||
|
||||
Result DumpToStdio(ui::ProgressBox* pbox, const location::StdioEntry& loc, BaseSource* source, std::span<const fs::FsPath> paths, const CustomTransfer& custom_transfer) {
|
||||
fs::FsStdio fs{};
|
||||
return DumpToFile(pbox, &fs, loc.mount, source, paths, custom_transfer);
|
||||
const auto mount_path = fs::AppendPath(loc.mount, loc.dump_path);
|
||||
return DumpToFile(pbox, &fs, mount_path, source, paths, custom_transfer);
|
||||
}
|
||||
|
||||
Result DumpToUsbS2SInternal(ui::ProgressBox* pbox, UsbTest* usb) {
|
||||
@@ -398,8 +417,14 @@ Result DumpToUsbS2S(ui::ProgressBox* pbox, BaseSource* source, std::span<const f
|
||||
file_list.emplace_back(path);
|
||||
}
|
||||
|
||||
auto usb = std::make_unique<UsbTest>(pbox, source, paths);
|
||||
constexpr u64 timeout = 3e+9;
|
||||
// create usb test instance and verify that it opened.
|
||||
constexpr u64 timeout = UINT64_MAX;
|
||||
auto usb = std::make_unique<UsbTest>(pbox, source, paths, timeout);
|
||||
R_TRY(usb->GetOpenResult());
|
||||
|
||||
// add cancel event.
|
||||
pbox->AddCancelEvent(usb->GetCancelEvent());
|
||||
ON_SCOPE_EXIT(pbox->RemoveCancelEvent(usb->GetCancelEvent()));
|
||||
|
||||
while (!pbox->ShouldExit()) {
|
||||
if (R_SUCCEEDED(usb->IsUsbConnected(timeout))) {
|
||||
@@ -461,54 +486,6 @@ Result DumpToDevNull(ui::ProgressBox* pbox, BaseSource* source, std::span<const
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result DumpToNetwork(ui::ProgressBox* pbox, const location::Entry& loc, BaseSource* source, std::span<const fs::FsPath> paths) {
|
||||
for (auto path : paths) {
|
||||
R_TRY(pbox->ShouldExitResult());
|
||||
|
||||
const auto file_size = source->GetSize(path);
|
||||
pbox->SetImage(source->GetIcon(path));
|
||||
pbox->SetTitle(source->GetName(path));
|
||||
pbox->NewTransfer(path);
|
||||
|
||||
R_TRY(thread::TransferPull(pbox, file_size,
|
||||
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
|
||||
return source->Read(path, data, off, size, bytes_read);
|
||||
},
|
||||
[&](thread::PullCallback pull) -> Result {
|
||||
s64 offset{};
|
||||
const auto result = curl::Api().FromMemory(
|
||||
CURL_LOCATION_TO_API(loc),
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()},
|
||||
curl::UploadInfo{
|
||||
path, file_size,
|
||||
[&](void *ptr, size_t size) -> size_t {
|
||||
// curl will request past the size of the file, causing an error.
|
||||
if (offset >= file_size) {
|
||||
log_write("finished file upload\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
u64 bytes_read{};
|
||||
if (R_FAILED(pull(ptr, size, &bytes_read))) {
|
||||
log_write("failed to read in custom callback: %zd size: %zd\n", offset, size);
|
||||
return 0;
|
||||
}
|
||||
|
||||
offset += bytes_read;
|
||||
return bytes_read;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
R_UNLESS(result.success, Result_DumpFailedNetworkUpload);
|
||||
R_SUCCEED();
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocation& on_loc, const CustomTransfer& custom_transfer) {
|
||||
@@ -516,19 +493,17 @@ void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocat
|
||||
ui::PopupList::Items items;
|
||||
std::vector<DumpEntry> dump_entries;
|
||||
|
||||
out.network = location::Load();
|
||||
if (!custom_transfer && location_flags & (1 << DumpLocationType_Network)) {
|
||||
for (s32 i = 0; i < std::size(out.network); i++) {
|
||||
dump_entries.emplace_back(DumpLocationType_Network, i);
|
||||
items.emplace_back(out.network[i].name);
|
||||
}
|
||||
}
|
||||
|
||||
out.stdio = location::GetStdio(true);
|
||||
const auto stdio_entries = location::GetStdio(true);
|
||||
if (location_flags & (1 << DumpLocationType_Stdio)) {
|
||||
for (s32 i = 0; i < std::size(out.stdio); i++) {
|
||||
dump_entries.emplace_back(DumpLocationType_Stdio, i);
|
||||
items.emplace_back(out.stdio[i].name);
|
||||
for (auto& e : stdio_entries) {
|
||||
if (e.dump_hidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto index = out.stdio.size();
|
||||
dump_entries.emplace_back(DumpLocationType_Stdio, index);
|
||||
items.emplace_back(e.name);
|
||||
out.stdio.emplace_back(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,9 +528,7 @@ void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocat
|
||||
}
|
||||
|
||||
Result Dump(ui::ProgressBox* pbox, const std::shared_ptr<BaseSource>& source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, const CustomTransfer& custom_transfer) {
|
||||
if (location.entry.type == DumpLocationType_Network) {
|
||||
R_TRY(DumpToNetwork(pbox, location.network[location.entry.index], source.get(), paths));
|
||||
} else if (location.entry.type == DumpLocationType_Stdio) {
|
||||
if (location.entry.type == DumpLocationType_Stdio) {
|
||||
R_TRY(DumpToStdio(pbox, location.stdio[location.entry.index], source.get(), paths, custom_transfer));
|
||||
} else if (location.entry.type == DumpLocationType_SdCard) {
|
||||
R_TRY(DumpToFileNative(pbox, source.get(), paths, custom_transfer));
|
||||
|
||||
10908
sphaira/source/ff16/ffunicode.c
Normal file
10908
sphaira/source/ff16/ffunicode.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -134,7 +134,7 @@ Result read_entire_file(Fs* fs, const FsPath& path, std::vector<u8>& out) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result write_entire_file(Fs* fs, const FsPath& path, const std::vector<u8>& in, bool ignore_read_only) {
|
||||
Result write_entire_file(Fs* fs, const FsPath& path, std::span<const u8> in, bool ignore_read_only) {
|
||||
R_UNLESS(ignore_read_only || !is_read_only(path), Result_FsReadOnly);
|
||||
|
||||
if (auto rc = fs->CreateFile(path, in.size(), 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
||||
@@ -215,14 +215,18 @@ Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& _path, bool ig
|
||||
rc = CreateDirectory(path, ignore_read_only);
|
||||
}
|
||||
|
||||
if (R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
||||
log_write("failed to create folder: %s\n", path.s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
// log_write("created_directory: %s\n", path);
|
||||
std::strcat(path, "/");
|
||||
}
|
||||
|
||||
// only check if the last folder creation failed.
|
||||
// reason being is that it may try to create "/" root folder, which some network
|
||||
// fs will return a EPERM/EACCES error.
|
||||
// however if the last directory failed, then it is a real error.
|
||||
if (R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
||||
log_write("failed to create folder: %s\n", path.s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
@@ -231,7 +235,7 @@ Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& _path,
|
||||
|
||||
// strip file name form path.
|
||||
const auto last_slash = std::strrchr(_path, '/');
|
||||
if (!last_slash) {
|
||||
if (!last_slash || last_slash == _path.s) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
@@ -317,12 +321,12 @@ Result CreateFile(const FsPathReal& path, u64 size, u32 option, bool ignore_read
|
||||
}
|
||||
|
||||
R_TRY(fsdevGetLastResult());
|
||||
return Result_FsUnknownStdioError;
|
||||
return Result_FsStdioFailedToCreate;
|
||||
}
|
||||
ON_SCOPE_EXIT(close(fd));
|
||||
|
||||
if (size) {
|
||||
R_UNLESS(!ftruncate(fd, size), Result_FsUnknownStdioError);
|
||||
R_UNLESS(!ftruncate(fd, size), Result_FsStdioFailedToTruncate);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
@@ -337,7 +341,7 @@ Result CreateDirectory(const FsPathReal& path, bool ignore_read_only) {
|
||||
}
|
||||
|
||||
R_TRY(fsdevGetLastResult());
|
||||
return Result_FsUnknownStdioError;
|
||||
return Result_FsStdioFailedToCreateDirectory;
|
||||
}
|
||||
R_SUCCEED();
|
||||
}
|
||||
@@ -359,7 +363,7 @@ Result DeleteFile(const FsPathReal& path, bool ignore_read_only) {
|
||||
|
||||
if (unlink(path)) {
|
||||
R_TRY(fsdevGetLastResult());
|
||||
return Result_FsUnknownStdioError;
|
||||
return Result_FsStdioFailedToDeleteFile;
|
||||
}
|
||||
R_SUCCEED();
|
||||
}
|
||||
@@ -369,7 +373,7 @@ Result DeleteDirectory(const FsPathReal& path, bool ignore_read_only) {
|
||||
|
||||
if (rmdir(path)) {
|
||||
R_TRY(fsdevGetLastResult());
|
||||
return Result_FsUnknownStdioError;
|
||||
return Result_FsStdioFailedToDeleteDirectory;
|
||||
}
|
||||
R_SUCCEED();
|
||||
}
|
||||
@@ -401,7 +405,7 @@ Result RenameFile(const FsPathReal& src, const FsPathReal& dst, bool ignore_read
|
||||
|
||||
if (rename(src, dst)) {
|
||||
R_TRY(fsdevGetLastResult());
|
||||
return Result_FsUnknownStdioError;
|
||||
return Result_FsStdioFailedToRename;
|
||||
}
|
||||
R_SUCCEED();
|
||||
}
|
||||
@@ -417,7 +421,7 @@ Result GetEntryType(const FsPathReal& path, FsDirEntryType* out) {
|
||||
struct stat st;
|
||||
if (stat(path, &st)) {
|
||||
R_TRY(fsdevGetLastResult());
|
||||
return Result_FsUnknownStdioError;
|
||||
return Result_FsStdioFailedToStat;
|
||||
}
|
||||
*out = S_ISREG(st.st_mode) ? FsDirEntryType_File : FsDirEntryType_Dir;
|
||||
R_SUCCEED();
|
||||
@@ -427,7 +431,7 @@ Result GetFileTimeStampRaw(const FsPathReal& path, FsTimeStampRaw *out) {
|
||||
struct stat st;
|
||||
if (stat(path, &st)) {
|
||||
R_TRY(fsdevGetLastResult());
|
||||
return Result_FsUnknownStdioError;
|
||||
return Result_FsStdioFailedToStat;
|
||||
}
|
||||
|
||||
out->is_valid = true;
|
||||
@@ -464,6 +468,10 @@ bool DirExists(const FsPath& path) {
|
||||
}
|
||||
|
||||
Result OpenFile(fs::Fs* fs, const FsPathReal& path, u32 mode, File* f) {
|
||||
const auto should_buffer = (mode & OpenMode_EnableBuffer);
|
||||
// remove the invalid flag so that native fs doesn't error.
|
||||
mode &= ~OpenMode_EnableBuffer;
|
||||
|
||||
f->m_fs = fs;
|
||||
f->m_mode = mode;
|
||||
|
||||
@@ -481,7 +489,14 @@ Result OpenFile(fs::Fs* fs, const FsPathReal& path, u32 mode, File* f) {
|
||||
f->m_stdio = std::fopen(path, "rb+");
|
||||
}
|
||||
|
||||
R_UNLESS(f->m_stdio, Result_FsUnknownStdioError);
|
||||
R_UNLESS(f->m_stdio, Result_FsStdioFailedToOpenFile);
|
||||
|
||||
// disable buffering to match native fs behavior.
|
||||
// this also causes problems with network io as it will do double reads.
|
||||
// which kills performance (see sftp).
|
||||
if (!should_buffer) {
|
||||
std::setvbuf(f->m_stdio, nullptr, _IONBF, 0);
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
@@ -500,9 +515,11 @@ Result File::Read( s64 off, void* buf, u64 read_size, u32 option, u64* bytes_rea
|
||||
} else {
|
||||
R_UNLESS(m_stdio, Result_FsUnknownStdioError);
|
||||
|
||||
if (m_stdio_off != off) {
|
||||
m_stdio_off = off;
|
||||
std::fseek(m_stdio, off, SEEK_SET);
|
||||
if (off != std::ftell(m_stdio)) {
|
||||
const auto ret = std::fseek(m_stdio, off, SEEK_SET);
|
||||
log_write("[FS] fseek to %ld ret: %d new_off: %zd\n", off, ret, std::ftell(m_stdio));
|
||||
R_UNLESS(ret == 0, Result_FsStdioFailedToSeek);
|
||||
R_UNLESS(off == std::ftell(m_stdio), Result_FsStdioFailedToSeek);
|
||||
}
|
||||
|
||||
*bytes_read = std::fread(buf, 1, read_size, m_stdio);
|
||||
@@ -510,11 +527,10 @@ Result File::Read( s64 off, void* buf, u64 read_size, u32 option, u64* bytes_rea
|
||||
// if we read less bytes than expected, check if there was an error (ignoring eof).
|
||||
if (*bytes_read < read_size) {
|
||||
if (!std::feof(m_stdio) && std::ferror(m_stdio)) {
|
||||
R_THROW(Result_FsUnknownStdioError);
|
||||
log_write("[FS] fread error: %d\n", std::ferror(m_stdio));
|
||||
R_THROW(Result_FsStdioFailedToRead);
|
||||
}
|
||||
}
|
||||
|
||||
m_stdio_off += *bytes_read;
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
@@ -528,17 +544,15 @@ Result File::Write(s64 off, const void* buf, u64 write_size, u32 option) {
|
||||
} else {
|
||||
R_UNLESS(m_stdio, Result_FsUnknownStdioError);
|
||||
|
||||
if (m_stdio_off != off) {
|
||||
log_write("[FS] diff seek\n");
|
||||
m_stdio_off = off;
|
||||
std::fseek(m_stdio, off, SEEK_SET);
|
||||
if (off != std::ftell(m_stdio)) {
|
||||
const auto ret = std::fseek(m_stdio, off, SEEK_SET);
|
||||
R_UNLESS(ret == 0, Result_FsStdioFailedToSeek);
|
||||
R_UNLESS(off == std::ftell(m_stdio), Result_FsStdioFailedToSeek);
|
||||
}
|
||||
|
||||
const auto result = std::fwrite(buf, 1, write_size, m_stdio);
|
||||
// log_write("[FS] fwrite res: %zu vs %zu\n", result, write_size);
|
||||
R_UNLESS(result == write_size, Result_FsUnknownStdioError);
|
||||
|
||||
m_stdio_off += write_size;
|
||||
R_UNLESS(result == write_size, Result_FsStdioFailedToWrite);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
@@ -552,8 +566,8 @@ Result File::SetSize(s64 sz) {
|
||||
} else {
|
||||
R_UNLESS(m_stdio, Result_FsUnknownStdioError);
|
||||
const auto fd = fileno(m_stdio);
|
||||
R_UNLESS(fd > 0, Result_FsUnknownStdioError);
|
||||
R_UNLESS(!ftruncate(fd, sz), Result_FsUnknownStdioError);
|
||||
R_UNLESS(fd > 0, Result_FsStdioFailedToTruncate);
|
||||
R_UNLESS(!ftruncate(fd, sz), Result_FsStdioFailedToTruncate);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
@@ -568,7 +582,7 @@ Result File::GetSize(s64* out) {
|
||||
R_UNLESS(m_stdio, Result_FsUnknownStdioError);
|
||||
|
||||
struct stat st;
|
||||
R_UNLESS(!fstat(fileno(m_stdio), &st), Result_FsUnknownStdioError);
|
||||
R_UNLESS(!fstat(fileno(m_stdio), &st), Result_FsStdioFailedToStat);
|
||||
*out = st.st_size;
|
||||
}
|
||||
|
||||
@@ -590,6 +604,7 @@ void File::Close() {
|
||||
}
|
||||
} else {
|
||||
if (m_stdio) {
|
||||
log_write("[FS] closing stdio file\n");
|
||||
std::fclose(m_stdio);
|
||||
m_stdio = {};
|
||||
}
|
||||
@@ -605,7 +620,7 @@ Result OpenDirectory(fs::Fs* fs, const FsPathReal& path, u32 mode, Dir* d) {
|
||||
R_TRY(fsFsOpenDirectory(&fs->m_fs, path, mode, &d->m_native));
|
||||
} else {
|
||||
d->m_stdio = opendir(path);
|
||||
R_UNLESS(d->m_stdio, Result_FsUnknownStdioError);
|
||||
R_UNLESS(d->m_stdio, Result_FsStdioFailedToOpenDirectory);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
@@ -673,6 +688,20 @@ Result Dir::GetEntryCount(s64* out) {
|
||||
if (!std::strcmp(d->d_name, ".") || !std::strcmp(d->d_name, "..")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (d->d_type == DT_DIR) {
|
||||
if (!(m_mode & FsDirOpenMode_ReadDirs)) {
|
||||
continue;
|
||||
}
|
||||
} else if (d->d_type == DT_REG) {
|
||||
if (!(m_mode & FsDirOpenMode_ReadFiles)) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
log_write("[FS] WARNING: unknown type when counting dir: %u\n", d->d_type);
|
||||
continue;
|
||||
}
|
||||
|
||||
(*out)++;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
#include "utils/thread.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <minIni.h>
|
||||
#include <ftpsrv.h>
|
||||
#include <ftpsrv_vfs.h>
|
||||
#include <nx/vfs_nx.h>
|
||||
#include <nx/utils.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
namespace sphaira::ftpsrv {
|
||||
namespace {
|
||||
@@ -28,19 +29,21 @@ struct InstallSharedData {
|
||||
bool enabled;
|
||||
};
|
||||
|
||||
const char* INI_PATH = "/config/ftpsrv/config.ini";
|
||||
FtpSrvConfig g_ftpsrv_config = {0};
|
||||
std::atomic_bool g_should_exit = false;
|
||||
bool g_is_running{false};
|
||||
Thread g_thread;
|
||||
FtpSrvConfig g_ftpsrv_config{};
|
||||
int g_ftpsrv_mount_flags{};
|
||||
std::vector<VfsNxCustomPath> g_custom_vfs{};
|
||||
std::atomic_bool g_should_exit{};
|
||||
bool g_is_running{};
|
||||
Thread g_thread{};
|
||||
Mutex g_mutex{};
|
||||
|
||||
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
|
||||
sphaira::App::NotifyFlashLed();
|
||||
log_write("[FTPSRV] %s\n", msg);
|
||||
App::NotifyFlashLed();
|
||||
}
|
||||
|
||||
void ftp_progress_callback(void) {
|
||||
sphaira::App::NotifyFlashLed();
|
||||
App::NotifyFlashLed();
|
||||
}
|
||||
|
||||
InstallSharedData g_shared_data{};
|
||||
@@ -274,65 +277,203 @@ FtpVfs g_vfs_install = {
|
||||
.rename = vfs_install_rename,
|
||||
};
|
||||
|
||||
struct FtpVfsFile {
|
||||
int fd;
|
||||
int valid;
|
||||
};
|
||||
|
||||
struct FtpVfsDir {
|
||||
DIR* fd;
|
||||
};
|
||||
|
||||
struct FtpVfsDirEntry {
|
||||
struct dirent* buf;
|
||||
};
|
||||
|
||||
auto vfs_stdio_fix_path(const char* str) -> fs::FsPath {
|
||||
while (*str == '/') {
|
||||
str++;
|
||||
}
|
||||
|
||||
fs::FsPath out = str;
|
||||
if (out.ends_with(":")) {
|
||||
out += '/';
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
int vfs_stdio_open(void* user, const char* _path, enum FtpVfsOpenMode mode) {
|
||||
auto f = static_cast<FtpVfsFile*>(user);
|
||||
const auto path = vfs_stdio_fix_path(_path);
|
||||
|
||||
int flags = 0, args = 0;
|
||||
switch (mode) {
|
||||
case FtpVfsOpenMode_READ:
|
||||
flags = O_RDONLY;
|
||||
args = 0;
|
||||
break;
|
||||
case FtpVfsOpenMode_WRITE:
|
||||
flags = O_WRONLY | O_CREAT | O_TRUNC;
|
||||
args = 0666;
|
||||
break;
|
||||
case FtpVfsOpenMode_APPEND:
|
||||
flags = O_WRONLY | O_CREAT | O_APPEND;
|
||||
args = 0666;
|
||||
break;
|
||||
}
|
||||
|
||||
f->fd = open(path, flags, args);
|
||||
if (f->fd >= 0) {
|
||||
f->valid = 1;
|
||||
}
|
||||
|
||||
return f->fd;
|
||||
}
|
||||
|
||||
int vfs_stdio_read(void* user, void* buf, size_t size) {
|
||||
auto f = static_cast<FtpVfsFile*>(user);
|
||||
return read(f->fd, buf, size);
|
||||
}
|
||||
|
||||
int vfs_stdio_write(void* user, const void* buf, size_t size) {
|
||||
auto f = static_cast<FtpVfsFile*>(user);
|
||||
return write(f->fd, buf, size);
|
||||
}
|
||||
|
||||
int vfs_stdio_seek(void* user, const void* buf, size_t size, size_t off) {
|
||||
auto f = static_cast<FtpVfsFile*>(user);
|
||||
const auto pos = lseek(f->fd, off, SEEK_SET);
|
||||
if (pos < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int vfs_stdio_isfile_open(void* user) {
|
||||
auto f = static_cast<FtpVfsFile*>(user);
|
||||
return f->valid && f->fd >= 0;
|
||||
}
|
||||
|
||||
int vfs_stdio_close(void* user) {
|
||||
auto f = static_cast<FtpVfsFile*>(user);
|
||||
int rc = 0;
|
||||
if (vfs_stdio_isfile_open(f)) {
|
||||
rc = close(f->fd);
|
||||
f->fd = -1;
|
||||
f->valid = 0;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
int vfs_stdio_opendir(void* user, const char* _path) {
|
||||
auto f = static_cast<FtpVfsDir*>(user);
|
||||
const auto path = vfs_stdio_fix_path(_path);
|
||||
|
||||
f->fd = opendir(path);
|
||||
if (!f->fd) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* vfs_stdio_readdir(void* user, void* user_entry) {
|
||||
auto f = static_cast<FtpVfsDir*>(user);
|
||||
auto entry = static_cast<FtpVfsDirEntry*>(user_entry);
|
||||
|
||||
entry->buf = readdir(f->fd);
|
||||
if (!entry->buf) {
|
||||
return NULL;
|
||||
}
|
||||
return entry->buf->d_name;
|
||||
}
|
||||
|
||||
int vfs_stdio_dirlstat(void* user, const void* user_entry, const char* _path, struct stat* st) {
|
||||
// could probably be optimised to th below, but we won't know its r/w perms.
|
||||
#if 0
|
||||
auto entry = static_cast<FtpVfsDirEntry*>(user_entry);
|
||||
if (entry->buf->d_type == DT_DIR) {
|
||||
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
|
||||
st->st_nlink = 1;
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
#endif
|
||||
|
||||
const auto path = vfs_stdio_fix_path(_path);
|
||||
return lstat(path, st);
|
||||
}
|
||||
|
||||
int vfs_stdio_isdir_open(void* user) {
|
||||
auto f = static_cast<FtpVfsDir*>(user);
|
||||
return f->fd != NULL;
|
||||
}
|
||||
|
||||
int vfs_stdio_closedir(void* user) {
|
||||
auto f = static_cast<FtpVfsDir*>(user);
|
||||
int rc = 0;
|
||||
if (vfs_stdio_isdir_open(f)) {
|
||||
rc = closedir(f->fd);
|
||||
f->fd = NULL;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
int vfs_stdio_stat(const char* _path, struct stat* st) {
|
||||
const auto path = vfs_stdio_fix_path(_path);
|
||||
return stat(path, st);
|
||||
}
|
||||
|
||||
int vfs_stdio_mkdir(const char* _path) {
|
||||
const auto path = vfs_stdio_fix_path(_path);
|
||||
return mkdir(path, 0777);
|
||||
}
|
||||
|
||||
int vfs_stdio_unlink(const char* _path) {
|
||||
const auto path = vfs_stdio_fix_path(_path);
|
||||
return unlink(path);
|
||||
}
|
||||
|
||||
int vfs_stdio_rmdir(const char* _path) {
|
||||
const auto path = vfs_stdio_fix_path(_path);
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
int vfs_stdio_rename(const char* _src, const char* _dst) {
|
||||
const auto src = vfs_stdio_fix_path(_src);
|
||||
const auto dst = vfs_stdio_fix_path(_dst);
|
||||
return rename(src, dst);
|
||||
}
|
||||
|
||||
FtpVfs g_vfs_stdio = {
|
||||
.open = vfs_stdio_open,
|
||||
.read = vfs_stdio_read,
|
||||
.write = vfs_stdio_write,
|
||||
.seek = vfs_stdio_seek,
|
||||
.close = vfs_stdio_close,
|
||||
.isfile_open = vfs_stdio_isfile_open,
|
||||
.opendir = vfs_stdio_opendir,
|
||||
.readdir = vfs_stdio_readdir,
|
||||
.dirlstat = vfs_stdio_dirlstat,
|
||||
.closedir = vfs_stdio_closedir,
|
||||
.isdir_open = vfs_stdio_isdir_open,
|
||||
.stat = vfs_stdio_stat,
|
||||
.lstat = vfs_stdio_stat,
|
||||
.mkdir = vfs_stdio_mkdir,
|
||||
.unlink = vfs_stdio_unlink,
|
||||
.rmdir = vfs_stdio_rmdir,
|
||||
.rename = vfs_stdio_rename,
|
||||
};
|
||||
|
||||
void loop(void* arg) {
|
||||
log_write("[FTP] loop entered\n");
|
||||
|
||||
// load config.
|
||||
{
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
|
||||
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);
|
||||
|
||||
g_should_exit = false;
|
||||
mount_devices = true;
|
||||
g_ftpsrv_config.timeout = 0;
|
||||
|
||||
if (!g_ftpsrv_config.port) {
|
||||
g_ftpsrv_config.port = 5000;
|
||||
log_write("[FTP] no port config, defaulting to 5000\n");
|
||||
}
|
||||
|
||||
// keep compat with older sphaira
|
||||
if (!user_len && !pass_len) {
|
||||
g_ftpsrv_config.anon = true;
|
||||
log_write("[FTP] no user pass, defaulting to anon\n");
|
||||
}
|
||||
|
||||
fsdev_wrapMountSdmc();
|
||||
|
||||
const VfsNxCustomPath custom = {
|
||||
.name = "install",
|
||||
.user = NULL,
|
||||
.func = &g_vfs_install,
|
||||
};
|
||||
|
||||
vfs_nx_init(&custom, mount_devices, save_writable, mount_bis, false);
|
||||
vfs_nx_init(g_custom_vfs.data(), std::size(g_custom_vfs), g_ftpsrv_mount_flags, false);
|
||||
}
|
||||
|
||||
ON_SCOPE_EXIT(
|
||||
@@ -363,16 +504,93 @@ bool Init() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if (R_FAILED(fsdev_wrapMountSdmc())) {
|
||||
// log_write("[FTP] cannot mount sdmc\n");
|
||||
// return false;
|
||||
// }
|
||||
g_ftpsrv_mount_flags = 0;
|
||||
g_ftpsrv_config = {};
|
||||
g_custom_vfs.clear();
|
||||
|
||||
// todo: replace everything with ini_browse for faster loading.
|
||||
// or load everything in the init thread.
|
||||
auto app = App::GetApp();
|
||||
g_ftpsrv_config.log_callback = ftp_log_callback;
|
||||
g_ftpsrv_config.progress_callback = ftp_progress_callback;
|
||||
g_ftpsrv_config.anon = app->m_ftp_anon.Get();
|
||||
std::strncpy(g_ftpsrv_config.user, app->m_ftp_user.Get().c_str(), sizeof(g_ftpsrv_config.user) - 1);
|
||||
std::strncpy(g_ftpsrv_config.pass, app->m_ftp_pass.Get().c_str(), sizeof(g_ftpsrv_config.pass) - 1);
|
||||
g_ftpsrv_config.port = app->m_ftp_port.Get();
|
||||
|
||||
if (app->m_ftp_show_album.Get()) {
|
||||
g_ftpsrv_mount_flags |= VfsNxMountFlag_ALBUM;
|
||||
}
|
||||
if (app->m_ftp_show_ams_contents.Get()) {
|
||||
g_ftpsrv_mount_flags |= VfsNxMountFlag_AMS_CONTENTS;
|
||||
}
|
||||
if (app->m_ftp_show_bis_storage.Get()) {
|
||||
g_ftpsrv_mount_flags |= VfsNxMountFlag_BIS_STORAGE;
|
||||
}
|
||||
if (app->m_ftp_show_bis_fs.Get()) {
|
||||
g_ftpsrv_mount_flags |= VfsNxMountFlag_BIS_FS;
|
||||
}
|
||||
if (app->m_ftp_show_content_system.Get()) {
|
||||
g_ftpsrv_mount_flags |= VfsNxMountFlag_CONTENT_SYSTEM;
|
||||
}
|
||||
if (app->m_ftp_show_content_user.Get()) {
|
||||
g_ftpsrv_mount_flags |= VfsNxMountFlag_CONTENT_USER;
|
||||
}
|
||||
if (app->m_ftp_show_content_sd.Get()) {
|
||||
g_ftpsrv_mount_flags |= VfsNxMountFlag_CONTENT_SDCARD;
|
||||
}
|
||||
#if 0
|
||||
if (app->m_ftp_show_content_sd0.Get()) {
|
||||
g_ftpsrv_mount_flags |= VfsNxMountFlag_CONTENT_SDCARD0;
|
||||
}
|
||||
if (app->m_ftp_show_custom_system.Get()) {
|
||||
g_ftpsrv_mount_flags |= VfsNxMountFlag_CUSTOM_SYSTEM;
|
||||
}
|
||||
if (app->m_ftp_show_custom_sd.Get()) {
|
||||
g_ftpsrv_mount_flags |= VfsNxMountFlag_CUSTOM_SDCARD;
|
||||
}
|
||||
#endif
|
||||
if (app->m_ftp_show_switch.Get()) {
|
||||
g_ftpsrv_mount_flags |= VfsNxMountFlag_SWITCH;
|
||||
}
|
||||
|
||||
if (app->m_ftp_show_install.Get()) {
|
||||
g_custom_vfs.push_back({
|
||||
.name = "install",
|
||||
.user = &g_shared_data,
|
||||
.func = &g_vfs_install,
|
||||
});
|
||||
}
|
||||
|
||||
if (app->m_ftp_show_games.Get()) {
|
||||
g_custom_vfs.push_back({
|
||||
.name = "games",
|
||||
.user = NULL,
|
||||
.func = &g_vfs_stdio,
|
||||
});
|
||||
}
|
||||
|
||||
if (app->m_ftp_show_mounts.Get()) {
|
||||
g_custom_vfs.push_back({
|
||||
.name = "mounts",
|
||||
.user = NULL,
|
||||
.func = &g_vfs_stdio,
|
||||
});
|
||||
}
|
||||
|
||||
g_ftpsrv_config.timeout = 0;
|
||||
|
||||
if (!g_ftpsrv_config.port) {
|
||||
g_ftpsrv_config.port = 5000;
|
||||
log_write("[FTP] no port config, defaulting to 5000\n");
|
||||
}
|
||||
|
||||
// keep compat with older sphaira
|
||||
if (!std::strlen(g_ftpsrv_config.user) && !std::strlen(g_ftpsrv_config.pass)) {
|
||||
g_ftpsrv_config.anon = true;
|
||||
log_write("[FTP] no user pass, defaulting to anon\n");
|
||||
}
|
||||
|
||||
Result rc;
|
||||
if (R_FAILED(rc = utils::CreateThread(&g_thread, loop, nullptr, 1024*16))) {
|
||||
if (R_FAILED(rc = utils::CreateThread(&g_thread, loop, nullptr))) {
|
||||
log_write("[FTP] failed to create nxlink thread: 0x%X\n", rc);
|
||||
return false;
|
||||
}
|
||||
@@ -399,7 +617,9 @@ void Exit() {
|
||||
threadWaitForExit(&g_thread);
|
||||
threadClose(&g_thread);
|
||||
|
||||
memset(&g_ftpsrv_config, 0, sizeof(g_ftpsrv_config));
|
||||
std::memset(&g_ftpsrv_config, 0, sizeof(g_ftpsrv_config));
|
||||
g_custom_vfs.clear();
|
||||
g_ftpsrv_mount_flags = 0;
|
||||
|
||||
log_write("[FTP] exitied\n");
|
||||
}
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
#include <mbedtls/md5.h>
|
||||
#include <utility>
|
||||
|
||||
#include <zlib.h>
|
||||
#include <zstd.h>
|
||||
|
||||
namespace sphaira::hash {
|
||||
namespace {
|
||||
|
||||
@@ -81,131 +78,6 @@ private:
|
||||
size_t m_in_size{};
|
||||
};
|
||||
|
||||
// this currently crashes when freeing the pool :/
|
||||
#define USE_THREAD_POOL 0
|
||||
struct HashZstd final : HashSource {
|
||||
HashZstd() {
|
||||
const auto num_threads = 3;
|
||||
const auto level = ZSTD_CLEVEL_DEFAULT;
|
||||
|
||||
m_ctx = ZSTD_createCCtx();
|
||||
if (!m_ctx) {
|
||||
log_write("[ZSTD] failed to create ctx\n");
|
||||
}
|
||||
|
||||
|
||||
#if USE_THREAD_POOL
|
||||
m_pool = ZSTD_createThreadPool(num_threads);
|
||||
if (!m_pool) {
|
||||
log_write("[ZSTD] failed to create pool\n");
|
||||
}
|
||||
|
||||
if (ZSTD_isError(ZSTD_CCtx_refThreadPool(m_ctx, m_pool))) {
|
||||
log_write("[ZSTD] failed ZSTD_CCtx_refThreadPool(m_pool)\n");
|
||||
}
|
||||
#endif
|
||||
if (ZSTD_isError(ZSTD_CCtx_setParameter(m_ctx, ZSTD_c_compressionLevel, level))) {
|
||||
log_write("[ZSTD] failed ZSTD_CCtx_setParameter(ZSTD_c_compressionLevel)\n");
|
||||
}
|
||||
if (ZSTD_isError(ZSTD_CCtx_setParameter(m_ctx, ZSTD_c_nbWorkers, num_threads))) {
|
||||
log_write("[ZSTD] failed ZSTD_CCtx_setParameter(ZSTD_c_nbWorkers)\n");
|
||||
}
|
||||
|
||||
m_out_buf.resize(ZSTD_CStreamOutSize());
|
||||
}
|
||||
|
||||
~HashZstd() {
|
||||
ZSTD_freeCCtx(m_ctx);
|
||||
#if USE_THREAD_POOL
|
||||
// crashes here during ZSTD_pthread_join()
|
||||
// ZSTD_freeThreadPool(m_pool);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Update(const void* buf, s64 size, s64 file_size) override {
|
||||
ZSTD_inBuffer input = { buf, (u64)size, 0 };
|
||||
|
||||
const auto last_chunk = m_in_size + size >= file_size;
|
||||
const auto mode = last_chunk ? ZSTD_e_end : ZSTD_e_continue;
|
||||
|
||||
while (input.pos < input.size) {
|
||||
ZSTD_outBuffer output = { m_out_buf.data(), m_out_buf.size(), 0 };
|
||||
const size_t remaining = ZSTD_compressStream2(m_ctx, &output , &input, mode);
|
||||
|
||||
if (ZSTD_isError(remaining)) {
|
||||
log_write("[ZSTD] error: %zu\n", remaining);
|
||||
break;
|
||||
}
|
||||
|
||||
m_out_size += output.pos;
|
||||
};
|
||||
|
||||
m_in_size += size;
|
||||
}
|
||||
|
||||
void Get(std::string& out) override {
|
||||
log_write("getting size: %zu vs %zu\n", m_out_size, m_in_size);
|
||||
char str[64];
|
||||
const u32 percentage = ((double)m_out_size / (double)m_in_size) * 100.0;
|
||||
std::snprintf(str, sizeof(str), "%u%%", percentage);
|
||||
out = str;
|
||||
log_write("got size: %zu vs %zu\n", m_out_size, m_in_size);
|
||||
}
|
||||
|
||||
private:
|
||||
ZSTD_CCtx* m_ctx{};
|
||||
ZSTD_threadPool* m_pool{};
|
||||
std::vector<u8> m_out_buf{};
|
||||
size_t m_in_size{};
|
||||
size_t m_out_size{};
|
||||
};
|
||||
|
||||
struct HashDeflate final : HashSource {
|
||||
HashDeflate() {
|
||||
deflateInit(&m_ctx, Z_DEFAULT_COMPRESSION);
|
||||
m_out_buf.resize(deflateBound(&m_ctx, 1024*1024*16)); // max chunk size.
|
||||
}
|
||||
|
||||
~HashDeflate() {
|
||||
deflateEnd(&m_ctx);
|
||||
}
|
||||
|
||||
void Update(const void* buf, s64 size, s64 file_size) override {
|
||||
m_ctx.avail_in = size;
|
||||
m_ctx.next_in = const_cast<Bytef*>((const Bytef*)buf);
|
||||
|
||||
const auto last_chunk = m_in_size + size >= file_size;
|
||||
const auto mode = last_chunk ? Z_FINISH : Z_NO_FLUSH;
|
||||
|
||||
while (m_ctx.avail_in != 0) {
|
||||
m_ctx.next_out = m_out_buf.data();
|
||||
m_ctx.avail_out = m_out_buf.size();
|
||||
|
||||
const auto rc = deflate(&m_ctx, mode);
|
||||
if (Z_OK != rc) {
|
||||
if (Z_STREAM_END != rc) {
|
||||
log_write("[ZLIB] deflate error: %d\n", rc);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
m_in_size += size;
|
||||
}
|
||||
|
||||
void Get(std::string& out) override {
|
||||
char str[64];
|
||||
const u32 percentage = ((double)m_ctx.total_out / (double)m_in_size) * 100.0;
|
||||
std::snprintf(str, sizeof(str), "%u%%", percentage);
|
||||
out = str;
|
||||
}
|
||||
|
||||
private:
|
||||
z_stream m_ctx{};
|
||||
std::vector<u8> m_out_buf{};
|
||||
size_t m_in_size{};
|
||||
};
|
||||
|
||||
struct HashCrc32 final : HashSource {
|
||||
void Update(const void* buf, s64 size, s64 file_size) override {
|
||||
m_seed = crc32CalculateWithSeed(m_seed, buf, size);
|
||||
@@ -328,8 +200,6 @@ auto GetTypeStr(Type type) -> const char* {
|
||||
case Type::Sha1: return "SHA1";
|
||||
case Type::Sha256: return "SHA256";
|
||||
case Type::Null: return "/dev/null (Speed Test)";
|
||||
case Type::Deflate: return "Deflate (Speed Test)";
|
||||
case Type::Zstd: return "ZSTD (Speed Test)";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -341,8 +211,6 @@ Result Hash(ui::ProgressBox* pbox, Type type, BaseSource* source, std::string& o
|
||||
case Type::Sha1: return Hash(pbox, std::make_unique<HashSha1>(), source, out);
|
||||
case Type::Sha256: return Hash(pbox, std::make_unique<HashSha256>(), source, out);
|
||||
case Type::Null: return Hash(pbox, std::make_unique<HashNull>(), source, out);
|
||||
case Type::Deflate: return Hash(pbox, std::make_unique<HashDeflate>(), source, out);
|
||||
case Type::Zstd: return Hash(pbox, std::make_unique<HashZstd>(), source, out);
|
||||
}
|
||||
std::unreachable();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include <algorithm>
|
||||
#include <haze.h>
|
||||
|
||||
namespace sphaira::haze {
|
||||
namespace sphaira::libhaze {
|
||||
namespace {
|
||||
|
||||
struct InstallSharedData {
|
||||
@@ -56,22 +56,23 @@ void on_thing() {
|
||||
}
|
||||
}
|
||||
|
||||
struct FsProxyBase : ::haze::FileSystemProxyImpl {
|
||||
struct FsProxyBase : haze::FileSystemProxyImpl {
|
||||
FsProxyBase(const char* name, const char* display_name) : m_name{name}, m_display_name{display_name} {
|
||||
|
||||
}
|
||||
|
||||
auto FixPath(const char* path) const {
|
||||
auto FixPath(const char* base, const char* path) const {
|
||||
fs::FsPath buf;
|
||||
const auto len = std::strlen(GetName());
|
||||
|
||||
if (len && !strncasecmp(path + 1, GetName(), len)) {
|
||||
std::snprintf(buf, sizeof(buf), "/%s", path + 1 + len);
|
||||
if (len && !strncasecmp(path, GetName(), len)) {
|
||||
std::snprintf(buf, sizeof(buf), "%s/%s", base, path + len);
|
||||
} else {
|
||||
std::strcpy(buf, path);
|
||||
std::snprintf(buf, sizeof(buf), "%s/%s", base, path);
|
||||
// std::strcpy(buf, path);
|
||||
}
|
||||
|
||||
// log_write("[FixPath] %s -> %s\n", path, buf.s);
|
||||
log_write("[FixPath] %s -> %s\n", path, buf.s);
|
||||
return buf;
|
||||
}
|
||||
|
||||
@@ -88,6 +89,10 @@ protected:
|
||||
};
|
||||
|
||||
struct FsProxy final : FsProxyBase {
|
||||
using File = fs::File;
|
||||
using Dir = fs::Dir;
|
||||
using DirEntry = FsDirectoryEntry;
|
||||
|
||||
FsProxy(std::unique_ptr<fs::Fs>&& fs, const char* name, const char* display_name)
|
||||
: FsProxyBase{name, display_name}
|
||||
, m_fs{std::forward<decltype(fs)>(fs)} {
|
||||
@@ -100,137 +105,183 @@ struct FsProxy final : FsProxyBase {
|
||||
}
|
||||
}
|
||||
|
||||
auto FixPath(const char* path) const {
|
||||
return FsProxyBase::FixPath(m_fs->Root(), path);
|
||||
}
|
||||
|
||||
// TODO: impl this for stdio
|
||||
Result GetTotalSpace(const char *path, s64 *out) override {
|
||||
if (m_fs->IsNative()) {
|
||||
auto fs = (fs::FsNative*)m_fs.get();
|
||||
return fsFsGetTotalSpace(&fs->m_fs, FixPath(path), out);
|
||||
}
|
||||
|
||||
// todo: use statvfs.
|
||||
// then fallback to 256gb if not available.
|
||||
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result GetFreeSpace(const char *path, s64 *out) override {
|
||||
if (m_fs->IsNative()) {
|
||||
auto fs = (fs::FsNative*)m_fs.get();
|
||||
return fsFsGetFreeSpace(&fs->m_fs, FixPath(path), out);
|
||||
}
|
||||
|
||||
// todo: use statvfs.
|
||||
// then fallback to 256gb if not available.
|
||||
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) override {
|
||||
const auto rc = m_fs->GetEntryType(FixPath(path), out_entry_type);
|
||||
log_write("[HAZE] GetEntryType(%s) 0x%X\n", path, rc);
|
||||
return rc;
|
||||
|
||||
Result GetEntryType(const char *path, haze::FileAttrType *out_entry_type) override {
|
||||
FsDirEntryType type;
|
||||
R_TRY(m_fs->GetEntryType(FixPath(path), &type));
|
||||
*out_entry_type = (type == FsDirEntryType_Dir) ? haze::FileAttrType_DIR : haze::FileAttrType_FILE;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result CreateFile(const char* path, s64 size, u32 option) override {
|
||||
|
||||
Result GetEntryAttributes(const char *path, haze::FileAttr *out) override {
|
||||
FsDirEntryType type;
|
||||
R_TRY(m_fs->GetEntryType(FixPath(path), &type));
|
||||
|
||||
if (type == FsDirEntryType_File) {
|
||||
out->type = haze::FileAttrType_FILE;
|
||||
|
||||
// it doesn't matter if this fails.
|
||||
s64 size{};
|
||||
FsTimeStampRaw timestamp{};
|
||||
R_TRY(m_fs->FileGetSizeAndTimestamp(FixPath(path), ×tamp, &size));
|
||||
|
||||
out->size = size;
|
||||
if (timestamp.is_valid) {
|
||||
out->ctime = timestamp.created;
|
||||
out->mtime = timestamp.modified;
|
||||
}
|
||||
} else {
|
||||
out->type = haze::FileAttrType_DIR;
|
||||
}
|
||||
|
||||
if (IsReadOnly()) {
|
||||
out->flag |= haze::FileAttrFlag_READ_ONLY;
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result CreateFile(const char* path, s64 size) override {
|
||||
log_write("[HAZE] CreateFile(%s)\n", path);
|
||||
return m_fs->CreateFile(FixPath(path), size, option);
|
||||
return m_fs->CreateFile(FixPath(path), 0, 0);
|
||||
}
|
||||
|
||||
Result DeleteFile(const char* path) override {
|
||||
log_write("[HAZE] DeleteFile(%s)\n", path);
|
||||
return m_fs->DeleteFile(FixPath(path));
|
||||
}
|
||||
|
||||
Result RenameFile(const char *old_path, const char *new_path) override {
|
||||
log_write("[HAZE] RenameFile(%s -> %s)\n", old_path, new_path);
|
||||
return m_fs->RenameFile(FixPath(old_path), FixPath(new_path));
|
||||
}
|
||||
Result OpenFile(const char *path, u32 mode, FsFile *out_file) override {
|
||||
log_write("[HAZE] OpenFile(%s)\n", path);
|
||||
auto fptr = new fs::File();
|
||||
const auto rc = m_fs->OpenFile(FixPath(path), mode, fptr);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
std::memcpy(&out_file->s, &fptr, sizeof(fptr));
|
||||
} else {
|
||||
delete fptr;
|
||||
Result OpenFile(const char *path, haze::FileOpenMode mode, haze::File *out_file) override {
|
||||
log_write("[HAZE] OpenFile(%s)\n", path);
|
||||
|
||||
u32 flags = FsOpenMode_Read;
|
||||
if (mode == haze::FileOpenMode_WRITE) {
|
||||
flags = FsOpenMode_Write | FsOpenMode_Append;
|
||||
}
|
||||
|
||||
return rc;
|
||||
auto f = new File();
|
||||
const auto rc = m_fs->OpenFile(FixPath(path), flags, f);
|
||||
if (R_FAILED(rc)) {
|
||||
log_write("[HAZE] OpenFile(%s) failed: 0x%X\n", path, rc);
|
||||
delete f;
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
||||
out_file->impl = f;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result GetFileSize(FsFile *file, s64 *out_size) override {
|
||||
log_write("[HAZE] GetFileSize()\n");
|
||||
fs::File* f;
|
||||
std::memcpy(&f, &file->s, sizeof(f));
|
||||
|
||||
Result GetFileSize(haze::File *file, s64 *out_size) override {
|
||||
auto f = static_cast<File*>(file->impl);
|
||||
return f->GetSize(out_size);
|
||||
}
|
||||
Result SetFileSize(FsFile *file, s64 size) override {
|
||||
log_write("[HAZE] SetFileSize(%zd)\n", size);
|
||||
fs::File* f;
|
||||
std::memcpy(&f, &file->s, sizeof(f));
|
||||
|
||||
Result SetFileSize(haze::File *file, s64 size) override {
|
||||
auto f = static_cast<File*>(file->impl);
|
||||
return f->SetSize(size);
|
||||
}
|
||||
Result ReadFile(FsFile *file, s64 off, void *buf, u64 read_size, u32 option, u64 *out_bytes_read) override {
|
||||
log_write("[HAZE] ReadFile(%zd, %zu)\n", off, read_size);
|
||||
fs::File* f;
|
||||
std::memcpy(&f, &file->s, sizeof(f));
|
||||
return f->Read(off, buf, read_size, option, out_bytes_read);
|
||||
|
||||
Result ReadFile(haze::File *file, s64 off, void *buf, u64 read_size, u64 *out_bytes_read) override {
|
||||
auto f = static_cast<File*>(file->impl);
|
||||
return f->Read(off, buf, read_size, FsReadOption_None, out_bytes_read);
|
||||
}
|
||||
Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) override {
|
||||
log_write("[HAZE] WriteFile(%zd, %zu)\n", off, write_size);
|
||||
fs::File* f;
|
||||
std::memcpy(&f, &file->s, sizeof(f));
|
||||
return f->Write(off, buf, write_size, option);
|
||||
|
||||
Result WriteFile(haze::File *file, s64 off, const void *buf, u64 write_size) override {
|
||||
auto f = static_cast<File*>(file->impl);
|
||||
return f->Write(off, buf, write_size, FsWriteOption_None);
|
||||
}
|
||||
void CloseFile(FsFile *file) override {
|
||||
log_write("[HAZE] CloseFile()\n");
|
||||
fs::File* f;
|
||||
std::memcpy(&f, &file->s, sizeof(f));
|
||||
|
||||
void CloseFile(haze::File *file) override {
|
||||
auto f = static_cast<File*>(file->impl);
|
||||
if (f) {
|
||||
delete f;
|
||||
file->impl = nullptr;
|
||||
}
|
||||
std::memset(file, 0, sizeof(*file));
|
||||
}
|
||||
|
||||
Result CreateDirectory(const char* path) override {
|
||||
log_write("[HAZE] DeleteFile(%s)\n", path);
|
||||
return m_fs->CreateDirectory(FixPath(path));
|
||||
}
|
||||
|
||||
Result DeleteDirectoryRecursively(const char* path) override {
|
||||
log_write("[HAZE] DeleteDirectoryRecursively(%s)\n", path);
|
||||
return m_fs->DeleteDirectoryRecursively(FixPath(path));
|
||||
}
|
||||
|
||||
Result RenameDirectory(const char *old_path, const char *new_path) override {
|
||||
log_write("[HAZE] RenameDirectory(%s -> %s)\n", old_path, new_path);
|
||||
return m_fs->RenameDirectory(FixPath(old_path), FixPath(new_path));
|
||||
}
|
||||
Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) override {
|
||||
auto fptr = new fs::Dir();
|
||||
const auto rc = m_fs->OpenDirectory(FixPath(path), mode, fptr);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
std::memcpy(&out_dir->s, &fptr, sizeof(fptr));
|
||||
} else {
|
||||
delete fptr;
|
||||
Result OpenDirectory(const char *path, haze::Dir *out_dir) override {
|
||||
auto dir = new Dir();
|
||||
const auto rc = m_fs->OpenDirectory(FixPath(path), FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize, dir);
|
||||
if (R_FAILED(rc)) {
|
||||
log_write("[HAZE] OpenDirectory(%s) failed: 0x%X\n", path, rc);
|
||||
delete dir;
|
||||
return rc;
|
||||
}
|
||||
|
||||
log_write("[HAZE] OpenDirectory(%s) 0x%X\n", path, rc);
|
||||
return rc;
|
||||
out_dir->impl = dir;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) override {
|
||||
fs::Dir* f;
|
||||
std::memcpy(&f, &d->s, sizeof(f));
|
||||
const auto rc = f->Read(out_total_entries, max_entries, buf);
|
||||
log_write("[HAZE] ReadDirectory(%zd) 0x%X\n", *out_total_entries, rc);
|
||||
return rc;
|
||||
}
|
||||
Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override {
|
||||
fs::Dir* f;
|
||||
std::memcpy(&f, &d->s, sizeof(f));
|
||||
const auto rc = f->GetEntryCount(out_count);
|
||||
log_write("[HAZE] GetDirectoryEntryCount(%zd) 0x%X\n", *out_count, rc);
|
||||
return rc;
|
||||
}
|
||||
void CloseDirectory(FsDir *d) override {
|
||||
log_write("[HAZE] CloseDirectory()\n");
|
||||
fs::Dir* f;
|
||||
std::memcpy(&f, &d->s, sizeof(f));
|
||||
if (f) {
|
||||
delete f;
|
||||
|
||||
Result ReadDirectory(haze::Dir *d, s64 *out_total_entries, size_t max_entries, haze::DirEntry *buf) override {
|
||||
auto dir = static_cast<Dir*>(d->impl);
|
||||
|
||||
std::vector<FsDirectoryEntry> entries(max_entries);
|
||||
R_TRY(dir->Read(out_total_entries, entries.size(), entries.data()));
|
||||
|
||||
for (s64 i = 0; i < *out_total_entries; i++) {
|
||||
std::strcpy(buf[i].name, entries[i].name);
|
||||
}
|
||||
std::memset(d, 0, sizeof(*d));
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
virtual bool MultiThreadTransfer(s64 size, bool read) override {
|
||||
return !App::IsFileBaseEmummc();
|
||||
|
||||
Result GetDirectoryEntryCount(haze::Dir *d, s64 *out_count) override {
|
||||
auto dir = static_cast<Dir*>(d->impl);
|
||||
return dir->GetEntryCount(out_count);
|
||||
}
|
||||
|
||||
void CloseDirectory(haze::Dir *d) override {
|
||||
auto dir = static_cast<Dir*>(d->impl);
|
||||
if (dir) {
|
||||
delete dir;
|
||||
d->impl = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -240,9 +291,22 @@ private:
|
||||
// fake fs that allows for files to create r/w on the root.
|
||||
// folders are not yet supported.
|
||||
struct FsProxyVfs : FsProxyBase {
|
||||
struct File {
|
||||
u64 index{};
|
||||
haze::FileOpenMode mode{};
|
||||
};
|
||||
|
||||
struct Dir {
|
||||
u64 pos{};
|
||||
};
|
||||
|
||||
using FsProxyBase::FsProxyBase;
|
||||
virtual ~FsProxyVfs() = default;
|
||||
|
||||
auto FixPath(const char* path) const {
|
||||
return FsProxyBase::FixPath("", path);
|
||||
}
|
||||
|
||||
auto GetFileName(const char* s) -> const char* {
|
||||
const auto file_name = std::strrchr(s, '/');
|
||||
if (!file_name || file_name[1] == '\0') {
|
||||
@@ -251,9 +315,9 @@ struct FsProxyVfs : FsProxyBase {
|
||||
return file_name + 1;
|
||||
}
|
||||
|
||||
virtual Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) {
|
||||
virtual Result GetEntryType(const char *path, haze::FileAttrType *out_entry_type) {
|
||||
if (FixPath(path) == "/") {
|
||||
*out_entry_type = FsDirEntryType_Dir;
|
||||
*out_entry_type = haze::FileAttrType_DIR;
|
||||
R_SUCCEED();
|
||||
} else {
|
||||
const auto file_name = GetFileName(path);
|
||||
@@ -264,11 +328,12 @@ struct FsProxyVfs : FsProxyBase {
|
||||
});
|
||||
R_UNLESS(it != m_entries.end(), FsError_PathNotFound);
|
||||
|
||||
*out_entry_type = FsDirEntryType_File;
|
||||
*out_entry_type = haze::FileAttrType_FILE;
|
||||
R_SUCCEED();
|
||||
}
|
||||
}
|
||||
virtual Result CreateFile(const char* path, s64 size, u32 option) {
|
||||
|
||||
virtual Result CreateFile(const char* path, s64 size) {
|
||||
const auto file_name = GetFileName(path);
|
||||
R_UNLESS(file_name, FsError_PathNotFound);
|
||||
|
||||
@@ -285,6 +350,7 @@ struct FsProxyVfs : FsProxyBase {
|
||||
m_entries.emplace_back(entry);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
virtual Result DeleteFile(const char* path) {
|
||||
const auto file_name = GetFileName(path);
|
||||
R_UNLESS(file_name, FsError_PathNotFound);
|
||||
@@ -297,6 +363,7 @@ struct FsProxyVfs : FsProxyBase {
|
||||
m_entries.erase(it);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
virtual Result RenameFile(const char *old_path, const char *new_path) {
|
||||
const auto file_name = GetFileName(old_path);
|
||||
R_UNLESS(file_name, FsError_PathNotFound);
|
||||
@@ -317,7 +384,8 @@ struct FsProxyVfs : FsProxyBase {
|
||||
std::strcpy(it->name, file_name_new);
|
||||
R_SUCCEED();
|
||||
}
|
||||
virtual Result OpenFile(const char *path, u32 mode, FsFile *out_file) {
|
||||
|
||||
virtual Result OpenFile(const char *path, haze::FileOpenMode mode, haze::File *out_file) {
|
||||
const auto file_name = GetFileName(path);
|
||||
R_UNLESS(file_name, FsError_PathNotFound);
|
||||
|
||||
@@ -326,65 +394,89 @@ struct FsProxyVfs : FsProxyBase {
|
||||
});
|
||||
R_UNLESS(it != m_entries.end(), FsError_PathNotFound);
|
||||
|
||||
out_file->s.object_id = std::distance(m_entries.begin(), it);
|
||||
out_file->s.own_handle = mode;
|
||||
auto f = new File();
|
||||
f->index = std::distance(m_entries.begin(), it);
|
||||
f->mode = mode;
|
||||
out_file->impl = f;
|
||||
R_SUCCEED();
|
||||
}
|
||||
virtual Result GetFileSize(FsFile *file, s64 *out_size) {
|
||||
auto& e = m_entries[file->s.object_id];
|
||||
*out_size = e.file_size;
|
||||
|
||||
virtual Result GetFileSize(haze::File *file, s64 *out_size) {
|
||||
auto f = static_cast<File*>(file->impl);
|
||||
*out_size = m_entries[f->index].file_size;
|
||||
R_SUCCEED();
|
||||
}
|
||||
virtual Result SetFileSize(FsFile *file, s64 size) {
|
||||
auto& e = m_entries[file->s.object_id];
|
||||
e.file_size = size;
|
||||
|
||||
virtual Result SetFileSize(haze::File *file, s64 size) {
|
||||
auto f = static_cast<File*>(file->impl);
|
||||
m_entries[f->index].file_size = size;
|
||||
R_SUCCEED();
|
||||
}
|
||||
virtual Result ReadFile(FsFile *file, s64 off, void *buf, u64 read_size, u32 option, u64 *out_bytes_read) {
|
||||
|
||||
virtual Result ReadFile(haze::File *file, s64 off, void *buf, u64 read_size, u64 *out_bytes_read) {
|
||||
// stub for now as it may confuse users who think that the returned file is valid.
|
||||
// the code below can be used to benchmark mtp reads.
|
||||
R_THROW(FsError_NotImplemented);
|
||||
// auto& e = m_entries[file->s.object_id];
|
||||
// read_size = std::min<s64>(e.file_size - off, read_size);
|
||||
// std::memset(buf, 0, read_size);
|
||||
// *out_bytes_read = read_size;
|
||||
// R_SUCCEED();
|
||||
}
|
||||
virtual Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) {
|
||||
auto& e = m_entries[file->s.object_id];
|
||||
|
||||
virtual Result WriteFile(haze::File *file, s64 off, const void *buf, u64 write_size) {
|
||||
auto f = static_cast<File*>(file->impl);
|
||||
auto& e = m_entries[f->index];
|
||||
e.file_size = std::max<s64>(e.file_size, off + write_size);
|
||||
R_SUCCEED();
|
||||
}
|
||||
virtual void CloseFile(FsFile *file) {
|
||||
std::memset(file, 0, sizeof(*file));
|
||||
|
||||
virtual void CloseFile(haze::File *file) {
|
||||
auto f = static_cast<File*>(file->impl);
|
||||
if (f) {
|
||||
delete f;
|
||||
file->impl = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Result CreateDirectory(const char* path) override {
|
||||
R_THROW(FsError_NotImplemented);
|
||||
}
|
||||
|
||||
Result DeleteDirectoryRecursively(const char* path) override {
|
||||
R_THROW(FsError_NotImplemented);
|
||||
}
|
||||
|
||||
Result RenameDirectory(const char *old_path, const char *new_path) override {
|
||||
R_THROW(FsError_NotImplemented);
|
||||
}
|
||||
Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) override {
|
||||
std::memset(out_dir, 0, sizeof(*out_dir));
|
||||
|
||||
Result OpenDirectory(const char *path, haze::Dir *out_dir) override {
|
||||
auto dir = new Dir();
|
||||
out_dir->impl = dir;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) override {
|
||||
max_entries = std::min<s64>(m_entries.size()- d->s.object_id, max_entries);
|
||||
std::memcpy(buf, m_entries.data() + d->s.object_id, max_entries * sizeof(*buf));
|
||||
d->s.object_id += max_entries;
|
||||
|
||||
Result ReadDirectory(haze::Dir *d, s64 *out_total_entries, size_t max_entries, haze::DirEntry *buf) override {
|
||||
auto dir = static_cast<Dir*>(d->impl);
|
||||
|
||||
max_entries = std::min<s64>(m_entries.size() - dir->pos, max_entries);
|
||||
|
||||
for (size_t i = 0; i < max_entries; i++) {
|
||||
std::strcpy(buf[i].name, m_entries[dir->pos + i].name);
|
||||
}
|
||||
|
||||
dir->pos += max_entries;
|
||||
*out_total_entries = max_entries;
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override {
|
||||
|
||||
Result GetDirectoryEntryCount(haze::Dir *d, s64 *out_count) override {
|
||||
*out_count = m_entries.size();
|
||||
R_SUCCEED();
|
||||
}
|
||||
void CloseDirectory(FsDir *d) override {
|
||||
std::memset(d, 0, sizeof(*d));
|
||||
|
||||
void CloseDirectory(haze::Dir *d) override {
|
||||
auto dir = static_cast<Dir*>(d->impl);
|
||||
if (dir) {
|
||||
delete dir;
|
||||
d->impl = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -398,13 +490,11 @@ struct FsDevNullProxy final : FsProxyVfs {
|
||||
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result GetFreeSpace(const char *path, s64 *out) override {
|
||||
*out = 1024ULL * 1024ULL * 1024ULL * 256ULL;
|
||||
R_SUCCEED();
|
||||
}
|
||||
bool MultiThreadTransfer(s64 size, bool read) override {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
struct FsInstallProxy final : FsProxyVfs {
|
||||
@@ -447,6 +537,7 @@ struct FsInstallProxy final : FsProxyVfs {
|
||||
return fs::FsNativeContentStorage(FsContentStorageId_User).GetTotalSpace("/", out);
|
||||
}
|
||||
}
|
||||
|
||||
Result GetFreeSpace(const char *path, s64 *out) override {
|
||||
if (App::GetApp()->m_install_sd.Get()) {
|
||||
return fs::FsNativeContentStorage(FsContentStorageId_SdCard).GetFreeSpace("/", out);
|
||||
@@ -455,27 +546,30 @@ struct FsInstallProxy final : FsProxyVfs {
|
||||
}
|
||||
}
|
||||
|
||||
Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) override {
|
||||
Result GetEntryType(const char *path, haze::FileAttrType *out_entry_type) override {
|
||||
R_TRY(FsProxyVfs::GetEntryType(path, out_entry_type));
|
||||
if (*out_entry_type == FsDirEntryType_File) {
|
||||
if (*out_entry_type == haze::FileAttrType_FILE) {
|
||||
R_TRY(FailedIfNotEnabled());
|
||||
}
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result CreateFile(const char* path, s64 size, u32 option) override {
|
||||
|
||||
Result CreateFile(const char* path, s64 size) override {
|
||||
R_TRY(FailedIfNotEnabled());
|
||||
R_TRY(IsValidFileType(path));
|
||||
R_TRY(FsProxyVfs::CreateFile(path, size, option));
|
||||
R_TRY(FsProxyVfs::CreateFile(path, size));
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result OpenFile(const char *path, u32 mode, FsFile *out_file) override {
|
||||
|
||||
Result OpenFile(const char *path, haze::FileOpenMode mode, haze::File *out_file) override {
|
||||
R_TRY(FailedIfNotEnabled());
|
||||
R_TRY(IsValidFileType(path));
|
||||
R_TRY(FsProxyVfs::OpenFile(path, mode, out_file));
|
||||
log_write("[MTP] done file open: %s mode: 0x%X\n", path, mode);
|
||||
|
||||
if (mode & FsOpenMode_Write) {
|
||||
const auto& e = m_entries[out_file->s.object_id];
|
||||
if (mode == haze::FileOpenMode_WRITE) {
|
||||
auto f = static_cast<File*>(out_file->impl);
|
||||
const auto& e = m_entries[f->index];
|
||||
|
||||
// check if we already have this file queued.
|
||||
log_write("[MTP] checking if empty\n");
|
||||
@@ -488,7 +582,8 @@ struct FsInstallProxy final : FsProxyVfs {
|
||||
log_write("[MTP] got file: %s\n", path);
|
||||
R_SUCCEED();
|
||||
}
|
||||
Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) override {
|
||||
|
||||
Result WriteFile(haze::File *file, s64 off, const void *buf, u64 write_size) override {
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
if (!g_shared_data.enabled) {
|
||||
log_write("[MTP] failing as not enabled\n");
|
||||
@@ -500,14 +595,20 @@ struct FsInstallProxy final : FsProxyVfs {
|
||||
R_THROW(FsError_NotImplemented);
|
||||
}
|
||||
|
||||
R_TRY(FsProxyVfs::WriteFile(file, off, buf, write_size, option));
|
||||
R_TRY(FsProxyVfs::WriteFile(file, off, buf, write_size));
|
||||
R_SUCCEED();
|
||||
}
|
||||
void CloseFile(FsFile *file) override {
|
||||
|
||||
void CloseFile(haze::File *file) override {
|
||||
auto f = static_cast<File*>(file->impl);
|
||||
if (!f) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool update{};
|
||||
{
|
||||
SCOPED_MUTEX(&g_shared_data.mutex);
|
||||
if (file->s.own_handle & FsOpenMode_Write) {
|
||||
if (f->mode == haze::FileOpenMode_WRITE) {
|
||||
log_write("[MTP] closing current file\n");
|
||||
if (g_shared_data.on_close) {
|
||||
g_shared_data.on_close();
|
||||
@@ -525,40 +626,36 @@ struct FsInstallProxy final : FsProxyVfs {
|
||||
|
||||
FsProxyVfs::CloseFile(file);
|
||||
}
|
||||
|
||||
// installs are already multi-threaded via yati.
|
||||
bool MultiThreadTransfer(s64 size, bool read) override {
|
||||
App::IsFileBaseEmummc();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
::haze::FsEntries g_fs_entries{};
|
||||
haze::FsEntries g_fs_entries{};
|
||||
|
||||
void haze_callback(const ::haze::CallbackData *data) {
|
||||
void haze_callback(const haze::CallbackData *data) {
|
||||
#if 0
|
||||
auto& e = *data;
|
||||
|
||||
switch (e.type) {
|
||||
case ::haze::CallbackType_OpenSession: log_write("[LIBHAZE] Opening Session\n"); break;
|
||||
case ::haze::CallbackType_CloseSession: log_write("[LIBHAZE] Closing Session\n"); break;
|
||||
case haze::CallbackType_OpenSession: log_write("[LIBHAZE] Opening Session\n"); break;
|
||||
case haze::CallbackType_CloseSession: log_write("[LIBHAZE] Closing Session\n"); break;
|
||||
|
||||
case ::haze::CallbackType_CreateFile: log_write("[LIBHAZE] Creating File: %s\n", e.file.filename); break;
|
||||
case ::haze::CallbackType_DeleteFile: log_write("[LIBHAZE] Deleting File: %s\n", e.file.filename); break;
|
||||
case haze::CallbackType_CreateFile: log_write("[LIBHAZE] Creating File: %s\n", e.file.filename); break;
|
||||
case haze::CallbackType_DeleteFile: log_write("[LIBHAZE] Deleting File: %s\n", e.file.filename); break;
|
||||
|
||||
case ::haze::CallbackType_RenameFile: log_write("[LIBHAZE] Rename File: %s -> %s\n", e.rename.filename, e.rename.newname); break;
|
||||
case ::haze::CallbackType_RenameFolder: log_write("[LIBHAZE] Rename Folder: %s -> %s\n", e.rename.filename, e.rename.newname); break;
|
||||
case haze::CallbackType_RenameFile: log_write("[LIBHAZE] Rename File: %s -> %s\n", e.rename.filename, e.rename.newname); break;
|
||||
case haze::CallbackType_RenameFolder: log_write("[LIBHAZE] Rename Folder: %s -> %s\n", e.rename.filename, e.rename.newname); break;
|
||||
|
||||
case ::haze::CallbackType_CreateFolder: log_write("[LIBHAZE] Creating Folder: %s\n", e.file.filename); break;
|
||||
case ::haze::CallbackType_DeleteFolder: log_write("[LIBHAZE] Deleting Folder: %s\n", e.file.filename); break;
|
||||
case haze::CallbackType_CreateFolder: log_write("[LIBHAZE] Creating Folder: %s\n", e.file.filename); break;
|
||||
case haze::CallbackType_DeleteFolder: log_write("[LIBHAZE] Deleting Folder: %s\n", e.file.filename); break;
|
||||
|
||||
case ::haze::CallbackType_ReadBegin: log_write("[LIBHAZE] Reading File Begin: %s \n", e.file.filename); break;
|
||||
case ::haze::CallbackType_ReadProgress: log_write("\t[LIBHAZE] Reading File: offset: %lld size: %lld\n", e.progress.offset, e.progress.size); break;
|
||||
case ::haze::CallbackType_ReadEnd: log_write("[LIBHAZE] Reading File Finished: %s\n", e.file.filename); break;
|
||||
case haze::CallbackType_ReadBegin: log_write("[LIBHAZE] Reading File Begin: %s \n", e.file.filename); break;
|
||||
case haze::CallbackType_ReadProgress: log_write("\t[LIBHAZE] Reading File: offset: %lld size: %lld\n", e.progress.offset, e.progress.size); break;
|
||||
case haze::CallbackType_ReadEnd: log_write("[LIBHAZE] Reading File Finished: %s\n", e.file.filename); break;
|
||||
|
||||
case ::haze::CallbackType_WriteBegin: log_write("[LIBHAZE] Writing File Begin: %s \n", e.file.filename); break;
|
||||
case ::haze::CallbackType_WriteProgress: log_write("\t[LIBHAZE] Writing File: offset: %lld size: %lld\n", e.progress.offset, e.progress.size); break;
|
||||
case ::haze::CallbackType_WriteEnd: log_write("[LIBHAZE] Writing File Finished: %s\n", e.file.filename); break;
|
||||
case haze::CallbackType_WriteBegin: log_write("[LIBHAZE] Writing File Begin: %s \n", e.file.filename); break;
|
||||
case haze::CallbackType_WriteProgress: log_write("\t[LIBHAZE] Writing File: offset: %lld size: %lld\n", e.progress.offset, e.progress.size); break;
|
||||
case haze::CallbackType_WriteEnd: log_write("[LIBHAZE] Writing File Finished: %s\n", e.file.filename); break;
|
||||
}
|
||||
#endif
|
||||
|
||||
App::NotifyFlashLed();
|
||||
}
|
||||
@@ -572,14 +669,43 @@ bool Init() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// add default mount of the sd card.
|
||||
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeSd>(), "", "microSD card"));
|
||||
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Nand), "image_nand", "Image nand"));
|
||||
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Sd), "image_sd", "Image sd"));
|
||||
g_fs_entries.emplace_back(std::make_shared<FsDevNullProxy>("DevNull", "DevNull (Speed Test)"));
|
||||
g_fs_entries.emplace_back(std::make_shared<FsInstallProxy>("install", "Install (NSP, XCI, NSZ, XCZ)"));
|
||||
|
||||
if (App::GetApp()->m_mtp_show_album.Get()) {
|
||||
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Sd), "Album", "Album (Image SD)"));
|
||||
}
|
||||
|
||||
if (App::GetApp()->m_mtp_show_content_sd.Get()) {
|
||||
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeContentStorage>(FsContentStorageId_SdCard), "ContentsM", "Contents (microSD card)"));
|
||||
}
|
||||
|
||||
if (App::GetApp()->m_mtp_show_content_system.Get()) {
|
||||
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeContentStorage>(FsContentStorageId_System), "ContentsS", "Contents (System)"));
|
||||
}
|
||||
|
||||
if (App::GetApp()->m_mtp_show_content_user.Get()) {
|
||||
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeContentStorage>(FsContentStorageId_User), "ContentsU", "Contents (User)"));
|
||||
}
|
||||
|
||||
if (App::GetApp()->m_mtp_show_games.Get()) {
|
||||
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsStdio>(true, "games:/"), "Games", "Games"));
|
||||
}
|
||||
|
||||
if (App::GetApp()->m_mtp_show_install.Get()) {
|
||||
g_fs_entries.emplace_back(std::make_shared<FsInstallProxy>("install", "Install (NSP, XCI, NSZ, XCZ)"));
|
||||
}
|
||||
|
||||
if (App::GetApp()->m_mtp_show_mounts.Get()) {
|
||||
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsStdio>(true, "mounts:/"), "Mounts", "Mounts"));
|
||||
}
|
||||
|
||||
if (App::GetApp()->m_mtp_show_speedtest.Get()) {
|
||||
g_fs_entries.emplace_back(std::make_shared<FsDevNullProxy>("DevNull", "DevNull (Speed Test)"));
|
||||
}
|
||||
|
||||
g_should_exit = false;
|
||||
if (!::haze::Initialize(haze_callback, THREAD_PRIO, THREAD_CORE, g_fs_entries)) {
|
||||
if (!haze::Initialize(haze_callback, g_fs_entries, App::GetApp()->m_mtp_vid.Get(), App::GetApp()->m_mtp_pid.Get())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -598,7 +724,7 @@ void Exit() {
|
||||
return;
|
||||
}
|
||||
|
||||
::haze::Exit();
|
||||
haze::Exit();
|
||||
g_is_running = false;
|
||||
g_should_exit = true;
|
||||
g_fs_entries.clear();
|
||||
@@ -619,4 +745,4 @@ void DisableInstallMode() {
|
||||
g_shared_data.enabled = false;
|
||||
}
|
||||
|
||||
} // namespace sphaira::haze
|
||||
} // namespace sphaira::libhaze
|
||||
|
||||
@@ -8,48 +8,97 @@
|
||||
namespace sphaira::i18n {
|
||||
namespace {
|
||||
|
||||
std::vector<u8> g_i18n_data;
|
||||
yyjson_doc* json;
|
||||
yyjson_val* root;
|
||||
std::unordered_map<std::string, std::string> g_tr_cache;
|
||||
std::vector<u8> g_i18n_data{};
|
||||
yyjson_doc* json{};
|
||||
yyjson_val* root{};
|
||||
std::unordered_map<std::string, std::string> g_tr_cache{};
|
||||
Mutex g_mutex{};
|
||||
|
||||
std::string get_internal(std::string_view str) {
|
||||
const std::string kkey = {str.data(), str.length()};
|
||||
static WordOrder g_word_order = WordOrder::PhraseName;
|
||||
|
||||
static WordOrder DetectWordOrder(const std::string& lang) {
|
||||
// SOV Language.
|
||||
if (lang == "ja" || lang == "ko")
|
||||
return WordOrder::NamePhrase;
|
||||
|
||||
// Default: SVO Language.
|
||||
return WordOrder::PhraseName;
|
||||
}
|
||||
|
||||
static std::string get_internal(std::string_view str, std::string_view fallback) {
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
|
||||
const std::string kkey{str.data(), str.length()};
|
||||
|
||||
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;
|
||||
const auto it = g_tr_cache.emplace(kkey, std::string{fallback}).first;
|
||||
|
||||
if (!json || !root) {
|
||||
log_write("no json or root\n");
|
||||
return kkey;
|
||||
return std::string{fallback};
|
||||
}
|
||||
|
||||
auto key = yyjson_obj_getn(root, str.data(), str.length());
|
||||
if (!key) {
|
||||
yyjson_val* node = yyjson_obj_getn(root, str.data(), str.length());
|
||||
if (!node && str != fallback) {
|
||||
node = yyjson_obj_getn(root, fallback.data(), fallback.length());
|
||||
if (node) {
|
||||
log_write("\tfallback-key matched: [%s]\n", std::string(fallback).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
log_write("\tfailed to find key: [%s]\n", kkey.c_str());
|
||||
return kkey;
|
||||
return std::string{fallback};
|
||||
}
|
||||
|
||||
auto val = yyjson_get_str(key);
|
||||
auto val_len = yyjson_get_len(key);
|
||||
if (!val || !val_len) {
|
||||
std::string ret;
|
||||
|
||||
// key > string
|
||||
if (const char* val = yyjson_get_str(node)) {
|
||||
size_t len = yyjson_get_len(node);
|
||||
if (len) {
|
||||
ret.assign(val, len);
|
||||
}
|
||||
}
|
||||
|
||||
// key > array of strings (multi-line)
|
||||
if (ret.empty() && yyjson_is_arr(node)) {
|
||||
size_t idx, max;
|
||||
yyjson_val* elem;
|
||||
yyjson_arr_foreach(node, idx, max, elem) {
|
||||
if (idx) ret.push_back('\n');
|
||||
|
||||
if (yyjson_is_str(elem)) {
|
||||
const char* s = yyjson_get_str(elem);
|
||||
size_t len = yyjson_get_len(elem);
|
||||
if (s && len) ret.append(s, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ret.empty()) {
|
||||
log_write("\tfailed to get value: [%s]\n", kkey.c_str());
|
||||
return kkey;
|
||||
ret = std::string{fallback};
|
||||
}
|
||||
|
||||
// update entry in cache
|
||||
const std::string ret = {val, val_len};
|
||||
g_tr_cache.insert_or_assign(it, kkey, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static std::string get_internal(std::string_view str) {
|
||||
return get_internal(str, str);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool init(long index) {
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
|
||||
g_tr_cache.clear();
|
||||
R_TRY_RESULT(romfsInit(), false);
|
||||
ON_SCOPE_EXIT( romfsExit() );
|
||||
@@ -71,14 +120,15 @@ bool init(long index) {
|
||||
case 4: setLanguage = SetLanguage_DE; break; // "German"
|
||||
case 5: setLanguage = SetLanguage_IT; break; // "Italian"
|
||||
case 6: setLanguage = SetLanguage_ES; break; // "Spanish"
|
||||
case 7: setLanguage = SetLanguage_ZHCN; break; // "Chinese"
|
||||
case 7: setLanguage = SetLanguage_ZHCN; break; // "Chinese (Simplified)"
|
||||
case 8: setLanguage = SetLanguage_KO; break; // "Korean"
|
||||
case 9: setLanguage = SetLanguage_NL; break; // "Dutch"
|
||||
case 10: setLanguage = SetLanguage_PT; break; // "Portuguese"
|
||||
case 11: setLanguage = SetLanguage_RU; break; // "Russian"
|
||||
case 12: lang_name = "se"; break; // "Swedish"
|
||||
case 13: lang_name = "vi"; break; // "Vietnamese"
|
||||
case 14: lang_name = "uk"; break; // "Ukrainian"
|
||||
case 12: setLanguage = SetLanguage_ZHTW; break; // "Chinese (Traditional)"
|
||||
case 13: lang_name = "se"; break; // "Swedish"
|
||||
case 14: lang_name = "vi"; break; // "Vietnamese"
|
||||
case 15: lang_name = "uk"; break; // "Ukrainian"
|
||||
}
|
||||
|
||||
switch (setLanguage) {
|
||||
@@ -87,15 +137,17 @@ bool init(long index) {
|
||||
case SetLanguage_DE: lang_name = "de"; break;
|
||||
case SetLanguage_IT: lang_name = "it"; break;
|
||||
case SetLanguage_ES: lang_name = "es"; break;
|
||||
case SetLanguage_ZHCN: lang_name = "zh"; break;
|
||||
case SetLanguage_ZHCN: lang_name = "zh-CN"; break;
|
||||
case SetLanguage_KO: lang_name = "ko"; break;
|
||||
case SetLanguage_NL: lang_name = "nl"; break;
|
||||
case SetLanguage_PT: lang_name = "pt"; break;
|
||||
case SetLanguage_RU: lang_name = "ru"; break;
|
||||
case SetLanguage_ZHTW: lang_name = "zh"; break;
|
||||
case SetLanguage_ZHTW: lang_name = "zh-TW"; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
g_word_order = DetectWordOrder(lang_name);
|
||||
|
||||
const fs::FsPath sdmc_path = "/config/sphaira/i18n/" + lang_name + ".json";
|
||||
const fs::FsPath romfs_path = "romfs:/i18n/" + lang_name + ".json";
|
||||
fs::FsPath path = sdmc_path;
|
||||
@@ -128,9 +180,12 @@ bool init(long index) {
|
||||
}
|
||||
|
||||
void exit() {
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
|
||||
if (json) {
|
||||
yyjson_doc_free(json);
|
||||
json = nullptr;
|
||||
root = nullptr;
|
||||
}
|
||||
g_i18n_data.clear();
|
||||
}
|
||||
@@ -139,12 +194,40 @@ std::string get(std::string_view str) {
|
||||
return get_internal(str);
|
||||
}
|
||||
|
||||
std::string get(std::string_view str, std::string_view fallback) {
|
||||
return get_internal(str, fallback);
|
||||
}
|
||||
|
||||
// Reorders sentence structure based on locale.
|
||||
WordOrder GetWordOrder() {
|
||||
return g_word_order;
|
||||
}
|
||||
|
||||
bool WordOrderLocale() {
|
||||
return g_word_order == WordOrder::NamePhrase;
|
||||
}
|
||||
|
||||
std::string Reorder(std::string_view phrase, std::string_view name) {
|
||||
std::string p = i18n::get(phrase);
|
||||
std::string out;
|
||||
out.reserve(phrase.length() + name.length());
|
||||
|
||||
if (g_word_order == WordOrder::NamePhrase) {
|
||||
out.append(name);
|
||||
out.append(p);
|
||||
} else {
|
||||
out.append(p);
|
||||
out.append(name);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace sphaira::i18n
|
||||
|
||||
namespace literals {
|
||||
|
||||
std::string operator""_i18n(const char* str, size_t len) {
|
||||
return sphaira::i18n::get_internal({str, len});
|
||||
return sphaira::i18n::get({str, len});
|
||||
}
|
||||
|
||||
} // namespace literals
|
||||
|
||||
@@ -1,84 +1,51 @@
|
||||
#include "location.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "app.hpp"
|
||||
#include "usbdvd.hpp"
|
||||
#include "utils/devoptab.hpp"
|
||||
#include "i18n.hpp"
|
||||
|
||||
#include <ff.h>
|
||||
#include <cstring>
|
||||
#include <minIni.h>
|
||||
#include <usbhsfs.h>
|
||||
|
||||
#ifdef ENABLE_LIBUSBDVD
|
||||
#include "usbdvd.hpp"
|
||||
#endif // ENABLE_LIBUSBDVD
|
||||
|
||||
#ifdef ENABLE_LIBUSBHSFS
|
||||
#include <usbhsfs.h>
|
||||
#endif // ENABLE_LIBUSBHSFS
|
||||
|
||||
namespace sphaira::location {
|
||||
namespace {
|
||||
|
||||
constexpr fs::FsPath location_path{"/config/sphaira/locations.ini"};
|
||||
|
||||
} // namespace
|
||||
|
||||
void Add(const Entry& e) {
|
||||
if (e.name.empty() || e.url.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ini_puts(e.name.c_str(), "url", e.url.c_str(), location_path);
|
||||
if (!e.user.empty()) {
|
||||
ini_puts(e.name.c_str(), "user", e.user.c_str(), location_path);
|
||||
}
|
||||
if (!e.pass.empty()) {
|
||||
ini_puts(e.name.c_str(), "pass", e.pass.c_str(), location_path);
|
||||
}
|
||||
if (!e.bearer.empty()) {
|
||||
ini_puts(e.name.c_str(), "bearer", e.bearer.c_str(), location_path);
|
||||
}
|
||||
if (!e.pub_key.empty()) {
|
||||
ini_puts(e.name.c_str(), "pub_key", e.pub_key.c_str(), location_path);
|
||||
}
|
||||
if (!e.priv_key.empty()) {
|
||||
ini_puts(e.name.c_str(), "priv_key", e.priv_key.c_str(), location_path);
|
||||
}
|
||||
if (e.port) {
|
||||
ini_putl(e.name.c_str(), "port", e.port, location_path);
|
||||
}
|
||||
}
|
||||
|
||||
auto Load() -> Entries {
|
||||
Entries out{};
|
||||
|
||||
auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
|
||||
auto e = static_cast<Entries*>(UserData);
|
||||
|
||||
// add new entry if use section changed.
|
||||
if (e->empty() || std::strcmp(Section, e->back().name.c_str())) {
|
||||
e->emplace_back(Section);
|
||||
}
|
||||
|
||||
if (!std::strcmp(Key, "url")) {
|
||||
e->back().url = Value;
|
||||
} else if (!std::strcmp(Key, "user")) {
|
||||
e->back().user = Value;
|
||||
} else if (!std::strcmp(Key, "pass")) {
|
||||
e->back().pass = Value;
|
||||
} else if (!std::strcmp(Key, "bearer")) {
|
||||
e->back().bearer = Value;
|
||||
} else if (!std::strcmp(Key, "pub_key")) {
|
||||
e->back().pub_key = Value;
|
||||
} else if (!std::strcmp(Key, "priv_key")) {
|
||||
e->back().priv_key = Value;
|
||||
} else if (!std::strcmp(Key, "port")) {
|
||||
e->back().port = std::atoi(Value);
|
||||
}
|
||||
|
||||
return 1;
|
||||
};
|
||||
|
||||
ini_browse(cb, &out, location_path);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
auto GetStdio(bool write) -> StdioEntries {
|
||||
StdioEntries out{};
|
||||
|
||||
const auto add_from_entries = [](StdioEntries& entries, StdioEntries& out, bool write) {
|
||||
for (auto& e : entries) {
|
||||
if (write && (e.flags & FsEntryFlag::FsEntryFlag_ReadOnly)) {
|
||||
log_write("[STDIO] skipping read only mount: %s\n", e.name.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e.flags & FsEntryFlag::FsEntryFlag_ReadOnly) {
|
||||
e.name += i18n::get(" (Read Only)");
|
||||
}
|
||||
|
||||
out.emplace_back(e);
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
StdioEntries entries;
|
||||
if (R_SUCCEEDED(devoptab::GetNetworkDevices(entries))) {
|
||||
log_write("[LOCATION] got devoptab mounts: %zu\n", entries.size());
|
||||
add_from_entries(entries, out, write);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ENABLE_LIBUSBDVD
|
||||
// try and load usbdvd entry.
|
||||
// todo: check if more than 1 entry is supported.
|
||||
// todo: only call if usbdvd is init.
|
||||
@@ -88,7 +55,9 @@ auto GetStdio(bool write) -> StdioEntries {
|
||||
out.emplace_back(entry);
|
||||
}
|
||||
}
|
||||
#endif // ENABLE_LIBUSBDVD
|
||||
|
||||
#ifdef ENABLE_LIBUSBHSFS
|
||||
// bail out early if usbhdd is disabled.
|
||||
if (!App::GetHddEnable()) {
|
||||
log_write("[USBHSFS] not enabled\n");
|
||||
@@ -111,25 +80,15 @@ auto GetStdio(bool write) -> StdioEntries {
|
||||
char display_name[0x100];
|
||||
std::snprintf(display_name, sizeof(display_name), "%s (%s - %s - %zu GB)", e.name, LIBUSBHSFS_FS_TYPE_STR(e.fs_type), e.product_name, e.capacity / 1024 / 1024 / 1024);
|
||||
|
||||
out.emplace_back(e.name, display_name, e.write_protect);
|
||||
u32 flags = 0;
|
||||
if (e.write_protect || (e.flags & UsbHsFsMountFlags_ReadOnly)) {
|
||||
flags |= FsEntryFlag::FsEntryFlag_ReadOnly;
|
||||
}
|
||||
|
||||
out.emplace_back(e.name, display_name, flags);
|
||||
log_write("\t[USBHSFS] %s name: %s serial: %s man: %s\n", e.name, e.product_name, e.serial_number, e.manufacturer);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
auto GetFat() -> StdioEntries {
|
||||
StdioEntries out{};
|
||||
|
||||
for (auto& e : VolumeStr) {
|
||||
char path[64];
|
||||
std::snprintf(path, sizeof(path), "%s:/", e);
|
||||
|
||||
char display_name[0x100];
|
||||
std::snprintf(display_name, sizeof(display_name), "%s (Read Only)", path);
|
||||
|
||||
out.emplace_back(path, display_name, true);
|
||||
}
|
||||
#endif // ENABLE_LIBUSBHSFS
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -56,8 +56,6 @@ void userAppInit(void) {
|
||||
diagAbortWithResult(rc);
|
||||
if (R_FAILED(rc = plInitialize(PlServiceType_User)))
|
||||
diagAbortWithResult(rc);
|
||||
if (R_FAILED(rc = psmInitialize()))
|
||||
diagAbortWithResult(rc);
|
||||
if (R_FAILED(rc = nifmInitialize(NifmServiceType_User)))
|
||||
diagAbortWithResult(rc);
|
||||
if (R_FAILED(rc = accountInitialize(is_application ? AccountServiceType_Application : AccountServiceType_System)))
|
||||
@@ -83,7 +81,6 @@ void userAppExit(void) {
|
||||
setExit();
|
||||
accountExit();
|
||||
nifmExit();
|
||||
psmExit();
|
||||
plExit();
|
||||
socketExit();
|
||||
// NOTE (DMC): prevents exfat corruption.
|
||||
|
||||
@@ -223,7 +223,6 @@ long minizip_seek_file_func_stdio(voidpf opaque, voidpf stream, ZPOS64_T offset,
|
||||
|
||||
uLong minizip_read_file_func_stdio(voidpf opaque, voidpf stream, void* buf, uLong size) {
|
||||
auto file = static_cast<std::FILE*>(stream);
|
||||
log_write("[ZIP] doing read\n");
|
||||
return std::fread(buf, 1, size, file);
|
||||
}
|
||||
|
||||
@@ -373,7 +372,7 @@ void FileFuncNative(zlib_filefunc64_def* funcs) {
|
||||
|
||||
Result PeekFirstFileName(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& name) {
|
||||
fs::File file;
|
||||
R_TRY(fs->OpenFile(path, FsOpenMode_Read, &file));
|
||||
R_TRY(fs->OpenFile(path, fs::OpenMode_ReadBuffered, &file));
|
||||
|
||||
mmz_LocalHeader local_hdr;
|
||||
u64 bytes_read;
|
||||
|
||||
@@ -20,7 +20,7 @@ auto OptionBase<T>::GetInternal(const char* name) -> T {
|
||||
} else if constexpr(std::is_same_v<T, float>) {
|
||||
m_value = ini_getf(m_section.c_str(), name, m_default_value, App::CONFIG_PATH);
|
||||
} else if constexpr(std::is_same_v<T, std::string>) {
|
||||
char buf[FS_MAX_PATH];
|
||||
char buf[PATH_MAX]{};
|
||||
ini_gets(m_section.c_str(), name, m_default_value.c_str(), buf, sizeof(buf), App::CONFIG_PATH);
|
||||
m_value = buf;
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ namespace sphaira::swkbd {
|
||||
namespace {
|
||||
|
||||
struct Config {
|
||||
char out_text[FS_MAX_PATH]{};
|
||||
char out_text[PATH_MAX]{};
|
||||
bool numpad{};
|
||||
};
|
||||
|
||||
Result ShowInternal(Config& cfg, const char* guide, const char* initial, s64 len_min, s64 len_max) {
|
||||
Result ShowInternal(Config& cfg, const char* header, const char* guide, const char* initial, s64 len_min, s64 len_max) {
|
||||
SwkbdConfig c;
|
||||
R_TRY(swkbdCreate(&c, 0));
|
||||
swkbdConfigMakePresetDefault(&c);
|
||||
@@ -20,7 +20,17 @@ Result ShowInternal(Config& cfg, const char* guide, const char* initial, s64 len
|
||||
swkbdConfigSetType(&c, SwkbdType_NumPad);
|
||||
}
|
||||
|
||||
// only works if len_max <= 32.
|
||||
if (header) {
|
||||
swkbdConfigSetHeaderText(&c, header);
|
||||
}
|
||||
|
||||
if (guide) {
|
||||
// only works if len_max <= 32.
|
||||
if (header) {
|
||||
swkbdConfigSetSubText(&c, guide);
|
||||
}
|
||||
|
||||
swkbdConfigSetGuideText(&c, guide);
|
||||
}
|
||||
|
||||
@@ -41,17 +51,17 @@ Result ShowInternal(Config& cfg, const char* guide, const char* initial, s64 len
|
||||
|
||||
} // namespace
|
||||
|
||||
Result ShowText(std::string& out, const char* guide, const char* initial, s64 len_min, s64 len_max) {
|
||||
Result ShowText(std::string& out, const char* header, const char* guide, const char* initial, s64 len_min, s64 len_max) {
|
||||
Config cfg{};
|
||||
R_TRY(ShowInternal(cfg, guide, initial, len_min, len_max));
|
||||
R_TRY(ShowInternal(cfg, header, guide, initial, len_min, len_max));
|
||||
out = cfg.out_text;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result ShowNumPad(s64& out, const char* guide, const char* initial, s64 len_min, s64 len_max) {
|
||||
Result ShowNumPad(s64& out, const char* header, const char* guide, const char* initial, s64 len_min, s64 len_max) {
|
||||
Config cfg{};
|
||||
cfg.numpad = true;
|
||||
R_TRY(ShowInternal(cfg, guide, initial, len_min, len_max));
|
||||
R_TRY(ShowInternal(cfg, header, guide, initial, len_min, len_max));
|
||||
out = std::atoll(cfg.out_text);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
@@ -82,6 +82,14 @@ struct ThreadData {
|
||||
return read_running || decompress_running || write_running;
|
||||
}
|
||||
|
||||
auto GetReadOffset() volatile const -> s64 {
|
||||
return read_offset;
|
||||
}
|
||||
|
||||
auto GetDecompressOffset() volatile const -> s64 {
|
||||
return decompress_offset;
|
||||
}
|
||||
|
||||
auto GetWriteOffset() volatile const -> s64 {
|
||||
return write_offset;
|
||||
}
|
||||
@@ -94,8 +102,16 @@ struct ThreadData {
|
||||
return &m_uevent_done;
|
||||
}
|
||||
|
||||
auto GetProgressEvent() {
|
||||
return &m_uevent_progres;
|
||||
auto GetReadProgressEvent() {
|
||||
return &m_uevent_read_progress;
|
||||
}
|
||||
|
||||
auto GetDecompressProgressEvent() {
|
||||
return &m_uevent_decompress_progress;
|
||||
}
|
||||
|
||||
auto GetWriteProgressEvent() {
|
||||
return &m_uevent_write_progress;
|
||||
}
|
||||
|
||||
void SetReadResult(Result result) {
|
||||
@@ -174,7 +190,9 @@ private:
|
||||
CondVar can_pull_write{};
|
||||
|
||||
UEvent m_uevent_done{};
|
||||
UEvent m_uevent_progres{};
|
||||
UEvent m_uevent_read_progress{};
|
||||
UEvent m_uevent_decompress_progress{};
|
||||
UEvent m_uevent_write_progress{};
|
||||
|
||||
RingBuf<2> read_buffers{};
|
||||
RingBuf<2> write_buffers{};
|
||||
@@ -219,8 +237,10 @@ ThreadData::ThreadData(ui::ProgressBox* _pbox, s64 size, const ReadCallback& _rf
|
||||
condvarInit(std::addressof(can_pull));
|
||||
condvarInit(std::addressof(can_pull_write));
|
||||
|
||||
ueventCreate(&m_uevent_done, false);
|
||||
ueventCreate(&m_uevent_progres, true);
|
||||
ueventCreate(GetDoneEvent(), false);
|
||||
ueventCreate(GetReadProgressEvent(), true);
|
||||
ueventCreate(GetDecompressProgressEvent(), true);
|
||||
ueventCreate(GetWriteProgressEvent(), true);
|
||||
}
|
||||
|
||||
auto ThreadData::GetResults() volatile -> Result {
|
||||
@@ -379,6 +399,7 @@ Result ThreadData::readFuncInternal() {
|
||||
break;
|
||||
}
|
||||
|
||||
ueventSignal(GetReadProgressEvent());
|
||||
auto buf_size = bytes_read;
|
||||
R_TRY(this->SetDecompressBuf(buf, buffer_offset, buf_size));
|
||||
}
|
||||
@@ -423,25 +444,17 @@ Result ThreadData::decompressFuncInternal() {
|
||||
}
|
||||
|
||||
size -= rsize;
|
||||
this->decompress_offset += rsize;
|
||||
data += rsize;
|
||||
|
||||
// const auto buf_off = temp_buf.size();
|
||||
// temp_buf.resize(buf_off + size);
|
||||
// std::memcpy(temp_buf.data() + buf_off, data, size);
|
||||
// this->decompress_offset += size;
|
||||
|
||||
// if (temp_buf.size() >= temp_buf_flush_max) {
|
||||
// // log_write("flushing data: %zu %.2f MiB\n", temp_buf.size(), temp_buf.size() / 1024.0 / 1024.0);
|
||||
// R_TRY(this->SetWriteBuf(temp_buf, temp_buf.size()));
|
||||
// temp_buf.resize(0);
|
||||
// }
|
||||
this->decompress_offset += rsize;
|
||||
ueventSignal(GetDecompressProgressEvent());
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}));
|
||||
} else {
|
||||
this->decompress_offset += buf.size();
|
||||
ueventSignal(GetDecompressProgressEvent());
|
||||
|
||||
R_TRY(this->SetWriteBuf(buf, buf.size()));
|
||||
}
|
||||
}
|
||||
@@ -479,7 +492,7 @@ Result ThreadData::writeFuncInternal() {
|
||||
}
|
||||
|
||||
this->write_offset += size;
|
||||
ueventSignal(GetProgressEvent());
|
||||
ueventSignal(GetWriteProgressEvent());
|
||||
}
|
||||
|
||||
log_write("finished write thread success!\n");
|
||||
@@ -586,7 +599,11 @@ Result TransferInternal(ui::ProgressBox* pbox, s64 size, const ReadCallback& rfu
|
||||
R_TRY(start_threads());
|
||||
log_write("[THREAD] started threads\n");
|
||||
|
||||
const auto waiter_progress = waiterForUEvent(t_data.GetProgressEvent());
|
||||
// use the read progress as the write output may be smaller due to compressing
|
||||
// so read will show a more accurate progress.
|
||||
// TODO: show progress bar for all 3 threads.
|
||||
// NOTE: went back to using write progress for now.
|
||||
const auto waiter_progress = waiterForUEvent(t_data.GetWriteProgressEvent());
|
||||
const auto waiter_cancel = waiterForUEvent(pbox->GetCancelEvent());
|
||||
const auto waiter_done = waiterForUEvent(t_data.GetDoneEvent());
|
||||
|
||||
@@ -777,9 +794,14 @@ Result TransferUnzipAll(ui::ProgressBox* pbox, void* zfile, fs::Fs* fs, const fs
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto path_len = std::strlen(path);
|
||||
if (!path_len) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pbox->NewTransfer(name);
|
||||
|
||||
if (path[std::strlen(path) -1] == '/') {
|
||||
if (path[path_len -1] == '/') {
|
||||
Result rc;
|
||||
if (R_FAILED(rc = fs->CreateDirectoryRecursively(path)) && rc != FsError_PathAlreadyExists) {
|
||||
log_write("failed to create folder: %s 0x%04X\n", path.s, rc);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "yati/nx/ncm.hpp"
|
||||
|
||||
#include "utils/thread.hpp"
|
||||
#include "i18n.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <atomic>
|
||||
@@ -28,6 +29,7 @@ struct ThreadData {
|
||||
void Clear();
|
||||
|
||||
void PushAsync(u64 id);
|
||||
void PushAsync(const std::span<const NsApplicationRecord> app_ids);
|
||||
auto GetAsync(u64 app_id) -> ThreadResultData*;
|
||||
auto Get(u64 app_id, bool* cached = nullptr) -> ThreadResultData*;
|
||||
|
||||
@@ -112,8 +114,8 @@ auto& GetNcmEntry(u8 storage_id) {
|
||||
void FakeNacpEntry(ThreadResultData* e) {
|
||||
e->status = NacpLoadStatus::Error;
|
||||
// fake the nacp entry
|
||||
std::strcpy(e->lang.name, "Corrupted");
|
||||
std::strcpy(e->lang.author, "Corrupted");
|
||||
std::strcpy(e->lang.name, "Corrupted"_i18n.c_str());
|
||||
std::strcpy(e->lang.author, "Corrupted"_i18n.c_str());
|
||||
}
|
||||
|
||||
Result LoadControlManual(u64 id, NacpStruct& nacp, ThreadResultData* data) {
|
||||
@@ -208,6 +210,30 @@ void ThreadData::PushAsync(u64 id) {
|
||||
}
|
||||
}
|
||||
|
||||
void ThreadData::PushAsync(const std::span<const NsApplicationRecord> app_ids) {
|
||||
SCOPED_MUTEX(&m_mutex_id);
|
||||
SCOPED_MUTEX(&m_mutex_result);
|
||||
bool added_at_least_one = false;
|
||||
|
||||
for (auto& record : app_ids) {
|
||||
const auto id = record.application_id;
|
||||
|
||||
const auto it_id = std::ranges::find(m_ids, id);
|
||||
const auto it_result = std::ranges::find_if(m_result, [id](auto& e){
|
||||
return id == e->id;
|
||||
});
|
||||
|
||||
if (it_id == m_ids.end() && it_result == m_result.end()) {
|
||||
m_ids.emplace_back(id);
|
||||
added_at_least_one = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (added_at_least_one) {
|
||||
ueventSignal(&m_uevent);
|
||||
}
|
||||
}
|
||||
|
||||
auto ThreadData::GetAsync(u64 app_id) -> ThreadResultData* {
|
||||
SCOPED_MUTEX(&m_mutex_result);
|
||||
|
||||
@@ -428,6 +454,13 @@ void PushAsync(u64 app_id) {
|
||||
}
|
||||
}
|
||||
|
||||
void PushAsync(const std::span<const NsApplicationRecord> app_ids) {
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
if (g_thread_data) {
|
||||
g_thread_data->PushAsync(app_ids);
|
||||
}
|
||||
}
|
||||
|
||||
auto GetAsync(u64 app_id) -> ThreadResultData* {
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
if (g_thread_data) {
|
||||
|
||||
@@ -51,6 +51,19 @@ auto GetCodeMessage(Result rc) -> const char* {
|
||||
case Result_FsLoadingCancelled: return "SphairaError_FsLoadingCancelled";
|
||||
case Result_FsBrokenRoot: return "SphairaError_FsBrokenRoot";
|
||||
case Result_FsUnknownStdioError: return "SphairaError_FsUnknownStdioError";
|
||||
case Result_FsStdioFailedToSeek: return "SphairaError_FsStdioFailedToSeek";
|
||||
case Result_FsStdioFailedToRead: return "SphairaError_FsStdioFailedToRead";
|
||||
case Result_FsStdioFailedToWrite: return "SphairaError_FsStdioFailedToWrite";
|
||||
case Result_FsStdioFailedToOpenFile: return "SphairaError_FsStdioFailedToOpenFile";
|
||||
case Result_FsStdioFailedToCreate: return "SphairaError_FsStdioFailedToCreate";
|
||||
case Result_FsStdioFailedToTruncate: return "SphairaError_FsStdioFailedToTruncate";
|
||||
case Result_FsStdioFailedToFlush: return "SphairaError_FsStdioFailedToFlush";
|
||||
case Result_FsStdioFailedToCreateDirectory: return "SphairaError_FsStdioFailedToCreateDirectory";
|
||||
case Result_FsStdioFailedToDeleteFile: return "SphairaError_FsStdioFailedToDeleteFile";
|
||||
case Result_FsStdioFailedToDeleteDirectory: return "SphairaError_FsStdioFailedToDeleteDirectory";
|
||||
case Result_FsStdioFailedToOpenDirectory: return "SphairaError_FsStdioFailedToOpenDirectory";
|
||||
case Result_FsStdioFailedToRename: return "SphairaError_FsStdioFailedToRename";
|
||||
case Result_FsStdioFailedToStat: return "SphairaError_FsStdioFailedToStat";
|
||||
case Result_FsReadOnly: return "SphairaError_FsReadOnly";
|
||||
case Result_FsNotActive: return "SphairaError_FsNotActive";
|
||||
case Result_FsFailedStdioStat: return "SphairaError_FsFailedStdioStat";
|
||||
|
||||
@@ -65,11 +65,7 @@ auto List::ScrollDown(s64& index, s64 step, s64 count) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index + step < count) {
|
||||
index += step;
|
||||
} else {
|
||||
index = count - 1;
|
||||
}
|
||||
index = std::min(index + step, count - 1);
|
||||
|
||||
if (index != old_index) {
|
||||
App::PlaySoundEffect(SoundEffect::Scroll);
|
||||
@@ -103,11 +99,7 @@ auto List::ScrollUp(s64& index, s64 step, s64 count) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index >= step) {
|
||||
index -= step;
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
index = std::max<s64>(0, index - step);
|
||||
|
||||
if (index != old_index) {
|
||||
App::PlaySoundEffect(SoundEffect::Scroll);
|
||||
@@ -169,20 +161,24 @@ void List::OnUpdateGrid(Controller* controller, TouchInfo* touch, s64 index, s64
|
||||
const auto page_up_button = GetPageJump() ? (m_row == 1 ? Button::DPAD_LEFT : Button::L2) : (Button::NONE);
|
||||
const auto page_down_button = GetPageJump() ? (m_row == 1 ? Button::DPAD_RIGHT : Button::R2) : (Button::NONE);
|
||||
|
||||
const auto hotkey = m_row == 1 ? controller->GotHeld(Button::R2) : false;
|
||||
const auto end_page = INT32_MAX;
|
||||
const auto hot_page = m_page * 4;
|
||||
|
||||
if (controller->GotDown(Button::DOWN)) {
|
||||
if (ScrollDown(index, m_row, count)) {
|
||||
if (ScrollDown(index, hotkey ? end_page : m_row, count)) {
|
||||
callback(false, index);
|
||||
}
|
||||
} else if (controller->GotDown(Button::UP)) {
|
||||
if (ScrollUp(index, m_row, count)) {
|
||||
if (ScrollUp(index, hotkey ? end_page : m_row, count)) {
|
||||
callback(false, index);
|
||||
}
|
||||
} else if (controller->GotDown(page_down_button)) {
|
||||
if (ScrollDown(index, m_page, count)) {
|
||||
if (ScrollDown(index, hotkey ? hot_page : m_page, count)) {
|
||||
callback(false, index);
|
||||
}
|
||||
} else if (controller->GotDown(page_up_button)) {
|
||||
if (ScrollUp(index, m_page, count)) {
|
||||
if (ScrollUp(index, hotkey ? hot_page : m_page, count)) {
|
||||
callback(false, index);
|
||||
}
|
||||
} else if (m_row > 1 && controller->GotDown(Button::RIGHT)) {
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#include "web.hpp"
|
||||
#include "minizip_helper.hpp"
|
||||
|
||||
#include "utils/utils.hpp"
|
||||
|
||||
#include <minIni.h>
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
@@ -352,7 +354,7 @@ auto UninstallApp(ProgressBox* pbox, const Entry& entry) -> Result {
|
||||
|
||||
// remove directory, this will also delete manifest and info
|
||||
const auto dir = BuildPackageCachePath(entry);
|
||||
pbox->NewTransfer("Removing "_i18n + dir.toString());
|
||||
pbox->NewTransfer(i18n::Reorder("Removing ", dir.toString()));
|
||||
if (R_FAILED(fs.DeleteDirectoryRecursively(dir))) {
|
||||
log_write("failed to delete folder: %s\n", dir.s);
|
||||
} else {
|
||||
@@ -382,7 +384,7 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> Result {
|
||||
|
||||
// 1. download the zip
|
||||
if (!pbox->ShouldExit()) {
|
||||
pbox->NewTransfer("Downloading "_i18n + entry.title);
|
||||
pbox->NewTransfer(i18n::Reorder("Downloading ", entry.title));
|
||||
log_write("starting download\n");
|
||||
|
||||
const auto url = BuildZipUrl(entry);
|
||||
@@ -597,7 +599,8 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
|
||||
|
||||
options->Add<SidebarEntryCallback>("Leave Feedback"_i18n, [this](){
|
||||
std::string out;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out)) && !out.empty()) {
|
||||
std::string header = "Leave feedback for " + m_entry.title;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, header.c_str())) && !out.empty()) {
|
||||
const auto post = "name=" "switch_user" "&package=" + m_entry.name + "&message=" + out;
|
||||
const auto file = BuildFeedbackCachePath(m_entry);
|
||||
|
||||
@@ -736,9 +739,9 @@ void EntryMenu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
text_start_y += text_inc_y;
|
||||
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "updated: %s"_i18n.c_str(), m_entry.updated.c_str());
|
||||
text_start_y += text_inc_y;
|
||||
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "category: %s"_i18n.c_str(), m_entry.category.c_str());
|
||||
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "category: %s"_i18n.c_str(), i18n::get(m_entry.category).c_str());
|
||||
text_start_y += text_inc_y;
|
||||
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "extracted: %.2f MiB"_i18n.c_str(), (double)m_entry.extracted / 1024.0);
|
||||
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "extracted: %s"_i18n.c_str(), utils::formatSizeStorage(m_entry.extracted).c_str());
|
||||
text_start_y += text_inc_y;
|
||||
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "app_dls: %s"_i18n.c_str(), AppDlToStr(m_entry.app_dls).c_str());
|
||||
text_start_y += text_inc_y;
|
||||
@@ -803,7 +806,7 @@ void EntryMenu::UpdateOptions() {
|
||||
App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Downloaded "_i18n + m_entry.title);
|
||||
App::Notify(i18n::Reorder("Downloaded ", m_entry.title));
|
||||
m_entry.status = EntryStatus::Installed;
|
||||
m_menu.SetDirty();
|
||||
UpdateOptions();
|
||||
@@ -819,7 +822,7 @@ void EntryMenu::UpdateOptions() {
|
||||
App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Removed "_i18n + m_entry.title);
|
||||
App::Notify(i18n::Reorder("Removed ", m_entry.title));
|
||||
m_entry.status = EntryStatus::Get;
|
||||
m_menu.SetDirty();
|
||||
UpdateOptions();
|
||||
@@ -830,7 +833,7 @@ void EntryMenu::UpdateOptions() {
|
||||
const Option install_option{"Install"_i18n, install};
|
||||
const Option update_option{"Update"_i18n, install};
|
||||
const Option launch_option{"Launch"_i18n, launch};
|
||||
const Option remove_option{"Remove"_i18n, "Completely remove "_i18n + m_entry.title + '?', uninstall};
|
||||
const Option remove_option{"Remove"_i18n, i18n::Reorder("Completely remove ", m_entry.title) + '?', uninstall};
|
||||
|
||||
m_options.clear();
|
||||
switch (m_entry.status) {
|
||||
@@ -968,7 +971,7 @@ Menu::Menu(u32 flags) : grid::Menu{"AppStore"_i18n, flags} {
|
||||
|
||||
options->Add<SidebarEntryCallback>("Search"_i18n, [this](){
|
||||
std::string out;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out)) && !out.empty()) {
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, "Search for app")) && !out.empty()) {
|
||||
SetSearch(out);
|
||||
log_write("got %s\n", out.c_str());
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "ui/error_box.hpp"
|
||||
#include "ui/music_player.hpp"
|
||||
|
||||
#include "utils/utils.hpp"
|
||||
#include "utils/devoptab.hpp"
|
||||
|
||||
#include "log.hpp"
|
||||
@@ -31,7 +32,6 @@
|
||||
#include "yati/yati.hpp"
|
||||
#include "yati/source/file.hpp"
|
||||
|
||||
#include <usbdvd.h>
|
||||
#include <minIni.h>
|
||||
#include <minizip/zip.h>
|
||||
#include <minizip/unzip.h>
|
||||
@@ -45,12 +45,16 @@
|
||||
#include <utility>
|
||||
#include <ranges>
|
||||
|
||||
#ifdef ENABLE_LIBUSBDVD
|
||||
#include <usbdvd.h>
|
||||
#endif // ENABLE_LIBUSBDVD
|
||||
|
||||
namespace sphaira::ui::menu::filebrowser {
|
||||
namespace {
|
||||
|
||||
using RomDatabaseIndexs = std::vector<size_t>;
|
||||
|
||||
struct ForwarderForm final : public Sidebar {
|
||||
struct ForwarderForm final : public FormSidebar {
|
||||
explicit ForwarderForm(const FileAssocEntry& assoc, const RomDatabaseIndexs& db_indexs, const FileEntry& entry, const fs::FsPath& arg_path);
|
||||
|
||||
private:
|
||||
@@ -70,7 +74,7 @@ private:
|
||||
SidebarEntryFilePicker* m_icon{};
|
||||
};
|
||||
|
||||
constinit UEvent g_change_uevent;
|
||||
std::atomic_bool g_change_signalled{};
|
||||
|
||||
constexpr FsEntry FS_ENTRY_DEFAULT{
|
||||
"microSD card", "/", FsType::Sd, FsEntryFlag_Assoc | FsEntryFlag_IsSd,
|
||||
@@ -78,12 +82,11 @@ constexpr FsEntry FS_ENTRY_DEFAULT{
|
||||
|
||||
constexpr FsEntry FS_ENTRIES[]{
|
||||
FS_ENTRY_DEFAULT,
|
||||
{ "Image System memory", "/", FsType::ImageNand },
|
||||
{ "Image microSD card", "/", FsType::ImageSd},
|
||||
{ "Album", "/", FsType::ImageSd},
|
||||
};
|
||||
|
||||
constexpr std::string_view AUDIO_EXTENSIONS[] = {
|
||||
"mp3", "ogg", "flac", "wav", "aac" "ac3", "aif", "asf", "bfwav",
|
||||
"mp3", "ogg", "flac", "wav", "aac", "ac3", "aif", "asf", "bfwav",
|
||||
"bfsar", "bfstm", "bwav",
|
||||
};
|
||||
constexpr std::string_view VIDEO_EXTENSIONS[] = {
|
||||
@@ -115,7 +118,7 @@ constexpr std::string_view ZIP_EXTENSIONS[] = {
|
||||
};
|
||||
// supported music playback extensions.
|
||||
constexpr std::string_view MUSIC_EXTENSIONS[] = {
|
||||
"bfstm", "bfwav", "wav", "mp3", "ogg", "adf",
|
||||
"bfstm", "bfwav", "wav", "mp3", "ogg", "flac", "adf",
|
||||
};
|
||||
// supported theme music playback extensions.
|
||||
constexpr std::span THEME_MUSIC_EXTENSIONS = MUSIC_EXTENSIONS;
|
||||
@@ -286,7 +289,7 @@ auto GetRomIcon(std::string filename, const RomDatabaseIndexs& db_indexs, const
|
||||
}
|
||||
|
||||
ForwarderForm::ForwarderForm(const FileAssocEntry& assoc, const RomDatabaseIndexs& db_indexs, const FileEntry& entry, const fs::FsPath& arg_path)
|
||||
: Sidebar{"Forwarder Creation", Side::RIGHT}
|
||||
: FormSidebar{"Forwarder Creation"}
|
||||
, m_assoc{assoc}
|
||||
, m_db_indexs{db_indexs}
|
||||
, m_arg_path{arg_path} {
|
||||
@@ -312,23 +315,23 @@ ForwarderForm::ForwarderForm(const FileAssocEntry& assoc, const RomDatabaseIndex
|
||||
const auto icon = m_assoc.path;
|
||||
|
||||
m_name = this->Add<SidebarEntryTextInput>(
|
||||
"Name", name, "", -1, sizeof(NacpLanguageEntry::name) - 1,
|
||||
"Name"_i18n, name, "", "", -1, sizeof(NacpLanguageEntry::name) - 1,
|
||||
"Set the name of the application"_i18n
|
||||
);
|
||||
|
||||
m_author = this->Add<SidebarEntryTextInput>(
|
||||
"Author", author, "", -1, sizeof(NacpLanguageEntry::author) - 1,
|
||||
"Author"_i18n, author, "", "", -1, sizeof(NacpLanguageEntry::author) - 1,
|
||||
"Set the author of the application"_i18n
|
||||
);
|
||||
|
||||
m_version = this->Add<SidebarEntryTextInput>(
|
||||
"Version", version, "", -1, sizeof(NacpStruct::display_version) - 1,
|
||||
"Version"_i18n, version, "", "", -1, sizeof(NacpStruct::display_version) - 1,
|
||||
"Set the display version of the application"_i18n
|
||||
);
|
||||
|
||||
const std::vector<std::string> filters{"nro", "png", "jpg"};
|
||||
m_icon = this->Add<SidebarEntryFilePicker>(
|
||||
"Icon", icon, filters,
|
||||
"Icon"_i18n, icon, filters,
|
||||
"Set the path to the icon for the forwarder"_i18n
|
||||
);
|
||||
|
||||
@@ -359,7 +362,7 @@ ForwarderForm::ForwarderForm(const FileAssocEntry& assoc, const RomDatabaseIndex
|
||||
// try and read icon file into memory, bail if this fails.
|
||||
const auto rc = fs::FsStdio().read_entire_file(m_icon->GetValue(), config.icon);
|
||||
if (R_FAILED(rc)) {
|
||||
App::PushErrorBox(rc, "Failed to load icon");
|
||||
App::PushErrorBox(rc, "Failed to load icon"_i18n);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -416,7 +419,7 @@ auto IsExtension(std::string_view ext, std::span<const std::string_view> list) -
|
||||
}
|
||||
|
||||
void SignalChange() {
|
||||
ueventSignal(&g_change_uevent);
|
||||
g_change_signalled = true;
|
||||
}
|
||||
|
||||
FsView::FsView(Base* menu, const std::shared_ptr<fs::Fs>& fs, const fs::FsPath& path, const FsEntry& entry, ViewSide side) : m_menu{menu}, m_side{side} {
|
||||
@@ -526,12 +529,31 @@ FsView::~FsView() {
|
||||
}
|
||||
|
||||
void FsView::Update(Controller* controller, TouchInfo* touch) {
|
||||
m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this](bool touch, auto i) {
|
||||
m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this, controller](bool touch, auto i) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect::Focus);
|
||||
auto old_index = m_index;
|
||||
SetIndex(i);
|
||||
const auto new_index = m_index;
|
||||
|
||||
// if L2 is helt, select all between old and new index.
|
||||
if (old_index != new_index && controller->GotHeld(Button::L2)) {
|
||||
const auto inc = old_index < new_index ? +1 : -1;
|
||||
|
||||
while (old_index != new_index) {
|
||||
old_index += inc;
|
||||
|
||||
auto& e = GetEntry(old_index);
|
||||
e.selected ^= 1;
|
||||
if (e.selected) {
|
||||
m_selected_count++;
|
||||
} else {
|
||||
m_selected_count--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -591,8 +613,7 @@ void FsView::Draw(NVGcontext* vg, Theme* theme) {
|
||||
|
||||
m_scroll_name.Draw(vg, selected, x + text_xoffset+65, y + (h / 2.f), w-(75+text_xoffset+65+50), 20, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), e.name);
|
||||
|
||||
// NOTE: make this native only if i disable dir scan from above.
|
||||
if (e.IsDir()) {
|
||||
if (e.IsDir() && !m_fs_entry.IsNoStatDir() && (e.dir_count != -1 || !e.done_stat)) {
|
||||
// NOTE: this takes longer than 16ms when opening a new folder due to it
|
||||
// checking all 9 folders at once.
|
||||
if (!got_dir_count && !e.done_stat && e.file_count == -1 && e.dir_count == -1) {
|
||||
@@ -602,12 +623,12 @@ void FsView::Draw(NVGcontext* vg, Theme* theme) {
|
||||
}
|
||||
|
||||
if (e.file_count != -1) {
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%zd files"_i18n.c_str(), e.file_count);
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), "%zd files"_i18n.c_str(), e.file_count);
|
||||
}
|
||||
if (e.dir_count != -1) {
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(text_id), "%zd dirs"_i18n.c_str(), e.dir_count);
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_INFO), "%zd dirs"_i18n.c_str(), e.dir_count);
|
||||
}
|
||||
} else if (e.IsFile()) {
|
||||
} else if (e.IsFile() && !m_fs_entry.IsNoStatFile() && (e.file_size != -1 || !e.time_stamp.is_valid)) {
|
||||
if (!e.time_stamp.is_valid && !e.done_stat) {
|
||||
e.done_stat = true;
|
||||
const auto path = GetNewPath(e);
|
||||
@@ -621,12 +642,9 @@ void FsView::Draw(NVGcontext* vg, Theme* theme) {
|
||||
const auto t = (time_t)(e.time_stamp.modified);
|
||||
struct tm tm{};
|
||||
localtime_r(&t, &tm);
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(text_id), "%02u/%02u/%u", tm.tm_mday, tm.tm_mon + 1, tm.tm_year + 1900);
|
||||
if ((double)e.file_size / 1024.0 / 1024.0 <= 0.009) {
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f KiB", (double)e.file_size / 1024.0);
|
||||
} else {
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f MiB", (double)e.file_size / 1024.0 / 1024.0);
|
||||
}
|
||||
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_INFO), "%02u/%02u/%u", tm.tm_mday, tm.tm_mon + 1, tm.tm_year + 1900);
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", utils::formatSizeStorage(e.file_size).c_str());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -703,27 +721,29 @@ void FsView::OnClick() {
|
||||
} else {
|
||||
// special case for nro
|
||||
if (IsSd() && IsSamePath(entry.GetExtension(), "nro")) {
|
||||
App::Push<OptionBox>("Launch "_i18n + entry.GetName() + '?',
|
||||
App::Push<OptionBox>(i18n::Reorder("Launch ", entry.GetName()) + '?',
|
||||
"No"_i18n, "Launch"_i18n, 1, [this](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
nro_launch(GetNewPathCurrent());
|
||||
}
|
||||
});
|
||||
} else if (IsExtension(entry.GetExtension(), NCA_EXTENSIONS)) {
|
||||
MountFileFs(devoptab::MountNca, devoptab::UmountNca);
|
||||
MountFileFs(devoptab::MountNca, devoptab::UmountNeworkDevice);
|
||||
} else if (IsExtension(entry.GetExtension(), NSP_EXTENSIONS)) {
|
||||
MountFileFs(devoptab::MountNsp, devoptab::UmountNsp);
|
||||
MountFileFs(devoptab::MountNsp, devoptab::UmountNeworkDevice);
|
||||
} else if (IsExtension(entry.GetExtension(), XCI_EXTENSIONS)) {
|
||||
MountFileFs(devoptab::MountXci, devoptab::UmountXci);
|
||||
MountFileFs(devoptab::MountXci, devoptab::UmountNeworkDevice);
|
||||
} else if (IsExtension(entry.GetExtension(), "zip")) {
|
||||
MountFileFs(devoptab::MountZip, devoptab::UmountZip);
|
||||
MountFileFs(devoptab::MountZip, devoptab::UmountNeworkDevice);
|
||||
} else if (IsExtension(entry.GetExtension(), "bfsar")) {
|
||||
MountFileFs(devoptab::MountBfsar, devoptab::UmountBfsar);
|
||||
MountFileFs(devoptab::MountBfsar, devoptab::UmountNeworkDevice);
|
||||
} else if (IsExtension(entry.GetExtension(), MUSIC_EXTENSIONS)) {
|
||||
App::Push<music::Menu>(GetFs(), GetNewPathCurrent());
|
||||
} else if (IsExtension(entry.GetExtension(), IMAGE_EXTENSIONS)) {
|
||||
App::Push<imageview::Menu>(GetFs(), GetNewPathCurrent());
|
||||
} else if (IsExtension(entry.GetExtension(), CDDVD_EXTENSIONS)) {
|
||||
}
|
||||
#ifdef ENABLE_LIBUSBDVD
|
||||
else if (IsExtension(entry.GetExtension(), CDDVD_EXTENSIONS)) {
|
||||
std::shared_ptr<CUSBDVD> usbdvd;
|
||||
|
||||
if (entry.GetExtension() == "cue") {
|
||||
@@ -747,7 +767,9 @@ void FsView::OnClick() {
|
||||
} else {
|
||||
log_write("[USBDVD] failed to mount\n");
|
||||
}
|
||||
} else if (IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) {
|
||||
}
|
||||
#endif // ENABLE_LIBUSBDVD
|
||||
else if (IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) {
|
||||
InstallFiles();
|
||||
} else if (IsSd()) {
|
||||
const auto assoc_list = m_menu->FindFileAssocFor();
|
||||
@@ -850,7 +872,7 @@ void FsView::InstallFiles() {
|
||||
App::Push<ui::ProgressBox>(0, "Installing "_i18n, "", [this, targets](auto pbox) -> Result {
|
||||
for (auto& e : targets) {
|
||||
R_TRY(yati::InstallFromFile(pbox, m_fs.get(), GetNewPath(e)));
|
||||
App::Notify("Installed "_i18n + e.GetName());
|
||||
App::Notify(i18n::Reorder("Installed ", e.GetName()));
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
@@ -946,7 +968,7 @@ void FsView::ZipFiles(fs::FsPath zip_out) {
|
||||
|
||||
auto zfile = zipOpen2_64(zip_out, APPEND_STATUS_CREATE, nullptr, &file_func);
|
||||
R_UNLESS(zfile, Result_ZipOpen2_64);
|
||||
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_VERSION_HASH));
|
||||
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_DISPLAY_VERSION));
|
||||
|
||||
const auto zip_add = [&](const fs::FsPath& file_path) -> Result {
|
||||
// the file name needs to be relative to the current directory.
|
||||
@@ -1004,114 +1026,6 @@ void FsView::ZipFiles(fs::FsPath zip_out) {
|
||||
});
|
||||
}
|
||||
|
||||
void FsView::UploadFiles() {
|
||||
const auto targets = GetSelectedEntries();
|
||||
|
||||
const auto network_locations = location::Load();
|
||||
if (network_locations.empty()) {
|
||||
App::Notify("No upload locations set!"_i18n);
|
||||
return;
|
||||
}
|
||||
|
||||
PopupList::Items items;
|
||||
for (const auto&p : network_locations) {
|
||||
items.emplace_back(p.name);
|
||||
}
|
||||
|
||||
App::Push<PopupList>(
|
||||
"Select upload location"_i18n, items, [this, network_locations](auto op_index){
|
||||
if (!op_index) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto loc = network_locations[*op_index];
|
||||
App::Push<ProgressBox>(0, "Uploading"_i18n, "", [this, loc](auto pbox) -> Result {
|
||||
auto targets = GetSelectedEntries();
|
||||
const auto is_file_based_emummc = App::IsFileBaseEmummc();
|
||||
|
||||
const auto file_add = [&](s64 file_size, const fs::FsPath& file_path, const char* name) -> Result {
|
||||
// the file name needs to be relative to the current directory.
|
||||
const auto relative_file_name = file_path.s + std::strlen(m_path);
|
||||
pbox->SetTitle(name);
|
||||
pbox->NewTransfer(relative_file_name);
|
||||
|
||||
fs::File f;
|
||||
R_TRY(m_fs->OpenFile(file_path, FsOpenMode_Read, &f));
|
||||
|
||||
return thread::TransferPull(pbox, file_size,
|
||||
[&](void* data, s64 off, s64 size, u64* bytes_read) -> Result {
|
||||
const auto rc = f.Read(off, data, size, FsReadOption_None, bytes_read);
|
||||
if (m_fs->IsNative() && is_file_based_emummc) {
|
||||
svcSleepThread(2e+6); // 2ms
|
||||
}
|
||||
return rc;
|
||||
},
|
||||
[&](thread::PullCallback pull) -> Result {
|
||||
s64 offset{};
|
||||
const auto result = curl::Api().FromMemory(
|
||||
CURL_LOCATION_TO_API(loc),
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()},
|
||||
curl::UploadInfo{
|
||||
relative_file_name, file_size,
|
||||
[&](void *ptr, size_t size) -> size_t {
|
||||
// curl will request past the size of the file, causing an error.
|
||||
if (offset >= file_size) {
|
||||
log_write("finished file upload\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
u64 bytes_read{};
|
||||
if (R_FAILED(pull(ptr, size, &bytes_read))) {
|
||||
log_write("failed to read in custom callback: %zd size: %zd\n", offset, size);
|
||||
return 0;
|
||||
}
|
||||
|
||||
offset += bytes_read;
|
||||
return bytes_read;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
R_UNLESS(result.success, Result_FileBrowserFailedUpload);
|
||||
R_SUCCEED();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
for (auto& e : targets) {
|
||||
if (e.IsFile()) {
|
||||
const auto file_path = GetNewPath(e);
|
||||
R_TRY(file_add(e.file_size, file_path, e.GetName().c_str()));
|
||||
} else {
|
||||
FsDirCollections collections;
|
||||
get_collections(GetNewPath(e), e.name, collections, true);
|
||||
|
||||
for (const auto& collection : collections) {
|
||||
for (const auto& file : collection.files) {
|
||||
const auto file_path = fs::AppendPath(collection.path, file.name);
|
||||
R_TRY(file_add(file.file_size, file_path, file.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}, [this](Result rc){
|
||||
App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n);
|
||||
m_menu->ResetSelection();
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Upload successfull!"_i18n);
|
||||
log_write("Upload successfull!!!\n");
|
||||
} else {
|
||||
App::Notify("Upload failed!"_i18n);
|
||||
log_write("Upload failed!!!\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
auto FsView::Scan(fs::FsPath new_path, bool is_walk_up) -> Result {
|
||||
App::SetBoostMode(true);
|
||||
ON_SCOPE_EXIT(App::SetBoostMode(false));
|
||||
@@ -1127,6 +1041,7 @@ auto FsView::Scan(fs::FsPath new_path, bool is_walk_up) -> Result {
|
||||
m_previous_highlighted_file.emplace_back(f);
|
||||
}
|
||||
|
||||
g_change_signalled = false;
|
||||
m_path = new_path;
|
||||
m_entries.clear();
|
||||
m_entries_index.clear();
|
||||
@@ -1340,7 +1255,7 @@ void FsView::OnDeleteCallback() {
|
||||
|
||||
const auto full_path = GetNewPath(selected.m_path, p.name);
|
||||
if (p.IsDir()) {
|
||||
pbox->NewTransfer("Scanning "_i18n + full_path);
|
||||
pbox->NewTransfer(i18n::Reorder("Scanning ", full_path));
|
||||
R_TRY(get_collections(src_fs, full_path, p.name, collections));
|
||||
}
|
||||
}
|
||||
@@ -1383,7 +1298,7 @@ void FsView::OnPasteCallback() {
|
||||
const auto dst_path = GetNewPath(m_path, p.name);
|
||||
|
||||
pbox->SetTitle(p.name);
|
||||
pbox->NewTransfer("Pasting "_i18n + src_path);
|
||||
pbox->NewTransfer(i18n::Reorder("Pasting ", src_path));
|
||||
|
||||
if (p.IsDir()) {
|
||||
m_fs->RenameDirectory(src_path, dst_path);
|
||||
@@ -1418,7 +1333,7 @@ void FsView::OnPasteCallback() {
|
||||
|
||||
const auto full_path = GetNewPath(selected.m_path, p.name);
|
||||
if (p.IsDir()) {
|
||||
pbox->NewTransfer("Scanning "_i18n + full_path);
|
||||
pbox->NewTransfer(i18n::Reorder("Scanning ", full_path));
|
||||
R_TRY(get_collections(src_fs, full_path, p.name, collections));
|
||||
}
|
||||
}
|
||||
@@ -1432,11 +1347,11 @@ void FsView::OnPasteCallback() {
|
||||
|
||||
if (p.IsDir()) {
|
||||
pbox->SetTitle(p.name);
|
||||
pbox->NewTransfer("Creating "_i18n + dst_path);
|
||||
pbox->NewTransfer(i18n::Reorder("Creating ", dst_path));
|
||||
m_fs->CreateDirectory(dst_path);
|
||||
} else {
|
||||
pbox->SetTitle(p.name);
|
||||
pbox->NewTransfer("Copying "_i18n + src_path);
|
||||
pbox->NewTransfer(i18n::Reorder("Copying ", src_path));
|
||||
R_TRY(pbox->CopyFile(src_fs, m_fs.get(), src_path, dst_path, is_same_fs));
|
||||
R_TRY(on_paste_file(src_path, dst_path));
|
||||
}
|
||||
@@ -1454,7 +1369,7 @@ void FsView::OnPasteCallback() {
|
||||
const auto dst_path = GetNewPath(base_dst_path, p.name);
|
||||
|
||||
pbox->SetTitle(p.name);
|
||||
pbox->NewTransfer("Creating "_i18n + dst_path);
|
||||
pbox->NewTransfer(i18n::Reorder("Creating ", dst_path));
|
||||
m_fs->CreateDirectory(dst_path);
|
||||
}
|
||||
|
||||
@@ -1466,7 +1381,7 @@ void FsView::OnPasteCallback() {
|
||||
const auto dst_path = GetNewPath(base_dst_path, p.name);
|
||||
|
||||
pbox->SetTitle(p.name);
|
||||
pbox->NewTransfer("Copying "_i18n + src_path);
|
||||
pbox->NewTransfer(i18n::Reorder("Copying ", src_path));
|
||||
R_TRY(pbox->CopyFile(src_fs, m_fs.get(), src_path, dst_path, is_same_fs));
|
||||
R_TRY(on_paste_file(src_path, dst_path));
|
||||
}
|
||||
@@ -1582,7 +1497,7 @@ auto FsView::get_collections(fs::Fs* fs, const fs::FsPath& path, const fs::FsPat
|
||||
}
|
||||
|
||||
auto FsView::get_collection(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollection& out, bool inc_file, bool inc_dir, bool inc_size) -> Result {
|
||||
return get_collection(m_fs.get(), path, parent_name, out, true, true, inc_size);
|
||||
return get_collection(m_fs.get(), path, parent_name, out, inc_file, inc_dir, inc_size);
|
||||
}
|
||||
|
||||
auto FsView::get_collections(const fs::FsPath& path, const fs::FsPath& parent_name, FsDirCollections& out, bool inc_size) -> Result {
|
||||
@@ -1599,7 +1514,7 @@ Result FsView::DeleteAllCollections(ProgressBox* pbox, fs::Fs* fs, const FsDirCo
|
||||
|
||||
const auto full_path = FsView::GetNewPath(c.path, p.name);
|
||||
pbox->SetTitle(p.name);
|
||||
pbox->NewTransfer("Deleting "_i18n + full_path.toString());
|
||||
pbox->NewTransfer(i18n::Reorder("Deleting ", full_path.toString()));
|
||||
if ((mode & FsDirOpenMode_ReadDirs) && p.type == FsDirEntryType_Dir) {
|
||||
log_write("deleting dir: %s\n", full_path.s);
|
||||
R_TRY(fs->DeleteDirectory(full_path));
|
||||
@@ -1630,7 +1545,7 @@ static Result DeleteAllCollectionsWithSelected(ProgressBox* pbox, fs::Fs* fs, co
|
||||
|
||||
const auto full_path = FsView::GetNewPath(selected.m_path, p.name);
|
||||
pbox->SetTitle(p.name);
|
||||
pbox->NewTransfer("Deleting "_i18n + full_path.toString());
|
||||
pbox->NewTransfer(i18n::Reorder("Deleting ", full_path.toString()));
|
||||
|
||||
if ((mode & FsDirOpenMode_ReadDirs) && p.type == FsDirEntryType_Dir) {
|
||||
log_write("deleting dir: %s\n", full_path.s);
|
||||
@@ -1702,17 +1617,6 @@ void FsView::DisplayOptions() {
|
||||
SidebarEntryArray::Items mount_items;
|
||||
std::vector<FsEntry> fs_entries;
|
||||
|
||||
const auto stdio_locations = location::GetStdio(false);
|
||||
for (const auto& e: stdio_locations) {
|
||||
u32 flags{};
|
||||
if (e.write_protect) {
|
||||
flags |= FsEntryFlag_ReadOnly;
|
||||
}
|
||||
|
||||
fs_entries.emplace_back(e.name, e.mount, FsType::Stdio, flags);
|
||||
mount_items.push_back(e.name);
|
||||
}
|
||||
|
||||
for (const auto& e: FS_ENTRIES) {
|
||||
fs_entries.emplace_back(e);
|
||||
mount_items.push_back(i18n::get(e.name));
|
||||
@@ -1723,14 +1627,13 @@ void FsView::DisplayOptions() {
|
||||
mount_items.push_back(m_menu->m_custom_fs_entry.name);
|
||||
}
|
||||
|
||||
const auto fat_entries = location::GetFat();
|
||||
for (const auto& e: fat_entries) {
|
||||
u32 flags{};
|
||||
if (e.write_protect) {
|
||||
flags |= FsEntryFlag_ReadOnly;
|
||||
const auto stdio_locations = location::GetStdio(false);
|
||||
for (const auto& e: stdio_locations) {
|
||||
if (e.fs_hidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fs_entries.emplace_back(e.name, e.mount, FsType::Stdio, flags);
|
||||
fs_entries.emplace_back(e.name, e.mount, FsType::Stdio, e.flags);
|
||||
mount_items.push_back(e.name);
|
||||
}
|
||||
|
||||
@@ -1808,7 +1711,8 @@ void FsView::DisplayOptions() {
|
||||
std::string out;
|
||||
const auto& entry = GetEntry();
|
||||
const auto name = entry.GetName();
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, "Set New File Name"_i18n.c_str(), name.c_str())) && !out.empty() && out != name) {
|
||||
const auto header = "Set new name"_i18n;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, header.c_str(), header.c_str(), name.c_str())) && !out.empty() && out != name) {
|
||||
App::PopToMenu();
|
||||
|
||||
const auto src_path = GetNewPath(entry);
|
||||
@@ -1901,7 +1805,7 @@ void FsView::DisplayOptions() {
|
||||
|
||||
options->Add<SidebarEntryCallback>("Extract to..."_i18n, [this](){
|
||||
std::string out;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, "Enter the path to the folder to extract into", fs::AppendPath(m_path, ""))) && !out.empty()) {
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, "Extract path", "Enter the path to the folder to extract into"_i18n.c_str(), fs::AppendPath(m_path, ""))) && !out.empty()) {
|
||||
UnzipFiles(out);
|
||||
}
|
||||
});
|
||||
@@ -1919,7 +1823,7 @@ void FsView::DisplayOptions() {
|
||||
|
||||
options->Add<SidebarEntryCallback>("Compress to..."_i18n, [this](){
|
||||
std::string out;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, "Enter the path to the folder to extract into", m_path)) && !out.empty()) {
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, "Compress path", "Enter the path to the folder to compress into"_i18n.c_str(), m_path)) && !out.empty()) {
|
||||
ZipFiles(out);
|
||||
}
|
||||
});
|
||||
@@ -1939,7 +1843,8 @@ void FsView::DisplayAdvancedOptions() {
|
||||
if (!m_fs_entry.IsReadOnly()) {
|
||||
options->Add<SidebarEntryCallback>("Create File"_i18n, [this](){
|
||||
std::string out;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, "Set File Name"_i18n.c_str(), fs::AppendPath(m_path, ""))) && !out.empty()) {
|
||||
const auto header = "Set File Name"_i18n;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, header.c_str(), header.c_str(), fs::AppendPath(m_path, ""))) && !out.empty()) {
|
||||
App::PopToMenu();
|
||||
|
||||
fs::FsPath full_path;
|
||||
@@ -1961,7 +1866,8 @@ void FsView::DisplayAdvancedOptions() {
|
||||
|
||||
options->Add<SidebarEntryCallback>("Create Folder"_i18n, [this](){
|
||||
std::string out;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, "Set Folder Name"_i18n.c_str(), fs::AppendPath(m_path, ""))) && !out.empty()) {
|
||||
const auto header = "Set Folder Name"_i18n;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, header.c_str(), header.c_str(), fs::AppendPath(m_path, ""))) && !out.empty()) {
|
||||
App::PopToMenu();
|
||||
|
||||
fs::FsPath full_path;
|
||||
@@ -1987,12 +1893,6 @@ void FsView::DisplayAdvancedOptions() {
|
||||
});
|
||||
}
|
||||
|
||||
if (m_entries_current.size() && (m_menu->m_options & FsOption_CanUpload)) {
|
||||
options->Add<SidebarEntryCallback>("Upload"_i18n, [this](){
|
||||
UploadFiles();
|
||||
});
|
||||
}
|
||||
|
||||
if (m_entries_current.size() && !m_selected_count && IsExtension(GetEntry().GetExtension(), THEME_MUSIC_EXTENSIONS)) {
|
||||
options->Add<SidebarEntryCallback>("Set as background music"_i18n, [this](){
|
||||
const auto rc = App::SetDefaultBackgroundMusic(GetFs(), GetNewPathCurrent());
|
||||
@@ -2020,12 +1920,6 @@ void FsView::DisplayAdvancedOptions() {
|
||||
options->Add<SidebarEntryCallback>("/dev/null (Speed Test)"_i18n, [this](){
|
||||
DisplayHash(hash::Type::Null);
|
||||
});
|
||||
options->Add<SidebarEntryCallback>("Deflate (Speed Test)"_i18n, [this](){
|
||||
DisplayHash(hash::Type::Deflate);
|
||||
});
|
||||
options->Add<SidebarEntryCallback>("ZSTD (Speed Test)"_i18n, [this](){
|
||||
DisplayHash(hash::Type::Zstd);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2062,7 +1956,8 @@ Base::Base(const std::shared_ptr<fs::Fs>& fs, const FsEntry& fs_entry, const fs:
|
||||
}
|
||||
|
||||
void Base::Update(Controller* controller, TouchInfo* touch) {
|
||||
if (R_SUCCEEDED(waitSingle(waiterForUEvent(&g_change_uevent), 0))) {
|
||||
if (g_change_signalled.exchange(false)) {
|
||||
|
||||
if (IsSplitScreen()) {
|
||||
view_left->SortAndFindLastFile(true);
|
||||
view_right->SortAndFindLastFile(true);
|
||||
@@ -2237,8 +2132,16 @@ void Base::LoadAssocEntriesPath(const fs::FsPath& path) {
|
||||
if (!assoc.path.empty()) {
|
||||
file_exists = view->m_fs->FileExists(assoc.path);
|
||||
} else {
|
||||
auto nros = homebrew::GetNroEntries();
|
||||
if (nros.empty()) {
|
||||
if (m_nro_entries.empty()) {
|
||||
nro_scan("/switch", m_nro_entries);
|
||||
nros = m_nro_entries;
|
||||
}
|
||||
}
|
||||
|
||||
const auto nro_name = assoc.name + ".nro";
|
||||
for (const auto& nro : homebrew::GetNroEntries()) {
|
||||
for (const auto& nro : nros) {
|
||||
const auto len = std::strlen(nro.path);
|
||||
if (len < nro_name.length()) {
|
||||
continue;
|
||||
@@ -2403,7 +2306,6 @@ void Base::Init(const std::shared_ptr<fs::Fs>& fs, const FsEntry& fs_entry, cons
|
||||
|
||||
view_left = std::make_unique<FsView>(this, fs, path, fs_entry, ViewSide::Left);
|
||||
view = view_left.get();
|
||||
ueventCreate(&g_change_uevent, true);
|
||||
}
|
||||
|
||||
void MountFsHelper(const std::shared_ptr<fs::Fs>& fs, const fs::FsPath& name) {
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
namespace sphaira::ui::menu::game {
|
||||
namespace {
|
||||
|
||||
std::atomic_bool g_change_signalled{};
|
||||
|
||||
struct NspSource final : dump::BaseSource {
|
||||
NspSource(const std::vector<NspEntry>& entries) : m_entries{entries} {
|
||||
m_is_file_based_emummc = App::IsFileBaseEmummc();
|
||||
@@ -110,6 +112,7 @@ private:
|
||||
bool m_is_file_based_emummc{};
|
||||
};
|
||||
|
||||
#ifdef ENABLE_NSZ
|
||||
Result NszExport(ProgressBox* pbox, const keys::Keys& keys, dump::BaseSource* _source, dump::WriteSource* writer, const fs::FsPath& path) {
|
||||
auto source = (NspSource*)_source;
|
||||
|
||||
@@ -145,6 +148,7 @@ Result NszExport(ProgressBox* pbox, const keys::Keys& keys, dump::BaseSource* _s
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
#endif // ENABLE_NSZ
|
||||
|
||||
Result Notify(Result rc, const std::string& error_message) {
|
||||
if (R_FAILED(rc)) {
|
||||
@@ -197,7 +201,7 @@ void FreeEntry(NVGcontext* vg, Entry& e) {
|
||||
|
||||
void LaunchEntry(const Entry& e) {
|
||||
const auto rc = appletRequestLaunchApplication(e.app_id, nullptr);
|
||||
Notify(rc, "Failed to launch application");
|
||||
Notify(rc, "Failed to launch application"_i18n);
|
||||
}
|
||||
|
||||
Result CreateSave(u64 app_id, AccountUid uid) {
|
||||
@@ -230,6 +234,12 @@ Result CreateSave(u64 app_id, AccountUid uid) {
|
||||
} // namespace
|
||||
|
||||
Result NspEntry::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||
if (off == nsp_size) {
|
||||
log_write("[NspEntry::Read] read at eof...\n");
|
||||
*bytes_read = 0;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
if (off < nsp_data.size()) {
|
||||
*bytes_read = size = ClipSize(off, size, nsp_data.size());
|
||||
std::memcpy(buf, nsp_data.data() + off, size);
|
||||
@@ -268,6 +278,10 @@ Result NspEntry::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||
return 0x1;
|
||||
}
|
||||
|
||||
void SignalChange() {
|
||||
g_change_signalled = true;
|
||||
}
|
||||
|
||||
Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
|
||||
this->SetActions(
|
||||
std::make_pair(Button::L3, Action{[this](){
|
||||
@@ -358,7 +372,7 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
|
||||
LoadControlEntry(e, true);
|
||||
|
||||
App::Push<OptionBox>(
|
||||
"Launch "_i18n + e.GetName(),
|
||||
i18n::Reorder("Launch ", e.GetName()) + '?',
|
||||
"Back"_i18n, "Launch"_i18n, 1, [this, &e](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
LaunchEntry(e);
|
||||
@@ -383,7 +397,7 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
|
||||
|
||||
// completely deletes the application record and all data.
|
||||
options->Add<SidebarEntryCallback>("Delete"_i18n, [this](){
|
||||
const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].GetName() + "?";
|
||||
const auto buf = i18n::Reorder("Are you sure you want to delete ", m_entries[m_index].GetName()) + "?";
|
||||
App::Push<OptionBox>(
|
||||
buf,
|
||||
"Back"_i18n, "Delete"_i18n, 0, [this](auto op_index){
|
||||
@@ -467,8 +481,13 @@ Menu::~Menu() {
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
// force update if gamecard state changed.
|
||||
m_dirty |= R_SUCCEEDED(eventWait(&m_gc_event, 0));
|
||||
if (g_change_signalled.exchange(false)) {
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
if (R_SUCCEEDED(eventWait(&m_gc_event, 0))) {
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
if (m_dirty) {
|
||||
App::Notify("Updating application record list"_i18n);
|
||||
@@ -558,6 +577,7 @@ void Menu::ScanHomebrew() {
|
||||
|
||||
FreeEntries();
|
||||
m_entries.reserve(ENTRY_CHUNK_COUNT);
|
||||
g_change_signalled = false;
|
||||
|
||||
std::vector<NsApplicationRecord> record_list(ENTRY_CHUNK_COUNT);
|
||||
s32 offset{};
|
||||
@@ -704,7 +724,6 @@ void Menu::ExportOptions(bool to_nsz) {
|
||||
|
||||
void Menu::DumpGames(u32 flags, bool to_nsz) {
|
||||
auto targets = GetSelectedEntries();
|
||||
ClearSelection();
|
||||
|
||||
std::vector<NspEntry> nsp_entries;
|
||||
for (auto& e : targets) {
|
||||
@@ -820,7 +839,7 @@ void DeleteMetaEntries(u64 app_id, int image, const std::string& name, const tit
|
||||
|
||||
R_SUCCEED();
|
||||
}, [](Result rc){
|
||||
App::PushErrorBox(rc, "Failed to delete meta entry");
|
||||
App::PushErrorBox(rc, "Failed to delete meta entry"_i18n);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -979,6 +998,7 @@ void DumpNsp(const std::vector<NspEntry>& entries, bool to_nsz) {
|
||||
auto source = std::make_shared<NspSource>(entries);
|
||||
|
||||
if (to_nsz) {
|
||||
#ifdef ENABLE_NSZ
|
||||
// todo: log keys error.
|
||||
keys::Keys keys;
|
||||
keys::parse_keys(keys, true);
|
||||
@@ -986,6 +1006,7 @@ void DumpNsp(const std::vector<NspEntry>& entries, bool to_nsz) {
|
||||
dump::Dump(source, paths, [keys](ProgressBox* pbox, dump::BaseSource* source, dump::WriteSource* writer, const fs::FsPath& path) {
|
||||
return NszExport(pbox, keys, source, writer, path);
|
||||
});
|
||||
#endif // ENABLE_NSZ
|
||||
} else {
|
||||
dump::Dump(source, paths);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include "yati/nx/ncm.hpp"
|
||||
#include "yati/nx/es.hpp"
|
||||
|
||||
#include "utils/utils.hpp"
|
||||
|
||||
#include "title_info.hpp"
|
||||
#include "app.hpp"
|
||||
#include "defines.hpp"
|
||||
@@ -179,23 +181,23 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
char req_vers_buf[128];
|
||||
const auto ver = e.content_meta.extened.application.required_system_version;
|
||||
switch (e.status.meta_type) {
|
||||
case NcmContentMetaType_Application: std::snprintf(req_vers_buf, sizeof(req_vers_buf), "Required System Version: %u.%u.%u", SYSVER_MAJOR(ver), SYSVER_MINOR(ver), SYSVER_MICRO(ver)); break;
|
||||
case NcmContentMetaType_Patch: std::snprintf(req_vers_buf, sizeof(req_vers_buf), "Required System Version: %u.%u.%u", SYSVER_MAJOR(ver), SYSVER_MINOR(ver), SYSVER_MICRO(ver)); break;
|
||||
case NcmContentMetaType_AddOnContent: std::snprintf(req_vers_buf, sizeof(req_vers_buf), "Required Application Version: v%u", ver >> 16); break;
|
||||
case NcmContentMetaType_Application: std::snprintf(req_vers_buf, sizeof(req_vers_buf), "Required System Version: %u.%u.%u"_i18n.c_str(), SYSVER_MAJOR(ver), SYSVER_MINOR(ver), SYSVER_MICRO(ver)); break;
|
||||
case NcmContentMetaType_Patch: std::snprintf(req_vers_buf, sizeof(req_vers_buf), "Required System Version: %u.%u.%u"_i18n.c_str(), SYSVER_MAJOR(ver), SYSVER_MINOR(ver), SYSVER_MICRO(ver)); break;
|
||||
case NcmContentMetaType_AddOnContent: std::snprintf(req_vers_buf, sizeof(req_vers_buf), "Required Application Version: v%u"_i18n.c_str(), ver >> 16); break;
|
||||
}
|
||||
|
||||
if (e.missing_count) {
|
||||
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Content Count: %u (%u missing)", e.content_meta.header.content_count, e.missing_count);
|
||||
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Content Count: %u (%u missing)"_i18n.c_str(), e.content_meta.header.content_count, e.missing_count);
|
||||
} else {
|
||||
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Content Count: %u", e.content_meta.header.content_count);
|
||||
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Content Count: %u"_i18n.c_str(), e.content_meta.header.content_count);
|
||||
}
|
||||
|
||||
gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Ticket: %s", TICKET_STR[e.ticket_type]);
|
||||
gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key Generation: %u (%s)", e.key_gen, nca::GetKeyGenStr(e.key_gen));
|
||||
gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Ticket: %s"_i18n.c_str(), i18n::get(TICKET_STR[e.ticket_type]).c_str());
|
||||
gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key Generation: %u (%s)"_i18n.c_str(), e.key_gen, nca::GetKeyGenStr(e.key_gen));
|
||||
gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", req_vers_buf);
|
||||
|
||||
if (e.status.meta_type == NcmContentMetaType_Application || e.status.meta_type == NcmContentMetaType_Patch) {
|
||||
gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Display Version: %s", e.nacp.display_version);
|
||||
gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Display Version: %s"_i18n.c_str(), e.nacp.display_version);
|
||||
}
|
||||
nvgRestore(vg);
|
||||
|
||||
@@ -222,7 +224,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
}
|
||||
}
|
||||
|
||||
gfx::drawTextArgs(vg, x + text_xoffset, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s", ncm::GetReadableMetaTypeStr(e.status.meta_type));
|
||||
gfx::drawTextArgs(vg, x + text_xoffset, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s", i18n::get(ncm::GetReadableMetaTypeStr(e.status.meta_type)).c_str());
|
||||
gfx::drawTextArgs(vg, x + text_xoffset + 150, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%016lX", e.status.application_id);
|
||||
gfx::drawTextArgs(vg, x + text_xoffset + 400, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "v%u (%u)", e.status.version >> 16, e.status.version);
|
||||
|
||||
@@ -231,14 +233,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
GetNcmSizeOfMetaStatus(e);
|
||||
}
|
||||
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(text_id), "%s", ncm::GetReadableStorageIdStr(e.status.storageID));
|
||||
if ((double)e.size / 1024.0 / 1024.0 <= 0.009) {
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f KiB", (double)e.size / 1024.0);
|
||||
} else if ((double)e.size / 1024.0 / 1024.0 / 1024.0 <= 0.009) {
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f MiB", (double)e.size / 1024.0 / 1024.0);
|
||||
} else {
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f GiB", (double)e.size / 1024.0 / 1024.0 / 1024.0);
|
||||
}
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", i18n::get(ncm::GetReadableStorageIdStr(e.status.storageID)).c_str());
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", utils::formatSizeStorage(e.size).c_str());
|
||||
|
||||
if (e.selected) {
|
||||
gfx::drawText(vg, x + text_xoffset - 80 / 2, y + (h / 2.f) - (24.f / 2), 24.f, "\uE14B", nullptr, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_SELECTED));
|
||||
|
||||
@@ -205,9 +205,10 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
|
||||
}
|
||||
}
|
||||
});
|
||||
}, "Performs sha256 hash over the NCA to check if it's valid.\n\n"
|
||||
"NOTE: This only detects if the hash is missmatched, it does not validate if \
|
||||
the content has been modified at all."_i18n);
|
||||
}, i18n::get("nca_validate_info",
|
||||
"Performs sha256 hash over the NCA to check if it's valid.\n\n"
|
||||
"NOTE: This only detects if the hash is missmatched, it does not validate if "
|
||||
"the content has been modified at all."));
|
||||
|
||||
options->Add<SidebarEntryCallback>("Verify NCA fixed key"_i18n, [this](){
|
||||
if (R_FAILED(nca::VerifyFixedKey(GetEntry().header))) {
|
||||
@@ -215,9 +216,10 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
|
||||
} else {
|
||||
App::Push<OptionBox>("NCA fixed key is valid."_i18n, "OK"_i18n);
|
||||
}
|
||||
}, "Performs RSA NCA fixed key verification. "\
|
||||
"This is a hash over the NCA header. It is used to verify that the header has not been modified. "\
|
||||
"The header is signed by nintendo, thus it cannot be forged, and is reliable to detect modified NCA headers (such as NSP/XCI converts)."_i18n);
|
||||
}, i18n::get("nca_fixedkey_info",
|
||||
"Performs RSA NCA fixed key verification. "
|
||||
"This is a hash over the NCA header. It is used to verify that the header has not been modified. "
|
||||
"The header is signed by nintendo, thus it cannot be forged, and is reliable to detect modified NCA headers (such as NSP/XCI converts)."));
|
||||
}
|
||||
}})
|
||||
);
|
||||
@@ -307,16 +309,16 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
gfx::drawImage(vg, 90, 130, 256, 256, m_entry.image ? m_entry.image : App::GetDefaultImage());
|
||||
|
||||
if (e.header.magic != NCA3_MAGIC) {
|
||||
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Failed to decrypt NCA");
|
||||
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Failed to decrypt NCA"_i18n.c_str());
|
||||
} else {
|
||||
nvgSave(vg);
|
||||
nvgIntersectScissor(vg, 50, 90, 325, 555);
|
||||
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Application Type: %s", ncm::GetReadableMetaTypeStr(m_meta_entry.status.meta_type));
|
||||
gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Content Type: %s", nca::GetContentTypeStr(e.header.content_type));
|
||||
gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Distribution Type: %s", nca::GetDistributionTypeStr(e.header.distribution_type));
|
||||
gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Program ID: %016lX", e.header.program_id);
|
||||
gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key Generation: %u (%s)", e.header.GetKeyGeneration(), nca::GetKeyGenStr(e.header.GetKeyGeneration()));
|
||||
gfx::drawTextArgs(vg, 50, 615, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "SDK Version: %u.%u.%u.%u", e.header.sdk_major, e.header.sdk_minor, e.header.sdk_micro, e.header.sdk_revision);
|
||||
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Application Type: %s"_i18n.c_str(), i18n::get(ncm::GetReadableMetaTypeStr(m_meta_entry.status.meta_type)).c_str());
|
||||
gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Content Type: %s"_i18n.c_str(), nca::GetContentTypeStr(e.header.content_type));
|
||||
gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Distribution Type: %s"_i18n.c_str(), nca::GetDistributionTypeStr(e.header.distribution_type));
|
||||
gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Program ID: %016lX"_i18n.c_str(), e.header.program_id);
|
||||
gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key Generation: %u (%s)"_i18n.c_str(), e.header.GetKeyGeneration(), nca::GetKeyGenStr(e.header.GetKeyGeneration()));
|
||||
gfx::drawTextArgs(vg, 50, 615, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "SDK Version: %u.%u.%u.%u"_i18n.c_str(), e.header.sdk_major, e.header.sdk_minor, e.header.sdk_micro, e.header.sdk_revision);
|
||||
nvgRestore(vg);
|
||||
}
|
||||
|
||||
@@ -344,15 +346,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
}
|
||||
|
||||
gfx::drawTextArgs(vg, x + text_xoffset, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s", ncm::GetContentTypeStr(e.content_type));
|
||||
gfx::drawTextArgs(vg, x + text_xoffset + 185, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s", utils::hexIdToStr(e.content_id).str);
|
||||
|
||||
if ((double)e.size / 1024.0 / 1024.0 <= 0.009) {
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%.2f KiB", (double)e.size / 1024.0);
|
||||
} else if ((double)e.size / 1024.0 / 1024.0 / 1024.0 <= 0.009) {
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%.2f MiB", (double)e.size / 1024.0 / 1024.0);
|
||||
} else {
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%.2f GiB", (double)e.size / 1024.0 / 1024.0 / 1024.0);
|
||||
}
|
||||
gfx::drawTextArgs(vg, x + text_xoffset + 150, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s", utils::hexIdToStr(e.content_id).str);
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", utils::formatSizeStorage(e.size).c_str());
|
||||
|
||||
if (e.missing) {
|
||||
gfx::drawText(vg, x + text_xoffset - 80 / 2, y + (h / 2.f) - (24.f / 2), 24.f, "\uE140", nullptr, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_ERROR));
|
||||
@@ -413,7 +408,7 @@ Result Menu::MountNcaFs() {
|
||||
R_TRY(devoptab::MountNcaNcm(m_meta.cs, &e.content_id, root));
|
||||
|
||||
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [root](){
|
||||
devoptab::UmountNca(root);
|
||||
devoptab::UmountNeworkDevice(root);
|
||||
});
|
||||
|
||||
filebrowser::MountFsHelper(fs, utils::hexIdToStr(e.content_id).str);
|
||||
|
||||
@@ -235,6 +235,12 @@ struct XciSource final : dump::BaseSource {
|
||||
int icon{};
|
||||
|
||||
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
|
||||
if (off == xci_size) {
|
||||
log_write("[XciSource::Read] read at eof...\n");
|
||||
*bytes_read = 0;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
if (path.ends_with(GetDumpTypeStr(DumpFileType_XCI)) || path.ends_with(GetDumpTypeStr(DumpFileType_XCZ))) {
|
||||
size = ClipSize(off, size, xci_size);
|
||||
*bytes_read = size;
|
||||
@@ -323,6 +329,7 @@ private:
|
||||
const s64 m_offset;
|
||||
};
|
||||
|
||||
#ifdef ENABLE_NSZ
|
||||
Result NszExport(ProgressBox* pbox, const keys::Keys& keys, dump::BaseSource* _source, dump::WriteSource* writer, const fs::FsPath& path) {
|
||||
auto source = (XciSource*)_source;
|
||||
|
||||
@@ -464,6 +471,7 @@ Result NszExport(ProgressBox* pbox, const keys::Keys& keys, dump::BaseSource* _s
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
#endif // ENABLE_NSZ
|
||||
|
||||
struct GcSource final : yati::source::Base {
|
||||
GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs);
|
||||
@@ -617,7 +625,9 @@ Menu::Menu(u32 flags) : MenuBase{"GameCard"_i18n, flags} {
|
||||
add("Export Certificate"_i18n, DumpFileFlag_Cert);
|
||||
add("Export Initial Data"_i18n, DumpFileFlag_Initial);
|
||||
} else if (m_option_index == 2) {
|
||||
#ifdef ENABLE_NSZ
|
||||
DumpXcz(0);
|
||||
#endif // ENABLE_NSZ
|
||||
} else if (m_option_index == 3) {
|
||||
const auto rc = MountGcFs();
|
||||
App::PushErrorBox(rc, "Failed to mount GameCard filesystem"_i18n);
|
||||
@@ -708,10 +718,10 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
nvgIntersectScissor(vg, 50, 90, 325, 555);
|
||||
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", e.lang_entry.name);
|
||||
gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", e.lang_entry.author);
|
||||
gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "App-ID: 0%lX", e.app_id);
|
||||
gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key-Gen: %u (%s)", e.key_gen, nca::GetKeyGenStr(e.key_gen));
|
||||
gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Size: %.2f GB", (double)size / 0x40000000);
|
||||
gfx::drawTextArgs(vg, 50, 615, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Base: %zu Patch: %zu Addon: %zu Data: %zu", e.application.size(), e.patch.size(), e.add_on.size(), e.data_patch.size());
|
||||
gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "App-ID: 0%lX"_i18n.c_str(), e.app_id);
|
||||
gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key-Gen: %u (%s)"_i18n.c_str(), e.key_gen, nca::GetKeyGenStr(e.key_gen));
|
||||
gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Size: %.2f GB"_i18n.c_str(), (double)size / 0x40000000);
|
||||
gfx::drawTextArgs(vg, 50, 615, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Base: %zu Patch: %zu Addon: %zu Data: %zu"_i18n.c_str(), e.application.size(), e.patch.size(), e.add_on.size(), e.data_patch.size());
|
||||
nvgRestore(vg);
|
||||
}
|
||||
|
||||
@@ -729,6 +739,11 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
if (!m_mounted) {
|
||||
colour = ThemeEntryID_TEXT_INFO;
|
||||
}
|
||||
if (i == 2) {
|
||||
#ifndef ENABLE_NSZ
|
||||
colour = ThemeEntryID_TEXT_INFO;
|
||||
#endif // ENABLE_NSZ
|
||||
}
|
||||
|
||||
gfx::drawTextArgs(vg, x + 15, y + (h / 2.f), 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(colour), "%s", i18n::get(g_option_list[i]).c_str());
|
||||
});
|
||||
@@ -1083,7 +1098,7 @@ void Menu::OnChangeIndex(s64 new_index) {
|
||||
m_entry_index = new_index;
|
||||
|
||||
if (m_entries.empty()) {
|
||||
this->SetSubHeading("No GameCard inserted");
|
||||
this->SetSubHeading("No GameCard inserted"_i18n);
|
||||
} else {
|
||||
const auto index = m_entries.empty() ? 0 : m_entry_index + 1;
|
||||
this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size()));
|
||||
@@ -1099,6 +1114,7 @@ void Menu::OnChangeIndex(s64 new_index) {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ENABLE_NSZ
|
||||
Result Menu::DumpXcz(u32 flags) {
|
||||
R_TRY(GcMountStorage());
|
||||
|
||||
@@ -1122,6 +1138,7 @@ Result Menu::DumpXcz(u32 flags) {
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
#endif // ENABLE_NSZ
|
||||
|
||||
Result Menu::DumpGames(u32 flags) {
|
||||
// first, try and mount the storage.
|
||||
@@ -1183,18 +1200,8 @@ Result Menu::DumpGames(u32 flags) {
|
||||
paths.emplace_back(BuildFullDumpPath(DumpFileType_Initial, m_entries));
|
||||
}
|
||||
|
||||
if (0) {
|
||||
// todo: log keys error.
|
||||
keys::Keys keys;
|
||||
keys::parse_keys(keys, true);
|
||||
|
||||
dump::Dump(source, paths, [keys](ProgressBox* pbox, dump::BaseSource* source, dump::WriteSource* writer, const fs::FsPath& path) {
|
||||
return NszExport(pbox, keys, source, writer, path);
|
||||
});
|
||||
} else {
|
||||
dump::Dump(source, paths, nullptr, location_flags);
|
||||
}
|
||||
|
||||
dump::Dump(source, paths, nullptr, location_flags);
|
||||
R_SUCCEED();
|
||||
};
|
||||
|
||||
@@ -1322,7 +1329,7 @@ Result Menu::MountGcFs() {
|
||||
R_TRY(devoptab::MountXciSource(source, m_storage_trimmed_size, e.lang_entry.name, root));
|
||||
|
||||
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [root](){
|
||||
devoptab::UmountXci(root);
|
||||
devoptab::UmountNeworkDevice(root);
|
||||
});
|
||||
|
||||
filebrowser::MountFsHelper(fs, e.lang_entry.name);
|
||||
|
||||
@@ -108,7 +108,7 @@ auto DownloadApp(ProgressBox* pbox, const GhApiAsset& gh_asset, const AssetEntry
|
||||
|
||||
// 2. download the asset
|
||||
if (!pbox->ShouldExit()) {
|
||||
pbox->NewTransfer("Downloading "_i18n + gh_asset.name);
|
||||
pbox->NewTransfer(i18n::Reorder("Downloading ", gh_asset.name));
|
||||
log_write("starting download: %s\n", gh_asset.browser_download_url.c_str());
|
||||
|
||||
const auto result = curl::Api().ToFile(
|
||||
@@ -233,7 +233,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
nvgRestore(vg);
|
||||
|
||||
if (!e.tag.empty()) {
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "version: %s", e.tag.c_str());
|
||||
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "version: %s"_i18n.c_str(), e.tag.c_str());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -427,7 +427,7 @@ void DownloadEntries(const Entry& entry) {
|
||||
App::PushErrorBox(rc, "Failed to download app!"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Downloaded "_i18n + entry.repo);
|
||||
App::Notify(i18n::Reorder("Downloaded ", entry.repo));
|
||||
auto post_install_message = entry.post_install_message;
|
||||
if (ptr && !ptr->post_install_message.empty()) {
|
||||
post_install_message = ptr->post_install_message;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include "ui/menus/grid_menu_base.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace sphaira::ui::menu::grid {
|
||||
|
||||
void Menu::DrawEntry(NVGcontext* vg, Theme* theme, int layout, const Vec4& v, bool selected, int image, const char* name, const char* author, const char* version) {
|
||||
@@ -16,8 +18,9 @@ Vec4 Menu::DrawEntry(NVGcontext* vg, Theme* theme, bool draw_image, int layout,
|
||||
const auto& [x, y, w, h] = v;
|
||||
|
||||
auto text_id = ThemeEntryID_TEXT;
|
||||
auto info_id = ThemeEntryID_TEXT_INFO;
|
||||
if (selected) {
|
||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||
text_id = info_id = ThemeEntryID_TEXT_SELECTED;
|
||||
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||
} else {
|
||||
DrawElement(v, ThemeEntryID_GRID);
|
||||
@@ -36,8 +39,8 @@ Vec4 Menu::DrawEntry(NVGcontext* vg, Theme* theme, bool draw_image, int layout,
|
||||
const auto text_clip_w = w - 30.f - text_off;
|
||||
const float font_size = 18;
|
||||
m_scroll_name.Draw(vg, selected, text_x, y + 45, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), name);
|
||||
m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), author);
|
||||
m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), version);
|
||||
m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(info_id), author);
|
||||
m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(info_id), version);
|
||||
} else {
|
||||
if (selected) {
|
||||
gfx::drawAppLable(vg, theme, m_scroll_name, x, y, w, name);
|
||||
@@ -45,7 +48,19 @@ Vec4 Menu::DrawEntry(NVGcontext* vg, Theme* theme, bool draw_image, int layout,
|
||||
}
|
||||
|
||||
if (draw_image) {
|
||||
gfx::drawImage(vg, image_v, image ?: App::GetDefaultImage(), 5);
|
||||
if (image > 0) {
|
||||
gfx::drawImage(vg, image_v, image, 5);
|
||||
} else {
|
||||
// https://www.mathopenref.com/arcradius.html
|
||||
auto spinner = image_v;
|
||||
spinner.w /= 2;
|
||||
spinner.h /= 2;
|
||||
spinner.x += (image_v.w / 2);
|
||||
spinner.y += (image_v.h / 2);
|
||||
|
||||
const auto rad = (spinner.h / 2) + (std::powf(spinner.w, 2) / (spinner.h * 8));
|
||||
gfx::drawSpinner(vg, theme, spinner.x, spinner.y, rad, armTicksToNs(armGetSystemTick()) / 1e+9);
|
||||
}
|
||||
}
|
||||
|
||||
return image_v;
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace sphaira::ui::menu::homebrew {
|
||||
namespace {
|
||||
|
||||
Menu* g_menu{};
|
||||
constinit UEvent g_change_uevent;
|
||||
std::atomic_bool g_change_signalled{};
|
||||
|
||||
auto GenerateStarPath(const fs::FsPath& nro_path) -> fs::FsPath {
|
||||
fs::FsPath out{};
|
||||
@@ -44,7 +44,7 @@ void FreeEntry(NVGcontext* vg, NroEntry& e) {
|
||||
} // namespace
|
||||
|
||||
void SignalChange() {
|
||||
ueventSignal(&g_change_uevent);
|
||||
g_change_signalled = true;
|
||||
}
|
||||
|
||||
auto GetNroEntries() -> std::span<const NroEntry> {
|
||||
@@ -55,7 +55,7 @@ auto GetNroEntries() -> std::span<const NroEntry> {
|
||||
return g_menu->GetHomebrewList();
|
||||
}
|
||||
|
||||
Menu::Menu() : grid::Menu{"Homebrew"_i18n, MenuFlag_Tab} {
|
||||
Menu::Menu(u32 flags) : grid::Menu{"Homebrew"_i18n, flags} {
|
||||
g_menu = this;
|
||||
|
||||
this->SetActions(
|
||||
@@ -67,8 +67,14 @@ Menu::Menu() : grid::Menu{"Homebrew"_i18n, MenuFlag_Tab} {
|
||||
}})
|
||||
);
|
||||
|
||||
// Add Back button only when entered via Menus (Not center, righit, left tab).
|
||||
if (!(flags & MenuFlag_Tab)) {
|
||||
this->SetAction(Button::B, Action{"Back"_i18n, [this](){
|
||||
this->SetPop();
|
||||
}});
|
||||
}
|
||||
|
||||
OnLayoutChange();
|
||||
ueventCreate(&g_change_uevent, true);
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
@@ -77,7 +83,7 @@ Menu::~Menu() {
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
if (R_SUCCEEDED(waitSingle(waiterForUEvent(&g_change_uevent), 0))) {
|
||||
if (g_change_signalled.exchange(false)) {
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
@@ -86,7 +92,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
}
|
||||
|
||||
MenuBase::Update(controller, touch);
|
||||
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
|
||||
m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this](bool touch, auto i) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
@@ -169,13 +175,13 @@ void Menu::SetIndex(s64 index) {
|
||||
if (fs::FsNativeSd().FileExists(star_path)) {
|
||||
SetAction(Button::R3, Action{"Unstar"_i18n, [this](){
|
||||
fs::FsNativeSd().DeleteFile(GenerateStarPath(GetEntry().path));
|
||||
App::Notify("Unstarred "_i18n + GetEntry().GetName());
|
||||
App::Notify(i18n::Reorder("Unstarred ", GetEntry().GetName()));
|
||||
SortAndFindLastFile();
|
||||
}});
|
||||
} else {
|
||||
SetAction(Button::R3, Action{"Star"_i18n, [this](){
|
||||
fs::FsNativeSd().CreateFile(GenerateStarPath(GetEntry().path));
|
||||
App::Notify("Starred "_i18n + GetEntry().GetName());
|
||||
App::Notify(i18n::Reorder("Starred ", GetEntry().GetName()));
|
||||
SortAndFindLastFile();
|
||||
}});
|
||||
}
|
||||
@@ -198,7 +204,9 @@ void Menu::InstallHomebrew() {
|
||||
}
|
||||
|
||||
void Menu::ScanHomebrew() {
|
||||
g_change_signalled = false;
|
||||
FreeEntries();
|
||||
|
||||
{
|
||||
SCOPED_TIMESTAMP("nro scan");
|
||||
nro_scan("/switch", m_entries);
|
||||
@@ -355,7 +363,7 @@ void Menu::Sort() {
|
||||
m_entries_current = m_entries_index[Filter_HideHidden];
|
||||
}
|
||||
|
||||
std::sort(m_entries_current.begin(), m_entries_current.end(), sorter);
|
||||
std::ranges::sort(m_entries_current, sorter);
|
||||
}
|
||||
|
||||
void Menu::SortAndFindLastFile(bool scan) {
|
||||
@@ -397,6 +405,7 @@ void Menu::FreeEntries() {
|
||||
}
|
||||
|
||||
m_entries.clear();
|
||||
m_entries_current = {};
|
||||
for (auto& e : m_entries_index) {
|
||||
e.clear();
|
||||
}
|
||||
@@ -467,7 +476,7 @@ void Menu::DisplayOptions() {
|
||||
|
||||
// for testing stuff.
|
||||
#if 0
|
||||
options->Add<SidebarEntrySlider>("Test", 1, 0, 2, 10, [](auto& v_out){
|
||||
options->Add<SidebarEntrySlider>("Test"_i18n, 1, 0, 2, 10, [](auto& v_out){
|
||||
|
||||
});
|
||||
#endif
|
||||
@@ -492,7 +501,7 @@ void Menu::DisplayOptions() {
|
||||
}, "Mounts the NRO FileSystem (icon, nacp and RomFS)."_i18n);
|
||||
|
||||
options->Add<SidebarEntryCallback>("Delete"_i18n, [this](){
|
||||
const auto buf = "Are you sure you want to delete "_i18n + GetEntry().path.toString() + "?";
|
||||
const auto buf = i18n::Reorder("Are you sure you want to delete ", GetEntry().path.toString()) + "?";
|
||||
App::Push<OptionBox>(
|
||||
buf,
|
||||
"Back"_i18n, "Delete"_i18n, 1, [this](auto op_index){
|
||||
@@ -508,9 +517,10 @@ void Menu::DisplayOptions() {
|
||||
}
|
||||
}, GetEntry().image
|
||||
);
|
||||
}, "Perminately delete the selected homebrew.\n\n"
|
||||
"Files and folders created by the homebrew will still remain. "
|
||||
"Use the FileBrowser to delete them."_i18n);
|
||||
}, i18n::get("hb_remove_info",
|
||||
"Perminately delete the selected homebrew.\n\n"
|
||||
"Files and folders created by the homebrew will still remain. "
|
||||
"Use the FileBrowser to delete them."));
|
||||
|
||||
auto forwarder_entry = options->Add<SidebarEntryCallback>("Install Forwarder"_i18n, [this](){
|
||||
InstallHomebrew();
|
||||
@@ -527,7 +537,7 @@ Result Menu::MountNroFs() {
|
||||
R_TRY(devoptab::MountNro(App::GetApp()->m_fs.get(), e.path, root));
|
||||
|
||||
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [root](){
|
||||
devoptab::UmountNro(root);
|
||||
devoptab::UmountNeworkDevice(root);
|
||||
});
|
||||
|
||||
filebrowser::MountFsHelper(fs, root);
|
||||
|
||||
@@ -16,34 +16,26 @@ enum class InstallState {
|
||||
Finished,
|
||||
};
|
||||
|
||||
constexpr u64 MAX_BUFFER_SIZE = 1024ULL*1024ULL*8ULL;
|
||||
constexpr u64 MAX_BUFFER_RESERVE_SIZE = 1024ULL*1024ULL*32ULL;
|
||||
constexpr u64 MAX_BUFFER_SIZE = 1024ULL*1024ULL*1ULL;
|
||||
std::atomic<InstallState> INSTALL_STATE{InstallState::None};
|
||||
|
||||
// don't use condivar here as windows mtp is very broken.
|
||||
// stalling for too longer (3s+) and having too varied transfer speeds
|
||||
// results in windows stalling the transfer for 1m until it kills it via timeout.
|
||||
// the workaround is to always accept new data, but stall for 1s.
|
||||
// UPDATE: it seems possible to trigger this bug during normal file transfer
|
||||
// including using stock haze.
|
||||
// it seems random, and ive been unable to trigger it personally.
|
||||
// for this reason, use condivar rather than trying to work around the issue.
|
||||
#define USE_CONDI_VAR 1
|
||||
|
||||
} // namespace
|
||||
|
||||
Stream::Stream(const fs::FsPath& path, std::stop_token token) {
|
||||
m_path = path;
|
||||
m_token = token;
|
||||
m_active = true;
|
||||
m_buffer.reserve(MAX_BUFFER_RESERVE_SIZE);
|
||||
m_buffer.reserve(MAX_BUFFER_SIZE);
|
||||
|
||||
mutexInit(&m_mutex);
|
||||
condvarInit(&m_can_read);
|
||||
condvarInit(&m_can_write);
|
||||
}
|
||||
|
||||
Result Stream::ReadChunk(void* buf, s64 size, u64* bytes_read) {
|
||||
Result Stream::ReadChunk(void* _buf, s64 size, u64* bytes_read) {
|
||||
auto buf = static_cast<u8*>(_buf);
|
||||
*bytes_read = 0;
|
||||
|
||||
log_write("[Stream::ReadChunk] inside\n");
|
||||
ON_SCOPE_EXIT(
|
||||
log_write("[Stream::ReadChunk] exiting\n");
|
||||
@@ -59,18 +51,30 @@ Result Stream::ReadChunk(void* buf, s64 size, u64* bytes_read) {
|
||||
break;
|
||||
}
|
||||
|
||||
size = std::min<s64>(size, m_buffer.size());
|
||||
std::memcpy(buf, m_buffer.data(), size);
|
||||
m_buffer.erase(m_buffer.begin(), m_buffer.begin() + size);
|
||||
*bytes_read = size;
|
||||
return condvarWakeOne(&m_can_write);
|
||||
const auto rsize = std::min<s64>(size, m_buffer.size());
|
||||
std::memcpy(buf, m_buffer.data(), rsize);
|
||||
m_buffer.erase(m_buffer.begin(), m_buffer.begin() + rsize);
|
||||
condvarWakeOne(&m_can_write);
|
||||
|
||||
size -= rsize;
|
||||
buf += rsize;
|
||||
*bytes_read += rsize;
|
||||
|
||||
if (!size) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
}
|
||||
|
||||
log_write("[Stream::ReadChunk] failed to read\n");
|
||||
R_THROW(Result_TransferCancelled);
|
||||
}
|
||||
|
||||
bool Stream::Push(const void* buf, s64 size) {
|
||||
bool Stream::Push(const void* _buf, s64 size) {
|
||||
auto buf = static_cast<const u8*>(_buf);
|
||||
if (!size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
log_write("[Stream::Push] inside\n");
|
||||
ON_SCOPE_EXIT(
|
||||
log_write("[Stream::Push] exiting\n");
|
||||
@@ -83,31 +87,27 @@ bool Stream::Push(const void* buf, s64 size) {
|
||||
}
|
||||
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
#if USE_CONDI_VAR
|
||||
if (m_active && m_buffer.size() >= MAX_BUFFER_SIZE) {
|
||||
R_TRY(condvarWait(std::addressof(m_can_write), std::addressof(m_mutex)));
|
||||
}
|
||||
#else
|
||||
if (m_active && m_buffer.size() >= MAX_BUFFER_SIZE) {
|
||||
// unlock the mutex and wait for 1s to bring transfer speed down to 1MiB/s.
|
||||
log_write("[Stream::Push] buffer is full, delaying\n");
|
||||
mutexUnlock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexLock(&m_mutex));
|
||||
|
||||
svcSleepThread(1e+9);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!m_active) {
|
||||
log_write("[Stream::Push] file not active\n");
|
||||
break;
|
||||
}
|
||||
|
||||
const auto wsize = std::min<s64>(size, MAX_BUFFER_SIZE - m_buffer.size());
|
||||
const auto offset = m_buffer.size();
|
||||
m_buffer.resize(offset + size);
|
||||
std::memcpy(m_buffer.data() + offset, buf, size);
|
||||
m_buffer.resize(offset + wsize);
|
||||
|
||||
std::memcpy(m_buffer.data() + offset, buf, wsize);
|
||||
condvarWakeOne(&m_can_read);
|
||||
return true;
|
||||
|
||||
size -= wsize;
|
||||
buf += wsize;
|
||||
if (!size) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
log_write("[Stream::Push] failed to push\n");
|
||||
|
||||
@@ -188,7 +188,7 @@ Menu::Menu(u32 flags) : MenuBase{"Irs"_i18n, flags} {
|
||||
return;
|
||||
}
|
||||
|
||||
static_assert(IRS_MAX_CAMERAS >= 9, "max camaeras has gotten smaller!");
|
||||
static_assert(IRS_MAX_CAMERAS >= 9, "max cameras has gotten smaller!");
|
||||
|
||||
// open all handles
|
||||
irsGetIrCameraHandle(&m_entries[0].m_handle, HidNpadIdType_No1);
|
||||
@@ -534,7 +534,7 @@ void Menu::updateColourArray() {
|
||||
|
||||
auto Menu::GetEntryName(s64 i) -> std::string {
|
||||
const auto& e = m_entries[i];
|
||||
std::string text = "Pad "_i18n + (i == 8 ? "HandHeld"_i18n : std::to_string(i));
|
||||
std::string text = i18n::Reorder("Pad ", (i == 8 ? "HandHeld"_i18n : std::to_string(i)));
|
||||
switch (e.status) {
|
||||
case IrsIrCameraStatus_Available:
|
||||
text += " (Available)"_i18n;
|
||||
|
||||
@@ -49,55 +49,57 @@ auto MiscMenuFuncGenerator(u32 flags) {
|
||||
}
|
||||
|
||||
const MiscMenuEntry MISC_MENU_ENTRIES[] = {
|
||||
{ .name = "Homebrew", .title = "Homebrew", .func = MiscMenuFuncGenerator<ui::menu::homebrew::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
|
||||
"The homebrew menu.\n\n"
|
||||
"Allows you to launch, delete and mount homebrew!"},
|
||||
|
||||
{ .name = "Appstore", .title = "Appstore", .func = MiscMenuFuncGenerator<ui::menu::appstore::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
|
||||
"Download and update apps.\n\n"\
|
||||
"Download and update apps.\n\n"
|
||||
"Internet connection required." },
|
||||
|
||||
{ .name = "Games", .title = "Games", .func = MiscMenuFuncGenerator<ui::menu::game::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
|
||||
"View all installed games. "\
|
||||
"View all installed games. "
|
||||
"In this menu you can launch, backup, create savedata and much more." },
|
||||
|
||||
{ .name = "FileBrowser", .title = "FileBrowser", .func = MiscMenuFuncGenerator<ui::menu::filebrowser::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
|
||||
"Browse files on you SD Card. "\
|
||||
"You can move, copy, delete, extract zip, create zip, upload and much more.\n\n"\
|
||||
"Browse files on you SD Card. "
|
||||
"You can move, copy, delete, extract zip, create zip, upload and much more.\n\n"
|
||||
"A connected USB/HDD can be opened by mounting it in the advanced options." },
|
||||
|
||||
{ .name = "Saves", .title = "Saves", .func = MiscMenuFuncGenerator<ui::menu::save::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
|
||||
"View save data for each user. "\
|
||||
"You can backup and restore saves.\n\n"\
|
||||
"View save data for each user. "
|
||||
"You can backup and restore saves.\n\n"
|
||||
"Experimental support for backing up system saves is possible." },
|
||||
|
||||
#if 0
|
||||
{ .name = "Themezer", .title = "Themezer", .func = MiscMenuFuncGenerator<ui::menu::themezer::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
|
||||
"Download themes from themezer.net. "\
|
||||
"Themes are downloaded to /themes/sphaira\n"\
|
||||
"Download themes from themezer.net. "
|
||||
"Themes are downloaded to /themes/sphaira\n"
|
||||
"To install the themes, NXThemesInstaller needs to be installed (can be downloaded via the AppStore)." },
|
||||
#endif
|
||||
|
||||
{ .name = "GitHub", .title = "GitHub", .func = MiscMenuFuncGenerator<ui::menu::gh::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
|
||||
"Download releases directly from GitHub. "\
|
||||
"Download releases directly from GitHub. "
|
||||
"Custom entries can be added to /config/sphaira/github" },
|
||||
|
||||
#ifdef ENABLE_FTPSRV
|
||||
{ .name = "FTP", .title = "FTP Install", .func = MiscMenuFuncGenerator<ui::menu::ftp::Menu>, .flag = MiscMenuFlag_Install, .info =
|
||||
"Install apps via FTP.\n\n"\
|
||||
"NOTE: This feature does not always work, use at your own risk. "\
|
||||
"If you encounter an issue, do not open an issue, it will not be fixed." },
|
||||
"Install apps via FTP." },
|
||||
#endif // ENABLE_FTPSRV
|
||||
|
||||
#ifdef ENABLE_LIBHAZE
|
||||
{ .name = "MTP", .title = "MTP Install", .func = MiscMenuFuncGenerator<ui::menu::mtp::Menu>, .flag = MiscMenuFlag_Install, .info =
|
||||
"Install apps via MTP.\n\n"\
|
||||
"NOTE: This feature does not always work, use at your own risk. "\
|
||||
"If you encounter an issue, do not open an issue, it will not be fixed." },
|
||||
"Install apps via MTP." },
|
||||
#endif // ENABLE_LIBHAZE
|
||||
|
||||
{ .name = "USB", .title = "USB Install", .func = MiscMenuFuncGenerator<ui::menu::usb::Menu>, .flag = MiscMenuFlag_Install, .info =
|
||||
"Install apps via USB.\n\n"\
|
||||
"A USB client is required on PC, such as ns-usbloader and fluffy.\n\n"\
|
||||
"NOTE: This feature does not always work, use at your own risk. "\
|
||||
"If you encounter an issue, do not open an issue, it will not be fixed." },
|
||||
"Install apps via USB.\n\n"
|
||||
"A USB client is required on PC." },
|
||||
|
||||
{ .name = "GameCard", .title = "GameCard", .func = MiscMenuFuncGenerator<ui::menu::gc::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
|
||||
"View info on the inserted Game Card (GC). "\
|
||||
"You can backup and install the inserted GC. "\
|
||||
"To swap GC's, simply remove the old GC and insert the new one. "\
|
||||
"View info on the inserted Game Card (GC). "
|
||||
"You can backup and install the inserted GC. "
|
||||
"To swap GC's, simply remove the old GC and insert the new one. "
|
||||
"You do not need to exit the menu." },
|
||||
|
||||
{ .name = "IRS", .title = "IRS (Infrared Joycon Camera)", .func = MiscMenuFuncGenerator<ui::menu::irs::Menu>, .flag = MiscMenuFlag_Shortcut, .info =
|
||||
@@ -112,7 +114,7 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
|
||||
|
||||
// 1. download the zip
|
||||
if (!pbox->ShouldExit()) {
|
||||
pbox->NewTransfer("Downloading "_i18n + version);
|
||||
pbox->NewTransfer(i18n::Reorder("Downloading ", version));
|
||||
log_write("starting download: %s\n", url.c_str());
|
||||
|
||||
const auto result = curl::Api().ToFile(
|
||||
@@ -165,10 +167,35 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
auto CreateLeftSideMenu(std::string& name_out) -> std::unique_ptr<MenuBase> {
|
||||
auto CreateCenterMenu(std::string& name_out) -> std::unique_ptr<MenuBase> {
|
||||
const auto name = App::GetApp()->m_center_menu.Get();
|
||||
|
||||
for (auto& e : GetMenuMenuEntries()) {
|
||||
if (e.name == name) {
|
||||
name_out = name;
|
||||
return e.func(MenuFlag_Tab);
|
||||
}
|
||||
}
|
||||
|
||||
name_out = "Homebrew";
|
||||
return std::make_unique<ui::menu::homebrew::Menu>(MenuFlag_Tab);
|
||||
}
|
||||
|
||||
auto CreateLeftSideMenu(std::string_view center_name, std::string& name_out) -> std::unique_ptr<MenuBase> {
|
||||
const auto name = App::GetApp()->m_left_menu.Get();
|
||||
|
||||
for (auto& e : GetMiscMenuEntries()) {
|
||||
// handle if the user tries to mount the same menu twice.
|
||||
if (name == center_name) {
|
||||
// check if we can mount the default.
|
||||
if (center_name != "FileBrowser") {
|
||||
return std::make_unique<ui::menu::filebrowser::Menu>(MenuFlag_Tab);
|
||||
} else {
|
||||
// otherwise, fallback to center default.
|
||||
return std::make_unique<ui::menu::homebrew::Menu>(MenuFlag_Tab);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& e : GetMenuMenuEntries()) {
|
||||
if (e.name == name) {
|
||||
name_out = name;
|
||||
return e.func(MenuFlag_Tab);
|
||||
@@ -179,6 +206,7 @@ auto CreateLeftSideMenu(std::string& name_out) -> std::unique_ptr<MenuBase> {
|
||||
return std::make_unique<ui::menu::filebrowser::Menu>(MenuFlag_Tab);
|
||||
}
|
||||
|
||||
// todo: handle center / left menu being the same.
|
||||
auto CreateRightSideMenu(std::string_view left_name) -> std::unique_ptr<MenuBase> {
|
||||
const auto name = App::GetApp()->m_right_menu.Get();
|
||||
|
||||
@@ -193,7 +221,7 @@ auto CreateRightSideMenu(std::string_view left_name) -> std::unique_ptr<MenuBase
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& e : GetMiscMenuEntries()) {
|
||||
for (auto& e : GetMenuMenuEntries()) {
|
||||
if (e.name == name) {
|
||||
return e.func(MenuFlag_Tab);
|
||||
}
|
||||
@@ -204,7 +232,7 @@ auto CreateRightSideMenu(std::string_view left_name) -> std::unique_ptr<MenuBase
|
||||
|
||||
} // namespace
|
||||
|
||||
auto GetMiscMenuEntries() -> std::span<const MiscMenuEntry> {
|
||||
auto GetMenuMenuEntries() -> std::span<const MiscMenuEntry> {
|
||||
return MISC_MENU_ENTRIES;
|
||||
}
|
||||
|
||||
@@ -276,9 +304,9 @@ MainMenu::MainMenu() {
|
||||
|
||||
this->SetActions(
|
||||
std::make_pair(Button::START, Action{App::Exit}),
|
||||
std::make_pair(Button::SELECT, Action{App::DisplayMiscOptions}),
|
||||
std::make_pair(Button::SELECT, Action{App::DisplayMenuOptions}),
|
||||
std::make_pair(Button::Y, Action{"Menu"_i18n, [this](){
|
||||
auto options = std::make_unique<Sidebar>("Menu Options"_i18n, "v" APP_VERSION_HASH, Sidebar::Side::LEFT);
|
||||
auto options = std::make_unique<Sidebar>("Menu Options"_i18n, "v" APP_DISPLAY_VERSION, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(std::move(options)));
|
||||
|
||||
SidebarEntryArray::Items language_items;
|
||||
@@ -289,18 +317,32 @@ MainMenu::MainMenu() {
|
||||
language_items.push_back("German"_i18n);
|
||||
language_items.push_back("Italian"_i18n);
|
||||
language_items.push_back("Spanish"_i18n);
|
||||
language_items.push_back("Chinese"_i18n);
|
||||
language_items.push_back("Chinese (Simplified)"_i18n);
|
||||
language_items.push_back("Korean"_i18n);
|
||||
language_items.push_back("Dutch"_i18n);
|
||||
language_items.push_back("Portuguese"_i18n);
|
||||
language_items.push_back("Russian"_i18n);
|
||||
language_items.push_back("Chinese (Traditional)"_i18n);
|
||||
language_items.push_back("Swedish"_i18n);
|
||||
language_items.push_back("Vietnamese"_i18n);
|
||||
language_items.push_back("Ukrainian"_i18n);
|
||||
|
||||
options->Add<SidebarEntryCallback>("Theme"_i18n, [](){
|
||||
App::DisplayThemeOptions();
|
||||
}, "Customise the look of Sphaira by changing the theme"_i18n);
|
||||
// build menus info.
|
||||
std::string menus_info = "Launch one of Sphaira's menus:\n"_i18n;
|
||||
for (auto& e : GetMenuMenuEntries()) {
|
||||
if (e.name == App::GetApp()->m_left_menu.Get()) {
|
||||
continue;
|
||||
} else if (e.name == App::GetApp()->m_right_menu.Get()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
menus_info += "- " + i18n::get(e.title) + "\n";
|
||||
}
|
||||
menus_info += "\nYou can change the left/right menu in the Advanced Options."_i18n;
|
||||
|
||||
options->Add<SidebarEntryCallback>("Menus"_i18n, [](){
|
||||
App::DisplayMenuOptions();
|
||||
}, menus_info);
|
||||
|
||||
options->Add<SidebarEntryCallback>("Network"_i18n, [this](){
|
||||
auto options = std::make_unique<Sidebar>("Network Options"_i18n, Sidebar::Side::LEFT);
|
||||
@@ -315,7 +357,7 @@ MainMenu::MainMenu() {
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
m_update_state = UpdateState::None;
|
||||
App::Notify("Updated to "_i18n + m_update_version);
|
||||
App::Notify(i18n::Reorder("Updated to ", m_update_version));
|
||||
App::Push<OptionBox>(
|
||||
"Press OK to restart Sphaira"_i18n, "OK"_i18n, [](auto){
|
||||
App::ExitRestart();
|
||||
@@ -326,56 +368,60 @@ MainMenu::MainMenu() {
|
||||
});
|
||||
}
|
||||
|
||||
options->Add<SidebarEntryBool>("Ftp"_i18n, App::GetFtpEnable(), [](bool& enable){
|
||||
App::SetFtpEnable(enable);
|
||||
}, "Enable FTP server to run in the background.\n\n"\
|
||||
"The default port is 5000 with no user/pass set. "\
|
||||
"You can change this behaviour in /config/ftpsrv/config.ini"_i18n);
|
||||
options->Add<SidebarEntryCallback>("FTP"_i18n, [](){ App::DisplayFtpOptions(); },
|
||||
i18n::get("ftp_settings_info",
|
||||
"Enable / modify the FTP server settings such as port, user/pass and the folders that are shown.\n\n"
|
||||
"NOTE: Changing any of the options will automatically restart the FTP server when exiting the options menu.")
|
||||
);
|
||||
|
||||
options->Add<SidebarEntryBool>("Mtp"_i18n, App::GetMtpEnable(), [](bool& enable){
|
||||
App::SetMtpEnable(enable);
|
||||
}, "Enable MTP server to run in the background."_i18n);
|
||||
options->Add<SidebarEntryCallback>("MTP"_i18n, [](){ App::DisplayMtpOptions(); },
|
||||
i18n::get("mtp_settings_info",
|
||||
"Enable / modify the MTP responder settings such as the folders that are shown.\n\n"
|
||||
"NOTE: Changing any of the options will automatically restart the MTP server when exiting the options menu.")
|
||||
);
|
||||
|
||||
options->Add<SidebarEntryBool>("Nxlink"_i18n, App::GetNxlinkEnable(), [](bool& enable){
|
||||
options->Add<SidebarEntryCallback>("HDD"_i18n, [](){
|
||||
App::DisplayHddOptions();
|
||||
}, "Enable / modify the HDD mount options."_i18n);
|
||||
|
||||
options->Add<SidebarEntryBool>("NXlink"_i18n, App::GetNxlinkEnable(), [](bool& enable){
|
||||
App::SetNxlinkEnable(enable);
|
||||
}, "Enable NXlink server to run in the background. "\
|
||||
"NXlink is used to send .nro's from PC to the switch\n\n"\
|
||||
"If you are not a developer, you can disable this option."_i18n);
|
||||
}, i18n::get("nxlink_enable_info",
|
||||
"Enable NXlink server to run in the background. "
|
||||
"NXlink is used to send .nro's from PC to the switch\n\n"
|
||||
"If you are not a developer, you can disable this option."));
|
||||
|
||||
options->Add<SidebarEntryBool>("Hdd"_i18n, App::GetHddEnable(), [](bool& enable){
|
||||
App::SetHddEnable(enable);
|
||||
}, "Enable mounting of connected USB/HDD devices. "\
|
||||
"Connected devices can be used in the FileBrowser, as well as a backup location when dumping games and saves."_i18n);
|
||||
}, i18n::get("nxlink_toggle_info",
|
||||
"Toggle FTP, MTP, HDD and NXlink\n\n"
|
||||
"If Sphaira has a update available, you can download it from this menu"));
|
||||
|
||||
options->Add<SidebarEntryBool>("Hdd write protect"_i18n, App::GetWriteProtect(), [](bool& enable){
|
||||
App::SetWriteProtect(enable);
|
||||
}, "Makes the connected HDD read-only."_i18n);
|
||||
}, "Toggle FTP, MTP, HDD and NXlink\n\n" \
|
||||
"If Sphaira has a update available, you can download it from this menu"_i18n);
|
||||
options->Add<SidebarEntryCallback>("Theme"_i18n, [](){
|
||||
App::DisplayThemeOptions();
|
||||
}, "Customise the look of Sphaira by changing the theme"_i18n);
|
||||
|
||||
options->Add<SidebarEntryArray>("Language"_i18n, language_items, [](s64& index_out){
|
||||
App::SetLanguage(index_out);
|
||||
}, (s64)App::GetLanguage(),
|
||||
"Change the language.\n\n"
|
||||
"If your language isn't found, or translations are missing, please consider opening a PR at "\
|
||||
"github.com/ITotalJustice/sphaira"_i18n);
|
||||
i18n::get("translation_info",
|
||||
"Change the language.\n\n"
|
||||
"If your language isn't found, or translations are missing, please consider opening a PR at "
|
||||
"github.com/ITotalJustice/sphaira"));
|
||||
|
||||
options->Add<SidebarEntryCallback>("Misc"_i18n, [](){
|
||||
App::DisplayMiscOptions();
|
||||
}, "View and launch one of Sphaira's menus"_i18n);
|
||||
|
||||
options->Add<SidebarEntryCallback>("Advanced"_i18n, [](){
|
||||
options->Add<SidebarEntryCallback>("Advanced Options"_i18n, [](){
|
||||
App::DisplayAdvancedOptions();
|
||||
}, "Change the advanced options. "\
|
||||
"Please view the info boxes to better understand each option."_i18n);
|
||||
}, i18n::get("advanced_options_info",
|
||||
"Change the advanced options. "
|
||||
"Please view the info boxes to better understand each option."));
|
||||
}}
|
||||
));
|
||||
|
||||
m_centre_menu = std::make_unique<homebrew::Menu>();
|
||||
std::string center_name;
|
||||
m_centre_menu = CreateCenterMenu(center_name);
|
||||
m_current_menu = m_centre_menu.get();
|
||||
|
||||
std::string left_side_name;
|
||||
m_left_menu = CreateLeftSideMenu(left_side_name);
|
||||
m_left_menu = CreateLeftSideMenu(center_name, left_side_name);
|
||||
|
||||
m_right_menu = CreateRightSideMenu(left_side_name);
|
||||
|
||||
AddOnLRPress();
|
||||
|
||||
@@ -20,20 +20,24 @@ auto MenuBase::GetPolledData(bool force_refresh) -> PolledData {
|
||||
// doesn't have focus.
|
||||
if (force_refresh || timestamp.GetSeconds() >= 1) {
|
||||
data.tm = {};
|
||||
data.battery_percetange = {};
|
||||
data.charger_type = {};
|
||||
data.type = {};
|
||||
data.status = {};
|
||||
data.strength = {};
|
||||
data.ip = {};
|
||||
// avoid divide by zero if getting the size fails, for whatever reason.
|
||||
data.sd_free = 1;
|
||||
data.sd_total = 1;
|
||||
data.emmc_free = 1;
|
||||
data.emmc_total = 1;
|
||||
|
||||
const auto t = std::time(NULL);
|
||||
localtime_r(&t, &data.tm);
|
||||
psmGetBatteryChargePercentage(&data.battery_percetange);
|
||||
psmGetChargerType(&data.charger_type);
|
||||
nifmGetInternetConnectionStatus(&data.type, &data.strength, &data.status);
|
||||
nifmGetCurrentIpAddress(&data.ip);
|
||||
|
||||
App::GetSdSize(&data.sd_free, &data.sd_total);
|
||||
App::GetEmmcSize(&data.emmc_free, &data.emmc_total);
|
||||
|
||||
timestamp.Update();
|
||||
}
|
||||
|
||||
@@ -60,7 +64,7 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
|
||||
const auto pdata = GetPolledData();
|
||||
|
||||
const float start_y = 70;
|
||||
const float font_size = 22;
|
||||
const float font_size = 20;
|
||||
const float spacing = 30;
|
||||
|
||||
float start_x = 1220;
|
||||
@@ -77,21 +81,60 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
|
||||
start_x -= spacing + (bounds[2] - bounds[0]); \
|
||||
}
|
||||
|
||||
draw(ThemeEntryID_TEXT, 90, "%u\uFE6A", pdata.battery_percetange);
|
||||
#define STORAGE_BAR_W 180
|
||||
#define STORAGE_BAR_H 8
|
||||
|
||||
if (App::Get12HourTimeEnable()) {
|
||||
draw(ThemeEntryID_TEXT, 132, "%02u:%02u %s", (pdata.tm.tm_hour == 0 || pdata.tm.tm_hour == 12) ? 12 : pdata.tm.tm_hour % 12, pdata.tm.tm_min, (pdata.tm.tm_hour < 12) ? "AM" : "PM");
|
||||
} else {
|
||||
draw(ThemeEntryID_TEXT, 90, "%02u:%02u", pdata.tm.tm_hour, pdata.tm.tm_min);
|
||||
}
|
||||
const auto rounding = 2;
|
||||
const auto storage_font = 19;
|
||||
const auto storage_y = start_y - 30;
|
||||
auto storage_x = start_x - STORAGE_BAR_W;
|
||||
|
||||
if (pdata.ip) {
|
||||
draw(ThemeEntryID_TEXT, 0, "%u.%u.%u.%u", pdata.ip&0xFF, (pdata.ip>>8)&0xFF, (pdata.ip>>16)&0xFF, (pdata.ip>>24)&0xFF);
|
||||
} else {
|
||||
draw(ThemeEntryID_TEXT, 0, ("No Internet"_i18n).c_str());
|
||||
}
|
||||
gfx::drawTextArgs(vg, storage_x, storage_y, storage_font, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "System %.1f GB"_i18n.c_str(), pdata.emmc_free / 1024.0 / 1024.0 / 1024.0);
|
||||
// gfx::drawTextArgs(vg, storage_x, storage_y, storage_font, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "eMMC %.1f GB"_i18n.c_str(), pdata.emmc_free / 1024.0 / 1024.0 / 1024.0);
|
||||
// Use progressbar colours (never BACKGROUND) so themes with custom background images work
|
||||
#if 0
|
||||
Vec4 prog_bar{storage_x, storage_y + 24, STORAGE_BAR_W, STORAGE_BAR_H};
|
||||
gfx::drawRect(vg, prog_bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND), rounding);
|
||||
gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((double)pdata.emmc_free / (double)pdata.emmc_total) * prog_bar.w, prog_bar.h, theme->GetColour(ThemeEntryID_PROGRESSBAR), rounding);
|
||||
#else
|
||||
gfx::drawRect(vg, storage_x, storage_y + 24, STORAGE_BAR_W, STORAGE_BAR_H, theme->GetColour(ThemeEntryID_PROGRESSBAR), rounding);
|
||||
gfx::drawRect(vg, storage_x + 1, storage_y + 24 + 1, STORAGE_BAR_W - 2, STORAGE_BAR_H - 2, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND), rounding);
|
||||
gfx::drawRect(vg, storage_x + 2, storage_y + 24 + 2, STORAGE_BAR_W - (((double)pdata.emmc_free / (double)pdata.emmc_total) * STORAGE_BAR_W) - 4, STORAGE_BAR_H - 4, theme->GetColour(ThemeEntryID_PROGRESSBAR), rounding);
|
||||
#endif
|
||||
|
||||
storage_x -= (STORAGE_BAR_W + spacing);
|
||||
gfx::drawTextArgs(vg, storage_x, storage_y, storage_font, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "microSD %.1f GB"_i18n.c_str(), pdata.sd_free / 1024.0 / 1024.0 / 1024.0);
|
||||
gfx::drawRect(vg, storage_x, storage_y + 24, STORAGE_BAR_W, STORAGE_BAR_H, theme->GetColour(ThemeEntryID_PROGRESSBAR), rounding);
|
||||
gfx::drawRect(vg, storage_x + 1, storage_y + 24 + 1, STORAGE_BAR_W - 2, STORAGE_BAR_H - 2, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND), rounding);
|
||||
gfx::drawRect(vg, storage_x + 2, storage_y + 24 + 2, STORAGE_BAR_W - (((double)pdata.sd_free / (double)pdata.sd_total) * STORAGE_BAR_W) - 4, STORAGE_BAR_H - 4, theme->GetColour(ThemeEntryID_PROGRESSBAR), rounding);
|
||||
start_x -= (STORAGE_BAR_W + spacing) * 2;
|
||||
|
||||
// ran out of space, its one or the other.
|
||||
if (!App::IsApplication()) {
|
||||
draw(ThemeEntryID_ERROR, 0, ("[Applet Mode]"_i18n).c_str());
|
||||
} else if (App::GetApp()->m_show_ip_addr.Get()) {
|
||||
if (pdata.ip) {
|
||||
char ip_buf[32];
|
||||
std::snprintf(ip_buf, sizeof(ip_buf), "%u.%u.%u.%u", pdata.ip & 0xFF, (pdata.ip >> 8) & 0xFF, (pdata.ip >> 16) & 0xFF, (pdata.ip >> 24) & 0xFF);
|
||||
gfx::textBounds(vg, 0, 0, bounds, ip_buf);
|
||||
|
||||
char type_buf[32];
|
||||
if (pdata.type == NifmInternetConnectionType_WiFi) {
|
||||
std::snprintf(type_buf, sizeof(type_buf), "Wi-Fi %.0f%%"_i18n.c_str(), ((float)pdata.strength / 3.F) * 100);
|
||||
} else if (pdata.type == NifmInternetConnectionType_Ethernet) {
|
||||
std::snprintf(type_buf, sizeof(type_buf), "Ethernet"_i18n.c_str());
|
||||
} else {
|
||||
std::snprintf(type_buf, sizeof(type_buf), "Unknown"_i18n.c_str());
|
||||
}
|
||||
|
||||
const auto ip_x = start_x;
|
||||
const auto ip_w = bounds[2] - bounds[0];
|
||||
const auto type_x = ip_x - ip_w / 2;
|
||||
gfx::drawTextArgs(vg, type_x, start_y - 25, storage_font - 1, NVG_ALIGN_CENTER | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", type_buf);
|
||||
gfx::drawTextArgs(vg, ip_x, start_y, storage_font, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT), "%s", ip_buf);
|
||||
} else {
|
||||
draw(ThemeEntryID_TEXT, 0, ("No Internet"_i18n).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
#undef draw
|
||||
@@ -107,7 +150,7 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
|
||||
|
||||
gfx::drawTextArgs(vg, 80, start_y, 28.f, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT), m_title.c_str());
|
||||
m_scroll_title_sub_heading.Draw(vg, true, title_sub_x, start_y, text_w - title_sub_x, 16, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), m_title_sub_heading.c_str());
|
||||
m_scroll_sub_heading.Draw(vg, true, 80, 685, text_w - 160, 18, NVG_ALIGN_LEFT, theme->GetColour(ThemeEntryID_TEXT), m_sub_heading.c_str());
|
||||
m_scroll_sub_heading.Draw(vg, true, 80, 675, text_w - 160, 18, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), m_sub_heading.c_str());
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
namespace sphaira::ui::menu::mtp {
|
||||
|
||||
Menu::Menu(u32 flags) : stream::Menu{"MTP Install"_i18n, flags} {
|
||||
m_was_mtp_enabled = haze::IsInit();
|
||||
m_was_mtp_enabled = libhaze::IsInit();
|
||||
if (!m_was_mtp_enabled) {
|
||||
log_write("[MTP] wasn't enabled, forcefully enabling\n");
|
||||
haze::Init();
|
||||
libhaze::Init();
|
||||
}
|
||||
|
||||
haze::InitInstallMode(
|
||||
libhaze::InitInstallMode(
|
||||
[this](const char* path){ return OnInstallStart(path); },
|
||||
[this](const void *buf, size_t size){ return OnInstallWrite(buf, size); },
|
||||
[this](){ return OnInstallClose(); }
|
||||
@@ -25,11 +25,11 @@ Menu::Menu(u32 flags) : stream::Menu{"MTP Install"_i18n, flags} {
|
||||
|
||||
Menu::~Menu() {
|
||||
// signal for thread to exit and wait.
|
||||
haze::DisableInstallMode();
|
||||
libhaze::DisableInstallMode();
|
||||
|
||||
if (!m_was_mtp_enabled) {
|
||||
log_write("[MTP] disabling on exit\n");
|
||||
haze::Exit();
|
||||
libhaze::Exit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,13 +47,13 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
usbDsGetSpeed(&speed);
|
||||
|
||||
char buf[128];
|
||||
std::snprintf(buf, sizeof(buf), "State: %s | Speed: %s", i18n::get(GetUsbDsStateStr(state)).c_str(), i18n::get(GetUsbDsSpeedStr(speed)).c_str());
|
||||
std::snprintf(buf, sizeof(buf), "State: %s | Speed: %s"_i18n.c_str(), i18n::get(GetUsbDsStateStr(state)).c_str(), i18n::get(GetUsbDsSpeedStr(speed)).c_str());
|
||||
SetSubHeading(buf);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::OnDisableInstallMode() {
|
||||
haze::DisableInstallMode();
|
||||
libhaze::DisableInstallMode();
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::mtp
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "threaded_file_transfer.hpp"
|
||||
#include "minizip_helper.hpp"
|
||||
#include "dumper.hpp"
|
||||
#include "swkbd.hpp"
|
||||
|
||||
#include "utils/devoptab.hpp"
|
||||
|
||||
@@ -40,7 +41,7 @@ constexpr u32 NX_SAVE_META_MAGIC = 0x4A4B5356; // JKSV
|
||||
constexpr u32 NX_SAVE_META_VERSION = 1;
|
||||
constexpr const char* NX_SAVE_META_NAME = ".nx_save_meta.bin";
|
||||
|
||||
constinit UEvent g_change_uevent;
|
||||
std::atomic_bool g_change_signalled{};
|
||||
|
||||
struct DumpSource final : dump::BaseSource {
|
||||
DumpSource(std::span<const std::reference_wrapper<Entry>> entries, std::span<const fs::FsPath> paths)
|
||||
@@ -319,7 +320,7 @@ void FreeEntry(NVGcontext* vg, Entry& e) {
|
||||
} // namespace
|
||||
|
||||
void SignalChange() {
|
||||
ueventSignal(&g_change_uevent);
|
||||
g_change_signalled = true;
|
||||
}
|
||||
|
||||
Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} {
|
||||
@@ -388,7 +389,6 @@ Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} {
|
||||
}
|
||||
|
||||
title::Init();
|
||||
ueventCreate(&g_change_uevent, true);
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
@@ -399,12 +399,12 @@ Menu::~Menu() {
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
if (R_SUCCEEDED(waitSingle(waiterForUEvent(&g_change_uevent), 0))) {
|
||||
if (g_change_signalled.exchange(false)) {
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
if (m_dirty) {
|
||||
App::Notify("Updating application record list");
|
||||
App::Notify("Updating application record list"_i18n);
|
||||
SortAndFindLastFile(true);
|
||||
}
|
||||
|
||||
@@ -508,9 +508,9 @@ void Menu::ScanHomebrew() {
|
||||
constexpr auto ENTRY_CHUNK_COUNT = 1000;
|
||||
TimeStamp ts;
|
||||
|
||||
g_change_signalled = false;
|
||||
FreeEntries();
|
||||
ClearSelection();
|
||||
ueventClear(&g_change_uevent);
|
||||
m_entries.reserve(ENTRY_CHUNK_COUNT);
|
||||
m_is_reversed = false;
|
||||
m_dirty = false;
|
||||
@@ -690,13 +690,40 @@ void Menu::DisplayOptions() {
|
||||
entries.emplace_back(m_entries[m_index]);
|
||||
}
|
||||
|
||||
BackupSaves(entries);
|
||||
}, true);
|
||||
BackupSaves(entries, BackupFlag_None);
|
||||
}, true,
|
||||
"Backup the selected save(s) to a location of your choice."_i18n
|
||||
);
|
||||
|
||||
if (!m_selected_count || m_selected_count == 1) {
|
||||
options->Add<SidebarEntryCallback>("Backup to..."_i18n, [this](){
|
||||
std::vector<std::reference_wrapper<Entry>> entries;
|
||||
if (m_selected_count) {
|
||||
for (auto& e : m_entries) {
|
||||
if (e.selected) {
|
||||
entries.emplace_back(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
entries.emplace_back(m_entries[m_index]);
|
||||
}
|
||||
|
||||
BackupSaves(entries, BackupFlag_SetName);
|
||||
}, true,
|
||||
"Backup the selected save(s) to a location of your choice, and set the name of the backup."_i18n
|
||||
);
|
||||
}
|
||||
|
||||
if (m_entries[m_index].save_data_type == FsSaveDataType_Account || m_entries[m_index].save_data_type == FsSaveDataType_Bcat) {
|
||||
options->Add<SidebarEntryCallback>("Restore"_i18n, [this](){
|
||||
RestoreSave();
|
||||
}, true);
|
||||
}, true,
|
||||
i18n::get("save_backuprestore_info",
|
||||
"Restore the save for the current title.\n"
|
||||
"if \"Auto backup\" is enabled, the save will first be backed up and then restored. "
|
||||
"Saves that are auto backed up will have \"Auto\" in their name."
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,18 +733,24 @@ void Menu::DisplayOptions() {
|
||||
|
||||
options->Add<SidebarEntryBool>("Auto backup on restore"_i18n, m_auto_backup_on_restore.Get(), [this](bool& v_out){
|
||||
m_auto_backup_on_restore.Set(v_out);
|
||||
});
|
||||
}, "If enabled, when restoring a save, the current save will first be backed up."_i18n);
|
||||
|
||||
options->Add<SidebarEntryBool>("Compress backup"_i18n, m_compress_save_backup.Get(), [this](bool& v_out){
|
||||
m_compress_save_backup.Set(v_out);
|
||||
});
|
||||
}, i18n::get("save_backup_compress_info",
|
||||
"If enabled, backups will be compressed to a zip file.\n\n"
|
||||
"NOTE: Disabling this option does not disable the zip file, it only disables compressing "
|
||||
"the files stored in the zip.\n"
|
||||
"Disabling will result in a much faster backup, at the cost of the file size."
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void Menu::BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries) {
|
||||
dump::DumpGetLocation("Select backup location"_i18n, dump::DumpLocationFlag_SdCard|dump::DumpLocationFlag_Stdio|dump::DumpLocationFlag_Usb, [this, entries](const dump::DumpLocation& location){
|
||||
App::Push<ProgressBox>(0, "Backup"_i18n, "", [this, entries, location](auto pbox) -> Result {
|
||||
return BackupSaveInternal(pbox, location, entries, m_compress_save_backup.Get());
|
||||
void Menu::BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries, u32 flags) {
|
||||
dump::DumpGetLocation("Select backup location"_i18n, dump::DumpLocationFlag_SdCard|dump::DumpLocationFlag_Stdio|dump::DumpLocationFlag_Usb, [this, entries, flags](const dump::DumpLocation& location){
|
||||
App::Push<ProgressBox>(0, "Backup"_i18n, "", [this, entries, location, flags](auto pbox) -> Result {
|
||||
return BackupSaveInternal(pbox, location, entries, flags);
|
||||
}, [](Result rc){
|
||||
App::PushErrorBox(rc, "Backup failed!"_i18n);
|
||||
|
||||
@@ -730,17 +763,23 @@ void Menu::BackupSaves(std::vector<std::reference_wrapper<Entry>>& entries) {
|
||||
|
||||
void Menu::RestoreSave() {
|
||||
dump::DumpGetLocation("Select restore location"_i18n, dump::DumpLocationFlag_SdCard|dump::DumpLocationFlag_Stdio, [this](const dump::DumpLocation& location){
|
||||
std::unique_ptr<fs::Fs> fs;
|
||||
std::unique_ptr<fs::Fs> fs{};
|
||||
fs::FsPath mount{};
|
||||
|
||||
if (location.entry.type == dump::DumpLocationType_Stdio) {
|
||||
mount = fs::AppendPath(location.stdio[location.entry.index].mount, location.stdio[location.entry.index].dump_path);
|
||||
fs = std::make_unique<fs::FsStdio>(true, location.stdio[location.entry.index].mount);
|
||||
} else if (location.entry.type == dump::DumpLocationType_SdCard) {
|
||||
fs = std::make_unique<fs::FsNativeSd>();
|
||||
} else {
|
||||
App::PushErrorBox(MAKERESULT(Module_Libnx, LibnxError_BadInput), "Invalid location type!"_i18n);
|
||||
return;
|
||||
}
|
||||
|
||||
// get saves in /Saves/Name and /Saves/app_id
|
||||
filebrowser::FsDirCollection collections[2]{};
|
||||
for (auto i = 0; i < std::size(collections); i++) {
|
||||
const auto save_path = fs::AppendPath(fs->Root(), BuildSaveBasePath(m_entries[m_index], i != 0));
|
||||
const auto save_path = fs::AppendPath(mount, BuildSaveBasePath(m_entries[m_index], i != 0));
|
||||
filebrowser::FsView::get_collection(fs.get(), save_path, "", collections[i], true, false, false);
|
||||
// reverse as they will be sorted in oldest -> newest.
|
||||
// todo: better impl when both id and normal app folders are used.
|
||||
@@ -763,7 +802,7 @@ void Menu::RestoreSave() {
|
||||
|
||||
if (paths.empty()) {
|
||||
App::Push<ui::OptionBox>(
|
||||
"No saves found in "_i18n + fs::AppendPath(fs->Root(), BuildSaveBasePath(m_entries[m_index])).toString(),
|
||||
i18n::Reorder("No saves found in ", fs::AppendPath(mount, BuildSaveBasePath(m_entries[m_index])).toString()),
|
||||
"OK"_i18n
|
||||
);
|
||||
return;
|
||||
@@ -780,7 +819,7 @@ void Menu::RestoreSave() {
|
||||
const auto file_path = paths[*op_index];
|
||||
|
||||
App::Push<OptionBox>(
|
||||
"Are you sure you want to restore "_i18n + file_name + "?",
|
||||
i18n::Reorder("Are you sure you want to restore ", file_name) + "?",
|
||||
"Back"_i18n, "Restore"_i18n, 0, [this, file_path, location](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
App::Push<ProgressBox>(0, "Restore"_i18n, "", [this, file_path, location](auto pbox) -> Result {
|
||||
@@ -789,7 +828,7 @@ void Menu::RestoreSave() {
|
||||
|
||||
if (m_auto_backup_on_restore.Get()) {
|
||||
pbox->SetActionName("Auto backup"_i18n);
|
||||
R_TRY(BackupSaveInternal(pbox, location, m_entries[m_index], m_compress_save_backup.Get(), true));
|
||||
R_TRY(BackupSaveInternal(pbox, location, m_entries[m_index], BackupFlag_IsAuto));
|
||||
}
|
||||
|
||||
pbox->SetActionName("Restore"_i18n);
|
||||
@@ -809,7 +848,7 @@ void Menu::RestoreSave() {
|
||||
});
|
||||
}
|
||||
|
||||
auto Menu::BuildSavePath(const Entry& e, bool is_auto) const -> fs::FsPath {
|
||||
auto Menu::BuildSavePath(const Entry& e, u32 flags) const -> fs::FsPath {
|
||||
const auto t = std::time(NULL);
|
||||
const auto tm = std::localtime(&t);
|
||||
const auto base = BuildSaveBasePath(e);
|
||||
@@ -817,27 +856,39 @@ auto Menu::BuildSavePath(const Entry& e, bool is_auto) const -> fs::FsPath {
|
||||
char time[64];
|
||||
std::snprintf(time, sizeof(time), "%u.%02u.%02u @ %02u.%02u.%02u", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
|
||||
|
||||
fs::FsPath path;
|
||||
fs::FsPath name;
|
||||
if (e.save_data_type == FsSaveDataType_Account) {
|
||||
const auto acc = m_accounts[m_account_index];
|
||||
|
||||
fs::FsPath name_buf;
|
||||
if (is_auto) {
|
||||
if (flags & BackupFlag_IsAuto) {
|
||||
std::snprintf(name_buf, sizeof(name_buf), "AUTO - %s", acc.nickname);
|
||||
} else {
|
||||
std::snprintf(name_buf, sizeof(name_buf), "%s", acc.nickname);
|
||||
}
|
||||
|
||||
title::utilsReplaceIllegalCharacters(name_buf, true);
|
||||
std::snprintf(path, sizeof(path), "%s/%s - %s.zip", base.s, name_buf.s, time);
|
||||
std::snprintf(name, sizeof(name), "%s - %s.zip", name_buf.s, time);
|
||||
} else {
|
||||
std::snprintf(path, sizeof(path), "%s/%s.zip", base.s, time);
|
||||
std::snprintf(name, sizeof(name), "%s.zip", time);
|
||||
}
|
||||
|
||||
return path;
|
||||
if (flags & BackupFlag_SetName) {
|
||||
std::string out;
|
||||
while (out.empty()) {
|
||||
const auto header = i18n::Reorder("Set name for ", e.GetName());
|
||||
if (R_FAILED(swkbd::ShowText(out, header.c_str(), "Set backup name"_i18n.c_str(), name, 1, 128))) {
|
||||
out.clear();
|
||||
}
|
||||
}
|
||||
|
||||
name = out;
|
||||
}
|
||||
|
||||
return fs::AppendPath(base, name);
|
||||
}
|
||||
|
||||
Result Menu::RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath& path) const {
|
||||
Result Menu::RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::FsPath& path) {
|
||||
pbox->SetTitle(e.GetName());
|
||||
if (e.image) {
|
||||
pbox->SetImage(e.image);
|
||||
@@ -955,14 +1006,15 @@ Result Menu::RestoreSaveInternal(ProgressBox* pbox, const Entry& e, const fs::Fs
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, std::span<const std::reference_wrapper<Entry>> entries, bool compressed, bool is_auto) const {
|
||||
Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, std::span<const std::reference_wrapper<Entry>> entries, u32 flags) {
|
||||
std::vector<fs::FsPath> paths;
|
||||
for (auto& e : entries) {
|
||||
// ensure that we have title name and icon loaded.
|
||||
LoadControlEntry(e);
|
||||
paths.emplace_back(BuildSavePath(e, is_auto));
|
||||
paths.emplace_back(BuildSavePath(e, flags));
|
||||
}
|
||||
|
||||
const auto compressed = m_compress_save_backup.Get();
|
||||
auto source = std::make_shared<DumpSource>(entries, paths);
|
||||
|
||||
return dump::Dump(pbox, source, location, paths, [&](ui::ProgressBox* pbox, dump::BaseSource* _source, dump::WriteSource* writer, const fs::FsPath& path) -> Result {
|
||||
@@ -1032,7 +1084,7 @@ Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& loc
|
||||
{
|
||||
auto zfile = zipOpen2_64(path, APPEND_STATUS_CREATE, nullptr, &file_func);
|
||||
R_UNLESS(zfile, Result_ZipOpen2_64);
|
||||
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_VERSION_HASH));
|
||||
ON_SCOPE_EXIT(zipClose(zfile, "sphaira v" APP_DISPLAY_VERSION));
|
||||
|
||||
// add save meta.
|
||||
{
|
||||
@@ -1091,7 +1143,7 @@ Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& loc
|
||||
|
||||
// if we dumped the save to ram, flush the data to file.
|
||||
if (!file_download) {
|
||||
pbox->NewTransfer("Flushing zip to file");
|
||||
pbox->NewTransfer("Flushing zip to file"_i18n);
|
||||
R_TRY(writer->SetSize(mz_mem.buf.size()));
|
||||
|
||||
R_TRY(thread::Transfer(pbox, mz_mem.buf.size(),
|
||||
@@ -1111,11 +1163,11 @@ Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& loc
|
||||
});
|
||||
}
|
||||
|
||||
Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, Entry& e, bool compressed, bool is_auto) const {
|
||||
Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& location, Entry& e, u32 flags) {
|
||||
std::vector<std::reference_wrapper<Entry>> entries;
|
||||
entries.emplace_back(e);
|
||||
|
||||
return BackupSaveInternal(pbox, location, entries, compressed, is_auto);
|
||||
return BackupSaveInternal(pbox, location, entries, flags);
|
||||
}
|
||||
|
||||
Result Menu::MountSaveFs() {
|
||||
@@ -1125,8 +1177,8 @@ Result Menu::MountSaveFs() {
|
||||
fs::FsPath root;
|
||||
R_TRY(devoptab::MountSaveSystem(e.system_save_data_id, root));
|
||||
|
||||
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [&e](){
|
||||
devoptab::UnmountSave(e.system_save_data_id);
|
||||
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [root](){
|
||||
devoptab::UmountNeworkDevice(root);
|
||||
});
|
||||
|
||||
filebrowser::MountFsHelper(fs, e.GetName());
|
||||
|
||||
@@ -252,7 +252,7 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> Result {
|
||||
|
||||
// 1. download the zip
|
||||
if (!pbox->ShouldExit()) {
|
||||
pbox->NewTransfer("Downloading "_i18n + entry.details.name);
|
||||
pbox->NewTransfer(i18n::Reorder("Downloading ", entry.details.name));
|
||||
log_write("starting download\n");
|
||||
|
||||
const auto url = apiBuildUrlDownloadPack(entry);
|
||||
@@ -272,7 +272,7 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> Result {
|
||||
|
||||
// 2. download the zip
|
||||
if (!pbox->ShouldExit()) {
|
||||
pbox->NewTransfer("Downloading "_i18n + entry.details.name);
|
||||
pbox->NewTransfer(i18n::Reorder("Downloading ", entry.details.name));
|
||||
log_write("starting download: %s\n", download_pack.url.c_str());
|
||||
|
||||
const auto result = curl::Api().ToFile(
|
||||
@@ -383,13 +383,13 @@ Menu::Menu(u32 flags) : MenuBase{"Themezer"_i18n, flags} {
|
||||
const auto& entry = page.m_packList[m_index];
|
||||
const auto url = apiBuildUrlDownloadPack(entry);
|
||||
|
||||
App::Push<ProgressBox>(entry.themes[0].preview.lazy_image.image, "Downloading "_i18n, entry.details.name, [this, &entry](auto pbox) -> Result {
|
||||
App::Push<ProgressBox>(entry.themes[0].preview.lazy_image.image, i18n::Reorder("Downloading ", entry.details.name), [this, &entry](auto pbox) -> Result {
|
||||
return InstallTheme(pbox, entry);
|
||||
}, [this, &entry](Result rc){
|
||||
App::PushErrorBox(rc, "Failed to download theme"_i18n);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
App::Notify("Downloaded "_i18n + entry.details.name);
|
||||
App::Notify(i18n::Reorder("Downloaded ", entry.details.name));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "log.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "haze_helper.hpp"
|
||||
|
||||
#include "utils/thread.hpp"
|
||||
|
||||
@@ -35,10 +34,10 @@ Menu::Menu(u32 flags) : MenuBase{"USB"_i18n, flags} {
|
||||
}});
|
||||
|
||||
// if mtp is enabled, disable it for now.
|
||||
m_was_mtp_enabled = haze::IsInit();
|
||||
m_was_mtp_enabled = App::GetMtpEnable();
|
||||
if (m_was_mtp_enabled) {
|
||||
App::Notify("Disable MTP for usb install"_i18n);
|
||||
haze::Exit();
|
||||
App::SetMtpEnable(false);
|
||||
}
|
||||
|
||||
// 3 second timeout for transfers.
|
||||
@@ -70,7 +69,7 @@ Menu::~Menu() {
|
||||
|
||||
if (m_was_mtp_enabled) {
|
||||
App::Notify("Re-enabled MTP"_i18n);
|
||||
haze::Init();
|
||||
App::SetMtpEnable(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +87,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
usbDsGetSpeed(&speed);
|
||||
|
||||
char buf[128];
|
||||
std::snprintf(buf, sizeof(buf), "State: %s | Speed: %s", i18n::get(GetUsbDsStateStr(state)).c_str(), i18n::get(GetUsbDsSpeedStr(speed)).c_str());
|
||||
std::snprintf(buf, sizeof(buf), "State: %s | Speed: %s"_i18n.c_str(), i18n::get(GetUsbDsStateStr(state)).c_str(), i18n::get(GetUsbDsSpeedStr(speed)).c_str());
|
||||
SetSubHeading(buf);
|
||||
}
|
||||
|
||||
@@ -97,6 +96,9 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
m_state = State::Progress;
|
||||
log_write("got connection\n");
|
||||
App::Push<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) -> Result {
|
||||
pbox->AddCancelEvent(m_usb_source->GetCancelEvent());
|
||||
ON_SCOPE_EXIT(pbox->RemoveCancelEvent(m_usb_source->GetCancelEvent()));
|
||||
|
||||
log_write("inside progress box\n");
|
||||
for (u32 i = 0; i < std::size(m_names); i++) {
|
||||
const auto& file_name = m_names[i];
|
||||
|
||||
@@ -90,11 +90,11 @@ Menu::Menu(fs::Fs* fs, const fs::FsPath& path) {
|
||||
}
|
||||
|
||||
if (m_meta.artist.empty()) {
|
||||
m_meta.artist = "Artist: Unknown";
|
||||
m_meta.artist = "Artist: Unknown"_i18n;
|
||||
}
|
||||
|
||||
if (m_meta.album.empty()) {
|
||||
m_meta.album = "Album: Unknown";
|
||||
m_meta.album = "Album: Unknown"_i18n;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -387,6 +387,36 @@ void drawAppLable(NVGcontext* vg, const Theme* theme, ScrollingText& st, float x
|
||||
st.Draw(vg, true, text_x, text_y, box_w - text_pad * 2, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED), name);
|
||||
}
|
||||
|
||||
// https://github.com/memononen/nanovg/blob/f93799c078fa11ed61c078c65a53914c8782c00b/example/demo.c#L500
|
||||
void drawSpinner(NVGcontext* vg, const Theme* theme, float cx, float cy, float r, float t)
|
||||
{
|
||||
float a0 = 0.0f + t*6;
|
||||
float a1 = NVG_PI + t*6;
|
||||
float r0 = r;
|
||||
float r1 = r * 0.75f;
|
||||
float ax,ay, bx,by;
|
||||
NVGpaint paint;
|
||||
|
||||
nvgSave(vg);
|
||||
|
||||
auto colourb = theme->GetColour(ThemeEntryID_PROGRESSBAR);
|
||||
colourb.a = 0.5;
|
||||
|
||||
nvgBeginPath(vg);
|
||||
nvgArc(vg, cx,cy, r0, a0, a1, NVG_CW);
|
||||
nvgArc(vg, cx,cy, r1, a1, a0, NVG_CCW);
|
||||
nvgClosePath(vg);
|
||||
ax = cx + cosf(a0) * (r0+r1)*0.5f;
|
||||
ay = cy + sinf(a0) * (r0+r1)*0.5f;
|
||||
bx = cx + cosf(a1) * (r0+r1)*0.5f;
|
||||
by = cy + sinf(a1) * (r0+r1)*0.5f;
|
||||
paint = nvgLinearGradient(vg, ax,ay, bx,by, nvgRGBA(0,0,0,0), colourb);
|
||||
nvgFillPaint(vg, paint);
|
||||
nvgFill(vg);
|
||||
|
||||
nvgRestore(vg);
|
||||
}
|
||||
|
||||
#define HIGHLIGHT_SPEED 350.0
|
||||
|
||||
static double highlightGradientX = 0;
|
||||
|
||||
@@ -110,6 +110,7 @@ auto PopupList::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
gfx::drawText(vg, m_pos + m_title_pos, 24.f, theme->GetColour(ThemeEntryID_TEXT), m_title.c_str());
|
||||
gfx::drawRect(vg, 30.f, m_line_top, m_line_width, 1.f, theme->GetColour(ThemeEntryID_LINE));
|
||||
gfx::drawRect(vg, 30.f, m_line_bottom, m_line_width, 1.f, theme->GetColour(ThemeEntryID_LINE));
|
||||
gfx::drawTextArgs(vg, 80, 675, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%zu / %zu", m_index + 1, m_items.size());
|
||||
|
||||
m_list->Draw(vg, theme, m_items.size(), [this](auto* vg, auto* theme, auto& v, auto i) {
|
||||
const auto& [x, y, w, h] = v;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "threaded_file_transfer.hpp"
|
||||
#include "i18n.hpp"
|
||||
|
||||
#include "utils/utils.hpp"
|
||||
#include "utils/thread.hpp"
|
||||
|
||||
#include <cstring>
|
||||
@@ -21,36 +22,6 @@ void threadFunc(void* arg) {
|
||||
d->pbox->RequestExit();
|
||||
}
|
||||
|
||||
// https://github.com/memononen/nanovg/blob/f93799c078fa11ed61c078c65a53914c8782c00b/example/demo.c#L500
|
||||
void drawSpinner(NVGcontext* vg, Theme* theme, float cx, float cy, float r, float t)
|
||||
{
|
||||
float a0 = 0.0f + t*6;
|
||||
float a1 = NVG_PI + t*6;
|
||||
float r0 = r;
|
||||
float r1 = r * 0.75f;
|
||||
float ax,ay, bx,by;
|
||||
NVGpaint paint;
|
||||
|
||||
nvgSave(vg);
|
||||
|
||||
auto colourb = theme->GetColour(ThemeEntryID_PROGRESSBAR);
|
||||
colourb.a = 0.5;
|
||||
|
||||
nvgBeginPath(vg);
|
||||
nvgArc(vg, cx,cy, r0, a0, a1, NVG_CW);
|
||||
nvgArc(vg, cx,cy, r1, a1, a0, NVG_CCW);
|
||||
nvgClosePath(vg);
|
||||
ax = cx + cosf(a0) * (r0+r1)*0.5f;
|
||||
ay = cy + sinf(a0) * (r0+r1)*0.5f;
|
||||
bx = cx + cosf(a1) * (r0+r1)*0.5f;
|
||||
by = cy + sinf(a1) * (r0+r1)*0.5f;
|
||||
paint = nvgLinearGradient(vg, ax,ay, bx,by, nvgRGBA(0,0,0,0), colourb);
|
||||
nvgFillPaint(vg, paint);
|
||||
nvgFill(vg);
|
||||
|
||||
nvgRestore(vg);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProgressBox::ProgressBox(int image, const std::string& action, const std::string& title, const ProgressBoxCallback& callback, const ProgressBoxDoneCallback& done)
|
||||
@@ -180,17 +151,7 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
gfx::drawTextArgs(vg, prog_bar.x + prog_bar.w + pad, prog_bar.y + prog_bar.h / 2, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT), "%u%%", percentage);
|
||||
|
||||
const auto rad = 15;
|
||||
drawSpinner(vg, theme, prog_bar.x - pad - rad, prog_bar.y + prog_bar.h / 2, rad, armTicksToNs(armGetSystemTick()) / 1e+9);
|
||||
|
||||
const double speed_mb = (double)speed / (1024.0 * 1024.0);
|
||||
const double speed_kb = (double)speed / (1024.0);
|
||||
|
||||
char speed_str[32];
|
||||
if (speed_mb >= 0.01) {
|
||||
std::snprintf(speed_str, sizeof(speed_str), "%.2f MiB/s", speed_mb);
|
||||
} else {
|
||||
std::snprintf(speed_str, sizeof(speed_str), "%.2f KiB/s", speed_kb);
|
||||
}
|
||||
gfx::drawSpinner(vg, theme, prog_bar.x - pad - rad, prog_bar.y + prog_bar.h / 2, rad, armTicksToNs(armGetSystemTick()) / 1e+9);
|
||||
|
||||
const auto left = size - last_offset;
|
||||
const auto left_seconds = left / speed;
|
||||
@@ -207,7 +168,7 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
std::snprintf(time_str, sizeof(time_str), "%zu seconds remaining"_i18n.c_str(), seconds);
|
||||
}
|
||||
|
||||
gfx::drawTextArgs(vg, center_x, prog_bar.y + prog_bar.h + 30, 18, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s (%s)", time_str, speed_str);
|
||||
gfx::drawTextArgs(vg, center_x, prog_bar.y + prog_bar.h + 30, 18, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s (%s)", time_str, utils::formatSizeNetwork(speed).c_str());
|
||||
}
|
||||
|
||||
gfx::drawTextArgs(vg, center_x, m_pos.y + 40, 24, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), action.c_str());
|
||||
@@ -232,79 +193,72 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
}
|
||||
|
||||
auto ProgressBox::SetActionName(const std::string& action) -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
m_action = action;
|
||||
mutexUnlock(&m_mutex);
|
||||
Yield();
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto ProgressBox::SetTitle(const std::string& title) -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
m_title = title;
|
||||
mutexUnlock(&m_mutex);
|
||||
Yield();
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
m_transfer = transfer;
|
||||
m_size = 0;
|
||||
m_offset = 0;
|
||||
m_last_offset = 0;
|
||||
m_timestamp.Update();
|
||||
mutexUnlock(&m_mutex);
|
||||
Yield();
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto ProgressBox::ResetTranfser() -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
m_size = 0;
|
||||
m_offset = 0;
|
||||
m_last_offset = 0;
|
||||
m_timestamp.Update();
|
||||
mutexUnlock(&m_mutex);
|
||||
Yield();
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto ProgressBox::UpdateTransfer(s64 offset, s64 size) -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
m_size = size;
|
||||
m_offset = offset;
|
||||
mutexUnlock(&m_mutex);
|
||||
Yield();
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto ProgressBox::SetImage(int image) -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
m_image_pending = image;
|
||||
m_is_image_pending = true;
|
||||
mutexUnlock(&m_mutex);
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto ProgressBox::SetImageData(std::vector<u8>& data) -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
std::swap(m_image_data, data);
|
||||
mutexUnlock(&m_mutex);
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto ProgressBox::SetImageDataConst(std::span<const u8> data) -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
m_image_data.resize(data.size());
|
||||
std::memcpy(m_image_data.data(), data.data(), m_image_data.size());
|
||||
mutexUnlock(&m_mutex);
|
||||
return *this;
|
||||
}
|
||||
|
||||
void ProgressBox::RequestExit() {
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
m_stop_source.request_stop();
|
||||
ueventSignal(GetCancelEvent());
|
||||
|
||||
// cancel any registered events.
|
||||
for (auto& e : m_cancel_events) {
|
||||
ueventSignal(e);
|
||||
}
|
||||
}
|
||||
|
||||
auto ProgressBox::ShouldExit() -> bool {
|
||||
@@ -318,6 +272,26 @@ auto ProgressBox::ShouldExitResult() -> Result {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void ProgressBox::AddCancelEvent(UEvent* event) {
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
if (std::ranges::find(m_cancel_events, event) == m_cancel_events.end()) {
|
||||
m_cancel_events.emplace_back(event);
|
||||
}
|
||||
}
|
||||
|
||||
void ProgressBox::RemoveCancelEvent(const UEvent* event) {
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
|
||||
SCOPED_MUTEX(&m_mutex);
|
||||
m_cancel_events.erase(std::remove(m_cancel_events.begin(), m_cancel_events.end(), event), m_cancel_events.end());
|
||||
}
|
||||
|
||||
auto ProgressBox::CopyFile(fs::Fs* fs_src, fs::Fs* fs_dst, const fs::FsPath& src_path, const fs::FsPath& dst_path, bool single_threaded) -> Result {
|
||||
const auto is_file_based_emummc = App::IsFileBaseEmummc();
|
||||
const auto is_both_native = fs_src->IsNative() && fs_dst->IsNative();
|
||||
|
||||
@@ -124,24 +124,27 @@ SidebarEntryBool::SidebarEntryBool(const std::string& title, bool option, const
|
||||
} else {
|
||||
m_option ^= 1;
|
||||
m_callback(m_option);
|
||||
SetDirty();
|
||||
} }
|
||||
});
|
||||
}
|
||||
|
||||
SidebarEntryBool::SidebarEntryBool(const std::string& title, bool& option, const std::string& info, const std::string& true_str, const std::string& false_str)
|
||||
: SidebarEntryBool{title, option, Callback{}, info, true_str, false_str} {
|
||||
m_callback = [&option](bool&){
|
||||
m_callback = [this, &option](bool&){
|
||||
option ^= 1;
|
||||
SetDirty();
|
||||
};
|
||||
}
|
||||
|
||||
SidebarEntryBool::SidebarEntryBool(const std::string& title, option::OptionBool& option, const Callback& cb, const std::string& info, const std::string& true_str, const std::string& false_str)
|
||||
: SidebarEntryBool{title, option.Get(), Callback{}, info, true_str, false_str} {
|
||||
m_callback = [&option, cb](bool& v_out){
|
||||
m_callback = [this, &option, cb](bool& v_out){
|
||||
if (cb) {
|
||||
cb(v_out);
|
||||
}
|
||||
option.Set(v_out);
|
||||
SetDirty();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -166,6 +169,7 @@ SidebarEntrySlider::SidebarEntrySlider(const std::string& title, float value, fl
|
||||
DependsClick();
|
||||
} else {
|
||||
m_value = std::clamp(m_value - m_inc, m_min, m_max);
|
||||
SetDirty();
|
||||
// m_callback(m_option);
|
||||
} }
|
||||
});
|
||||
@@ -174,6 +178,7 @@ SidebarEntrySlider::SidebarEntrySlider(const std::string& title, float value, fl
|
||||
DependsClick();
|
||||
} else {
|
||||
m_value = std::clamp(m_value + m_inc, m_min, m_max);
|
||||
SetDirty();
|
||||
// m_callback(m_option);
|
||||
} }
|
||||
});
|
||||
@@ -240,6 +245,8 @@ SidebarEntryArray::SidebarEntryArray(const std::string& title, const Items& item
|
||||
App::Push<PopupList>(
|
||||
m_title, m_items, index, m_index
|
||||
);
|
||||
|
||||
SetDirty();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -275,6 +282,7 @@ SidebarEntryArray::SidebarEntryArray(const std::string& title, const Items& item
|
||||
} else {
|
||||
// m_callback(m_index);
|
||||
m_list_callback();
|
||||
SetDirty();
|
||||
}}
|
||||
});
|
||||
}
|
||||
@@ -291,6 +299,7 @@ SidebarEntryTextBase::SidebarEntryTextBase(const std::string& title, const std::
|
||||
SetAction(Button::A, Action{"OK"_i18n, [this](){
|
||||
if (m_callback) {
|
||||
m_callback();
|
||||
SetDirty();
|
||||
}
|
||||
}});
|
||||
}
|
||||
@@ -300,16 +309,36 @@ void SidebarEntryTextBase::Draw(NVGcontext* vg, Theme* theme, const Vec4& root_p
|
||||
SidebarEntryBase::DrawEntry(vg, theme, m_title, m_value, true);
|
||||
}
|
||||
|
||||
SidebarEntryTextInput::SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& guide, s64 len_min, s64 len_max, const std::string& info)
|
||||
SidebarEntryTextInput::SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& header, const std::string& guide, s64 len_min, s64 len_max, const std::string& info, const Callback& callback)
|
||||
: SidebarEntryTextBase{title, value, {}, info}
|
||||
, m_guide{guide}
|
||||
, m_header{header.empty() ? title : header}
|
||||
, m_guide{guide.empty() ? title : guide}
|
||||
, m_len_min{len_min}
|
||||
, m_len_max{len_max} {
|
||||
, m_len_max{len_max}
|
||||
, m_callback{callback} {
|
||||
|
||||
SetCallback([this](){
|
||||
std::string out;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, m_guide.c_str(), GetValue().c_str(), m_len_min, m_len_max))) {
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out, m_header.c_str(), m_guide.c_str(), GetValue().c_str(), m_len_min, m_len_max))) {
|
||||
SetValue(out);
|
||||
|
||||
if (m_callback) {
|
||||
m_callback(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
SidebarEntryTextInput::SidebarEntryTextInput(const std::string& title, s64 value, const std::string& header, const std::string& guide, s64 len_min, s64 len_max, const std::string& info, const Callback& callback)
|
||||
: SidebarEntryTextInput{title, std::to_string(value), header, guide, len_min, len_max, info, callback} {
|
||||
SetCallback([this](){
|
||||
s64 out = std::stoul(GetValue());
|
||||
if (R_SUCCEEDED(swkbd::ShowNumPad(out, m_header.c_str(), m_guide.c_str(), GetValue().c_str(), m_len_min, m_len_max))) {
|
||||
SetValue(std::to_string(out));
|
||||
|
||||
if (m_callback) {
|
||||
m_callback(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -321,6 +350,7 @@ SidebarEntryFilePicker::SidebarEntryFilePicker(const std::string& title, const s
|
||||
App::Push<menu::filebrowser::picker::Menu>(
|
||||
[this](const fs::FsPath& path) {
|
||||
SetValue(path);
|
||||
SetDirty();
|
||||
return true;
|
||||
},
|
||||
m_filter
|
||||
@@ -328,26 +358,21 @@ SidebarEntryFilePicker::SidebarEntryFilePicker(const std::string& title, const s
|
||||
});
|
||||
}
|
||||
|
||||
Sidebar::Sidebar(const std::string& title, Side side, Items&& items)
|
||||
: Sidebar{title, "", side, std::forward<decltype(items)>(items)} {
|
||||
Sidebar::Sidebar(const std::string& title, Side side, float width)
|
||||
: Sidebar{title, "", side, width} {
|
||||
}
|
||||
|
||||
Sidebar::Sidebar(const std::string& title, Side side)
|
||||
: Sidebar{title, "", side, {}} {
|
||||
}
|
||||
|
||||
Sidebar::Sidebar(const std::string& title, const std::string& sub, Side side, Items&& items)
|
||||
Sidebar::Sidebar(const std::string& title, const std::string& sub, Side side, float width)
|
||||
: m_title{title}
|
||||
, m_sub{sub}
|
||||
, m_side{side}
|
||||
, m_items{std::forward<decltype(items)>(items)} {
|
||||
, m_side{side} {
|
||||
switch (m_side) {
|
||||
case Side::LEFT:
|
||||
SetPos(Vec4{0.f, 0.f, 450.f, SCREEN_HEIGHT});
|
||||
SetPos(Vec4{0.f, 0.f, width, SCREEN_HEIGHT});
|
||||
break;
|
||||
|
||||
case Side::RIGHT:
|
||||
SetPos(Vec4{SCREEN_WIDTH - 450.f, 0.f, 450.f, SCREEN_HEIGHT});
|
||||
SetPos(Vec4{SCREEN_WIDTH - width, 0.f, width, SCREEN_HEIGHT});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -365,11 +390,17 @@ Sidebar::Sidebar(const std::string& title, const std::string& sub, Side side, It
|
||||
m_list->SetScrollBarPos(GetX() + GetW() - 20, m_base_pos.y - 10, pos.h - m_base_pos.y + 48);
|
||||
}
|
||||
|
||||
Sidebar::Sidebar(const std::string& title, const std::string& sub, Side side)
|
||||
: Sidebar{title, sub, side, {}} {
|
||||
Sidebar::~Sidebar() {
|
||||
if (m_on_exit_when_changed) {
|
||||
for (const auto& item : m_items) {
|
||||
if (item->IsDirty()) {
|
||||
m_on_exit_when_changed();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||
Widget::Update(controller, touch);
|
||||
|
||||
@@ -405,6 +436,7 @@ auto Sidebar::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
}
|
||||
gfx::drawRect(vg, m_top_bar, theme->GetColour(ThemeEntryID_LINE));
|
||||
gfx::drawRect(vg, m_bottom_bar, theme->GetColour(ThemeEntryID_LINE));
|
||||
gfx::drawTextArgs(vg, m_pos.x + 30, 675, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%zu / %zu", m_index + 1, m_items.size());
|
||||
|
||||
Widget::Draw(vg, theme);
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ Base::Base(u64 transfer_timeout) {
|
||||
App::SetAutoSleepDisabled(true);
|
||||
|
||||
m_transfer_timeout = transfer_timeout;
|
||||
ueventCreate(GetCancelEvent(), true);
|
||||
ueventCreate(GetCancelEvent(), false);
|
||||
m_aligned = std::make_unique<u8*>(new(std::align_val_t{TRANSFER_ALIGN}) u8[TRANSFER_MAX]);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Usb::Usb(u64 transfer_timeout) {
|
||||
Usb::~Usb() {
|
||||
if (m_was_connected && R_SUCCEEDED(m_usb->IsUsbConnected(0))) {
|
||||
const auto send_header = SendPacket::Build(CMD_QUIT);
|
||||
SendAndVerify(&send_header, sizeof(send_header));
|
||||
SendAndVerify(&send_header, sizeof(send_header), 1e+9);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Usb::Usb(u64 transfer_timeout) {
|
||||
Usb::~Usb() {
|
||||
if (m_was_connected && R_SUCCEEDED(m_usb->IsUsbConnected(0))) {
|
||||
const auto send_header = SendPacket::Build(CMD_QUIT);
|
||||
SendAndVerify(&send_header, sizeof(send_header));
|
||||
SendAndVerify(&send_header, sizeof(send_header), 1e+9);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ bool GetMountPoint(location::StdioEntry& out) {
|
||||
|
||||
out.mount = fs.mountpoint;
|
||||
out.name = display_name;
|
||||
out.write_protect = true;
|
||||
out.flags = location::FsEntryFlag::FsEntryFlag_ReadOnly;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -21,52 +21,55 @@
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wcast-qual"
|
||||
#pragma GCC diagnostic ignored "-Wunused-function"
|
||||
#if 0
|
||||
#define DR_FLAC_IMPLEMENTATION
|
||||
#define DR_FLAC_NO_OGG
|
||||
#define DR_FLAC_NO_STDIO
|
||||
#define DRFLAC_API static
|
||||
#define DRFLAC_PRIVATE static
|
||||
#include <dr_flac.h>
|
||||
#endif
|
||||
|
||||
#define DR_WAV_IMPLEMENTATION
|
||||
#define DR_WAV_NO_STDIO
|
||||
#define DRWAV_API static
|
||||
#define DRWAV_PRIVATE static
|
||||
#include <dr_wav.h>
|
||||
|
||||
#define DR_MP3_IMPLEMENTATION
|
||||
#define DR_MP3_NO_STDIO
|
||||
#define DRMP3_API static
|
||||
#define DRMP3_PRIVATE static
|
||||
// improves load / seek times.
|
||||
// hopefully drmp3 will have binary seek rather than linear.
|
||||
// this also improves
|
||||
#define DRMP3_DATA_CHUNK_SIZE (1024*64)
|
||||
#include <dr_mp3.h>
|
||||
|
||||
#if 0
|
||||
#define DR_VORBIS_IMPLEMENTATION
|
||||
#define DR_VORBIS_NO_STDIO
|
||||
#define DR_VORBIS_API static
|
||||
#include "dr_vorbis.h"
|
||||
#endif
|
||||
#pragma GCC diagnostic pop
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wunused-variable"
|
||||
#pragma GCC diagnostic ignored "-Walloca"
|
||||
#pragma GCC diagnostic ignored "-Wunused-variable"
|
||||
#define STB_VORBIS_NO_PUSHDATA_API
|
||||
#define STB_VORBIS_NO_STDIO
|
||||
#define STB_VORBIS_NO_OPENMEM
|
||||
#include "stb_vorbis.h"
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wunused-variable"
|
||||
#include <id3v2lib.h>
|
||||
#ifdef ENABLE_AUDIO_FLAC
|
||||
#define DR_FLAC_IMPLEMENTATION
|
||||
#define DR_FLAC_NO_OGG
|
||||
#define DR_FLAC_NO_STDIO
|
||||
#define DRFLAC_API static
|
||||
#define DRFLAC_PRIVATE static
|
||||
#include <dr_flac.h>
|
||||
#endif // ENABLE_AUDIO_FLAC
|
||||
|
||||
#ifdef ENABLE_AUDIO_WAV
|
||||
#define DR_WAV_IMPLEMENTATION
|
||||
#define DR_WAV_NO_STDIO
|
||||
#define DRWAV_API static
|
||||
#define DRWAV_PRIVATE static
|
||||
#include <dr_wav.h>
|
||||
#endif // ENABLE_AUDIO_WAV
|
||||
|
||||
#ifdef ENABLE_AUDIO_MP3
|
||||
#define DR_MP3_IMPLEMENTATION
|
||||
#define DR_MP3_NO_STDIO
|
||||
#define DRMP3_API static
|
||||
#define DRMP3_PRIVATE static
|
||||
// improves load / seek times.
|
||||
// hopefully drmp3 will have binary seek rather than linear.
|
||||
// this also improves
|
||||
#define DRMP3_DATA_CHUNK_SIZE (1024*64)
|
||||
#include <dr_mp3.h>
|
||||
#include <id3v2lib.h>
|
||||
#endif // ENABLE_AUDIO_MP3
|
||||
|
||||
#ifdef ENABLE_AUDIO_OGG
|
||||
#if 0
|
||||
#define DR_VORBIS_IMPLEMENTATION
|
||||
#define DR_VORBIS_NO_STDIO
|
||||
#define DR_VORBIS_API static
|
||||
#include "dr_vorbis.h"
|
||||
#endif
|
||||
#define STB_VORBIS_NO_PUSHDATA_API
|
||||
#define STB_VORBIS_NO_STDIO
|
||||
#define STB_VORBIS_NO_OPENMEM
|
||||
#include "stb_vorbis.h"
|
||||
#endif // ENABLE_AUDIO_OGG
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
#pragma GCC diagnostic pop
|
||||
#pragma GCC diagnostic pop
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
#include <pulsar.h>
|
||||
@@ -130,6 +133,7 @@ private:
|
||||
s64 m_size{};
|
||||
};
|
||||
|
||||
#ifdef ENABLE_AUDIO_MP3
|
||||
// gta vice "encrypted" mp3's using xor 0x22, very cool.
|
||||
struct GTAViceCityFile final : File {
|
||||
size_t ReadFile(void* _buf, size_t read_size) override {
|
||||
@@ -164,6 +168,7 @@ auto convert_utf16(const ID3v2_TextFrameData* data) -> std::string{
|
||||
buf[sz] = 0;
|
||||
return buf;
|
||||
}
|
||||
#endif // ENABLE_AUDIO_MP3
|
||||
|
||||
struct Base {
|
||||
virtual ~Base() = default;
|
||||
@@ -458,6 +463,7 @@ struct PlsrBFWAV final : PlsrBase {
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef ENABLE_AUDIO_WAV
|
||||
struct DrWAV final : CustomBase {
|
||||
~DrWAV() {
|
||||
drwav_uninit(&m_wav);
|
||||
@@ -532,7 +538,9 @@ private:
|
||||
drwav m_wav{};
|
||||
File m_file{};
|
||||
};
|
||||
#endif // ENABLE_AUDIO_WAV
|
||||
|
||||
#ifdef ENABLE_AUDIO_MP3
|
||||
struct DrMP3 final : CustomBase {
|
||||
DrMP3(std::unique_ptr<File>&& file = std::make_unique<File>()) : m_file{std::forward<decltype(file)>(file)} {
|
||||
|
||||
@@ -668,8 +676,9 @@ private:
|
||||
std::vector<drmp3_seek_point> m_seek_points{};
|
||||
#endif
|
||||
};
|
||||
#endif // ENABLE_AUDIO_MP3
|
||||
|
||||
#if 0
|
||||
#ifdef ENABLE_AUDIO_FLAC
|
||||
struct DrFLAC final : CustomBase {
|
||||
~DrFLAC() {
|
||||
drflac_close(m_flac);
|
||||
@@ -718,8 +727,9 @@ private:
|
||||
drflac* m_flac{};
|
||||
File m_file{};
|
||||
};
|
||||
#endif
|
||||
#endif // ENABLE_AUDIO_FLAC
|
||||
|
||||
#ifdef ENABLE_AUDIO_OGG
|
||||
// api is not ready, leaving this here for when it is.
|
||||
#if 0
|
||||
struct DrOGG final : CustomBase {
|
||||
@@ -864,6 +874,7 @@ private:
|
||||
stb_vorbis* m_ogg{};
|
||||
File m_file{};
|
||||
};
|
||||
#endif // ENABLE_AUDIO_OGG
|
||||
|
||||
constexpr u32 MAX_SONGS = 4;
|
||||
|
||||
@@ -886,11 +897,12 @@ struct SongEntry {
|
||||
}
|
||||
};
|
||||
|
||||
Mutex g_mutex;
|
||||
SongEntry g_songs[MAX_SONGS];
|
||||
Mutex g_mutex{};
|
||||
SongEntry g_songs[MAX_SONGS]{};
|
||||
PLSR_PlayerSoundId g_sound_ids[std::to_underlying(SoundEffect::MAX)]{};
|
||||
Thread g_thread{};
|
||||
UEvent g_cancel_uevent{};
|
||||
std::atomic_bool g_is_init{};
|
||||
|
||||
void thread_func(void* arg) {
|
||||
auto player = plsrPlayerGetInstance();
|
||||
@@ -950,6 +962,10 @@ void thread_func(void* arg) {
|
||||
} // namespace
|
||||
|
||||
Result Init() {
|
||||
if (g_is_init) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
R_TRY(plsrPlayerInit());
|
||||
|
||||
@@ -996,14 +1012,21 @@ Result Init() {
|
||||
R_TRY(utils::CreateThread(&g_thread, thread_func, nullptr, 1024*128, 0x20));
|
||||
R_TRY(threadStart(&g_thread));
|
||||
|
||||
g_is_init = true;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void ExitSignal() {
|
||||
ueventSignal(&g_cancel_uevent);
|
||||
if (g_is_init) {
|
||||
ueventSignal(&g_cancel_uevent);
|
||||
}
|
||||
}
|
||||
|
||||
void Exit() {
|
||||
if (!g_is_init) {
|
||||
return;
|
||||
}
|
||||
|
||||
ExitSignal();
|
||||
threadWaitForExit(&g_thread);
|
||||
threadClose(&g_thread);
|
||||
@@ -1025,9 +1048,12 @@ void Exit() {
|
||||
|
||||
std::memset(g_songs, 0, sizeof(g_songs));
|
||||
std::memset(g_sound_ids, 0, sizeof(g_sound_ids));
|
||||
g_is_init = false;
|
||||
}
|
||||
|
||||
Result PlaySoundEffect(SoundEffect effect) {
|
||||
R_UNLESS(g_is_init, 0x1);
|
||||
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
const auto id = g_sound_ids[std::to_underlying(effect)];
|
||||
|
||||
@@ -1040,6 +1066,8 @@ Result PlaySoundEffect(SoundEffect effect) {
|
||||
}
|
||||
|
||||
Result OpenSong(fs::Fs* fs, const fs::FsPath& path, u32 flags, SongID* id) {
|
||||
R_UNLESS(g_is_init, 0x1);
|
||||
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
R_UNLESS(fs && id && !path.empty(), 0x1);
|
||||
|
||||
@@ -1055,24 +1083,32 @@ Result OpenSong(fs::Fs* fs, const fs::FsPath& path, u32 flags, SongID* id) {
|
||||
else if (path.ends_with(".bfwav")) {
|
||||
source = std::make_unique<PlsrBFWAV>();
|
||||
}
|
||||
#ifdef ENABLE_AUDIO_WAV
|
||||
else if (path.ends_with(".wav")) {
|
||||
source = std::make_unique<DrWAV>();
|
||||
}
|
||||
#endif // ENABLE_AUDIO_WAV
|
||||
#ifdef ENABLE_AUDIO_MP3
|
||||
else if (path.ends_with(".mp3") || path.ends_with(".mp2") || path.ends_with(".mp1")) {
|
||||
source = std::make_unique<DrMP3>();
|
||||
}
|
||||
else if (path.ends_with(".adf")) {
|
||||
source = std::make_unique<DrMP3>(std::make_unique<GTAViceCityFile>());
|
||||
}
|
||||
// else if (path.ends_with(".flac")) {
|
||||
// source = std::make_unique<DrFLAC>();
|
||||
// }
|
||||
#endif // ENABLE_AUDIO_MP3
|
||||
#ifdef ENABLE_AUDIO_FLAC
|
||||
else if (path.ends_with(".flac")) {
|
||||
source = std::make_unique<DrFLAC>();
|
||||
}
|
||||
#endif // ENABLE_AUDIO_FLAC
|
||||
#ifdef ENABLE_AUDIO_OGG
|
||||
// else if (path.ends_with(".ogg")) {
|
||||
// source = std::make_unique<DrOGG>();
|
||||
// }
|
||||
else if (path.ends_with(".ogg")) {
|
||||
source = std::make_unique<stbOGG>();
|
||||
}
|
||||
#endif // ENABLE_AUDIO_OGG
|
||||
|
||||
R_UNLESS(source, 0x1);
|
||||
R_TRY(source->LoadFile(fs, path, flags));
|
||||
@@ -1090,6 +1126,8 @@ Result OpenSong(fs::Fs* fs, const fs::FsPath& path, u32 flags, SongID* id) {
|
||||
}
|
||||
|
||||
Result CloseSong(SongID* id) {
|
||||
R_UNLESS(g_is_init, 0x1);
|
||||
|
||||
R_UNLESS(id && *id, 0x1);
|
||||
auto e = static_cast<SongEntry*>(*id);
|
||||
|
||||
@@ -1104,6 +1142,8 @@ Result CloseSong(SongID* id) {
|
||||
}
|
||||
|
||||
#define LockSongAndDo(cond_func, ...) do { \
|
||||
R_UNLESS(g_is_init, 0x1); \
|
||||
\
|
||||
R_UNLESS(id, 0x1); \
|
||||
auto e = static_cast<SongEntry*>(id); \
|
||||
\
|
||||
|
||||
345
sphaira/source/utils/devoptab.cpp
Normal file
345
sphaira/source/utils/devoptab.cpp
Normal file
@@ -0,0 +1,345 @@
|
||||
#include "utils/devoptab_common.hpp"
|
||||
#include "utils/thread.hpp"
|
||||
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/popup_list.hpp"
|
||||
#include "ui/option_box.hpp"
|
||||
|
||||
#include "app.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include "download.hpp"
|
||||
#include "i18n.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
#include <fcntl.h>
|
||||
#include <minIni.h>
|
||||
#include <curl/curl.h>
|
||||
|
||||
namespace sphaira::devoptab {
|
||||
namespace {
|
||||
|
||||
#define MOUNT_PATH "/config/sphaira/mount/"
|
||||
|
||||
using namespace sphaira::ui;
|
||||
using namespace sphaira::devoptab::common;
|
||||
|
||||
// todo: support for disabling some / all mounts.
|
||||
enum class DevoptabType {
|
||||
HTTP,
|
||||
FTP,
|
||||
#ifdef ENABLE_DEVOPTAB_SFTP
|
||||
SFTP,
|
||||
#endif
|
||||
NFS,
|
||||
SMB,
|
||||
WEBDAV,
|
||||
};
|
||||
|
||||
struct TypeEntry {
|
||||
const char* name;
|
||||
const char* scheme;
|
||||
long port;
|
||||
DevoptabType type;
|
||||
};
|
||||
|
||||
const TypeEntry TYPE_ENTRIES[] = {
|
||||
{"HTTP", "http://", 80, DevoptabType::HTTP},
|
||||
{"FTP", "ftp://", 21, DevoptabType::FTP},
|
||||
#ifdef ENABLE_DEVOPTAB_SFTP
|
||||
{"SFTP", "sftp://", 22, DevoptabType::SFTP},
|
||||
#endif
|
||||
{"NFS", "nfs://", 2049, DevoptabType::NFS},
|
||||
{"SMB", "smb://", 445, DevoptabType::SMB},
|
||||
{"WEBDAV", "webdav://", 80, DevoptabType::WEBDAV},
|
||||
};
|
||||
|
||||
struct TypeConfig {
|
||||
TypeEntry type;
|
||||
MountConfig config;
|
||||
};
|
||||
using TypeConfigs = std::vector<TypeConfig>;
|
||||
|
||||
auto BuildIniPathFromType(DevoptabType type) -> fs::FsPath {
|
||||
switch (type) {
|
||||
case DevoptabType::HTTP: return MOUNT_PATH "/http.ini";
|
||||
case DevoptabType::FTP: return MOUNT_PATH "/ftp.ini";
|
||||
#ifdef ENABLE_DEVOPTAB_SFTP
|
||||
case DevoptabType::SFTP: return MOUNT_PATH "/sftp.ini";
|
||||
#endif
|
||||
case DevoptabType::NFS: return MOUNT_PATH "/nfs.ini";
|
||||
case DevoptabType::SMB: return MOUNT_PATH "/smb.ini";
|
||||
case DevoptabType::WEBDAV: return MOUNT_PATH "/webdav.ini";
|
||||
}
|
||||
|
||||
std::unreachable();
|
||||
}
|
||||
|
||||
auto GetTypeName(const TypeConfig& type_config) -> std::string {
|
||||
char name[128]{};
|
||||
std::snprintf(name, sizeof(name), "[%s] %s", type_config.type.name, type_config.config.name.c_str());
|
||||
return name;
|
||||
}
|
||||
|
||||
void LoadAllConfigs(TypeConfigs& out_configs) {
|
||||
out_configs.clear();
|
||||
|
||||
for (const auto& e : TYPE_ENTRIES) {
|
||||
const auto ini_path = BuildIniPathFromType(e.type);
|
||||
|
||||
MountConfigs configs{};
|
||||
LoadConfigsFromIni(ini_path, configs);
|
||||
|
||||
for (const auto& config : configs) {
|
||||
out_configs.emplace_back(e, config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DevoptabForm final : public FormSidebar {
|
||||
// create new.
|
||||
explicit DevoptabForm();
|
||||
// modify existing.
|
||||
explicit DevoptabForm(DevoptabType type, const MountConfig& config);
|
||||
|
||||
private:
|
||||
void SetupButtons(bool type_change);
|
||||
void UpdateSchemeURL();
|
||||
|
||||
private:
|
||||
DevoptabType m_type{};
|
||||
MountConfig m_config{};
|
||||
|
||||
SidebarEntryTextInput* m_name{};
|
||||
SidebarEntryTextInput* m_url{};
|
||||
SidebarEntryTextInput* m_port{};
|
||||
// SidebarEntryTextInput* m_timeout{};
|
||||
SidebarEntryTextInput* m_user{};
|
||||
SidebarEntryTextInput* m_pass{};
|
||||
SidebarEntryTextInput* m_dump_path{};
|
||||
};
|
||||
|
||||
DevoptabForm::DevoptabForm(DevoptabType type, const MountConfig& config)
|
||||
: FormSidebar{"Mount Creator"_i18n}
|
||||
, m_type{type}
|
||||
, m_config{config} {
|
||||
SetupButtons(false);
|
||||
}
|
||||
|
||||
DevoptabForm::DevoptabForm() : FormSidebar{"Mount Creator"_i18n} {
|
||||
SetupButtons(true);
|
||||
}
|
||||
|
||||
void DevoptabForm::UpdateSchemeURL() {
|
||||
for (const auto& e : TYPE_ENTRIES) {
|
||||
if (e.type == m_type) {
|
||||
const auto scheme_start = m_url->GetValue().find("://");
|
||||
if (scheme_start != std::string::npos) {
|
||||
m_url->SetValue(e.scheme + m_url->GetValue().substr(scheme_start + 3));
|
||||
} else if (m_url->GetValue().starts_with("://")) {
|
||||
m_url->SetValue(e.scheme + m_url->GetValue().substr(3));
|
||||
} else if (m_url->GetValue().empty()) {
|
||||
m_url->SetValue(e.scheme);
|
||||
}
|
||||
|
||||
m_port->SetNumValue(e.port);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DevoptabForm::SetupButtons(bool type_change) {
|
||||
if (type_change) {
|
||||
SidebarEntryArray::Items items;
|
||||
for (const auto& e : TYPE_ENTRIES) {
|
||||
items.emplace_back(e.name);
|
||||
}
|
||||
|
||||
this->Add<SidebarEntryArray>(
|
||||
"Type"_i18n, items, [this](s64& index) {
|
||||
m_type = TYPE_ENTRIES[index].type;
|
||||
UpdateSchemeURL();
|
||||
},
|
||||
(s64)m_type,
|
||||
"Select the type of the forwarder."_i18n
|
||||
);
|
||||
}
|
||||
|
||||
m_name = this->Add<SidebarEntryTextInput>(
|
||||
"Name"_i18n, m_config.name, "", "", -1, 32,
|
||||
"Set the name of the application"_i18n
|
||||
);
|
||||
|
||||
m_url = this->Add<SidebarEntryTextInput>(
|
||||
"URL"_i18n, m_config.url, "", "", -1, PATH_MAX,
|
||||
"Set the URL of the application"_i18n
|
||||
);
|
||||
|
||||
m_port = this->Add<SidebarEntryTextInput>(
|
||||
"Port"_i18n, m_config.port, "", "", 1, 5,
|
||||
"Optional: Set the port of the server. If left empty, the default port for the protocol will be used."_i18n
|
||||
);
|
||||
|
||||
#if 0
|
||||
m_timeout = this->Add<SidebarEntryTextInput>(
|
||||
"Timeout"_i18n, m_config.timeout, "Timeout in milliseconds", 1, 5,
|
||||
"Optional: Set the timeout in seconds."_i18n
|
||||
);
|
||||
#endif
|
||||
|
||||
m_user = this->Add<SidebarEntryTextInput>(
|
||||
"User"_i18n, m_config.user, "", "", -1, PATH_MAX,
|
||||
"Optional: Set the username of the application"_i18n
|
||||
);
|
||||
|
||||
m_pass = this->Add<SidebarEntryTextInput>(
|
||||
"Pass"_i18n, m_config.pass, "", "", -1, PATH_MAX,
|
||||
"Optional: Set the password of the application"_i18n
|
||||
);
|
||||
|
||||
m_dump_path = this->Add<SidebarEntryTextInput>(
|
||||
"Dump path"_i18n, m_config.dump_path, "", "", -1, PATH_MAX,
|
||||
"Optional: Set the dump path used when exporting games and saves."_i18n
|
||||
);
|
||||
|
||||
this->Add<SidebarEntryBool>(
|
||||
"Read only"_i18n, m_config.read_only,
|
||||
i18n::get("mount_readonly_info",
|
||||
"Mount the filesystem as read only.\n\n"
|
||||
"Setting this option also hidens the mount from being show as an export option.")
|
||||
);
|
||||
|
||||
this->Add<SidebarEntryBool>(
|
||||
"No stat file"_i18n, m_config.no_stat_file,
|
||||
i18n::get("filecheck_disable_info",
|
||||
"Enabling stops the file browser from checking the file size and timestamp of each file. "
|
||||
"This improves browsing performance.")
|
||||
);
|
||||
|
||||
this->Add<SidebarEntryBool>(
|
||||
"No stat dir"_i18n, m_config.no_stat_dir,
|
||||
i18n::get("dircheck_disable_info",
|
||||
"Enabling stops the file browser from checking how many files and folders are in a folder. "
|
||||
"This improves browsing performance, especially for servers that has slow directory listing.")
|
||||
);
|
||||
|
||||
this->Add<SidebarEntryBool>(
|
||||
"FS hidden"_i18n, m_config.fs_hidden,
|
||||
"Hide the mount from being visible in the file browser."_i18n
|
||||
);
|
||||
|
||||
this->Add<SidebarEntryBool>(
|
||||
"Export hidden"_i18n, m_config.dump_hidden,
|
||||
"Hide the mount from being visible as a export option for games and saves."_i18n
|
||||
);
|
||||
|
||||
// set default scheme when creating a new entry.
|
||||
if (type_change) {
|
||||
UpdateSchemeURL();
|
||||
}
|
||||
|
||||
const auto callback = this->Add<SidebarEntryCallback>("Save"_i18n, [this](){
|
||||
m_config.name = m_name->GetValue();
|
||||
m_config.url = m_url->GetValue();
|
||||
m_config.user = m_user->GetValue();
|
||||
m_config.pass = m_pass->GetValue();
|
||||
m_config.dump_path = m_dump_path->GetValue();
|
||||
m_config.port = std::stoul(m_port->GetValue());
|
||||
// m_config.timeout = m_timeout->GetValue();
|
||||
|
||||
const auto ini_path = BuildIniPathFromType(m_type);
|
||||
|
||||
fs::FsNativeSd().CreateDirectoryRecursively(MOUNT_PATH);
|
||||
ini_puts(m_config.name.c_str(), "url", m_config.url.c_str(), ini_path);
|
||||
ini_puts(m_config.name.c_str(), "user", m_config.user.c_str(), ini_path);
|
||||
ini_puts(m_config.name.c_str(), "pass", m_config.pass.c_str(), ini_path);
|
||||
ini_puts(m_config.name.c_str(), "dump_path", m_config.dump_path.c_str(), ini_path);
|
||||
ini_putl(m_config.name.c_str(), "port", m_config.port, ini_path);
|
||||
ini_putl(m_config.name.c_str(), "timeout", m_config.timeout, ini_path);
|
||||
// todo: update minini to have put_bool.
|
||||
ini_puts(m_config.name.c_str(), "read_only", m_config.read_only ? "true" : "false", ini_path);
|
||||
ini_puts(m_config.name.c_str(), "no_stat_file", m_config.no_stat_file ? "true" : "false", ini_path);
|
||||
ini_puts(m_config.name.c_str(), "no_stat_dir", m_config.no_stat_dir ? "true" : "false", ini_path);
|
||||
ini_puts(m_config.name.c_str(), "fs_hidden", m_config.fs_hidden ? "true" : "false", ini_path);
|
||||
ini_puts(m_config.name.c_str(), "dump_hidden", m_config.dump_hidden ? "true" : "false", ini_path);
|
||||
|
||||
App::Notify("Mount entry saved. Restart Sphaira to apply changes."_i18n);
|
||||
|
||||
this->SetPop();
|
||||
}, "Saves the mount entry.\n\n"
|
||||
"NOTE: You must restart Sphaira for changes to take effect!"_i18n);
|
||||
|
||||
// ensure that all fields are valid.
|
||||
callback->Depends([this](){
|
||||
return
|
||||
!m_name->GetValue().empty() &&
|
||||
!m_url->GetValue().empty() &&
|
||||
!m_url->GetValue().ends_with("://");
|
||||
}, "Name and URL must be set!"_i18n);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void DisplayDevoptabSideBar() {
|
||||
auto options = std::make_unique<Sidebar>("Devoptab Options"_i18n, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(std::move(options)));
|
||||
|
||||
options->Add<SidebarEntryCallback>("Create New Entry"_i18n, [](){
|
||||
App::Push<DevoptabForm>();
|
||||
}, "Creates a new mount option.\n\n"
|
||||
"NOTE: You must restart Sphaira for changes to take effect!"_i18n);
|
||||
|
||||
options->Add<SidebarEntryCallback>("Modify Existing Entry"_i18n, [](){
|
||||
PopupList::Items items;
|
||||
TypeConfigs configs;
|
||||
LoadAllConfigs(configs);
|
||||
|
||||
for (const auto& e : configs) {
|
||||
items.emplace_back(GetTypeName(e));
|
||||
}
|
||||
|
||||
if (items.empty()) {
|
||||
App::Notify("No mount entries found."_i18n);
|
||||
return;
|
||||
}
|
||||
|
||||
App::Push<PopupList>("Modify Entry"_i18n, items, [configs](std::optional<s64> index){
|
||||
if (!index.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& entry = configs[index.value()];
|
||||
App::Push<DevoptabForm>(entry.type.type, entry.config);
|
||||
});
|
||||
}, "Modify an existing mount option.\n\n"
|
||||
"NOTE: You must restart Sphaira for changes to take effect!"_i18n);
|
||||
|
||||
options->Add<SidebarEntryCallback>("Delete Existing Entry"_i18n, [](){
|
||||
PopupList::Items items;
|
||||
TypeConfigs configs;
|
||||
LoadAllConfigs(configs);
|
||||
|
||||
for (const auto& e : configs) {
|
||||
items.emplace_back(GetTypeName(e));
|
||||
}
|
||||
|
||||
if (items.empty()) {
|
||||
App::Notify("No mount entries found."_i18n);
|
||||
return;
|
||||
}
|
||||
|
||||
App::Push<PopupList>("Delete Entry"_i18n, items, [configs](std::optional<s64> index){
|
||||
if (!index.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& entry = configs[index.value()];
|
||||
const auto ini_path = BuildIniPathFromType(entry.type.type);
|
||||
ini_puts(entry.config.name.c_str(), nullptr, nullptr, ini_path);
|
||||
});
|
||||
}, "Delete an existing mount option.\n\n"
|
||||
"NOTE: You must restart Sphaira for changes to take effect!"_i18n);
|
||||
}
|
||||
|
||||
} // namespace sphaira::devoptab
|
||||
@@ -4,8 +4,6 @@
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
#include "yati/container/nsp.hpp"
|
||||
#include "yati/container/xci.hpp"
|
||||
#include "yati/source/file.hpp"
|
||||
|
||||
#include <pulsar.h>
|
||||
@@ -15,24 +13,16 @@
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <algorithm>
|
||||
#include <sys/iosupport.h>
|
||||
|
||||
namespace sphaira::devoptab {
|
||||
namespace {
|
||||
|
||||
struct Device {
|
||||
PLSR_BFSAR bfsar;
|
||||
std::FILE* file; // points to archive file.
|
||||
};
|
||||
|
||||
struct File {
|
||||
Device* device;
|
||||
PLSR_BFWARFileInfo info;
|
||||
size_t off;
|
||||
};
|
||||
|
||||
struct Dir {
|
||||
Device* device;
|
||||
u32 index;
|
||||
};
|
||||
|
||||
@@ -82,56 +72,69 @@ PLSR_RC GetFileInfo(const PLSR_BFSAR *bfsar, std::string_view path, PLSR_BFWARFi
|
||||
|
||||
}
|
||||
|
||||
int set_errno(struct _reent *r, int err) {
|
||||
r->_errno = err;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int devoptab_open(struct _reent *r, void *fileStruct, const char *_path, int flags, int mode) {
|
||||
auto device = (Device*)r->deviceData;
|
||||
auto file = static_cast<File*>(fileStruct);
|
||||
std::memset(file, 0, sizeof(*file));
|
||||
|
||||
char path[FS_MAX_PATH];
|
||||
if (!common::fix_path(_path, path)) {
|
||||
return set_errno(r, ENOENT);
|
||||
struct Device final : common::MountDevice {
|
||||
Device(const PLSR_BFSAR& _bfsar, const common::MountConfig& _config)
|
||||
: MountDevice{_config}
|
||||
, bfsar{_bfsar} {
|
||||
this->file = this->bfsar.ar.handle->f;
|
||||
}
|
||||
|
||||
~Device() {
|
||||
plsrBFSARClose(&bfsar);
|
||||
}
|
||||
|
||||
private:
|
||||
bool Mount() override { return true; }
|
||||
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
|
||||
int devoptab_close(void *fd) override;
|
||||
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
|
||||
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
|
||||
int devoptab_fstat(void *fd, struct stat *st) override;
|
||||
int devoptab_diropen(void* fd, const char *path) override;
|
||||
int devoptab_dirreset(void* fd) override;
|
||||
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
|
||||
int devoptab_dirclose(void* fd) override;
|
||||
int devoptab_lstat(const char *path, struct stat *st) override;
|
||||
|
||||
private:
|
||||
PLSR_BFSAR bfsar;
|
||||
std::FILE* file; // points to archive file.
|
||||
};
|
||||
|
||||
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
|
||||
auto file = static_cast<File*>(fileStruct);
|
||||
|
||||
PLSR_BFWARFileInfo info;
|
||||
if (R_FAILED(GetFileInfo(&device->bfsar, path, info))) {
|
||||
return set_errno(r, ENOENT);
|
||||
if (R_FAILED(GetFileInfo(&this->bfsar, path, info))) {
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
file->device = device;
|
||||
file->info = info;
|
||||
return r->_errno = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int devoptab_close(struct _reent *r, void *fd) {
|
||||
int Device::devoptab_close(void *fd) {
|
||||
auto file = static_cast<File*>(fd);
|
||||
std::memset(file, 0, sizeof(*file));
|
||||
|
||||
return r->_errno = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) {
|
||||
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
|
||||
auto file = static_cast<File*>(fd);
|
||||
const auto& info = file->info;
|
||||
|
||||
// const auto real_len = len;
|
||||
// plsr seems to read oob, so allow for some tollerance.
|
||||
const auto oob_allowed = 64;
|
||||
len = std::min(len, info.size + oob_allowed - file->off);
|
||||
std::fseek(file->device->file, file->info.offset + file->off, SEEK_SET);
|
||||
const auto bytes_read = std::fread(ptr, 1, len, file->device->file);
|
||||
|
||||
// log_write("bytes read: %zu len: %zu real_len: %zu off: %zu size: %u\n", bytes_read, len, real_len, file->off, info.size);
|
||||
std::fseek(this->file, file->info.offset + file->off, SEEK_SET);
|
||||
const auto bytes_read = std::fread(ptr, 1, len, this->file);
|
||||
|
||||
file->off += bytes_read;
|
||||
return bytes_read;
|
||||
}
|
||||
|
||||
off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) {
|
||||
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
|
||||
auto file = static_cast<File*>(fd);
|
||||
const auto& info = file->info;
|
||||
|
||||
@@ -141,11 +144,10 @@ off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) {
|
||||
pos = info.size;
|
||||
}
|
||||
|
||||
r->_errno = 0;
|
||||
return file->off = std::clamp<u64>(pos, 0, info.size);
|
||||
}
|
||||
|
||||
int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) {
|
||||
int Device::devoptab_fstat(void *fd, struct stat *st) {
|
||||
auto file = static_cast<File*>(fd);
|
||||
const auto& info = file->info;
|
||||
|
||||
@@ -153,51 +155,36 @@ int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) {
|
||||
st->st_nlink = 1;
|
||||
st->st_size = info.size;
|
||||
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
|
||||
return r->_errno = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
DIR_ITER* devoptab_diropen(struct _reent *r, DIR_ITER *dirState, const char *_path) {
|
||||
auto device = (Device*)r->deviceData;
|
||||
auto dir = static_cast<Dir*>(dirState->dirStruct);
|
||||
std::memset(dir, 0, sizeof(*dir));
|
||||
|
||||
char path[FS_MAX_PATH];
|
||||
if (!common::fix_path(_path, path)) {
|
||||
set_errno(r, ENOENT);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int Device::devoptab_diropen(void* fd, const char *path) {
|
||||
if (!std::strcmp(path, "/")) {
|
||||
dir->device = device;
|
||||
} else {
|
||||
set_errno(r, ENOENT);
|
||||
return NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
r->_errno = 0;
|
||||
return dirState;
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) {
|
||||
auto dir = static_cast<Dir*>(dirState->dirStruct);
|
||||
int Device::devoptab_dirreset(void* fd) {
|
||||
auto dir = static_cast<Dir*>(fd);
|
||||
|
||||
dir->index = 0;
|
||||
|
||||
return r->_errno = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) {
|
||||
auto dir = static_cast<Dir*>(dirState->dirStruct);
|
||||
std::memset(filestat, 0, sizeof(*filestat));
|
||||
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
|
||||
auto dir = static_cast<Dir*>(fd);
|
||||
|
||||
do {
|
||||
if (dir->index >= plsrBFSARSoundCount(&dir->device->bfsar)) {
|
||||
log_write("finished getting call entries: %u vs %u\n", dir->index, plsrBFSARSoundCount(&dir->device->bfsar));
|
||||
return set_errno(r, ENOENT);
|
||||
if (dir->index >= plsrBFSARSoundCount(&this->bfsar)) {
|
||||
log_write("finished getting call entries: %u vs %u\n", dir->index, plsrBFSARSoundCount(&this->bfsar));
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
PLSR_BFSARSoundInfo info{};
|
||||
if (R_FAILED(plsrBFSARSoundGet(&dir->device->bfsar, dir->index, &info))) {
|
||||
if (R_FAILED(plsrBFSARSoundGet(&this->bfsar, dir->index, &info))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -206,7 +193,7 @@ int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struc
|
||||
continue;
|
||||
}
|
||||
|
||||
if (R_FAILED(plsrBFSARStringGet(&dir->device->bfsar, info.stringIndex, filename, NAME_MAX))) {
|
||||
if (R_FAILED(plsrBFSARStringGet(&this->bfsar, info.stringIndex, filename, NAME_MAX))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -228,141 +215,54 @@ int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struc
|
||||
filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
|
||||
dir->index++;
|
||||
break;
|
||||
} while (dir->index++);
|
||||
} while (++dir->index);
|
||||
|
||||
return r->_errno = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) {
|
||||
auto dir = static_cast<Dir*>(dirState->dirStruct);
|
||||
int Device::devoptab_dirclose(void* fd) {
|
||||
auto dir = static_cast<Dir*>(fd);
|
||||
std::memset(dir, 0, sizeof(*dir));
|
||||
|
||||
log_write("[BFSAR] devoptab_dirclose\n");
|
||||
return r->_errno = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) {
|
||||
auto device = (Device*)r->deviceData;
|
||||
|
||||
char path[FS_MAX_PATH];
|
||||
if (!common::fix_path(_path, path)) {
|
||||
return set_errno(r, ENOENT);
|
||||
}
|
||||
|
||||
std::memset(st, 0, sizeof(*st));
|
||||
int Device::devoptab_lstat(const char *path, struct stat *st) {
|
||||
st->st_nlink = 1;
|
||||
|
||||
if (!std::strcmp(path, "/")) {
|
||||
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
|
||||
} else {
|
||||
PLSR_BFWARFileInfo info{};
|
||||
if (R_FAILED(GetFileInfo(&device->bfsar, path, info))) {
|
||||
return set_errno(r, ENOENT);
|
||||
if (R_FAILED(GetFileInfo(&this->bfsar, path, info))) {
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
|
||||
st->st_size = info.size;
|
||||
}
|
||||
|
||||
st->st_nlink = 1;
|
||||
|
||||
return r->_errno = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
constexpr devoptab_t DEVOPTAB = {
|
||||
.structSize = sizeof(File),
|
||||
.open_r = devoptab_open,
|
||||
.close_r = devoptab_close,
|
||||
.read_r = devoptab_read,
|
||||
.seek_r = devoptab_seek,
|
||||
.fstat_r = devoptab_fstat,
|
||||
.stat_r = devoptab_lstat,
|
||||
.dirStateSize = sizeof(Dir),
|
||||
.diropen_r = devoptab_diropen,
|
||||
.dirreset_r = devoptab_dirreset,
|
||||
.dirnext_r = devoptab_dirnext,
|
||||
.dirclose_r = devoptab_dirclose,
|
||||
.lstat_r = devoptab_lstat,
|
||||
};
|
||||
|
||||
struct Entry {
|
||||
Device device{};
|
||||
devoptab_t devoptab{};
|
||||
fs::FsPath path{};
|
||||
fs::FsPath mount{};
|
||||
char name[32]{};
|
||||
s32 ref_count{};
|
||||
|
||||
~Entry() {
|
||||
log_write("[BFSAR] entry called\n");
|
||||
RemoveDevice(mount);
|
||||
plsrBFSARClose(&device.bfsar);
|
||||
}
|
||||
};
|
||||
|
||||
Mutex g_mutex;
|
||||
std::array<std::unique_ptr<Entry>, common::MAX_ENTRIES> g_entries;
|
||||
|
||||
} // namespace
|
||||
|
||||
Result MountBfsar(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) {
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
PLSR_BFSAR bfsar{};
|
||||
PLSR_RC_TRY(plsrBFSAROpen(path, &bfsar));
|
||||
|
||||
// check if we already have the save mounted.
|
||||
for (auto& e : g_entries) {
|
||||
if (e && e->path == path) {
|
||||
e->ref_count++;
|
||||
out_path = e->mount;
|
||||
R_SUCCEED();
|
||||
}
|
||||
if (!common::MountReadOnlyIndexDevice(
|
||||
[&bfsar](const common::MountConfig& config) {
|
||||
return std::make_unique<Device>(bfsar, config);
|
||||
},
|
||||
sizeof(File), sizeof(Dir),
|
||||
"BFSAR", out_path
|
||||
)) {
|
||||
log_write("[BFSAR] Failed to mount %s\n", path.s);
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
// otherwise, find next free entry.
|
||||
auto itr = std::ranges::find_if(g_entries, [](auto& e){
|
||||
return !e;
|
||||
});
|
||||
R_UNLESS(itr != g_entries.end(), 0x1);
|
||||
|
||||
const auto index = std::distance(g_entries.begin(), itr);
|
||||
|
||||
auto entry = std::make_unique<Entry>();
|
||||
entry->path = path;
|
||||
entry->devoptab = DEVOPTAB;
|
||||
entry->devoptab.name = entry->name;
|
||||
entry->devoptab.deviceData = &entry->device;
|
||||
std::snprintf(entry->name, sizeof(entry->name), "BFSAR_%zu", index);
|
||||
std::snprintf(entry->mount, sizeof(entry->mount), "BFSAR_%zu:/", index);
|
||||
|
||||
PLSR_RC_TRY(plsrBFSAROpen(path, &entry->device.bfsar));
|
||||
entry->device.file = entry->device.bfsar.ar.handle->f;
|
||||
|
||||
R_UNLESS(AddDevice(&entry->devoptab) >= 0, 0x1);
|
||||
log_write("[BFSAR] DEVICE SUCCESS %s %s\n", path.s, entry->name);
|
||||
|
||||
out_path = entry->mount;
|
||||
entry->ref_count++;
|
||||
*itr = std::move(entry);
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void UmountBfsar(const fs::FsPath& mount) {
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
|
||||
auto itr = std::ranges::find_if(g_entries, [&mount](auto& e){
|
||||
return e && e->mount == mount;
|
||||
});
|
||||
|
||||
if (itr == g_entries.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((*itr)->ref_count) {
|
||||
(*itr)->ref_count--;
|
||||
}
|
||||
|
||||
if (!(*itr)->ref_count) {
|
||||
itr->reset();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sphaira::devoptab
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,27 +1,49 @@
|
||||
#include "utils/devoptab.hpp"
|
||||
#include "utils/devoptab_common.hpp"
|
||||
#include "fatfs.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "utils/profile.hpp"
|
||||
|
||||
#include "log.hpp"
|
||||
#include "ff.h"
|
||||
#include "defines.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <span>
|
||||
#include <fcntl.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <cerrno>
|
||||
#include <sys/iosupport.h>
|
||||
#include <sys/stat.h>
|
||||
#include <ff.h>
|
||||
|
||||
namespace sphaira::fatfs {
|
||||
namespace sphaira::devoptab {
|
||||
namespace {
|
||||
|
||||
auto is_archive(BYTE attr) -> bool {
|
||||
const auto archive_attr = AM_DIR | AM_ARC;
|
||||
return (attr & archive_attr) == archive_attr;
|
||||
}
|
||||
enum BisMountType {
|
||||
BisMountType_PRODINFOF,
|
||||
BisMountType_SAFE,
|
||||
BisMountType_USER,
|
||||
BisMountType_SYSTEM,
|
||||
};
|
||||
|
||||
struct FatStorageEntry {
|
||||
FsStorage storage;
|
||||
std::unique_ptr<common::LruBufferedData> buffered;
|
||||
FATFS fs;
|
||||
};
|
||||
|
||||
struct BisMountEntry {
|
||||
const FsBisPartitionId id;
|
||||
const char* volume_name;
|
||||
const char* mount_name;
|
||||
};
|
||||
|
||||
constexpr BisMountEntry BIS_MOUNT_ENTRIES[] {
|
||||
[BisMountType_PRODINFOF] = { FsBisPartitionId_CalibrationFile, "PRODINFOF", "PRODINFOF:/" },
|
||||
[BisMountType_SAFE] = { FsBisPartitionId_SafeMode, "SAFE", "SAFE:/" },
|
||||
[BisMountType_USER] = { FsBisPartitionId_User, "USER", "USER:/" },
|
||||
[BisMountType_SYSTEM] = { FsBisPartitionId_System, "SYSTEM", "SYSTEM:/" },
|
||||
};
|
||||
static_assert(std::size(BIS_MOUNT_ENTRIES) == FF_VOLUMES);
|
||||
|
||||
FatStorageEntry g_fat_storage[FF_VOLUMES];
|
||||
|
||||
// todo: replace with off+size and have the data be in another struct
|
||||
// in order to be more lcache efficient.
|
||||
@@ -48,14 +70,51 @@ struct File {
|
||||
FIL* files;
|
||||
u32 file_count;
|
||||
size_t off;
|
||||
char path[256];
|
||||
char path[PATH_MAX];
|
||||
};
|
||||
|
||||
struct Dir {
|
||||
FDIR dir;
|
||||
char path[256];
|
||||
char path[PATH_MAX];
|
||||
};
|
||||
|
||||
struct Device final : common::MountDevice {
|
||||
Device(BisMountType type, const common::MountConfig& _config)
|
||||
: MountDevice{_config}
|
||||
, m_type{type} {
|
||||
|
||||
}
|
||||
|
||||
~Device();
|
||||
|
||||
private:
|
||||
bool fix_path(const char* str, char* out, bool strip_leading_slash = false) override {
|
||||
std::strcpy(out, str);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Mount() override;
|
||||
int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override;
|
||||
int devoptab_close(void *fd) override;
|
||||
ssize_t devoptab_read(void *fd, char *ptr, size_t len) override;
|
||||
ssize_t devoptab_seek(void *fd, off_t pos, int dir) override;
|
||||
int devoptab_fstat(void *fd, struct stat *st) override;
|
||||
int devoptab_diropen(void* fd, const char *path) override;
|
||||
int devoptab_dirreset(void* fd) override;
|
||||
int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override;
|
||||
int devoptab_dirclose(void* fd) override;
|
||||
int devoptab_lstat(const char *path, struct stat *st) override;
|
||||
|
||||
private:
|
||||
const BisMountType m_type;
|
||||
bool mounted{};
|
||||
};
|
||||
|
||||
auto is_archive(BYTE attr) -> bool {
|
||||
const auto archive_attr = AM_DIR | AM_ARC;
|
||||
return (attr & archive_attr) == archive_attr;
|
||||
}
|
||||
|
||||
u64 get_size_from_files(const File* file) {
|
||||
u64 size = 0;
|
||||
for (u32 i = 0; i < file->file_count; i++) {
|
||||
@@ -90,38 +149,8 @@ void set_current_file_pos(File* file) {
|
||||
}
|
||||
}
|
||||
|
||||
enum BisMountType {
|
||||
BisMountType_PRODINFOF,
|
||||
BisMountType_SAFE,
|
||||
BisMountType_USER,
|
||||
BisMountType_SYSTEM,
|
||||
};
|
||||
|
||||
struct FatStorageEntry {
|
||||
FsStorage storage;
|
||||
std::unique_ptr<devoptab::common::LruBufferedData> buffered;
|
||||
FATFS fs;
|
||||
devoptab_t devoptab;
|
||||
};
|
||||
|
||||
struct BisMountEntry {
|
||||
const FsBisPartitionId id;
|
||||
const char* volume_name;
|
||||
const char* mount_name;
|
||||
};
|
||||
|
||||
constexpr BisMountEntry BIS_MOUNT_ENTRIES[] {
|
||||
[BisMountType_PRODINFOF] = { FsBisPartitionId_CalibrationFile, "PRODINFOF", "PRODINFOF:/" },
|
||||
[BisMountType_SAFE] = { FsBisPartitionId_SafeMode, "SAFE", "SAFE:/" },
|
||||
[BisMountType_USER] = { FsBisPartitionId_User, "USER", "USER:/" },
|
||||
[BisMountType_SYSTEM] = { FsBisPartitionId_System, "SYSTEM", "SYSTEM:/" },
|
||||
};
|
||||
static_assert(std::size(BIS_MOUNT_ENTRIES) == FF_VOLUMES);
|
||||
|
||||
FatStorageEntry g_fat_storage[FF_VOLUMES];
|
||||
|
||||
void fill_stat(const char* path, const FILINFO* fno, struct stat *st) {
|
||||
memset(st, 0, sizeof(*st));
|
||||
std::memset(st, 0, sizeof(*st));
|
||||
|
||||
st->st_nlink = 1;
|
||||
|
||||
@@ -133,7 +162,7 @@ void fill_stat(const char* path, const FILINFO* fno, struct stat *st) {
|
||||
tm.tm_mon = ((fno->fdate >> 5) & 0xF) - 1;
|
||||
tm.tm_year = (fno->fdate >> 9) + 80;
|
||||
|
||||
st->st_atime = mktime(&tm);
|
||||
st->st_atime = std::mktime(&tm);
|
||||
st->st_mtime = st->st_atime;
|
||||
st->st_ctime = st->st_atime;
|
||||
|
||||
@@ -143,7 +172,7 @@ void fill_stat(const char* path, const FILINFO* fno, struct stat *st) {
|
||||
char file_path[256];
|
||||
for (u16 i = 0; i < 256; i++) {
|
||||
std::snprintf(file_path, sizeof(file_path), "%s/%02u", path, i);
|
||||
FILINFO file_info;
|
||||
FILINFO file_info{};
|
||||
if (FR_OK != f_stat(file_path, &file_info)) {
|
||||
break;
|
||||
}
|
||||
@@ -152,8 +181,7 @@ void fill_stat(const char* path, const FILINFO* fno, struct stat *st) {
|
||||
}
|
||||
|
||||
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
|
||||
} else
|
||||
if (fno->fattrib & AM_DIR) {
|
||||
} else if (fno->fattrib & AM_DIR) {
|
||||
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
|
||||
} else {
|
||||
st->st_size = fno->fsize;
|
||||
@@ -161,31 +189,80 @@ void fill_stat(const char* path, const FILINFO* fno, struct stat *st) {
|
||||
}
|
||||
}
|
||||
|
||||
static int set_errno(struct _reent *r, int err) {
|
||||
r->_errno = err;
|
||||
return -1;
|
||||
Device::~Device() {
|
||||
if (mounted) {
|
||||
auto& fat = g_fat_storage[m_type];
|
||||
f_unmount(BIS_MOUNT_ENTRIES[m_type].mount_name);
|
||||
fat.buffered.reset();
|
||||
fsStorageClose(&fat.storage);
|
||||
}
|
||||
}
|
||||
|
||||
int fat_open(struct _reent *r, void *fileStruct, const char *path, int flags, int mode) {
|
||||
bool Device::Mount() {
|
||||
if (mounted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto& fat = g_fat_storage[m_type];
|
||||
|
||||
if (!serviceIsActive(&fat.storage.s)) {
|
||||
const auto res = fsOpenBisStorage(&fat.storage, BIS_MOUNT_ENTRIES[m_type].id);
|
||||
if (R_FAILED(res)) {
|
||||
log_write("[FATFS] fsOpenBisStorage(%d) failed: 0x%x\n", BIS_MOUNT_ENTRIES[m_type].id, res);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
log_write("[FATFS] Storage for %s already opened\n", BIS_MOUNT_ENTRIES[m_type].mount_name);
|
||||
}
|
||||
|
||||
if (!fat.buffered) {
|
||||
auto source = std::make_shared<FsStorageSource>(&fat.storage);
|
||||
|
||||
s64 size;
|
||||
if (R_FAILED(source->GetSize(&size))) {
|
||||
log_write("[FATFS] Failed to get size of storage source\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
fat.buffered = std::make_unique<common::LruBufferedData>(source, size);
|
||||
if (!fat.buffered) {
|
||||
log_write("[FATFS] Failed to create LruBufferedData\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (FR_OK != f_mount(&fat.fs, BIS_MOUNT_ENTRIES[m_type].mount_name, 1)) {
|
||||
log_write("[FATFS] f_mount(%s) failed\n", BIS_MOUNT_ENTRIES[m_type].mount_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
log_write("[FATFS] Mounted %s at %s\n", BIS_MOUNT_ENTRIES[m_type].volume_name, BIS_MOUNT_ENTRIES[m_type].mount_name);
|
||||
return mounted = true;
|
||||
}
|
||||
|
||||
int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) {
|
||||
auto file = static_cast<File*>(fileStruct);
|
||||
std::memset(file, 0, sizeof(*file));
|
||||
|
||||
// todo: init array
|
||||
// todo: handle dir.
|
||||
FIL fil{};
|
||||
if (FR_OK == f_open(&fil, path, FA_READ)) {
|
||||
file->file_count = 1;
|
||||
file->files = (FIL*)std::malloc(sizeof(*file->files));
|
||||
if (!file->files) {
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
file->file_count = 1;
|
||||
std::memcpy(file->files, &fil, sizeof(*file->files));
|
||||
// todo: check what error code is returned here.
|
||||
} else {
|
||||
FILINFO info{};
|
||||
if (FR_OK != f_stat(path, &info)) {
|
||||
return set_errno(r, ENOENT);
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
if (!(info.fattrib & AM_ARC)) {
|
||||
return set_errno(r, ENOENT);
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
char file_path[256];
|
||||
@@ -198,33 +275,35 @@ int fat_open(struct _reent *r, void *fileStruct, const char *path, int flags, in
|
||||
}
|
||||
|
||||
file->files = (FIL*)std::realloc(file->files, (i + 1) * sizeof(*file->files));
|
||||
if (!file->files) {
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
std::memcpy(&file->files[i], &fil, sizeof(fil));
|
||||
file->file_count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!file->files) {
|
||||
return set_errno(r, ENOENT);
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
std::snprintf(file->path, sizeof(file->path), "%s", path);
|
||||
return r->_errno = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fat_close(struct _reent *r, void *fd) {
|
||||
int Device::devoptab_close(void *fd) {
|
||||
auto file = static_cast<File*>(fd);
|
||||
|
||||
if (file->files) {
|
||||
for (u32 i = 0; i < file->file_count; i++) {
|
||||
f_close(&file->files[i]);
|
||||
}
|
||||
free(file->files);
|
||||
for (u32 i = 0; i < file->file_count; i++) {
|
||||
f_close(&file->files[i]);
|
||||
}
|
||||
|
||||
return r->_errno = 0;
|
||||
std::free(file->files);
|
||||
return 0;
|
||||
}
|
||||
|
||||
ssize_t fat_read(struct _reent *r, void *fd, char *ptr, size_t len) {
|
||||
ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) {
|
||||
auto file = static_cast<File*>(fd);
|
||||
UINT total_bytes_read = 0;
|
||||
|
||||
@@ -233,11 +312,11 @@ ssize_t fat_read(struct _reent *r, void *fd, char *ptr, size_t len) {
|
||||
auto fil = get_current_file(file);
|
||||
if (!fil) {
|
||||
log_write("[FATFS] failed to get fil\n");
|
||||
return set_errno(r, ENOENT);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
if (FR_OK != f_read(fil, ptr, len, &bytes_read)) {
|
||||
return set_errno(r, ENOENT);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
if (!bytes_read) {
|
||||
@@ -252,7 +331,7 @@ ssize_t fat_read(struct _reent *r, void *fd, char *ptr, size_t len) {
|
||||
return total_bytes_read;
|
||||
}
|
||||
|
||||
off_t fat_seek(struct _reent *r, void *fd, off_t pos, int dir) {
|
||||
ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) {
|
||||
auto file = static_cast<File*>(fd);
|
||||
const auto size = get_size_from_files(file);
|
||||
|
||||
@@ -265,11 +344,10 @@ off_t fat_seek(struct _reent *r, void *fd, off_t pos, int dir) {
|
||||
file->off = std::clamp<u64>(pos, 0, size);
|
||||
set_current_file_pos(file);
|
||||
|
||||
r->_errno = 0;
|
||||
return file->off;
|
||||
}
|
||||
|
||||
int fat_fstat(struct _reent *r, void *fd, struct stat *st) {
|
||||
int Device::devoptab_fstat(void *fd, struct stat *st) {
|
||||
auto file = static_cast<File*>(fd);
|
||||
|
||||
/* Only fill the attr and size field, leaving the timestamp blank. */
|
||||
@@ -279,174 +357,118 @@ int fat_fstat(struct _reent *r, void *fd, struct stat *st) {
|
||||
/* Fill stat info. */
|
||||
fill_stat(nullptr, &info, st);
|
||||
|
||||
return r->_errno = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
DIR_ITER* fat_diropen(struct _reent *r, DIR_ITER *dirState, const char *path) {
|
||||
auto dir = static_cast<Dir*>(dirState->dirStruct);
|
||||
std::memset(dir, 0, sizeof(*dir));
|
||||
int Device::devoptab_diropen(void* fd, const char *path) {
|
||||
auto dir = static_cast<Dir*>(fd);
|
||||
|
||||
log_write("[FATFS] diropen: %s\n", path);
|
||||
if (FR_OK != f_opendir(&dir->dir, path)) {
|
||||
set_errno(r, ENOENT);
|
||||
return NULL;
|
||||
log_write("[FATFS] f_opendir(%s) failed\n", path);
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
r->_errno = 0;
|
||||
return dirState;
|
||||
log_write("[FATFS] Opened dir: %s\n", path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fat_dirreset(struct _reent *r, DIR_ITER *dirState) {
|
||||
auto dir = static_cast<Dir*>(dirState->dirStruct);
|
||||
int Device::devoptab_dirreset(void* fd) {
|
||||
auto dir = static_cast<Dir*>(fd);
|
||||
|
||||
if (FR_OK != f_rewinddir(&dir->dir)) {
|
||||
return set_errno(r, ENOENT);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
return r->_errno = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fat_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) {
|
||||
auto dir = static_cast<Dir*>(dirState->dirStruct);
|
||||
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
|
||||
auto dir = static_cast<Dir*>(fd);
|
||||
FILINFO fno{};
|
||||
|
||||
if (FR_OK != f_readdir(&dir->dir, &fno)) {
|
||||
return set_errno(r, ENOENT);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
if (!fno.fname[0]) {
|
||||
return set_errno(r, ENOENT);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
strcpy(filename, fno.fname);
|
||||
std::strcpy(filename, fno.fname);
|
||||
fill_stat(dir->path, &fno, filestat);
|
||||
|
||||
return r->_errno = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fat_dirclose(struct _reent *r, DIR_ITER *dirState) {
|
||||
auto dir = static_cast<Dir*>(dirState->dirStruct);
|
||||
int Device::devoptab_dirclose(void* fd) {
|
||||
auto dir = static_cast<Dir*>(fd);
|
||||
|
||||
if (FR_OK != f_closedir(&dir->dir)) {
|
||||
return set_errno(r, ENOENT);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
return r->_errno = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fat_statvfs(struct _reent *r, const char *path, struct statvfs *buf) {
|
||||
memset(buf, 0, sizeof(*buf));
|
||||
|
||||
// todo: find out how to calculate free size in read only.
|
||||
const auto fat = (FatStorageEntry*)r->deviceData;
|
||||
buf->f_bsize = FF_MAX_SS;
|
||||
buf->f_frsize = FF_MAX_SS;
|
||||
buf->f_blocks = ((fat->fs.n_fatent - 2) * (DWORD)fat->fs.csize);
|
||||
buf->f_namemax = FF_LFN_BUF;
|
||||
|
||||
return r->_errno = 0;
|
||||
}
|
||||
|
||||
int fat_lstat(struct _reent *r, const char *file, struct stat *st) {
|
||||
int Device::devoptab_lstat(const char *path, struct stat *st) {
|
||||
FILINFO fno;
|
||||
if (FR_OK != f_stat(file, &fno)) {
|
||||
return set_errno(r, ENOENT);
|
||||
if (FR_OK != f_stat(path, &fno)) {
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
fill_stat(file, &fno, st);
|
||||
return r->_errno = 0;
|
||||
fill_stat(path, &fno, st);
|
||||
return 0;
|
||||
}
|
||||
|
||||
constexpr devoptab_t DEVOPTAB = {
|
||||
.structSize = sizeof(File),
|
||||
.open_r = fat_open,
|
||||
.close_r = fat_close,
|
||||
.read_r = fat_read,
|
||||
.seek_r = fat_seek,
|
||||
.fstat_r = fat_fstat,
|
||||
.stat_r = fat_lstat,
|
||||
.dirStateSize = sizeof(Dir),
|
||||
.diropen_r = fat_diropen,
|
||||
.dirreset_r = fat_dirreset,
|
||||
.dirnext_r = fat_dirnext,
|
||||
.dirclose_r = fat_dirclose,
|
||||
.statvfs_r = fat_statvfs,
|
||||
.lstat_r = fat_lstat,
|
||||
};
|
||||
|
||||
Mutex g_mutex{};
|
||||
bool g_is_init{};
|
||||
|
||||
} // namespace
|
||||
|
||||
Result MountAll() {
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
|
||||
if (g_is_init) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result MountFatfsAll() {
|
||||
for (u32 i = 0; i < FF_VOLUMES; i++) {
|
||||
auto& fat = g_fat_storage[i];
|
||||
const auto& bis = BIS_MOUNT_ENTRIES[i];
|
||||
|
||||
// log_write("[FAT] %s\n", bis.volume_name);
|
||||
common::MountConfig config{};
|
||||
config.read_only = true;
|
||||
config.dump_hidden = true;
|
||||
|
||||
fat.devoptab = DEVOPTAB;
|
||||
fat.devoptab.name = bis.volume_name;
|
||||
fat.devoptab.deviceData = &fat;
|
||||
|
||||
R_TRY(fsOpenBisStorage(&fat.storage, bis.id));
|
||||
auto source = std::make_shared<FsStorageSource>(&fat.storage);
|
||||
|
||||
s64 size;
|
||||
R_TRY(source->GetSize(&size));
|
||||
// log_write("[FAT] BIS SUCCESS %s\n", bis.volume_name);
|
||||
|
||||
fat.buffered = std::make_unique<devoptab::common::LruBufferedData>(source, size);
|
||||
|
||||
R_UNLESS(FR_OK == f_mount(&fat.fs, bis.mount_name, 1), 0x1);
|
||||
// log_write("[FAT] MOUNT SUCCESS %s\n", bis.volume_name);
|
||||
|
||||
R_UNLESS(AddDevice(&fat.devoptab) >= 0, 0x1);
|
||||
// log_write("[FAT] DEVICE SUCCESS %s\n", bis.volume_name);
|
||||
if (!common::MountNetworkDevice2(
|
||||
std::make_unique<Device>((BisMountType)i, config),
|
||||
config,
|
||||
sizeof(File), sizeof(Dir),
|
||||
bis.volume_name, bis.mount_name
|
||||
)) {
|
||||
log_write("[FATFS] Failed to mount %s\n", bis.volume_name);
|
||||
}
|
||||
}
|
||||
|
||||
g_is_init = true;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
void UnmountAll() {
|
||||
SCOPED_MUTEX(&g_mutex);
|
||||
|
||||
if (!g_is_init) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (u32 i = 0; i < FF_VOLUMES; i++) {
|
||||
auto& fat = g_fat_storage[i];
|
||||
const auto& bis = BIS_MOUNT_ENTRIES[i];
|
||||
|
||||
RemoveDevice(bis.mount_name);
|
||||
f_unmount(bis.mount_name);
|
||||
fsStorageClose(&fat.storage);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sphaira::fatfs
|
||||
} // namespace sphaira::devoptab
|
||||
|
||||
extern "C" {
|
||||
|
||||
const char* VolumeStr[] {
|
||||
sphaira::fatfs::BIS_MOUNT_ENTRIES[0].volume_name,
|
||||
sphaira::fatfs::BIS_MOUNT_ENTRIES[1].volume_name,
|
||||
sphaira::fatfs::BIS_MOUNT_ENTRIES[2].volume_name,
|
||||
sphaira::fatfs::BIS_MOUNT_ENTRIES[3].volume_name,
|
||||
sphaira::devoptab::BIS_MOUNT_ENTRIES[0].volume_name,
|
||||
sphaira::devoptab::BIS_MOUNT_ENTRIES[1].volume_name,
|
||||
sphaira::devoptab::BIS_MOUNT_ENTRIES[2].volume_name,
|
||||
sphaira::devoptab::BIS_MOUNT_ENTRIES[3].volume_name,
|
||||
};
|
||||
|
||||
Result fatfs_read(u8 num, void* dst, u64 offset, u64 size) {
|
||||
// log_write("[FAT] num: %u\n", num);
|
||||
auto& fat = sphaira::fatfs::g_fat_storage[num];
|
||||
auto& fat = sphaira::devoptab::g_fat_storage[num];
|
||||
return fat.buffered->Read2(dst, offset, size);
|
||||
}
|
||||
|
||||
// libusbhsfs also defines these, so only define if not using it.
|
||||
#ifndef ENABLE_LIBUSBHSFS
|
||||
void* ff_memalloc (UINT msize) {
|
||||
return std::malloc(msize);
|
||||
}
|
||||
|
||||
void ff_memfree (void* mblock) {
|
||||
std::free(mblock);
|
||||
}
|
||||
#endif // ENABLE_LIBUSBHSFS
|
||||
|
||||
} // extern "C"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user