Compare commits

..

4 Commits

Author SHA1 Message Date
7d9a5feccb Avalonia - Couple fixes and improvements (#3451)
* fix updater check crash

* remove line

* reduce cheat window sizes

* enable tiered compilation and r2r

* remove warning on LaunchState

* remove warnings related to tasks

* addressed review

* undo csproj indentation

* fix tabs in axaml file

* remove double line

* remove R2R
2022-07-12 00:25:33 +02:00
14ae4e276f Avalonia - Further Optimize Chinese Translation (#3452)
* Optimize Chinese Translation

* Update zh_CN.json

* Update zh_CN.json

* Update zh_CN.json

* Update zh_CN.json

* Update zh_CN.json

* Update zh_CN.json

* Update zh_CN.json

* test

* Update zh_CN.json

* Update zh_CN.json

* Update zh_CN.json

* Update zh_CN.json

* Delete zh_CN.json

* Add files via upload

* Update zh_CN.json

* Update zh_CN.json
2022-07-12 00:12:52 +02:00
3af42d6c7e UI - Avalonia Part 3 (#3441)
* Add all other windows

* addreesed review

* Prevent "No Update" option from being deleted

* Select no update is the current update is removed from the title update window

* fix amiibo crash
2022-07-08 15:47:11 -03:00
bccf5e8b5a Avalonia - Use loaded config when assigning controller input (#3447)
* Use loaded config when assigning controller input

* Fix crash when switch player in controller window
2022-07-08 15:28:45 -03:00
37 changed files with 3156 additions and 196 deletions

View File

@ -553,6 +553,20 @@
"SettingsTabHotkeysToggleMuteHotkey": "Mute:",
"ControllerMotionTitle": "Motion Control Settings",
"ControllerRumbleTitle": "Rumble Settings",
"SettingsSelectThemeFileDialogTitle" : "Select Theme File",
"SettingsXamlThemeFile" : "Xaml Theme File"
"SettingsSelectThemeFileDialogTitle": "Select Theme File",
"SettingsXamlThemeFile": "Xaml Theme File",
"AvatarWindowTitle": "Manage Accounts - Avatar",
"Amiibo": "Amiibo",
"Unknown": "Unknown",
"Usage": "Usage",
"Writable": "Writable",
"SelectDlcDialogTitle": "Select DLC files",
"SelectUpdateDialogTitle": "Select update files",
"UserProfileWindowTitle": "Manage User Profiles",
"CheatWindowTitle": "Manage Game Cheats",
"DlcWindowTitle": "Manage Game DLC",
"UpdateWindowTitle": "Manage Game Updates",
"CheatWindowHeading": "Cheats Available for {0} [{1}]",
"DlcWindowHeading": "DLC Available for {0} [{1}]",
"GameUpdateWindowHeading": "DLC Available for {0} [{1}]"
}

View File

@ -1,34 +1,34 @@
{
"MenuBarFileOpenApplet": "打开小程序",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "打开独立的Mii小程序",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "打开独立的 Mii 小程序",
"SettingsTabInputDirectMouseAccess": "直通鼠标操作",
"SettingsTabSystemMemoryManagerMode": "内存管理模式:",
"SettingsTabSystemMemoryManagerModeSoftware": "软件",
"SettingsTabSystemMemoryManagerModeHost": "本机 (快速)",
"SettingsTabSystemMemoryManagerModeHostUnchecked": "跳过检查的本机 (最快)",
"MenuBarFile": "文件(_F)",
"MenuBarFileOpenFromFile": "加载文件中的程序(_L)",
"MenuBarFileOpenUnpacked": "加载解包后的游戏(_U)",
"MenuBarFileOpenEmuFolder": "打开Ryujinx文件夹",
"SettingsTabSystemMemoryManagerModeHostUnchecked": "跳过检查的本机 (最快)",
"MenuBarFile": "文件",
"MenuBarFileOpenFromFile": "加载文件",
"MenuBarFileOpenUnpacked": "加载解包后的游戏",
"MenuBarFileOpenEmuFolder": "打开 Ryujinx 文件夹",
"MenuBarFileOpenLogsFolder": "打开日志文件夹",
"MenuBarFileExit": "退出(_E)",
"MenuBarFileExit": "退出",
"MenuBarOptions": "选项",
"MenuBarOptionsToggleFullscreen": "切换全屏",
"MenuBarOptionsStartGamesInFullscreen": "全屏模式启动游戏",
"MenuBarOptionsStopEmulation": "止模拟",
"MenuBarOptionsSettings": "设置(_S)",
"MenuBarOptionsManageUserProfiles": "管理用户账户(_M)",
"MenuBarActions": "行动(_A)",
"MenuBarOptionsStartGamesInFullscreen": "全屏模式启动游戏",
"MenuBarOptionsStopEmulation": "止模拟",
"MenuBarOptionsSettings": "设置",
"MenuBarOptionsManageUserProfiles": "管理用户账户",
"MenuBarActions": "操作",
"MenuBarOptionsSimulateWakeUpMessage": "模拟唤醒消息",
"MenuBarActionsScanAmiibo": "扫描Amiibo",
"MenuBarTools": "工具(_T)",
"MenuBarActionsScanAmiibo": "扫描 Amiibo",
"MenuBarTools": "工具",
"MenuBarToolsInstallFirmware": "安装固件",
"MenuBarFileToolsInstallFirmwareFromFile": "从 XCI 或 ZIP 安装固件",
"MenuBarFileToolsInstallFirmwareFromDirectory": "从文件夹安装固件",
"MenuBarHelp": "帮助",
"MenuBarHelpCheckForUpdates": "检查更新",
"MenuBarHelpAbout": "关于",
"MenuSearch": "搜索...",
"MenuSearch": "搜索……",
"GameListHeaderFavorite": "收藏",
"GameListHeaderIcon": "图标",
"GameListHeaderApplication": "名称",
@ -43,13 +43,13 @@
"GameListContextMenuOpenUserSaveDirectoryToolTip": "打开储存游戏存档的目录",
"GameListContextMenuOpenUserDeviceDirectory": "打开应用系统目录",
"GameListContextMenuOpenUserDeviceDirectoryToolTip": "打开包含游戏系统设置的目录",
"GameListContextMenuOpenUserBcatDirectory": "打开BCAT目录",
"GameListContextMenuOpenUserBcatDirectoryToolTip": "打开包含游戏BCAT数据的目录",
"GameListContextMenuOpenUserBcatDirectory": "打开 BCAT 目录",
"GameListContextMenuOpenUserBcatDirectoryToolTip": "打开包含游戏 BCAT 数据的目录",
"GameListContextMenuManageTitleUpdates": "管理游戏更新",
"GameListContextMenuManageTitleUpdatesToolTip": "打开更新管理窗口",
"GameListContextMenuManageDlc": "管理DLC",
"GameListContextMenuManageDlc": "管理 DLC",
"GameListContextMenuManageDlcToolTip": "打开 DLC 管理窗口",
"GameListContextMenuOpenModsDirectory": "打开MOD目录",
"GameListContextMenuOpenModsDirectory": "打开 MOD 目录",
"GameListContextMenuOpenModsDirectoryToolTip": "打开存放游戏MOD的目录",
"GameListContextMenuCacheManagement": "缓存管理",
"GameListContextMenuCacheManagementPurgePptc": "清除 PPTC 缓存",
@ -72,10 +72,10 @@
"Settings": "设置",
"SettingsTabGeneral": "用户界面",
"SettingsTabGeneralGeneral": "常规",
"SettingsTabGeneralEnableDiscordRichPresence": "启用Discord在线状态展示",
"SettingsTabGeneralEnableDiscordRichPresence": "启用 Discord 在线状态展示",
"SettingsTabGeneralCheckUpdatesOnLaunch": "自动检查更新",
"SettingsTabGeneralShowConfirmExitDialog": "显示 \"确认退出\" 对话框",
"SettingsTabGeneralHideCursorOnIdle": "空闲时隐藏鼠标",
"SettingsTabGeneralHideCursorOnIdle": "自动隐藏鼠标",
"SettingsTabGeneralGameDirectories": "游戏目录",
"SettingsTabGeneralAdd": "添加",
"SettingsTabGeneralRemove": "删除",
@ -119,7 +119,7 @@
"SettingsTabSystemAudioBackendSDL2": "SDL2",
"SettingsTabSystemHacks": "修正",
"SettingsTabSystemHacksNote": " - 会引起模拟器不稳定",
"SettingsTabSystemExpandDramSize": "将模拟RAM大小扩展 6GB",
"SettingsTabSystemExpandDramSize": "将模拟RAM大小扩展 6GB",
"SettingsTabSystemIgnoreMissingServices": "忽略缺少的服务",
"SettingsTabGraphics": "图像",
"SettingsTabGraphicsEnhancements": "增强",
@ -184,12 +184,12 @@
"ControllerSettingsDeviceDisabled": "关闭",
"ControllerSettingsControllerType": "手柄类型",
"ControllerSettingsControllerTypeHandheld": "掌机",
"ControllerSettingsControllerTypeProController": "Pro手柄",
"ControllerSettingsControllerTypeProController": "Pro 手柄",
"ControllerSettingsControllerTypeJoyConPair": "JoyCon",
"ControllerSettingsControllerTypeJoyConLeft": "左JoyCon",
"ControllerSettingsControllerTypeJoyConRight": "右JoyCon",
"ControllerSettingsControllerTypeJoyConLeft": "左 JoyCon",
"ControllerSettingsControllerTypeJoyConRight": "右 JoyCon",
"ControllerSettingsProfile": "预设",
"ControllerSettingsProfileDefault": "默认",
"ControllerSettingsProfileDefault": "Default",
"ControllerSettingsLoad": "加载",
"ControllerSettingsAdd": "新建",
"ControllerSettingsRemove": "删除",
@ -243,19 +243,17 @@
"ControllerSettingsMisc": "其他",
"ControllerSettingsTriggerThreshold": "扳机阈值:",
"ControllerSettingsMotion": "体感",
"ControllerSettingsCemuHook": "CemuHook",
"ControllerSettingsMotionEnableMotionControls": "启用体感操作",
"ControllerSettingsMotionUseCemuhookCompatibleMotion": "使用CemuHook体感协议",
"ControllerSettingsMotionUseCemuhookCompatibleMotion": "使用 CemuHook 体感协议",
"ControllerSettingsMotionControllerSlot": "手柄:",
"ControllerSettingsMotionMirrorInput": "镜像操作",
"ControllerSettingsMotionRightJoyConSlot": "右JoyCon:",
"ControllerSettingsMotionServerHost": "服务器Host:",
"ControllerSettingsMotionRightJoyConSlot": "右 JoyCon:",
"ControllerSettingsMotionServerHost": "服务器 Host:",
"ControllerSettingsMotionGyroSensitivity": "陀螺仪敏感度:",
"ControllerSettingsMotionGyroDeadzone": "陀螺仪死区:",
"ControllerSettingsSave": "保存",
"ControllerSettingsClose": "关闭",
"UserProfilesSelectedUserProfile": "选择用户账户:",
"UserProfilesSaveProfileName": "保存账户名",
"UserProfilesSelectedUserProfile": "选择用户账户:",
"UserProfilesSaveProfileName": "保存名",
"UserProfilesChangeProfileImage": "更换头像",
"UserProfilesAvailableUserProfiles": "现有的账户:",
"UserProfilesAddNewProfile": "新建账户",
@ -281,7 +279,7 @@
"ControllerSettingsSaveProfileToolTip": "保存预设",
"MenuBarFileToolsTakeScreenshot": "保存截图",
"MenuBarFileToolsHideUi": "隐藏UI",
"GameListContextMenuToggleFavorite": "标记为收藏",
"GameListContextMenuToggleFavorite": "收藏",
"GameListContextMenuToggleFavoriteToolTip": "启用或取消收藏标记",
"SettingsTabGeneralTheme": "主题",
"SettingsTabGeneralThemeCustomTheme": "自选主题路径",
@ -290,9 +288,8 @@
"SettingsTabGeneralThemeBaseStyleLight": "浅色",
"SettingsTabGeneralThemeEnableCustomTheme": "使用自选主题界面",
"ButtonBrowse": "浏览",
"ControllerSettingsMotionConfigureCemuHookSettings": "配置CemuHook体感",
"ControllerSettingsConfigureGeneral": "配置",
"ControllerSettingsRumble": "震动",
"ControllerSettingsRumbleEnable": "启用震动",
"ControllerSettingsRumbleStrongMultiplier": "强震动调节",
"ControllerSettingsRumbleWeakMultiplier": "弱震动调节",
"DialogMessageSaveNotAvailableMessage": "没有{0} [{1:x16}]的游戏存档",
@ -302,9 +299,9 @@
"DialogErrorTitle": "Ryujinx - 错误",
"DialogWarningTitle": "Ryujinx - 警告",
"DialogExitTitle": "Ryujinx - 关闭",
"DialogErrorMessage": "Ryujinx遇到了错误",
"DialogExitMessage": "是否关闭Ryujinx",
"DialogExitSubMessage": "所有未保存的进度会丢失",
"DialogErrorMessage": "Ryujinx 遇到了错误",
"DialogExitMessage": "是否关闭 Ryujinx",
"DialogExitSubMessage": "未保存的进度会丢失",
"DialogMessageCreateSaveErrorMessage": "创建特定的存档时出错: {0}",
"DialogMessageFindSaveErrorMessage": "查找特定的存档时出错: {0}",
"FolderDialogExtractTitle": "选择要解压到的文件夹",
@ -315,9 +312,9 @@
"DialogNcaExtractionSuccessMessage": "提取成功。",
"DialogUpdaterConvertFailedMessage": "无法转换当前 Ryujinx 版本。",
"DialogUpdaterCancelUpdateMessage": "更新取消!",
"DialogUpdaterAlreadyOnLatestVersionMessage": "您使用的Ryujinx是最新版本。",
"DialogUpdaterFailedToGetVersionMessage": "尝试从 Github 获取版本信息时无效。可能由于 GitHub Actions 正在编译新版本。请过几分钟重试。",
"DialogUpdaterConvertFailedGithubMessage": "无法转换从 Github 接收到的 Ryujinx 版本转换。",
"DialogUpdaterAlreadyOnLatestVersionMessage": "您使用的 Ryujinx 是最新版本。",
"DialogUpdaterFailedToGetVersionMessage": "尝试从 Github 获取版本信息时无效。\\n可能由于 GitHub Actions 正在编译新版本。请过几分钟重试。",
"DialogUpdaterConvertFailedGithubMessage": "无法转换从 Github 接收到的 Ryujinx 版本。",
"DialogUpdaterDownloadingMessage": "下载新版本中...",
"DialogUpdaterExtractionMessage": "正在提取更新...",
"DialogUpdaterRenamingMessage": "正在删除旧文件...",
@ -328,7 +325,7 @@
"DialogUpdaterArchNotSupportedSubMessage": "(仅支持 x64 系统)",
"DialogUpdaterNoInternetMessage": "没有连接到互联网",
"DialogUpdaterNoInternetSubMessage": "请确保互联网连接正常。",
"DialogUpdaterDirtyBuildMessage": "不能更新第三方版本的 Ryujinx",
"DialogUpdaterDirtyBuildMessage": "不能更新非官方版本的 Ryujinx",
"DialogUpdaterDirtyBuildSubMessage": "如果希望使用受支持的版本,请您在 https://ryujinx.org/ 下载。",
"DialogRestartRequiredMessage": "需要重启模拟器",
"DialogThemeRestartMessage": "主题设置已保存。需要重新启动才能生效。",
@ -344,7 +341,7 @@
"DialogErrorAppletErrorExceptionMessage": "显示错误对话框时出错: {0}",
"DialogUserErrorDialogMessage": "{0}: {1}",
"DialogUserErrorDialogInfoMessage": "\n有关修复此错误的更多信息可以遵循我们的设置指南。",
"DialogUserErrorDialogTitle": "Ryujinx错误 ({0})",
"DialogUserErrorDialogTitle": "Ryujinx 错误 ({0})",
"DialogAmiiboApiTitle": "Amiibo API",
"DialogAmiiboApiFailFetchMessage": "从 API 获取信息时出错。",
"DialogAmiiboApiConnectErrorMessage": "无法连接到 Amiibo API 服务器。服务器可能已关闭,或者您没有网络连接。",
@ -357,7 +354,7 @@
"DialogPPTCDeletionErrorMessage": "清除位于{0}的 PPTC 缓存时出错: {1}",
"DialogShaderDeletionMessage": "您即将删除:\n\n{0}的着色器缓存\n\n确定吗",
"DialogShaderDeletionErrorMessage": "清除位于{0}的着色器缓存时出错: {1}",
"DialogRyujinxErrorMessage": "Ryujinx遇到错误",
"DialogRyujinxErrorMessage": "Ryujinx 遇到错误",
"DialogInvalidTitleIdErrorMessage": "UI 错误:所选游戏没有有效的标题ID",
"DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "路径{0}找不到有效的系统固件。",
"DialogFirmwareInstallerFirmwareInstallTitle": "安装固件{0}",
@ -376,11 +373,11 @@
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "为了获得最佳性能,建议禁用跟踪日志记录。您是否要立即禁用?",
"DialogPerformanceCheckShaderDumpEnabledMessage": "您启用了着色器转储,仅供开发人员使用。",
"DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "为了获得最佳性能,建议禁用着色器转储。您是否要立即禁用?",
"DialogLoadAppGameAlreadyLoadedMessage": "当前已加载有游戏",
"DialogLoadAppGameAlreadyLoadedMessage": "已有游戏正在运行",
"DialogLoadAppGameAlreadyLoadedSubMessage": "请停止模拟或关闭程序,再启动另一个游戏。",
"DialogUpdateAddUpdateErrorMessage": "选择的文件不包含所选游戏的更新!",
"DialogSettingsBackendThreadingWarningTitle": "警告 - 后端多线程",
"DialogSettingsBackendThreadingWarningMessage": "改变此选项后必须重启 Ryujinx 才能生效。根据您的硬件您开启该选项时可能需要手动禁用驱动程序本身的GL多线程。",
"DialogSettingsBackendThreadingWarningMessage": "改变此选项后必须重启 Ryujinx 才能生效。\\n根据您的硬件您开启该选项时可能需要手动禁用驱动程序本身的GL多线程。",
"SettingsTabGraphicsFeaturesOptions": "功能",
"SettingsTabGraphicsBackendMultithreading": "后端多线程:",
"CommonAuto": "自动(推荐)",
@ -391,23 +388,23 @@
"DialogProfileInvalidProfileNameErrorMessage": "文件名包含无效字符,请重试。",
"MenuBarOptionsPauseEmulation": "暂停",
"MenuBarOptionsResumeEmulation": "继续",
"AboutUrlTooltipMessage": "在浏览器中打开Ryujinx的官网。",
"AboutDisclaimerMessage": "Ryujinx以任何方式与Nintendo™及任何商业伙伴没有关联",
"AboutAmiiboDisclaimerMessage": "我们的Amiibo模拟使用了\nAmiiboAPI (www.amiiboapi.com) ",
"AboutPatreonUrlTooltipMessage": "在浏览器中打开RyujinxPatreon赞助页。",
"AboutGithubUrlTooltipMessage": "在浏览器中打开RyujinxGitHub代码库。",
"AboutDiscordUrlTooltipMessage": "在浏览器中打开RyujinxDiscord邀请链接。",
"AboutTwitterUrlTooltipMessage": "在浏览器中打开RyujinxTwitter主页。",
"AboutUrlTooltipMessage": "在浏览器中打开 Ryujinx 的官网。",
"AboutDisclaimerMessage": "Ryujinx 以任何方式与Nintendo™及任何商业伙伴没有关联",
"AboutAmiiboDisclaimerMessage": "我们的 Amiibo 模拟使用了\nAmiiboAPI (www.amiiboapi.com) ",
"AboutPatreonUrlTooltipMessage": "在浏览器中打开 RyujinxPatreon 赞助页。",
"AboutGithubUrlTooltipMessage": "在浏览器中打开 RyujinxGitHub 代码库。",
"AboutDiscordUrlTooltipMessage": "在浏览器中打开 RyujinxDiscord 邀请链接。",
"AboutTwitterUrlTooltipMessage": "在浏览器中打开 RyujinxTwitter 主页。",
"AboutRyujinxAboutTitle": "关于:",
"AboutRyujinxAboutContent": "Ryujinx是Nintendo Switch™模拟器.\n您可以在Patreon上支持Ryujinx。\n关注Twitter或者Discord可以获取模拟器最新动态。\n如果您对开发感兴趣欢迎来GitHubDiscord加入我们",
"AboutRyujinxAboutContent": "Ryujinx是一款Nintendo Switch™模拟器\n您可以在 Patreon 上赞助 Ryujinx。\n关注 TwitterDiscord 可以获取模拟器最新动态。\n如果您对开发感兴趣欢迎来 GitHubDiscord 加入我们!",
"AboutRyujinxMaintainersTitle": "由以下作者维护:",
"AboutRyujinxMaintainersContentTooltipMessage": "在浏览器中打开贡献者的网页",
"AboutRyujinxSupprtersTitle": "感谢Patreon的赞助者:",
"AmiiboSeriesLabel": "Amiibo系列",
"AboutRyujinxSupprtersTitle": "感谢 Patreon 的赞助者:",
"AmiiboSeriesLabel": "Amiibo 系列",
"AmiiboCharacterLabel": "角色",
"AmiiboScanButtonLabel": "扫描",
"AmiiboOptionsShowAllLabel": "显示所有 Amiibo",
"AmiiboOptionsUsRandomTagLabel": "修正: 使用随机标记的Uuid",
"AmiiboOptionsUsRandomTagLabel": "修正使用随机标记的 Uuid",
"DlcManagerTableHeadingEnabledLabel": "启用",
"DlcManagerTableHeadingTitleIdLabel": "游戏ID",
"DlcManagerTableHeadingContainerPathLabel": "文件夹路径",
@ -421,59 +418,59 @@
"OrderDescending": "从大到小",
"SettingsTabGraphicsFeatures": "额外功能",
"ErrorWindowTitle": "错误窗口",
"ToggleDiscordTooltip": "启用或关闭Discord详细在线状态展示",
"ToggleDiscordTooltip": "启用或关闭 Discord 详细在线状态展示",
"AddGameDirBoxTooltip": "输入要添加的游戏目录",
"AddGameDirTooltip": "添加游戏目录到列表中",
"RemoveGameDirTooltip": "移除选中的目录",
"CustomThemeCheckTooltip": "启用或关闭自定义主题",
"CustomThemePathTooltip": "自定义主题的目录",
"CustomThemeBrowseTooltip": "查找自定义主题",
"DockModeToggleTooltip": "是否开启Swith的主机模式",
"DirectKeyboardTooltip": "是否开启\"直连键盘访问(HID)支持\" (部分游戏可以使用您的键盘输入文字)",
"DirectMouseTooltip": "是否开启\"直连鼠标访问(HID)支持\" (部分游戏可以使用您的鼠标导航)",
"DockModeToggleTooltip": "是否开启 Switch 的主机模式",
"DirectKeyboardTooltip": "是否开启\"直连键盘访问(HID)支持\"\n(部分游戏可以使用您的键盘输入文字)",
"DirectMouseTooltip": "是否开启\"直连鼠标访问(HID)支持\"\n(部分游戏可以使用您的鼠标导航)",
"RegionTooltip": "更改系统区域",
"LanguageTooltip": "更改系统语言",
"TimezoneTooltip": "更改系统时区",
"TimeTooltip": "更改系统时钟",
"VSyncToggleTooltip": "开启可以消除帧撕裂,关闭可以提高性能",
"PptcToggleTooltip": "开启以后减少游戏启动时间",
"VSyncToggleTooltip": "关闭后部分使用动态帧率的游戏可以超过60Hz的刷新率",
"PptcToggleTooltip": "开启以后减少游戏启动时间和卡顿",
"FsIntegrityToggleTooltip": "是否检查游戏文件内容的完整性",
"AudioBackendTooltip": "默认推荐SDL但每种音频后端对各类游戏兼容性可能不同",
"MemoryManagerTooltip": "改变Switch内存映射到电脑内存的方式会影响CPU性能消耗",
"AudioBackendTooltip": "默认推荐SDL但每种音频后端对各类游戏兼容性不同,遇到音频问题可以尝试切换后端",
"MemoryManagerTooltip": "改变 Switch 内存映射到电脑内存的方式会影响CPU性能消耗",
"MemoryManagerSoftwareTooltip": "使用软件内存页管理,最精确但是速度最慢",
"MemoryManagerHostTooltip": "直接映射内存页到电脑内存JIT效率高",
"MemoryManagerUnsafeTooltip": "直接映射内存页,但不检查内存溢出JIT效率最高。Ryujinx可以访问任何位置的内存所以相对不安全。此模式下只应运行您信任的游戏或软件即官方游戏",
"DRamTooltip": "扩展模拟的Switch内存为6GB某些高清纹理MOD或4K MOD需要此选项",
"MemoryManagerHostTooltip": "直接映射内存页到电脑内存JIT效率高",
"MemoryManagerUnsafeTooltip": "直接映射内存页但不检查内存溢出JIT效率最高。\nRyujinx可以访问任何位置的内存因而相对不安全。此模式下只应运行您信任的游戏或软件(即官方游戏)",
"DRamTooltip": "扩展模拟的 Switch 内存为6GB。\n某些高清纹理MOD或4K MOD需要此选项",
"IgnoreMissingServicesTooltip": "忽略某些未实现的系统服务,少部分游戏需要此选项才能启动",
"GraphicsBackendThreadingTooltip": "启用后端多线程",
"GalThreadingTooltip": "使用模拟器自带的多线程调度,减少着色器编译的卡顿,并提高驱动程序的性能(尤其是缺失多线程的AMD)。NVIDIA用户需要重启模拟器才能禁用驱动本身的多线程否则您需手动执行禁用获得最佳性能",
"ShaderCacheToggleTooltip": "开启后缓存着色器到硬盘,减少画面卡顿",
"GalThreadingTooltip": "使用模拟器自带的多线程调度,减少着色器编译的卡顿,并提高驱动程序的性能(尤其是缺失多线程的AMD)。\nNVIDIA用户需要重启模拟器才能禁用驱动本身的多线程否则您需手动执行禁用获得最佳性能",
"ShaderCacheToggleTooltip": "开启后缓存着色器到本地,减少游戏卡顿",
"ResolutionScaleTooltip": "缩放渲染的分辨率",
"ResolutionScaleEntryTooltip": "尽量使用如1.5的浮点倍数。非整数的倍率易引起错误",
"AnisotropyTooltip": "各向异性过滤等级。提高倾斜视角纹理的清晰度('自动'使用游戏默认指定的等级)",
"AspectRatioTooltip": "模拟器渲染窗口的宽高比",
"ResolutionScaleEntryTooltip": "尽量使用如1.5的浮点倍数。非整数的倍率易引起 BUG",
"AnisotropyTooltip": "各向异性过滤等级。提高倾斜视角纹理的清晰度\n('自动'使用游戏默认指定的等级)",
"AspectRatioTooltip": "渲染窗口的宽高比",
"ShaderDumpPathTooltip": "转储图形着色器的路径",
"FileLogTooltip": "是否保存日志文件到硬盘",
"StubLogTooltip": "记录stub消息",
"InfoLogTooltip": "记录info消息",
"WarnLogTooltip": "记录warning消息",
"ErrorLogTooltip": "记录error消息",
"TraceLogTooltip": "记录trace消息",
"GuestLogTooltip": "记录guest消息",
"StubLogTooltip": "记录Stub消息",
"InfoLogTooltip": "记录Info消息",
"WarnLogTooltip": "记录Warning消息",
"ErrorLogTooltip": "记录Error消息",
"TraceLogTooltip": "记录Trace消息",
"GuestLogTooltip": "记录Guest消息",
"FileAccessLogTooltip": "记录文件访问消息",
"FSAccessLogModeTooltip": "记录FS访问消息输出到控制台。可选的模式是0-3",
"DeveloperOptionTooltip": "使用请谨慎",
"OpenGlLogLevel": "需要打开适当的日志等级",
"DebugLogTooltip": "记录debug消息",
"LoadApplicationFileTooltip": "选择Switch格式的游戏并加载",
"LoadApplicationFolderTooltip": "选择一个解包后格式的Switch游戏并加载",
"OpenRyujinxFolderTooltip": "打开Ryujinx系统目录",
"DebugLogTooltip": "记录Debug消息",
"LoadApplicationFileTooltip": "选择 Switch 支持的游戏格式并加载",
"LoadApplicationFolderTooltip": "选择解包后的 Switch 游戏并加载",
"OpenRyujinxFolderTooltip": "打开 Ryujinx 系统目录",
"OpenRyujinxLogsTooltip": "打开日志存放的目录",
"ExitTooltip": "关闭Ryujinx",
"ExitTooltip": "关闭 Ryujinx",
"OpenSettingsTooltip": "打开设置窗口",
"OpenProfileManagerTooltip": "打开用户账管理",
"StopEmulationTooltip": "停止运行当前游戏并回到选择界面",
"CheckUpdatesTooltip": "检查新版本Ryujinx",
"OpenProfileManagerTooltip": "打开用户账管理界面",
"StopEmulationTooltip": "停止运行当前游戏并回到界面",
"CheckUpdatesTooltip": "检查 Ryujinx 新版本",
"OpenAboutTooltip": "打开'关于'窗口",
"GridSize": "网格尺寸",
"GridSizeTooltip": "调整网格模式的大小",
@ -482,7 +479,7 @@
"SettingsTabSystemAudioVolume": "音量: ",
"AudioVolumeTooltip": "调节音量",
"SettingsTabSystemEnableInternetAccess": "启用网络连接",
"EnableInternetAccessTooltip": "开启互联网访问。此选项打开后效果类似于Switch连接到互联网的状态。注意即使此选项关闭应用程序偶尔有可能连接到网络",
"EnableInternetAccessTooltip": "开启互联网访问。此选项打开后,效果类似于 Switch 连接到互联网的状态。\n注意即使此选项关闭,应用程序偶尔有可能连接到网络",
"GameListContextMenuManageCheatToolTip": "管理金手指",
"GameListContextMenuManageCheat": "管理金手指",
"ControllerSettingsStickRange": "范围",
@ -496,8 +493,8 @@
"SettingsTabCpuMemory": "CPU 内存",
"DialogUpdaterFlatpakNotSupportedMessage": "请通过 FlatHub 更新 Ryujinx。",
"UpdaterDisabledWarningTitle": "更新已禁用!",
"GameListContextMenuOpenSdModsDirectory": "打开Atmosphere MOD目录",
"GameListContextMenuOpenSdModsDirectoryToolTip": "打开包含应用程序MOD的其他Atmosphere SD卡目录",
"GameListContextMenuOpenSdModsDirectory": "打开 Atmosphere MOD 目录",
"GameListContextMenuOpenSdModsDirectoryToolTip": "打开包含应用程序 MOD 的额外 Atmosphere SD 卡目录",
"ControllerSettingsRotate90": "顺时针旋转 90°",
"IconSize": "图标尺寸",
"IconSizeTooltip": "更改游戏图标大小",
@ -509,14 +506,14 @@
"UserErrorApplicationNotFound": "找不到应用程序",
"UserErrorUnknown": "未知错误",
"UserErrorUndefined": "未定义错误",
"UserErrorNoKeysDescription": "Ryujinx找不到 'prod.keys' 文件",
"UserErrorNoFirmwareDescription": "Ryujinx找不到任何已安装的固件",
"UserErrorFirmwareParsingFailedDescription": "Ryujinx无法解密选择的固件。通常是由于过旧的密钥。",
"UserErrorApplicationNotFoundDescription": "Ryujinx在选中路径找不到有效的应用程序。",
"UserErrorNoKeysDescription": "Ryujinx 找不到 'prod.keys' 文件",
"UserErrorNoFirmwareDescription": "Ryujinx 找不到任何已安装的固件",
"UserErrorFirmwareParsingFailedDescription": "Ryujinx 无法解密选择的固件。通常是由于密钥过旧。",
"UserErrorApplicationNotFoundDescription": "Ryujinx 在选中路径找不到有效的应用程序。",
"UserErrorUnknownDescription": "发生未知错误!",
"UserErrorUndefinedDescription": "发生了未定义错误!此类错误不应出现,请联系开发人员!",
"OpenSetupGuideMessage": "打开设置教程",
"NoUpdate": "没有新版",
"NoUpdate": "无更新",
"TitleUpdateVersionLabel": "版本 {0} - {1}",
"RyujinxInfo": "Ryujinx - 信息",
"RyujinxConfirm": "Ryujinx - 确认",
@ -527,7 +524,7 @@
"SoftwareKeyboard": "软件键盘",
"DialogControllerAppletMessagePlayerRange": "游戏需要 {0} 个玩家()持有:\n\nTYPES: {1}\n\nPLAYERS: {2}\n\n{3}请打开设置界面,配置手柄;或者关闭窗口。",
"DialogControllerAppletMessage": "游戏需要刚好 {0} 个玩家()持有 with:\n\nTYPES: {1}\n\nPLAYERS: {2}\n\n{3}请打开设置界面,配置手柄;或者关闭窗口。",
"DialogControllerAppletDockModeSet": "现在处于主机模式,无法使用掌机操作方式\n\n",
"DialogControllerAppletDockModeSet": "目前处于主机模式,无法使用掌机操作方式\n\n",
"UpdaterRenaming": "正在删除旧文件...",
"UpdaterRenameFailed": "更新过程中无法重命名文件: {0}",
"UpdaterAddingFiles": "安装更新中...",
@ -537,14 +534,39 @@
"Docked": "主机模式",
"Handheld": "掌机模式",
"ConnectionError": "连接错误。",
"AboutPageDeveloperListMore": "{0} 以及等人...",
"AboutPageDeveloperListMore": "{0} 等开发者...",
"ApiError": "API错误。",
"LoadingHeading": "正在加载 {0}",
"CompilingPPTC": "编译PPTC中",
"LoadingHeading": "正在启动 {0}",
"CompilingPPTC": "编译PPTC缓存中",
"CompilingShaders": "编译着色器中",
"AllKeyboards": "所有键盘",
"OpenFileDialogTitle": "选择支持的文件格式",
"OpenFolderDialogTitle": "选择一个包含解包游戏的文件夹",
"AllSupportedFormats": "全部支持的格式",
"RyujinxUpdater": "Ryujinx 更新程序"
}
"RyujinxUpdater": "Ryujinx 更新程序",
"SettingsTabHotkeys": "快捷键",
"SettingsTabHotkeysHotkeys": "键盘快捷键",
"SettingsTabHotkeysToggleVsyncHotkey": "切换VSync",
"SettingsTabHotkeysScreenshotHotkey": "截屏",
"SettingsTabHotkeysShowUiHotkey": "隐藏UI",
"SettingsTabHotkeysPauseHotkey": "暂停",
"SettingsTabHotkeysToggleMuteHotkey": "静音",
"ControllerMotionTitle": "体感操作设置",
"ControllerRumbleTitle": "震动设置",
"SettingsSelectThemeFileDialogTitle": "选择主题文件",
"SettingsXamlThemeFile": "Xaml 主题文件",
"AvatarWindowTitle": "管理账户 - 头像",
"Amiibo": "Amiibo",
"Unknown": "未知",
"Usage": "使用",
"Writable": "可写入",
"SelectDlcDialogTitle": "选择 DLC 文件",
"SelectUpdateDialogTitle": "选择更新文件",
"UserProfileWindowTitle": "管理用户账户",
"CheatWindowTitle": "管理游戏金手指",
"DlcWindowTitle": "管理游戏 DLC",
"UpdateWindowTitle": "管理游戏更新",
"CheatWindowHeading": "适用于 {0} [{1}] 的金手指",
"DlcWindowHeading": "适用于 {0} [{1}] 的 DLC",
"GameUpdateWindowHeading": "适用于 {0} [{1}] 的更新"
}

View File

@ -23,6 +23,7 @@ using System;
using System.Buffers;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using static LibHac.Fs.ApplicationSaveDataManagement;
using Path = System.IO.Path;
@ -77,8 +78,12 @@ namespace Ryujinx.Ava.Common
if (result.IsFailure())
{
ContentDialogHelper.CreateErrorDialog(_owner,
string.Format(LocaleManager.Instance["DialogMessageCreateSaveErrorMessage"], result.ToStringWithName()));
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateErrorDialog(
_owner,
string.Format(LocaleManager.Instance["DialogMessageCreateSaveErrorMessage"], result.ToStringWithName()));
});
return false;
}
@ -94,8 +99,11 @@ namespace Ryujinx.Ava.Common
return true;
}
ContentDialogHelper.CreateErrorDialog(_owner,
string.Format(LocaleManager.Instance["DialogMessageFindSaveErrorMessage"], result.ToStringWithName()));
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateErrorDialog(_owner,
string.Format(LocaleManager.Instance["DialogMessageFindSaveErrorMessage"], result.ToStringWithName()));
});
return false;
}
@ -137,7 +145,7 @@ namespace Ryujinx.Ava.Common
}
}
public static async void ExtractSection(NcaSectionType ncaSectionType, string titleFilePath,
public static async Task ExtractSection(NcaSectionType ncaSectionType, string titleFilePath,
int programIndex = 0)
{
OpenFolderDialog folderDialog = new() { Title = LocaleManager.Instance["FolderDialogExtractTitle"] };
@ -222,9 +230,9 @@ namespace Ryujinx.Ava.Common
{
Logger.Error?.Print(LogClass.Application,
"Extraction failure. The main NCA was not present in the selected file");
Dispatcher.UIThread.InvokeAsync(() =>
Dispatcher.UIThread.InvokeAsync(async () =>
{
ContentDialogHelper.CreateErrorDialog(_owner, LocaleManager.Instance["DialogNcaExtractionMainNcaNotFoundErrorMessage"]);
await ContentDialogHelper.CreateErrorDialog(_owner, LocaleManager.Instance["DialogNcaExtractionMainNcaNotFoundErrorMessage"]);
});
return;
}
@ -263,9 +271,9 @@ namespace Ryujinx.Ava.Common
{
Logger.Error?.Print(LogClass.Application,
$"LibHac returned error code: {resultCode.Value.ErrorCode}");
Dispatcher.UIThread.InvokeAsync(() =>
Dispatcher.UIThread.InvokeAsync(async () =>
{
ContentDialogHelper.CreateErrorDialog(_owner, LocaleManager.Instance["DialogNcaExtractionCheckLogErrorMessage"]);
await ContentDialogHelper.CreateErrorDialog(_owner, LocaleManager.Instance["DialogNcaExtractionCheckLogErrorMessage"]);
});
}
else if (resultCode.Value.IsSuccess())
@ -288,9 +296,9 @@ namespace Ryujinx.Ava.Common
}
catch (ArgumentException ex)
{
Dispatcher.UIThread.InvokeAsync(() =>
Dispatcher.UIThread.InvokeAsync(async () =>
{
ContentDialogHelper.CreateErrorDialog(_owner, ex.Message);
await ContentDialogHelper.CreateErrorDialog(_owner, ex.Message);
});
}
}

View File

@ -73,8 +73,11 @@ namespace Ryujinx.Modules
}
catch
{
ContentDialogHelper.CreateWarningDialog(mainWindow, LocaleManager.Instance["DialogUpdaterConvertFailedMessage"], LocaleManager.Instance["DialogUpdaterCancelUpdateMessage"]);
Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!");
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateWarningDialog(mainWindow, LocaleManager.Instance["DialogUpdaterConvertFailedMessage"], LocaleManager.Instance["DialogUpdaterCancelUpdateMessage"]);
});
return;
}
@ -106,7 +109,10 @@ namespace Ryujinx.Modules
{
if (showVersionUpToDate)
{
ContentDialogHelper.CreateUpdaterInfoDialog(mainWindow, LocaleManager.Instance["DialogUpdaterAlreadyOnLatestVersionMessage"], "");
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateUpdaterInfoDialog(mainWindow, LocaleManager.Instance["DialogUpdaterAlreadyOnLatestVersionMessage"], "");
});
}
return;
@ -121,7 +127,10 @@ namespace Ryujinx.Modules
{
if (showVersionUpToDate)
{
ContentDialogHelper.CreateUpdaterInfoDialog(mainWindow, LocaleManager.Instance["DialogUpdaterAlreadyOnLatestVersionMessage"], "");
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateUpdaterInfoDialog(mainWindow, LocaleManager.Instance["DialogUpdaterAlreadyOnLatestVersionMessage"], "");
});
}
return;
@ -131,7 +140,10 @@ namespace Ryujinx.Modules
catch (Exception exception)
{
Logger.Error?.Print(LogClass.Application, exception.Message);
ContentDialogHelper.CreateErrorDialog(mainWindow, LocaleManager.Instance["DialogUpdaterFailedToGetVersionMessage"]);
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateErrorDialog(mainWindow, LocaleManager.Instance["DialogUpdaterFailedToGetVersionMessage"]);
});
return;
}
@ -142,8 +154,11 @@ namespace Ryujinx.Modules
}
catch
{
ContentDialogHelper.CreateWarningDialog(mainWindow, LocaleManager.Instance["DialogUpdaterConvertFailedGithubMessage"], LocaleManager.Instance["DialogUpdaterCancelUpdateMessage"]);
Logger.Error?.Print(LogClass.Application, "Failed to convert the received Ryujinx version from Github!");
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateWarningDialog(mainWindow, LocaleManager.Instance["DialogUpdaterConvertFailedGithubMessage"], LocaleManager.Instance["DialogUpdaterCancelUpdateMessage"]);
});
return;
}
@ -152,7 +167,10 @@ namespace Ryujinx.Modules
{
if (showVersionUpToDate)
{
ContentDialogHelper.CreateUpdaterInfoDialog(mainWindow, LocaleManager.Instance["DialogUpdaterAlreadyOnLatestVersionMessage"], "");
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateUpdaterInfoDialog(mainWindow, LocaleManager.Instance["DialogUpdaterAlreadyOnLatestVersionMessage"], "");
});
}
Running = false;
@ -180,10 +198,12 @@ namespace Ryujinx.Modules
_buildSize = -1;
}
}
// Show a message asking the user if they want to update
UpdaterWindow updateDialog = new(mainWindow, newVersion, _buildUrl);
await updateDialog.ShowDialog(mainWindow);
Dispatcher.UIThread.Post(async () =>
{
// Show a message asking the user if they want to update
UpdaterWindow updateDialog = new(mainWindow, newVersion, _buildUrl);
await updateDialog.ShowDialog(mainWindow);
});
}
private static HttpClient ConstructHttpClient()
@ -522,6 +542,7 @@ namespace Ryujinx.Modules
updateDialog.ButtonBox.IsVisible = true;
}
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
public static bool CanUpdate(bool showWarnings, StyleableWindow parent)
{
#if !DISABLE_UPDATER
@ -577,6 +598,7 @@ namespace Ryujinx.Modules
return false;
#endif
}
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
// NOTE: This method should always reflect the latest build layout.s
private static IEnumerable<string> EnumerateFilesToDelete()

View File

@ -5,8 +5,6 @@
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>1.0.0-dirty</Version>
<TieredCompilation>false</TieredCompilation>
<TieredCompilationQuickJit>false</TieredCompilationQuickJit>
<DefineConstants Condition=" '$(ExtraDefineConstants)' != '' ">$(DefineConstants);$(ExtraDefineConstants)</DefineConstants>
<RootNamespace>Ryujinx.Ava</RootNamespace>
<ApplicationIcon>Ryujinx.ico</ApplicationIcon>

View File

@ -92,7 +92,7 @@ namespace Ryujinx.Ava.Ui.Applet
}
catch (Exception ex)
{
ContentDialogHelper.CreateErrorDialog(_parent, string.Format(LocaleManager.Instance["DialogMessageDialogErrorExceptionMessage"], ex));
await ContentDialogHelper.CreateErrorDialog(_parent, string.Format(LocaleManager.Instance["DialogMessageDialogErrorExceptionMessage"], ex));
dialogCloseEvent.Set();
}
@ -126,7 +126,7 @@ namespace Ryujinx.Ava.Ui.Applet
catch (Exception ex)
{
error = true;
ContentDialogHelper.CreateErrorDialog(_parent, string.Format(LocaleManager.Instance["DialogSoftwareKeyboardErrorExceptionMessage"], ex));
await ContentDialogHelper.CreateErrorDialog(_parent, string.Format(LocaleManager.Instance["DialogSoftwareKeyboardErrorExceptionMessage"], ex));
}
finally
{
@ -181,7 +181,7 @@ namespace Ryujinx.Ava.Ui.Applet
catch (Exception ex)
{
dialogCloseEvent.Set();
ContentDialogHelper.CreateErrorDialog(_parent, string.Format(LocaleManager.Instance["DialogErrorAppletErrorExceptionMessage"], ex));
await ContentDialogHelper.CreateErrorDialog(_parent, string.Format(LocaleManager.Instance["DialogErrorAppletErrorExceptionMessage"], ex));
}
});

View File

@ -235,7 +235,7 @@ namespace Ryujinx.Ava.Ui.Controls
return new(mainText, secondaryText);
}
internal static async void CreateUpdaterInfoDialog(StyleableWindow window, string primary, string secondaryText)
internal static async Task CreateUpdaterInfoDialog(StyleableWindow window, string primary, string secondaryText)
{
await ShowContentDialog(
window,
@ -248,7 +248,7 @@ namespace Ryujinx.Ava.Ui.Controls
(int)Symbol.Important);
}
internal static async void ShowNotAvailableMessage(StyleableWindow window)
internal static async Task ShowNotAvailableMessage(StyleableWindow window)
{
// Temporary placeholder for features to be added
await ShowContentDialog(
@ -262,7 +262,7 @@ namespace Ryujinx.Ava.Ui.Controls
(int)Symbol.Important);
}
internal static async void CreateWarningDialog(StyleableWindow window, string primary, string secondaryText)
internal static async Task CreateWarningDialog(StyleableWindow window, string primary, string secondaryText)
{
await ShowContentDialog(
window,
@ -275,7 +275,7 @@ namespace Ryujinx.Ava.Ui.Controls
(int)Symbol.Important);
}
internal static async void CreateErrorDialog(StyleableWindow owner, string errorMessage, string secondaryErrorMessage = "")
internal static async Task CreateErrorDialog(StyleableWindow owner, string errorMessage, string secondaryErrorMessage = "")
{
Logger.Error?.Print(LogClass.Application, errorMessage);

View File

@ -0,0 +1,35 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
x:Class="Ryujinx.Ava.Ui.Controls.ProfileImageSelectionDialog"
SizeToContent="WidthAndHeight"
WindowStartupLocation="CenterOwner"
Title="{Locale:Locale ProfileImageSelectionTitle}"
CanResize="false">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="5,10,5, 5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="70" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock FontWeight="Bold" FontSize="18" HorizontalAlignment="Center" Grid.Row="1"
Text="{Locale:Locale ProfileImageSelectionHeader}" />
<TextBlock FontWeight="Bold" Grid.Row="2" Margin="10" MaxWidth="400" TextWrapping="Wrap"
HorizontalAlignment="Center" TextAlignment="Center" Text="{Locale:Locale ProfileImageSelectionNote}" />
<StackPanel Margin="5,0" Spacing="10" Grid.Row="4" HorizontalAlignment="Center"
Orientation="Horizontal">
<Button Name="Import" Click="Import_OnClick" Width="200">
<TextBlock Text="{Locale:Locale ProfileImageSelectionImportImage}" />
</Button>
<Button Name="SelectFirmwareImage" IsEnabled="{Binding FirmwareFound}" Click="SelectFirmwareImage_OnClick"
Width="200">
<TextBlock Text="{Locale:Locale ProfileImageSelectionSelectAvatar}" />
</Button>
</StackPanel>
</Grid>
</Window>

View File

@ -0,0 +1,105 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.Windows;
using Ryujinx.HLE.FileSystem;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using System.IO;
using Image = SixLabors.ImageSharp.Image;
namespace Ryujinx.Ava.Ui.Controls
{
public class ProfileImageSelectionDialog : StyleableWindow
{
private readonly ContentManager _contentManager;
public bool FirmwareFound => _contentManager.GetCurrentFirmwareVersion() != null;
public byte[] BufferImageProfile { get; set; }
public ProfileImageSelectionDialog(ContentManager contentManager)
{
_contentManager = contentManager;
DataContext = this;
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
public ProfileImageSelectionDialog()
{
DataContext = this;
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private async void Import_OnClick(object sender, RoutedEventArgs e)
{
OpenFileDialog dialog = new();
dialog.Filters.Add(new FileDialogFilter
{
Name = LocaleManager.Instance["AllSupportedFormats"],
Extensions = { "jpg", "jpeg", "png", "bmp" }
});
dialog.Filters.Add(new FileDialogFilter { Name = "JPEG", Extensions = { "jpg", "jpeg" } });
dialog.Filters.Add(new FileDialogFilter { Name = "PNG", Extensions = { "png" } });
dialog.Filters.Add(new FileDialogFilter { Name = "BMP", Extensions = { "bmp" } });
dialog.AllowMultiple = false;
string[] image = await dialog.ShowAsync(this);
if (image != null)
{
if (image.Length > 0)
{
string imageFile = image[0];
ProcessProfileImage(File.ReadAllBytes(imageFile));
}
Close();
}
}
private async void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e)
{
if (FirmwareFound)
{
AvatarWindow window = new(_contentManager);
await window.ShowDialog(this);
BufferImageProfile = window.SelectedImage;
Close();
}
}
private void ProcessProfileImage(byte[] buffer)
{
using (Image image = Image.Load(buffer))
{
image.Mutate(x => x.Resize(256, 256));
using (MemoryStream streamJpg = new())
{
image.SaveAsJpeg(streamJpg);
BufferImageProfile = streamJpg.ToArray();
}
}
}
}
}

View File

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Ui.Models
{
public class Amiibo
{
public struct AmiiboJson
{
[JsonPropertyName("amiibo")] public List<AmiiboApi> Amiibo { get; set; }
[JsonPropertyName("lastUpdated")] public DateTime LastUpdated { get; set; }
}
public struct AmiiboApi
{
[JsonPropertyName("name")] public string Name { get; set; }
[JsonPropertyName("head")] public string Head { get; set; }
[JsonPropertyName("tail")] public string Tail { get; set; }
[JsonPropertyName("image")] public string Image { get; set; }
[JsonPropertyName("amiiboSeries")] public string AmiiboSeries { get; set; }
[JsonPropertyName("character")] public string Character { get; set; }
[JsonPropertyName("gameSeries")] public string GameSeries { get; set; }
[JsonPropertyName("type")] public string Type { get; set; }
[JsonPropertyName("release")] public Dictionary<string, string> Release { get; set; }
[JsonPropertyName("gamesSwitch")] public List<AmiiboApiGamesSwitch> GamesSwitch { get; set; }
public override string ToString()
{
return Name;
}
public string GetId()
{
return Head + Tail;
}
public override bool Equals(object obj)
{
if (obj is AmiiboApi amiibo)
{
return amiibo.Head + amiibo.Tail == Head + Tail;
}
return false;
}
public override int GetHashCode()
{
return base.GetHashCode();
}
}
public class AmiiboApiGamesSwitch
{
[JsonPropertyName("amiiboUsage")] public List<AmiiboApiUsage> AmiiboUsage { get; set; }
[JsonPropertyName("gameID")] public List<string> GameId { get; set; }
[JsonPropertyName("gameName")] public string GameName { get; set; }
}
public class AmiiboApiUsage
{
[JsonPropertyName("Usage")] public string Usage { get; set; }
[JsonPropertyName("write")] public bool Write { get; set; }
}
}
}

View File

@ -0,0 +1,37 @@
using Ryujinx.Ava.Ui.ViewModels;
using System;
namespace Ryujinx.Ava.Ui.Models
{
public class CheatModel : BaseModel
{
private bool _isEnabled;
public event EventHandler<bool> EnableToggled;
public CheatModel(string name, string buildId, bool isEnabled)
{
Name = name;
BuildId = buildId;
IsEnabled = isEnabled;
}
public bool IsEnabled
{
get => _isEnabled;
set
{
_isEnabled = value;
EnableToggled?.Invoke(this, _isEnabled);
OnPropertyChanged();
}
}
public string BuildId { get; }
public string BuildIdKey => $"{BuildId}-{Name}";
public string Name { get; }
public string CleanName => Name.Substring(1, Name.Length - 8);
}
}

View File

@ -0,0 +1,51 @@
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
namespace Ryujinx.Ava.Ui.Models
{
public class CheatsList : ObservableCollection<CheatModel>
{
public CheatsList(string buildId, string path)
{
BuildId = buildId;
Path = path;
CollectionChanged += CheatsList_CollectionChanged;
}
private void CheatsList_CollectionChanged(object sender,
NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
(e.NewItems[0] as CheatModel).EnableToggled += Item_EnableToggled;
}
}
private void Item_EnableToggled(object sender, bool e)
{
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsEnabled)));
}
public string BuildId { get; }
public string Path { get; }
public bool IsEnabled
{
get
{
return this.ToList().TrueForAll(x => x.IsEnabled);
}
set
{
foreach (var cheat in this)
{
cheat.IsEnabled = value;
}
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsEnabled)));
}
}
}
}

View File

@ -0,0 +1,18 @@
namespace Ryujinx.Ava.Ui.Models
{
public class DlcModel
{
public bool IsEnabled { get; set; }
public string TitleId { get; }
public string ContainerPath { get; }
public string FullPath { get; }
public DlcModel(string titleId, string containerPath, string fullPath, bool isEnabled)
{
TitleId = titleId;
ContainerPath = containerPath;
FullPath = fullPath;
IsEnabled = isEnabled;
}
}
}

View File

@ -1,6 +1,6 @@
namespace Ryujinx.Ava.Ui.Models
{
internal class ProfileImageModel
public class ProfileImageModel
{
public ProfileImageModel(string name, byte[] data)
{

View File

@ -9,8 +9,11 @@ namespace Ryujinx.Ava.Ui.Models
public bool IsNoUpdate { get; }
public ApplicationControlProperty Control { get; }
public string Path { get; }
public string Label => IsNoUpdate ? LocaleManager.Instance["NoUpdate"] :
string.Format(LocaleManager.Instance["TitleUpdateVersionLabel"], Control.DisplayVersionString.ToString(), Path);
public string Label => IsNoUpdate
? LocaleManager.Instance["NoUpdate"]
: string.Format(LocaleManager.Instance["TitleUpdateVersionLabel"], Control.DisplayVersionString.ToString(),
Path);
public TitleUpdateModel(ApplicationControlProperty control, string path, bool isNoUpdate = false)
{

View File

@ -0,0 +1,61 @@
using Ryujinx.Ava.Ui.ViewModels;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Profile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
namespace Ryujinx.Ava.Ui.Models
{
public class UserProfile : BaseModel
{
private readonly Profile _profile;
private byte[] _image;
private string _name;
private UserId _userId;
public byte[] Image
{
get => _image;
set
{
_image = value;
OnPropertyChanged();
}
}
public UserId UserId
{
get => _userId;
set
{
_userId = value;
OnPropertyChanged();
}
}
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged();
}
}
public UserProfile(Profile profile)
{
_profile = profile;
Image = profile.Image;
Name = profile.Name;
UserId = profile.UserId;
}
public bool IsOpened => _profile.AccountState == AccountState.Open;
public void UpdateState()
{
OnPropertyChanged(nameof(IsOpened));
OnPropertyChanged(nameof(Name));
}
}
}

View File

@ -0,0 +1,450 @@
using Avalonia;
using Avalonia.Collections;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.Controls;
using Ryujinx.Ava.Ui.Models;
using Ryujinx.Ava.Ui.Windows;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace Ryujinx.Ava.Ui.ViewModels
{
public class AmiiboWindowViewModel : BaseModel, IDisposable
{
private const string DefaultJson = "{ \"amiibo\": [] }";
private const float AmiiboImageSize = 350f;
private readonly string _amiiboJsonPath;
private readonly byte[] _amiiboLogoBytes;
private readonly HttpClient _httpClient;
private readonly StyleableWindow _owner;
private Bitmap _amiiboImage;
private List<Amiibo.AmiiboApi> _amiiboList;
private AvaloniaList<Amiibo.AmiiboApi> _amiibos;
private ObservableCollection<string> _amiiboSeries;
private int _amiiboSelectedIndex;
private int _seriesSelectedIndex;
private bool _enableScanning;
private bool _showAllAmiibo;
private bool _useRandomUuid;
private string _usage;
public AmiiboWindowViewModel(StyleableWindow owner, string lastScannedAmiiboId, string titleId)
{
_owner = owner;
_httpClient = new HttpClient { Timeout = TimeSpan.FromMilliseconds(5000) };
LastScannedAmiiboId = lastScannedAmiiboId;
TitleId = titleId;
Directory.CreateDirectory(Path.Join(AppDataManager.BaseDirPath, "system", "amiibo"));
_amiiboJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", "Amiibo.json");
_amiiboList = new List<Amiibo.AmiiboApi>();
_amiiboSeries = new ObservableCollection<string>();
_amiibos = new AvaloniaList<Amiibo.AmiiboApi>();
_amiiboLogoBytes = EmbeddedResources.Read("Ryujinx.Ui.Common/Resources/Logo_Amiibo.png");
_ = LoadContentAsync();
}
public AmiiboWindowViewModel() { }
public string TitleId { get; set; }
public string LastScannedAmiiboId { get; set; }
public UserResult Response { get; private set; }
public bool UseRandomUuid
{
get => _useRandomUuid;
set
{
_useRandomUuid = value;
OnPropertyChanged();
}
}
public bool ShowAllAmiibo
{
get => _showAllAmiibo;
set
{
_showAllAmiibo = value;
#pragma warning disable 4014
ParseAmiiboData();
#pragma warning restore 4014
OnPropertyChanged();
}
}
public AvaloniaList<Amiibo.AmiiboApi> AmiiboList
{
get => _amiibos;
set
{
_amiibos = value;
OnPropertyChanged();
}
}
public ObservableCollection<string> AmiiboSeries
{
get => _amiiboSeries;
set
{
_amiiboSeries = value;
OnPropertyChanged();
}
}
public int SeriesSelectedIndex
{
get => _seriesSelectedIndex;
set
{
_seriesSelectedIndex = value;
FilterAmiibo();
OnPropertyChanged();
}
}
public int AmiiboSelectedIndex
{
get => _amiiboSelectedIndex;
set
{
_amiiboSelectedIndex = value;
EnableScanning = _amiiboSelectedIndex >= 0 && _amiiboSelectedIndex < _amiibos.Count;
SetAmiiboDetails();
OnPropertyChanged();
}
}
public Bitmap AmiiboImage
{
get => _amiiboImage;
set
{
_amiiboImage = value;
OnPropertyChanged();
}
}
public string Usage
{
get => _usage;
set
{
_usage = value;
OnPropertyChanged();
}
}
public bool EnableScanning
{
get => _enableScanning;
set
{
_enableScanning = value;
OnPropertyChanged();
}
}
public void Dispose()
{
_httpClient.Dispose();
}
private async Task LoadContentAsync()
{
string amiiboJsonString = DefaultJson;
if (File.Exists(_amiiboJsonPath))
{
amiiboJsonString = File.ReadAllText(_amiiboJsonPath);
if (await NeedsUpdate(JsonSerializer.Deserialize<Amiibo.AmiiboJson>(amiiboJsonString).LastUpdated))
{
amiiboJsonString = await DownloadAmiiboJson();
}
}
else
{
try
{
amiiboJsonString = await DownloadAmiiboJson();
}
catch
{
ShowInfoDialog();
}
}
_amiiboList = JsonSerializer.Deserialize<Amiibo.AmiiboJson>(amiiboJsonString).Amiibo;
_amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList();
ParseAmiiboData();
}
private void ParseAmiiboData()
{
_amiiboSeries.Clear();
_amiibos.Clear();
for (int i = 0; i < _amiiboList.Count; i++)
{
if (!_amiiboSeries.Contains(_amiiboList[i].AmiiboSeries))
{
if (!ShowAllAmiibo)
{
foreach (Amiibo.AmiiboApiGamesSwitch game in _amiiboList[i].GamesSwitch)
{
if (game != null)
{
if (game.GameId.Contains(TitleId))
{
AmiiboSeries.Add(_amiiboList[i].AmiiboSeries);
break;
}
}
}
}
else
{
AmiiboSeries.Add(_amiiboList[i].AmiiboSeries);
}
}
}
if (LastScannedAmiiboId != "")
{
SelectLastScannedAmiibo();
}
else
{
SeriesSelectedIndex = 0;
}
}
private void SelectLastScannedAmiibo()
{
Amiibo.AmiiboApi scanned = _amiiboList.FirstOrDefault(amiibo => amiibo.GetId() == LastScannedAmiiboId);
SeriesSelectedIndex = AmiiboSeries.IndexOf(scanned.AmiiboSeries);
AmiiboSelectedIndex = AmiiboList.IndexOf(scanned);
}
private void FilterAmiibo()
{
_amiibos.Clear();
if (_seriesSelectedIndex < 0)
{
return;
}
List<Amiibo.AmiiboApi> amiiboSortedList = _amiiboList
.Where(amiibo => amiibo.AmiiboSeries == _amiiboSeries[SeriesSelectedIndex])
.OrderBy(amiibo => amiibo.Name).ToList();
for (int i = 0; i < amiiboSortedList.Count; i++)
{
if (!_amiibos.Contains(amiiboSortedList[i]))
{
if (!_showAllAmiibo)
{
foreach (Amiibo.AmiiboApiGamesSwitch game in amiiboSortedList[i].GamesSwitch)
{
if (game != null)
{
if (game.GameId.Contains(TitleId))
{
_amiibos.Add(amiiboSortedList[i]);
break;
}
}
}
}
else
{
_amiibos.Add(amiiboSortedList[i]);
}
}
}
AmiiboSelectedIndex = 0;
}
private void SetAmiiboDetails()
{
ResetAmiiboPreview();
Usage = string.Empty;
if (_amiiboSelectedIndex < 0)
{
return;
}
Amiibo.AmiiboApi selected = _amiibos[_amiiboSelectedIndex];
string imageUrl = _amiiboList.FirstOrDefault(amiibo => amiibo.Equals(selected)).Image;
string usageString = "";
for (int i = 0; i < _amiiboList.Count; i++)
{
if (_amiiboList[i].Equals(selected))
{
bool writable = false;
foreach (Amiibo.AmiiboApiGamesSwitch item in _amiiboList[i].GamesSwitch)
{
if (item.GameId.Contains(TitleId))
{
foreach (Amiibo.AmiiboApiUsage usageItem in item.AmiiboUsage)
{
usageString += Environment.NewLine +
$"- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}";
writable = usageItem.Write;
}
}
}
if (usageString.Length == 0)
{
usageString = LocaleManager.Instance["Unknown"] + ".";
}
Usage = $"{LocaleManager.Instance["Usage"]} {(writable ? $" ({LocaleManager.Instance["Writable"]})" : "")} : {usageString}";
}
}
_ = UpdateAmiiboPreview(imageUrl);
}
private async Task<bool> NeedsUpdate(DateTime oldLastModified)
{
try
{
HttpResponseMessage response =
await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, "https://amiibo.ryujinx.org/"));
if (response.IsSuccessStatusCode)
{
return response.Content.Headers.LastModified != oldLastModified;
}
return false;
}
catch
{
ShowInfoDialog();
return false;
}
}
private async Task<string> DownloadAmiiboJson()
{
HttpResponseMessage response = await _httpClient.GetAsync("https://amiibo.ryujinx.org/");
if (response.IsSuccessStatusCode)
{
string amiiboJsonString = await response.Content.ReadAsStringAsync();
using (FileStream dlcJsonStream = File.Create(_amiiboJsonPath, 4096, FileOptions.WriteThrough))
{
dlcJsonStream.Write(Encoding.UTF8.GetBytes(amiiboJsonString));
}
return amiiboJsonString;
}
await ContentDialogHelper.CreateInfoDialog(_owner, LocaleManager.Instance["DialogAmiiboApiTitle"],
LocaleManager.Instance["DialogAmiiboApiFailFetchMessage"],
LocaleManager.Instance["InputDialogOk"],
"",
LocaleManager.Instance["RyujinxInfo"]);
Close();
return DefaultJson;
}
private void Close()
{
Dispatcher.UIThread.Post(_owner.Close);
}
private async Task UpdateAmiiboPreview(string imageUrl)
{
HttpResponseMessage response = await _httpClient.GetAsync(imageUrl);
if (response.IsSuccessStatusCode)
{
byte[] amiiboPreviewBytes = await response.Content.ReadAsByteArrayAsync();
using (MemoryStream memoryStream = new(amiiboPreviewBytes))
{
Bitmap bitmap = new(memoryStream);
double ratio = Math.Min(AmiiboImageSize / bitmap.Size.Width,
AmiiboImageSize / bitmap.Size.Height);
int resizeHeight = (int)(bitmap.Size.Height * ratio);
int resizeWidth = (int)(bitmap.Size.Width * ratio);
AmiiboImage = bitmap.CreateScaledBitmap(new PixelSize(resizeWidth, resizeHeight));
}
}
}
private void ResetAmiiboPreview()
{
using (MemoryStream memoryStream = new(_amiiboLogoBytes))
{
Bitmap bitmap = new(memoryStream);
AmiiboImage = bitmap;
}
}
private async void ShowInfoDialog()
{
await ContentDialogHelper.CreateInfoDialog(_owner, LocaleManager.Instance["DialogAmiiboApiTitle"],
LocaleManager.Instance["DialogAmiiboApiConnectErrorMessage"],
LocaleManager.Instance["InputDialogOk"],
"",
LocaleManager.Instance["RyujinxInfo"]);
}
}
}

View File

@ -0,0 +1,363 @@
using Avalonia.Media;
using DynamicData;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Ncm;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Ui.Models;
using Ryujinx.HLE.FileSystem;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Color = Avalonia.Media.Color;
namespace Ryujinx.Ava.Ui.ViewModels
{
internal class AvatarProfileViewModel : BaseModel, IDisposable
{
private const int MaxImageTasks = 4;
private static readonly Dictionary<string, byte[]> _avatarStore = new();
private static bool _isPreloading;
private static Action _loadCompleteAction;
private ObservableCollection<ProfileImageModel> _images;
private Color _backgroundColor = Colors.White;
private int _selectedIndex;
private int _imagesLoaded;
private bool _isActive;
private byte[] _selectedImage;
private bool _isIndeterminate = true;
public bool IsActive
{
get => _isActive;
set => _isActive = value;
}
public AvatarProfileViewModel()
{
_images = new ObservableCollection<ProfileImageModel>();
}
public AvatarProfileViewModel(Action loadCompleteAction)
{
_images = new ObservableCollection<ProfileImageModel>();
if (_isPreloading)
{
_loadCompleteAction = loadCompleteAction;
}
else
{
ReloadImages();
}
}
public Color BackgroundColor
{
get => _backgroundColor;
set
{
_backgroundColor = value;
IsActive = false;
ReloadImages();
}
}
public ObservableCollection<ProfileImageModel> Images
{
get => _images;
set
{
_images = value;
OnPropertyChanged();
}
}
public bool IsIndeterminate
{
get => _isIndeterminate;
set
{
_isIndeterminate = value;
OnPropertyChanged();
}
}
public int ImageCount => _avatarStore.Count;
public int ImagesLoaded
{
get => _imagesLoaded;
set
{
_imagesLoaded = value;
OnPropertyChanged();
}
}
public int SelectedIndex
{
get => _selectedIndex;
set
{
_selectedIndex = value;
if (_selectedIndex == -1)
{
SelectedImage = null;
}
else
{
SelectedImage = _images[_selectedIndex].Data;
}
OnPropertyChanged();
}
}
public byte[] SelectedImage
{
get => _selectedImage;
private set => _selectedImage = value;
}
public void ReloadImages()
{
if (_isPreloading)
{
IsIndeterminate = false;
return;
}
Task.Run(() =>
{
IsActive = true;
Images.Clear();
int selectedIndex = _selectedIndex;
int index = 0;
ImagesLoaded = 0;
IsIndeterminate = false;
var keys = _avatarStore.Keys.ToList();
var newImages = new List<ProfileImageModel>();
var tasks = new List<Task>();
for (int i = 0; i < MaxImageTasks; i++)
{
var start = i;
tasks.Add(Task.Run(() => ImageTask(start)));
}
Task.WaitAll(tasks.ToArray());
Images.AddRange(newImages);
void ImageTask(int start)
{
for (int i = start; i < keys.Count; i += MaxImageTasks)
{
if (!IsActive)
{
return;
}
var key = keys[i];
var image = _avatarStore[keys[i]];
var data = ProcessImage(image);
newImages.Add(new ProfileImageModel(key, data));
if (index++ == selectedIndex)
{
SelectedImage = data;
}
Interlocked.Increment(ref _imagesLoaded);
OnPropertyChanged(nameof(ImagesLoaded));
}
}
});
}
private byte[] ProcessImage(byte[] data)
{
using (MemoryStream streamJpg = new())
{
Image avatarImage = Image.Load(data, new PngDecoder());
avatarImage.Mutate(x => x.BackgroundColor(new Rgba32(BackgroundColor.R,
BackgroundColor.G,
BackgroundColor.B,
BackgroundColor.A)));
avatarImage.SaveAsJpeg(streamJpg);
return streamJpg.ToArray();
}
}
public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem)
{
try
{
if (_avatarStore.Count > 0)
{
return;
}
_isPreloading = true;
string contentPath =
contentManager.GetInstalledContentPath(0x010000000000080A, StorageId.BuiltInSystem,
NcaContentType.Data);
string avatarPath = virtualFileSystem.SwitchPathToSystemPath(contentPath);
if (!string.IsNullOrWhiteSpace(avatarPath))
{
using (IStorage ncaFileStream = new LocalStorage(avatarPath, FileAccess.Read, FileMode.Open))
{
Nca nca = new(virtualFileSystem.KeySet, ncaFileStream);
IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
foreach (DirectoryEntryEx item in romfs.EnumerateEntries())
{
// TODO: Parse DatabaseInfo.bin and table.bin files for more accuracy.
if (item.Type == DirectoryEntryType.File && item.FullPath.Contains("chara") &&
item.FullPath.Contains("szs"))
{
using var file = new UniqueRef<IFile>();
romfs.OpenFile(ref file.Ref(), ("/" + item.FullPath).ToU8Span(), OpenMode.Read)
.ThrowIfFailure();
using (MemoryStream stream = new())
using (MemoryStream streamPng = new())
{
file.Get.AsStream().CopyTo(stream);
stream.Position = 0;
Image avatarImage = Image.LoadPixelData<Rgba32>(DecompressYaz0(stream), 256, 256);
avatarImage.SaveAsPng(streamPng);
_avatarStore.Add(item.FullPath, streamPng.ToArray());
}
}
}
}
}
}
finally
{
_isPreloading = false;
_loadCompleteAction?.Invoke();
}
}
private static byte[] DecompressYaz0(Stream stream)
{
using (BinaryReader reader = new(stream))
{
reader.ReadInt32(); // Magic
uint decodedLength = BinaryPrimitives.ReverseEndianness(reader.ReadUInt32());
reader.ReadInt64(); // Padding
byte[] input = new byte[stream.Length - stream.Position];
stream.Read(input, 0, input.Length);
uint inputOffset = 0;
byte[] output = new byte[decodedLength];
uint outputOffset = 0;
ushort mask = 0;
byte header = 0;
while (outputOffset < decodedLength)
{
if ((mask >>= 1) == 0)
{
header = input[inputOffset++];
mask = 0x80;
}
if ((header & mask) != 0)
{
if (outputOffset == output.Length)
{
break;
}
output[outputOffset++] = input[inputOffset++];
}
else
{
byte byte1 = input[inputOffset++];
byte byte2 = input[inputOffset++];
uint dist = (uint)((byte1 & 0xF) << 8) | byte2;
uint position = outputOffset - (dist + 1);
uint length = (uint)byte1 >> 4;
if (length == 0)
{
length = (uint)input[inputOffset++] + 0x12;
}
else
{
length += 2;
}
uint gap = outputOffset - position;
uint nonOverlappingLength = length;
if (nonOverlappingLength > gap)
{
nonOverlappingLength = gap;
}
Buffer.BlockCopy(output, (int)position, output, (int)outputOffset, (int)nonOverlappingLength);
outputOffset += nonOverlappingLength;
position += nonOverlappingLength;
length -= nonOverlappingLength;
while (length-- > 0)
{
output[outputOffset++] = output[position++];
}
}
}
return output;
}
}
public void Dispose()
{
_loadCompleteAction = null;
IsActive = false;
}
}
}

View File

@ -658,7 +658,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
return config;
}
public void LoadProfile()
public async void LoadProfile()
{
if (Device == 0)
{
@ -700,9 +700,9 @@ namespace Ryujinx.Ava.Ui.ViewModels
catch (JsonException) { }
catch (InvalidOperationException)
{
ContentDialogHelper.CreateErrorDialog(_owner.GetVisualRoot() as StyleableWindow,
String.Format(LocaleManager.Instance["DialogProfileInvalidProfileErrorMessage"], ProfileName));
Logger.Error?.Print(LogClass.Configuration, $"Profile {ProfileName} is incompatible with the current input configuration system.");
await ContentDialogHelper.CreateErrorDialog(_owner.GetVisualRoot() as StyleableWindow,
String.Format(LocaleManager.Instance["DialogProfileInvalidProfileErrorMessage"], ProfileName));
return;
}
@ -736,7 +736,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
if (ProfileName == LocaleManager.Instance["ControllerSettingsProfileDefault"])
{
ContentDialogHelper.CreateErrorDialog(_owner.GetVisualRoot() as StyleableWindow, LocaleManager.Instance["DialogProfileDefaultProfileOverwriteErrorMessage"]);
await ContentDialogHelper.CreateErrorDialog(_owner.GetVisualRoot() as StyleableWindow, LocaleManager.Instance["DialogProfileDefaultProfileOverwriteErrorMessage"]);
return;
}
@ -769,7 +769,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
}
else
{
ContentDialogHelper.CreateErrorDialog(_owner.GetVisualRoot() as StyleableWindow, LocaleManager.Instance["DialogProfileInvalidProfileNameErrorMessage"]);
await ContentDialogHelper.CreateErrorDialog(_owner.GetVisualRoot() as StyleableWindow, LocaleManager.Instance["DialogProfileInvalidProfileNameErrorMessage"]);
}
}
}

View File

@ -67,6 +67,8 @@ namespace Ryujinx.Ava.Ui.ViewModels
private bool _isPaused;
private bool _showContent = true;
private bool _isLoadingIndeterminate = true;
private bool _showAll;
private string _lastScannedAmiiboId;
private ReadOnlyObservableCollection<ApplicationData> _appsObservableList;
public string TitleName { get; internal set; }
@ -695,15 +697,28 @@ namespace Ryujinx.Ava.Ui.ViewModels
}
}
public void OpenAmiiboWindow()
public async void OpenAmiiboWindow()
{
if (!_isAmiiboRequested)
{
return;
}
if (_owner.AppHost.Device.System.SearchingForAmiibo(out int deviceId))
{
string titleId = _owner.AppHost.Device.Application.TitleIdText.ToUpper();
AmiiboWindow window = new(_showAll, _lastScannedAmiiboId, titleId);
// TODO : Implement Amiibo window
ContentDialogHelper.ShowNotAvailableMessage(_owner);
await window.ShowDialog(_owner);
if (window.IsScanned)
{
_showAll = window.ViewModel.ShowAllAmiibo;
_lastScannedAmiiboId = window.ScannedAmiibo.GetId();
_owner.AppHost.Device.System.ScanAmiibo(deviceId, _lastScannedAmiiboId, window.ViewModel.UseRandomUuid);
}
}
}
public void HandleShaderProgress(Switch emulationContext)
@ -953,10 +968,11 @@ namespace Ryujinx.Ava.Ui.ViewModels
LoadConfigurableHotKeys();
}
public void ManageProfiles()
public async void ManageProfiles()
{
// TODO : Implement Profiles window
ContentDialogHelper.ShowNotAvailableMessage(_owner);
UserProfileWindow window = new(_owner.AccountManager, _owner.ContentManager, _owner.VirtualFileSystem);
await window.ShowDialog(_owner);
}
public async void OpenAboutWindow()
@ -1031,8 +1047,11 @@ namespace Ryujinx.Ava.Ui.ViewModels
if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture,
out ulong titleIdNumber))
{
ContentDialogHelper.CreateErrorDialog(_owner,
LocaleManager.Instance["DialogRyujinxErrorMessage"], LocaleManager.Instance["DialogInvalidTitleIdErrorMessage"]);
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateErrorDialog(_owner,
LocaleManager.Instance["DialogRyujinxErrorMessage"], LocaleManager.Instance["DialogInvalidTitleIdErrorMessage"]);
});
return;
}
@ -1139,7 +1158,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
}
catch (Exception e)
{
ContentDialogHelper.CreateErrorDialog(_owner, string.Format(LocaleManager.Instance["DialogPPTCDeletionErrorMessage"], file.Name, e));
await ContentDialogHelper.CreateErrorDialog(_owner, string.Format(LocaleManager.Instance["DialogPPTCDeletionErrorMessage"], file.Name, e));
}
}
}
@ -1200,7 +1219,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
}
catch (Exception e)
{
ContentDialogHelper.CreateErrorDialog(_owner, string.Format(LocaleManager.Instance["DialogPPTCDeletionErrorMessage"], directory.Name, e));
await ContentDialogHelper.CreateErrorDialog(_owner, string.Format(LocaleManager.Instance["DialogPPTCDeletionErrorMessage"], directory.Name, e));
}
}
}
@ -1213,7 +1232,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
}
catch (Exception e)
{
ContentDialogHelper.CreateErrorDialog(_owner, string.Format(LocaleManager.Instance["ShaderCachePurgeError"], file.Name, e));
await ContentDialogHelper.CreateErrorDialog(_owner, string.Format(LocaleManager.Instance["ShaderCachePurgeError"], file.Name, e));
}
}
}
@ -1227,33 +1246,60 @@ namespace Ryujinx.Ava.Ui.ViewModels
}
}
public void OpenTitleUpdateManager()
public async void OpenTitleUpdateManager()
{
// TODO : Implement Update window
ContentDialogHelper.ShowNotAvailableMessage(_owner);
var selection = SelectedApplication;
if (selection != null)
{
TitleUpdateWindow titleUpdateManager =
new(_owner.VirtualFileSystem, selection.TitleId, selection.TitleName);
await titleUpdateManager.ShowDialog(_owner);
}
}
public void OpenDlcManager()
public async void OpenDlcManager()
{
// TODO : Implement Dlc window
ContentDialogHelper.ShowNotAvailableMessage(_owner);
var selection = SelectedApplication;
if (selection != null)
{
DlcManagerWindow dlcManager = new(_owner.VirtualFileSystem, ulong.Parse(selection.TitleId, NumberStyles.HexNumber), selection.TitleName);
await dlcManager.ShowDialog(_owner);
}
}
public void OpenCheatManager()
public async void OpenCheatManager()
{
// TODO : Implement cheat window
ContentDialogHelper.ShowNotAvailableMessage(_owner);
var selection = SelectedApplication;
if (selection != null)
{
CheatWindow cheatManager = new(_owner.VirtualFileSystem, selection.TitleId, selection.TitleName);
await cheatManager.ShowDialog(_owner);
}
}
public void OpenCheatManagerForCurrentApp()
public async void OpenCheatManagerForCurrentApp()
{
if (!IsGameRunning)
{
return;
}
// TODO : Implement cheat window
ContentDialogHelper.ShowNotAvailableMessage(_owner);
var application = _owner.AppHost.Device.Application;
if (application != null)
{
CheatWindow cheatManager = new(_owner.VirtualFileSystem, application.TitleIdText, application.TitleName);
await cheatManager.ShowDialog(_owner);
_owner.AppHost.Device.EnableCheats();
}
}
public void OpenDeviceSaveDirectory()
@ -1267,8 +1313,11 @@ namespace Ryujinx.Ava.Ui.ViewModels
if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture,
out ulong titleIdNumber))
{
ContentDialogHelper.CreateErrorDialog(_owner,
LocaleManager.Instance["DialogRyujinxErrorMessage"], LocaleManager.Instance["DialogInvalidTitleIdErrorMessage"]);
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateErrorDialog(_owner,
LocaleManager.Instance["DialogRyujinxErrorMessage"], LocaleManager.Instance["DialogInvalidTitleIdErrorMessage"]);
});
return;
}
@ -1290,8 +1339,11 @@ namespace Ryujinx.Ava.Ui.ViewModels
if (!ulong.TryParse(selection.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture,
out ulong titleIdNumber))
{
ContentDialogHelper.CreateErrorDialog(_owner,
LocaleManager.Instance["DialogRyujinxErrorMessage"], LocaleManager.Instance["DialogInvalidTitleIdErrorMessage"]);
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateErrorDialog(_owner,
LocaleManager.Instance["DialogRyujinxErrorMessage"], LocaleManager.Instance["DialogInvalidTitleIdErrorMessage"]);
});
return;
}
@ -1307,30 +1359,30 @@ namespace Ryujinx.Ava.Ui.ViewModels
ApplicationHelper.OpenSaveDir(in filter, titleId, data.ControlHolder, data.TitleName);
}
private void ExtractLogo()
private async void ExtractLogo()
{
var selection = SelectedApplication;
if (selection != null)
{
ApplicationHelper.ExtractSection(NcaSectionType.Logo, selection.Path);
await ApplicationHelper.ExtractSection(NcaSectionType.Logo, selection.Path);
}
}
private void ExtractRomFs()
private async void ExtractRomFs()
{
var selection = SelectedApplication;
if (selection != null)
{
ApplicationHelper.ExtractSection(NcaSectionType.Data, selection.Path);
await ApplicationHelper.ExtractSection(NcaSectionType.Data, selection.Path);
}
}
private void ExtractExeFs()
private async void ExtractExeFs()
{
var selection = SelectedApplication;
if (selection != null)
{
ApplicationHelper.ExtractSection(NcaSectionType.Code, selection.Path);
await ApplicationHelper.ExtractSection(NcaSectionType.Code, selection.Path);
}
}
@ -1339,7 +1391,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
_owner.Close();
}
private async void HandleFirmwareInstallation(string path)
private async Task HandleFirmwareInstallation(string path)
{
try
{
@ -1349,7 +1401,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
if (firmwareVersion == null)
{
ContentDialogHelper.CreateErrorDialog(_owner, string.Format(LocaleManager.Instance["DialogFirmwareInstallerFirmwareNotFoundErrorMessage"], filename));
await ContentDialogHelper.CreateErrorDialog(_owner, string.Format(LocaleManager.Instance["DialogFirmwareInstallerFirmwareNotFoundErrorMessage"], filename));
return;
}
@ -1414,11 +1466,11 @@ namespace Ryujinx.Ava.Ui.ViewModels
}
catch (Exception ex)
{
Dispatcher.UIThread.InvokeAsync(() =>
Dispatcher.UIThread.InvokeAsync(async () =>
{
waitingDialog.Close();
ContentDialogHelper.CreateErrorDialog(_owner, ex.Message);
await ContentDialogHelper.CreateErrorDialog(_owner, ex.Message);
});
}
finally
@ -1439,7 +1491,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
}
catch (Exception ex)
{
ContentDialogHelper.CreateErrorDialog(_owner, ex.Message);
await ContentDialogHelper.CreateErrorDialog(_owner, ex.Message);
}
}
@ -1454,7 +1506,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
if (file != null && file.Length > 0)
{
HandleFirmwareInstallation(file[0]);
await HandleFirmwareInstallation(file[0]);
}
}
@ -1466,7 +1518,7 @@ namespace Ryujinx.Ava.Ui.ViewModels
if (!string.IsNullOrWhiteSpace(folder))
{
HandleFirmwareInstallation(folder);
await HandleFirmwareInstallation(folder);
}
}
}

View File

@ -0,0 +1,171 @@
using Avalonia.Threading;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.Controls;
using Ryujinx.Ava.Ui.Windows;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using UserProfile = Ryujinx.Ava.Ui.Models.UserProfile;
namespace Ryujinx.Ava.Ui.ViewModels
{
public class UserProfileViewModel : BaseModel, IDisposable
{
private const uint MaxProfileNameLength = 0x20;
private readonly UserProfileWindow _owner;
private UserProfile _selectedProfile;
private string _tempUserName;
public UserProfileViewModel()
{
Profiles = new ObservableCollection<UserProfile>();
}
public UserProfileViewModel(UserProfileWindow owner) : this()
{
_owner = owner;
LoadProfiles();
}
public ObservableCollection<UserProfile> Profiles { get; set; }
public UserProfile SelectedProfile
{
get => _selectedProfile;
set
{
_selectedProfile = value;
OnPropertyChanged(nameof(SelectedProfile));
OnPropertyChanged(nameof(IsSelectedProfileDeletable));
}
}
public bool IsSelectedProfileDeletable =>
_selectedProfile != null && _selectedProfile.UserId != AccountManager.DefaultUserId;
public void Dispose()
{
}
public void LoadProfiles()
{
Profiles.Clear();
var profiles = _owner.AccountManager.GetAllUsers()
.OrderByDescending(x => x.AccountState == AccountState.Open);
foreach (var profile in profiles)
{
Profiles.Add(new UserProfile(profile));
}
SelectedProfile = Profiles.FirstOrDefault(x => x.UserId == _owner.AccountManager.LastOpenedUser.UserId);
if (SelectedProfile == null)
{
SelectedProfile = Profiles.First();
if (SelectedProfile != null)
{
_owner.AccountManager.OpenUser(_selectedProfile.UserId);
}
}
}
public async void ChooseProfileImage()
{
await SelectProfileImage();
}
public async Task SelectProfileImage(bool isNewUser = false)
{
ProfileImageSelectionDialog selectionDialog = new(_owner.ContentManager);
await selectionDialog.ShowDialog(_owner);
if (selectionDialog.BufferImageProfile != null)
{
if (isNewUser)
{
if (!string.IsNullOrWhiteSpace(_tempUserName))
{
_owner.AccountManager.AddUser(_tempUserName, selectionDialog.BufferImageProfile);
}
}
else if (SelectedProfile != null)
{
_owner.AccountManager.SetUserImage(SelectedProfile.UserId, selectionDialog.BufferImageProfile);
SelectedProfile.Image = selectionDialog.BufferImageProfile;
SelectedProfile = null;
}
LoadProfiles();
}
}
public async void AddUser()
{
var dlgTitle = LocaleManager.Instance["InputDialogAddNewProfileTitle"];
var dlgMainText = LocaleManager.Instance["InputDialogAddNewProfileHeader"];
var dlgSubText = string.Format(LocaleManager.Instance["InputDialogAddNewProfileSubtext"],
MaxProfileNameLength);
_tempUserName =
await ContentDialogHelper.CreateInputDialog(dlgTitle, dlgMainText, dlgSubText, _owner,
MaxProfileNameLength);
if (!string.IsNullOrWhiteSpace(_tempUserName))
{
await SelectProfileImage(true);
}
_tempUserName = String.Empty;
}
public async void DeleteUser()
{
if (_selectedProfile != null)
{
var lastUserId = _owner.AccountManager.LastOpenedUser.UserId;
if (_selectedProfile.UserId == lastUserId)
{
// If we are deleting the currently open profile, then we must open something else before deleting.
var profile = Profiles.FirstOrDefault(x => x.UserId != lastUserId);
if (profile == null)
{
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateErrorDialog(_owner,
LocaleManager.Instance["DialogUserProfileDeletionWarningMessage"]);
});
return;
}
_owner.AccountManager.OpenUser(profile.UserId);
}
var result =
await ContentDialogHelper.CreateConfirmationDialog(_owner,
LocaleManager.Instance["DialogUserProfileDeletionConfirmMessage"], "",
LocaleManager.Instance["InputDialogYes"], LocaleManager.Instance["InputDialogNo"], "");
if (result == UserResult.Yes)
{
_owner.AccountManager.DeleteUser(_selectedProfile.UserId);
}
}
LoadProfiles();
}
}
}

View File

@ -0,0 +1,68 @@
<window:StyleableWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350"
x:Class="Ryujinx.Ava.Ui.Windows.AmiiboWindow"
xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
CanResize="False"
WindowStartupLocation="CenterOwner"
Width="800" MinHeight="650" Height="650"
SizeToContent="Manual"
MinWidth="600">
<Design.DataContext>
<viewModels:AmiiboWindowViewModel />
</Design.DataContext>
<Grid Margin="15" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="1" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel Spacing="10" Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale AmiiboSeriesLabel}" />
<ComboBox SelectedIndex="{Binding SeriesSelectedIndex}" Items="{Binding AmiiboSeries}" MinWidth="100" />
</StackPanel>
<StackPanel Spacing="10" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale AmiiboCharacterLabel}" />
<ComboBox SelectedIndex="{Binding AmiiboSelectedIndex}" MinWidth="100" Items="{Binding AmiiboList}" />
</StackPanel>
</Grid>
<StackPanel Margin="20" Grid.Row="2">
<Image Source="{Binding AmiiboImage}" Height="350" Width="350" HorizontalAlignment="Center" />
<ScrollViewer MaxHeight="120" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"
Margin="20" VerticalAlignment="Top" HorizontalAlignment="Stretch">
<TextBlock TextWrapping="Wrap" Text="{Binding Usage}" HorizontalAlignment="Center"
TextAlignment="Center" />
</ScrollViewer>
</StackPanel>
<Grid Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<CheckBox Margin="10" Grid.Column="0" VerticalContentAlignment="Center" IsChecked="{Binding ShowAllAmiibo}"
Content="{locale:Locale AmiiboOptionsShowAllLabel}" />
<CheckBox Margin="10" VerticalContentAlignment="Center" Grid.Column="1" IsChecked="{Binding UseRandomUuid}"
Content="{locale:Locale AmiiboOptionsUsRandomTagLabel}" />
<Button Grid.Column="3" IsEnabled="{Binding EnableScanning}" Width="80"
Content="{locale:Locale AmiiboScanButtonLabel}" Name="ScanButton"
Click="ScanButton_Click" />
<Button Grid.Column="4" Margin="10,0" Width="80" Content="{locale:Locale InputDialogCancel}"
Name="CancelButton"
Click="CancelButton_Click" />
</Grid>
</Grid>
</window:StyleableWindow>

View File

@ -0,0 +1,70 @@
using Avalonia;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.Models;
using Ryujinx.Ava.Ui.ViewModels;
namespace Ryujinx.Ava.Ui.Windows
{
public class AmiiboWindow : StyleableWindow
{
public AmiiboWindow(bool showAll, string lastScannedAmiiboId, string titleId)
{
ViewModel = new AmiiboWindowViewModel(this, lastScannedAmiiboId, titleId);
ViewModel.ShowAllAmiibo = showAll;
DataContext = ViewModel;
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
}
public AmiiboWindow()
{
ViewModel = new AmiiboWindowViewModel(this, string.Empty, string.Empty);
DataContext = ViewModel;
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
if (Program.PreviewerDetached)
{
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
}
}
public bool IsScanned { get; set; }
public Amiibo.AmiiboApi ScannedAmiibo { get; set; }
public AmiiboWindowViewModel ViewModel { get; set; }
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void ScanButton_Click(object sender, RoutedEventArgs e)
{
if (ViewModel.AmiiboSelectedIndex > -1)
{
Amiibo.AmiiboApi amiibo = ViewModel.AmiiboList[ViewModel.AmiiboSelectedIndex];
ScannedAmiibo = amiibo;
IsScanned = true;
Close();
}
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
IsScanned = false;
Close();
}
}
}

View File

@ -0,0 +1,53 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350"
x:Class="Ryujinx.Ava.Ui.Windows.AvatarWindow"
CanResize="False"
xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
WindowStartupLocation="CenterOwner"
x:CompileBindings="True"
x:DataType="viewModels:AvatarProfileViewModel"
SizeToContent="WidthAndHeight">
<Design.DataContext>
<viewModels:AvatarProfileViewModel />
</Design.DataContext>
<Window.Resources>
<controls:BitmapArrayValueConverter x:Key="ByteImage" />
</Window.Resources>
<Grid Margin="5" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Grid.Row="1" BorderThickness="0" SelectedIndex="{Binding SelectedIndex}" Width="600" Height="500"
Items="{Binding Images}" HorizontalAlignment="Stretch" VerticalAlignment="Center">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" MaxWidth="600" Margin="0" HorizontalAlignment="Center" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Image Margin="5" Height="96" Width="96"
Source="{Binding Data, Converter={StaticResource ByteImage}}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ProgressBar Grid.Row="2" IsIndeterminate="{Binding IsIndeterminate}" Value="{Binding ImagesLoaded}" HorizontalAlignment="Stretch" Margin="5"
Maximum="{Binding ImageCount}" Minimum="0" />
<StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="10" Margin="10" HorizontalAlignment="Center">
<Button Content="{Locale:Locale AvatarChoose}" Width="200" Name="ChooseButton" Click="ChooseButton_OnClick" />
<ui:ColorPickerButton Color="{Binding BackgroundColor, Mode=TwoWay}" Name="ColorButton" />
<Button HorizontalAlignment="Right" Content="{Locale:Locale AvatarClose}" Click="CloseButton_OnClick"
Name="CloseButton"
Width="200" />
</StackPanel>
</Grid>
</Window>

View File

@ -0,0 +1,71 @@
using Avalonia;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.ViewModels;
using Ryujinx.HLE.FileSystem;
using System;
namespace Ryujinx.Ava.Ui.Windows
{
public class AvatarWindow : StyleableWindow
{
public AvatarWindow(ContentManager contentManager)
{
ContentManager = contentManager;
ViewModel = new AvatarProfileViewModel(() => ViewModel.ReloadImages());
DataContext = ViewModel;
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["AvatarWindowTitle"];
}
public AvatarWindow()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
if (Program.PreviewerDetached)
{
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["AvatarWindowTitle"];
}
}
public ContentManager ContentManager { get; }
public byte[] SelectedImage { get; set; }
internal AvatarProfileViewModel ViewModel { get; set; }
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override void OnClosed(EventArgs e)
{
ViewModel.Dispose();
base.OnClosed(e);
}
private void CloseButton_OnClick(object sender, RoutedEventArgs e)
{
Close();
}
private void ChooseButton_OnClick(object sender, RoutedEventArgs e)
{
if (ViewModel.SelectedIndex > -1)
{
SelectedImage = ViewModel.SelectedImage;
Close();
}
}
}
}

View File

@ -0,0 +1,96 @@
<window:StyleableWindow x:Class="Ryujinx.Ava.Ui.Windows.CheatWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:model="clr-namespace:Ryujinx.Ava.Ui.Models"
xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
mc:Ignorable="d"
Width="500" MinHeight="500" Height="500"
WindowStartupLocation="CenterOwner"
MinWidth="500">
<Window.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="True" />
</Style>
</Window.Styles>
<Grid Name="DlcGrid" Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="1"
Margin="20,15,20,20"
HorizontalAlignment="Center"
VerticalAlignment="Center"
MaxWidth="500"
LineHeight="18"
TextWrapping="Wrap"
Text="{Binding Heading}"
TextAlignment="Center" />
<Border
Grid.Row="2"
Margin="5"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderBrush="Gray"
BorderThickness="1">
<TreeView Items="{Binding LoadedCheats}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Name="CheatsView"
MinHeight="300">
<TreeView.Styles>
<Styles>
<Style Selector="TreeViewItem:empty /template/ ItemsPresenter">
<Setter Property="IsVisible" Value="False"/>
</Style>
</Styles>
</TreeView.Styles>
<TreeView.DataTemplates>
<TreeDataTemplate DataType="model:CheatsList" ItemsSource="{Binding}">
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsEnabled}" MinWidth="20" />
<TextBlock Width="150"
Text="{Binding BuildId}" />
<TextBlock
Text="{Binding Path}" />
</StackPanel>
</TreeDataTemplate>
<DataTemplate x:DataType="model:CheatModel">
<StackPanel Orientation="Horizontal" Margin="0" HorizontalAlignment="Left">
<CheckBox IsChecked="{Binding IsEnabled}" Padding="0" Margin="5,0" MinWidth="20" />
<TextBlock Text="{Binding CleanName}" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
</TreeView.DataTemplates>
</TreeView>
</Border>
<DockPanel
Grid.Row="3"
Margin="0"
HorizontalAlignment="Stretch">
<DockPanel Margin="0" HorizontalAlignment="Right">
<Button
Name="SaveButton"
MinWidth="90"
Margin="5"
IsVisible="{Binding !NoCheatsFound}"
Command="{Binding Save}">
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
</Button>
<Button
Name="CancelButton"
MinWidth="90"
Margin="5"
Command="{Binding Close}">
<TextBlock Text="{locale:Locale InputDialogCancel}" />
</Button>
</DockPanel>
</DockPanel>
</Grid>
</window:StyleableWindow>

View File

@ -0,0 +1,137 @@
using Avalonia;
using Avalonia.Collections;
using Avalonia.Markup.Xaml;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.Models;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
namespace Ryujinx.Ava.Ui.Windows
{
public class CheatWindow : StyleableWindow
{
private readonly string _enabledCheatsPath;
public bool NoCheatsFound { get; }
private AvaloniaList<CheatsList> LoadedCheats { get; }
public string Heading { get; }
public CheatWindow()
{
DataContext = this;
InitializeComponent();
AttachDebugDevTools();
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
}
public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName)
{
LoadedCheats = new AvaloniaList<CheatsList>();
Heading = string.Format(LocaleManager.Instance["CheatWindowHeading"], titleName, titleId.ToUpper());
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath();
string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId);
ulong titleIdValue = ulong.Parse(titleId, System.Globalization.NumberStyles.HexNumber);
_enabledCheatsPath = Path.Combine(titleModsPath, "cheats", "enabled.txt");
string[] enabled = { };
if (File.Exists(_enabledCheatsPath))
{
enabled = File.ReadAllLines(_enabledCheatsPath);
}
int cheatAdded = 0;
var mods = new ModLoader.ModCache();
ModLoader.QueryContentsDir(mods, new DirectoryInfo(Path.Combine(modsBasePath, "contents")), titleIdValue);
string currentCheatFile = string.Empty;
string buildId = string.Empty;
string parentPath = string.Empty;
CheatsList currentGroup = null;
foreach (var cheat in mods.Cheats)
{
if (cheat.Path.FullName != currentCheatFile)
{
currentCheatFile = cheat.Path.FullName;
parentPath = currentCheatFile.Replace(titleModsPath, "");
buildId = Path.GetFileNameWithoutExtension(currentCheatFile).ToUpper();
currentGroup = new CheatsList(buildId, parentPath);
LoadedCheats.Add(currentGroup);
}
var model = new CheatModel(cheat.Name, buildId, enabled.Contains($"{buildId}-{cheat.Name}"));
currentGroup?.Add(model);
cheatAdded++;
}
if (cheatAdded == 0)
{
NoCheatsFound = true;
}
DataContext = this;
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
}
[Conditional("DEBUG")]
private void AttachDebugDevTools()
{
this.AttachDevTools();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public void Save()
{
if (NoCheatsFound)
{
return;
}
List<string> enabledCheats = new List<string>();
foreach (var cheats in LoadedCheats)
{
foreach (var cheat in cheats)
{
if (cheat.IsEnabled)
{
enabledCheats.Add(cheat.BuildIdKey);
}
}
}
Directory.CreateDirectory(Path.GetDirectoryName(_enabledCheatsPath));
File.WriteAllLines(_enabledCheatsPath, enabledCheats);
Close();
}
}
}

View File

@ -8,6 +8,7 @@ using Avalonia.Threading;
using Avalonia.VisualTree;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.Controls;
using Ryujinx.Ava.Ui.Models;
using Ryujinx.Ava.Ui.ViewModels;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
@ -127,9 +128,7 @@ namespace Ryujinx.Ava.Ui.Windows
}
else if (device.Type == Models.DeviceType.Controller)
{
InputConfig config = ConfigurationState.Instance.Hid.InputConfig.Value.Find(inputConfig => inputConfig.Id == ViewModel.SelectedGamepad.Id);
assigner = new GamepadButtonAssigner(ViewModel.SelectedGamepad, (config as StandardControllerInputConfig).TriggerThreshold, forStick);
assigner = new GamepadButtonAssigner(ViewModel.SelectedGamepad, (ViewModel.Config as StandardControllerInputConfig).TriggerThreshold, forStick);
}
else
{
@ -184,8 +183,8 @@ namespace Ryujinx.Ava.Ui.Windows
if (e.AddedItems.Count > 0)
{
(PlayerIndex key, _) = (KeyValuePair<PlayerIndex, string>)e.AddedItems[0];
ViewModel.PlayerId = key;
var player = (PlayerModel)e.AddedItems[0];
ViewModel.PlayerId = player.Id;
}
}
}

View File

@ -0,0 +1,132 @@
<window:StyleableWindow
x:Class="Ryujinx.Ava.Ui.Windows.DlcManagerWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
SizeToContent="Height"
Width="600" MinHeight="500" Height="500"
WindowStartupLocation="CenterOwner"
MinWidth="600"
mc:Ignorable="d">
<Grid Name="DlcGrid" Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="1"
Margin="20,15,20,20"
HorizontalAlignment="Center"
VerticalAlignment="Center"
MaxWidth="500"
LineHeight="18"
TextWrapping="Wrap"
Text="{Binding Heading}"
TextAlignment="Center" />
<Border
Grid.Row="2"
Margin="5"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderBrush="Gray"
BorderThickness="1">
<DataGrid
MinHeight="200"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Auto"
Items="{Binding Dlcs}"
VerticalScrollBarVisibility="Auto">
<DataGrid.Columns>
<DataGridTemplateColumn Width="90">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox
Width="50"
MinWidth="40"
HorizontalAlignment="Right"
IsChecked="{Binding IsEnabled}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.Header>
<TextBlock Text="{locale:Locale DlcManagerTableHeadingEnabledLabel}" />
</DataGridTemplateColumn.Header>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="190"
Binding="{Binding TitleId}"
CanUserResize="True">
<DataGridTextColumn.Header>
<TextBlock Text="{locale:Locale DlcManagerTableHeadingTitleIdLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn
Width="*"
Binding="{Binding ContainerPath}"
CanUserResize="True">
<DataGridTextColumn.Header>
<TextBlock Text="{locale:Locale DlcManagerTableHeadingContainerPathLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn
Width="*"
Binding="{Binding FullPath}"
CanUserResize="True">
<DataGridTextColumn.Header>
<TextBlock Text="{locale:Locale DlcManagerTableHeadingFullPathLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</Border>
<DockPanel
Grid.Row="3"
Margin="0"
HorizontalAlignment="Stretch">
<DockPanel Margin="0" HorizontalAlignment="Left">
<Button
Name="AddButton"
MinWidth="90"
Margin="5"
Command="{Binding Add}">
<TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" />
</Button>
<Button
Name="RemoveButton"
MinWidth="90"
Margin="5"
Command="{Binding RemoveSelected}">
<TextBlock Text="{locale:Locale SettingsTabGeneralRemove}" />
</Button>
<Button
Name="RemoveAllButton"
MinWidth="90"
Margin="5"
Command="{Binding RemoveAll}">
<TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
</Button>
</DockPanel>
<DockPanel Margin="0" HorizontalAlignment="Right">
<Button
Name="SaveButton"
MinWidth="90"
Margin="5"
Command="{Binding Save}">
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
</Button>
<Button
Name="CancelButton"
MinWidth="90"
Margin="5"
Command="{Binding Close}">
<TextBlock Text="{locale:Locale InputDialogCancel}" />
</Button>
</DockPanel>
</DockPanel>
</Grid>
</window:StyleableWindow>

View File

@ -0,0 +1,266 @@
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.Controls;
using Ryujinx.Ava.Ui.Models;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Path = System.IO.Path;
namespace Ryujinx.Ava.Ui.Windows
{
public class DlcManagerWindow : StyleableWindow
{
private readonly List<DlcContainer> _dlcContainerList;
private readonly string _dlcJsonPath;
public VirtualFileSystem VirtualFileSystem { get; }
public AvaloniaList<DlcModel> Dlcs { get; set; }
public Grid DlcGrid { get; private set; }
public ulong TitleId { get; }
public string TitleName { get; }
public string Heading => string.Format(LocaleManager.Instance["DlcWindowHeading"], TitleName, TitleId.ToString("X16"));
public DlcManagerWindow()
{
DataContext = this;
InitializeComponent();
AttachDebugDevTools();
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["DlcWindowTitle"];
}
public DlcManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
{
VirtualFileSystem = virtualFileSystem;
TitleId = titleId;
TitleName = titleName;
_dlcJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
try
{
_dlcContainerList = JsonHelper.DeserializeFromFile<List<DlcContainer>>(_dlcJsonPath);
}
catch
{
_dlcContainerList = new List<DlcContainer>();
}
DataContext = this;
InitializeComponent();
AttachDebugDevTools();
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["DlcWindowTitle"];
LoadDlcs();
}
[Conditional("DEBUG")]
private void AttachDebugDevTools()
{
this.AttachDevTools();
}
private void InitializeComponent()
{
Dlcs = new AvaloniaList<DlcModel>();
AvaloniaXamlLoader.Load(this);
DlcGrid = this.FindControl<Grid>("DlcGrid");
}
private void LoadDlcs()
{
foreach (DlcContainer dlcContainer in _dlcContainerList)
{
using FileStream containerFile = File.OpenRead(dlcContainer.Path);
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
VirtualFileSystem.ImportTickets(pfs);
foreach (DlcNca dlcNca in dlcContainer.DlcNcaList)
{
using var ncaFile = new UniqueRef<IFile>();
pfs.OpenFile(ref ncaFile.Ref(), dlcNca.Path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), dlcContainer.Path);
if (nca != null)
{
Dlcs.Add(new DlcModel(nca.Header.TitleId.ToString("X16"), dlcContainer.Path, dlcNca.Path,
dlcNca.Enabled));
}
}
}
}
private Nca TryCreateNca(IStorage ncaStorage, string containerPath)
{
try
{
return new Nca(VirtualFileSystem.KeySet, ncaStorage);
}
catch (Exception ex)
{
Dispatcher.UIThread.InvokeAsync(async () =>
{
await ContentDialogHelper.CreateErrorDialog(this,
string.Format(LocaleManager.Instance[
"DialogDlcLoadNcaErrorMessage"], ex.Message, containerPath));
});
}
return null;
}
private async Task AddDlc(string path)
{
if (!File.Exists(path) || Dlcs.FirstOrDefault(x => x.ContainerPath == path) != null)
{
return;
}
using (FileStream containerFile = File.OpenRead(path))
{
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
bool containsDlc = false;
VirtualFileSystem.ImportTickets(pfs);
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
using var ncaFile = new UniqueRef<IFile>();
pfs.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), path);
if (nca == null)
{
continue;
}
if (nca.Header.ContentType == NcaContentType.PublicData)
{
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != TitleId)
{
break;
}
Dlcs.Add(new DlcModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true));
containsDlc = true;
}
}
if (!containsDlc)
{
await ContentDialogHelper.CreateErrorDialog(this, LocaleManager.Instance["DialogDlcNoDlcErrorMessage"]);
}
}
}
private void RemoveDlcs(bool removeSelectedOnly = false)
{
if (removeSelectedOnly)
{
Dlcs.RemoveAll(Dlcs.Where(x => x.IsEnabled).ToList());
}
else
{
Dlcs.Clear();
}
}
public void RemoveSelected()
{
RemoveDlcs(true);
}
public void RemoveAll()
{
RemoveDlcs();
}
public async void Add()
{
OpenFileDialog dialog = new OpenFileDialog() { Title = LocaleManager.Instance["SelectDlcDialogTitle"], AllowMultiple = true };
dialog.Filters.Add(new FileDialogFilter { Name = "NSP", Extensions = { "nsp" } });
string[] files = await dialog.ShowAsync(this);
if (files != null)
{
foreach (string file in files)
{
await AddDlc(file);
}
}
}
public void Save()
{
_dlcContainerList.Clear();
DlcContainer container = default;
foreach (DlcModel dlc in Dlcs)
{
if (container.Path != dlc.ContainerPath)
{
if (!string.IsNullOrWhiteSpace(container.Path))
{
_dlcContainerList.Add(container);
}
container = new DlcContainer { Path = dlc.ContainerPath, DlcNcaList = new List<DlcNca>() };
}
container.DlcNcaList.Add(new DlcNca
{
Enabled = dlc.IsEnabled,
TitleId = Convert.ToUInt64(dlc.TitleId, 16),
Path = dlc.FullPath
});
}
if (!string.IsNullOrWhiteSpace(container.Path))
{
_dlcContainerList.Add(container);
}
using (FileStream dlcJsonStream = File.Create(_dlcJsonPath, 4096, FileOptions.WriteThrough))
{
dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_dlcContainerList, true)));
}
Close();
}
}
}

View File

@ -120,8 +120,6 @@ namespace Ryujinx.Ava.Ui.Windows
InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver());
LoadGameList();
CheckLaunchState();
}
_rendererWaitEvent = new AutoResetEvent(false);
@ -451,7 +449,7 @@ namespace Ryujinx.Ava.Ui.Windows
RefreshFirmwareStatus();
}
protected async void CheckLaunchState()
protected void CheckLaunchState()
{
if (ShowKeyErrorOnLoad)
{
@ -470,7 +468,7 @@ namespace Ryujinx.Ava.Ui.Windows
if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false, this))
{
await Updater.BeginParse(this, false).ContinueWith(task =>
Updater.BeginParse(this, false).ContinueWith(task =>
{
Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}");
}, TaskContinuationOptions.OnlyOnFaulted);
@ -537,6 +535,13 @@ namespace Ryujinx.Ava.Ui.Windows
LoadHotKeys();
}
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
CheckLaunchState();
}
public static void UpdateGraphicsConfig()
{
int resScale = ConfigurationState.Instance.Graphics.ResScale;

View File

@ -0,0 +1,104 @@
<window:StyleableWindow
x:Class="Ryujinx.Ava.Ui.Windows.TitleUpdateWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
SizeToContent="Height"
Width="600" MinHeight="500" Height="500"
WindowStartupLocation="CenterOwner"
MinWidth="600"
mc:Ignorable="d">
<Grid Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="1"
Margin="20,15,20,20"
HorizontalAlignment="Center"
VerticalAlignment="Center"
MaxWidth="500"
LineHeight="18"
TextWrapping="Wrap"
Text="{Binding Heading}"
TextAlignment="Center" />
<Border
Grid.Row="2"
Margin="5"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderBrush="Gray"
BorderThickness="1">
<ScrollViewer
Width="550"
MinHeight="200"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ItemsControl
Margin="10"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Items="{Binding TitleUpdates}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton Padding="8, 0" VerticalContentAlignment="Center" GroupName="Update" IsChecked="{Binding IsEnabled, Mode=TwoWay}">
<Label Margin="0" VerticalAlignment="Center" Content="{Binding Label}" />
</RadioButton>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
<DockPanel
Grid.Row="3"
Margin="0"
HorizontalAlignment="Stretch">
<DockPanel Margin="0" HorizontalAlignment="Left">
<Button
Name="AddButton"
MinWidth="90"
Margin="5"
Command="{Binding Add}">
<TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" />
</Button>
<Button
Name="RemoveButton"
MinWidth="90"
Margin="5"
Command="{Binding RemoveSelected}">
<TextBlock Text="{locale:Locale SettingsTabGeneralRemove}" />
</Button>
<Button
Name="RemoveAllButton"
MinWidth="90"
Margin="5"
Command="{Binding RemoveAll}">
<TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
</Button>
</DockPanel>
<DockPanel Margin="0" HorizontalAlignment="Right">
<Button
Name="SaveButton"
MinWidth="90"
Margin="5"
Command="{Binding Save}">
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
</Button>
<Button
Name="CancelButton"
MinWidth="90"
Margin="5"
Command="{Binding Close}">
<TextBlock Text="{locale:Locale InputDialogCancel}" />
</Button>
</DockPanel>
</DockPanel>
</Grid>
</window:StyleableWindow>

View File

@ -0,0 +1,272 @@
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using LibHac.Ns;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.Controls;
using Ryujinx.Ava.Ui.Models;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using Path = System.IO.Path;
using SpanHelpers = LibHac.Common.SpanHelpers;
using LibHac.Tools.FsSystem;
using Avalonia.Threading;
namespace Ryujinx.Ava.Ui.Windows
{
public class TitleUpdateWindow : StyleableWindow
{
private readonly string _updateJsonPath;
private TitleUpdateMetadata _titleUpdateWindowData;
public VirtualFileSystem VirtualFileSystem { get; }
internal AvaloniaList<TitleUpdateModel> TitleUpdates { get; set; }
public string TitleId { get; }
public string TitleName { get; }
public string Heading => string.Format(LocaleManager.Instance["GameUpdateWindowHeading"], TitleName, TitleId.ToUpper());
public TitleUpdateWindow()
{
DataContext = this;
InitializeComponent();
AttachDebugDevTools();
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
}
public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName)
{
VirtualFileSystem = virtualFileSystem;
TitleId = titleId;
TitleName = titleName;
_updateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json");
try
{
_titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_updateJsonPath);
}
catch
{
_titleUpdateWindowData = new TitleUpdateMetadata {Selected = "", Paths = new List<string>()};
}
DataContext = this;
InitializeComponent();
AttachDebugDevTools();
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UpdateWindowTitle"];
LoadUpdates();
}
[Conditional("DEBUG")]
private void AttachDebugDevTools()
{
this.AttachDevTools();
}
private void InitializeComponent()
{
TitleUpdates = new AvaloniaList<TitleUpdateModel>();
AvaloniaXamlLoader.Load(this);
}
private void LoadUpdates()
{
TitleUpdates.Add(new TitleUpdateModel(default, string.Empty, true));
foreach (string path in _titleUpdateWindowData.Paths)
{
AddUpdate(path);
}
if (_titleUpdateWindowData.Selected == "")
{
TitleUpdates[0].IsEnabled = true;
}
else
{
TitleUpdateModel selected = TitleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected);
List<TitleUpdateModel> enabled = TitleUpdates.Where(x => x.IsEnabled).ToList();
foreach (TitleUpdateModel update in enabled)
{
update.IsEnabled = false;
}
if (selected != null)
{
selected.IsEnabled = true;
}
}
SortUpdates();
}
private void AddUpdate(string path)
{
if (File.Exists(path) && !TitleUpdates.Any(x => x.Path == path))
{
using (FileStream file = new(path, FileMode.Open, FileAccess.Read))
{
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
try
{
(Nca patchNca, Nca controlNca) =
ApplicationLoader.GetGameUpdateDataFromPartition(VirtualFileSystem, nsp, TitleId, 0);
if (controlNca != null && patchNca != null)
{
ApplicationControlProperty controlData = new ApplicationControlProperty();
using var nacpFile = new UniqueRef<IFile>();
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None)
.OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read)
.ThrowIfFailure();
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None)
.ThrowIfFailure();
TitleUpdates.Add(new TitleUpdateModel(controlData, path));
}
else
{
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateErrorDialog(this,
LocaleManager.Instance["DialogUpdateAddUpdateErrorMessage"]);
});
}
}
catch (Exception ex)
{
Dispatcher.UIThread.Post(async () =>
{
await ContentDialogHelper.CreateErrorDialog(this,
string.Format(LocaleManager.Instance["DialogDlcLoadNcaErrorMessage"], ex.Message, path));
});
}
}
}
}
private void RemoveUpdates(bool removeSelectedOnly = false)
{
if (removeSelectedOnly)
{
TitleUpdates.RemoveAll(TitleUpdates.Where(x => x.IsEnabled && !x.IsNoUpdate).ToList());
}
else
{
TitleUpdates.RemoveAll(TitleUpdates.Where(x => !x.IsNoUpdate).ToList());
}
TitleUpdates.FirstOrDefault(x => x.IsNoUpdate).IsEnabled = true;
SortUpdates();
}
public void RemoveSelected()
{
RemoveUpdates(true);
}
public void RemoveAll()
{
RemoveUpdates();
}
public async void Add()
{
OpenFileDialog dialog = new OpenFileDialog() { Title = LocaleManager.Instance["SelectUpdateDialogTitle"], AllowMultiple = true };
dialog.Filters.Add(new FileDialogFilter { Name = "NSP", Extensions = { "nsp" } });
string[] files = await dialog.ShowAsync(this);
if (files != null)
{
foreach (string file in files)
{
AddUpdate(file);
}
}
SortUpdates();
}
private void SortUpdates()
{
var list = TitleUpdates.ToList();
list.Sort((first, second) =>
{
if (string.IsNullOrEmpty(first.Control.DisplayVersionString.ToString()))
{
return -1;
}
else if (string.IsNullOrEmpty(second.Control.DisplayVersionString.ToString()))
{
return 1;
}
return Version.Parse(first.Control.DisplayVersionString.ToString())
.CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
});
TitleUpdates.Clear();
TitleUpdates.AddRange(list);
}
public void Save()
{
_titleUpdateWindowData.Paths.Clear();
_titleUpdateWindowData.Selected = "";
foreach (TitleUpdateModel update in TitleUpdates)
{
_titleUpdateWindowData.Paths.Add(update.Path);
if (update.IsEnabled)
{
_titleUpdateWindowData.Selected = update.Path;
}
}
using (FileStream dlcJsonStream = File.Create(_updateJsonPath, 4096, FileOptions.WriteThrough))
{
dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true)));
}
if (Owner is MainWindow window)
{
window.ViewModel.LoadApplications();
}
Close();
}
}
}

View File

@ -0,0 +1,107 @@
<window:StyleableWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350"
x:Class="Ryujinx.Ava.Ui.Windows.UserProfileWindow"
xmlns:Locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.Ui.ViewModels"
xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
CanResize="False"
Width="850" MinHeight="550" Height="550"
WindowStartupLocation="CenterOwner"
SizeToContent="Manual"
MinWidth="600">
<Design.DataContext>
<viewModels:UserProfileViewModel />
</Design.DataContext>
<Window.Resources>
<controls:BitmapArrayValueConverter x:Key="ByteImage" />
</Window.Resources>
<Grid Margin="15" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<ContentControl
Focusable="False"
IsVisible="False"
KeyboardNavigation.IsTabStop="False">
<ui:ContentDialog Name="ContentDialog"
IsPrimaryButtonEnabled="True"
IsSecondaryButtonEnabled="True"
IsVisible="False" />
</ContentControl>
<TextBlock Text="{Locale:Locale UserProfilesSelectedUserProfile}" />
<Grid Grid.Row="1" Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Image Height="96" Width="96"
Source="{Binding SelectedProfile.Image, Converter={StaticResource ByteImage}}" />
<StackPanel Orientation="Vertical" HorizontalAlignment="Stretch" Grid.Column="1" Spacing="10"
Margin="5, 10">
<TextBox Name="NameBox" Text="{Binding SelectedProfile.Name, Mode=OneWay}"
HorizontalAlignment="Stretch" />
<TextBlock Text="{Binding SelectedProfile.UserId}" />
</StackPanel>
<StackPanel Orientation="Vertical" HorizontalAlignment="Stretch" Grid.Column="2" Spacing="10"
Margin="5">
<Button Content="{Locale:Locale UserProfilesSaveProfileName}" Name="SetNameButton"
Click="SetNameButton_OnClick" />
<Button Name="SelectProfileImage" Command="{Binding ChooseProfileImage}"
Content="{Locale:Locale UserProfilesChangeProfileImage}" />
</StackPanel>
</Grid>
</Grid>
<Grid Grid.Row="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="{Locale:Locale UserProfilesAvailableUserProfiles}" />
<ListBox Grid.Row="1" Margin="10" Name="ProfilesList" DoubleTapped="ProfilesList_DoubleTapped"
Items="{Binding Profiles}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Background="{DynamicResource ThemeAccentColorBrush}"
Grid.ColumnSpan="2"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinHeight="5" MinWidth="5"
IsVisible="{Binding IsOpened}" />
<Image Grid.Column="0" Height="96" Width="96"
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
<StackPanel Margin="10" Orientation="Vertical" HorizontalAlignment="Stretch"
VerticalAlignment="Center" Grid.Column="1">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding UserId}" />
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
<StackPanel Grid.Row="3" Orientation="Horizontal" Margin="10,0" Spacing="10" HorizontalAlignment="Stretch">
<Button Content="{Locale:Locale UserProfilesAddNewProfile}" Command="{Binding AddUser}" />
<Button IsEnabled="{Binding IsSelectedProfileDeletable}"
Content="{Locale:Locale UserProfilesDeleteSelectedProfile}" Command="{Binding DeleteUser}" />
<Button HorizontalAlignment="Right" Content="{Locale:Locale UserProfilesClose}" Click="CloseButton_OnClick"
Name="CloseButton" />
</StackPanel>
</Grid>
</window:StyleableWindow>

View File

@ -0,0 +1,102 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.ViewModels;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using System.Threading.Tasks;
using UserProfile = Ryujinx.Ava.Ui.Models.UserProfile;
namespace Ryujinx.Ava.Ui.Windows
{
public class UserProfileWindow : StyleableWindow
{
private TextBox _nameBox;
public UserProfileWindow(AccountManager accountManager, ContentManager contentManager,
VirtualFileSystem virtualFileSystem)
{
AccountManager = accountManager;
ContentManager = contentManager;
ViewModel = new UserProfileViewModel(this);
DataContext = ViewModel;
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
if (contentManager.GetCurrentFirmwareVersion() != null)
{
Task.Run(() =>
{
AvatarProfileViewModel.PreloadAvatars(contentManager, virtualFileSystem);
});
}
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UserProfileWindowTitle"];
}
public UserProfileWindow()
{
ViewModel = new UserProfileViewModel();
DataContext = ViewModel;
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["UserProfileWindowTitle"];
}
public AccountManager AccountManager { get; }
public ContentManager ContentManager { get; }
public UserProfileViewModel ViewModel { get; set; }
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
_nameBox = this.FindControl<TextBox>("NameBox");
}
private void ProfilesList_DoubleTapped(object sender, RoutedEventArgs e)
{
if (sender is ListBox listBox)
{
int selectedIndex = listBox.SelectedIndex;
if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count)
{
ViewModel.SelectedProfile = ViewModel.Profiles[selectedIndex];
AccountManager.OpenUser(ViewModel.SelectedProfile.UserId);
ViewModel.LoadProfiles();
foreach (UserProfile profile in ViewModel.Profiles)
{
profile.UpdateState();
}
}
}
}
private void CloseButton_OnClick(object sender, RoutedEventArgs e)
{
Close();
}
private void SetNameButton_OnClick(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(_nameBox.Text))
{
ViewModel.SelectedProfile.Name = _nameBox.Text;
AccountManager.SetUserName(ViewModel.SelectedProfile.UserId, _nameBox.Text);
}
}
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
@ -6,8 +6,6 @@
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>1.0.0-dirty</Version>
<TieredCompilation>false</TieredCompilation>
<TieredCompilationQuickJit>false</TieredCompilationQuickJit>
<DefineConstants Condition=" '$(ExtraDefineConstants)' != '' ">$(DefineConstants);$(ExtraDefineConstants)</DefineConstants>
</PropertyGroup>

View File

@ -6,8 +6,6 @@
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>1.0.0-dirty</Version>
<TieredCompilation>false</TieredCompilation>
<TieredCompilationQuickJit>false</TieredCompilationQuickJit>
<DefineConstants Condition=" '$(ExtraDefineConstants)' != '' ">$(DefineConstants);$(ExtraDefineConstants)</DefineConstants>
<!-- As we already provide GTK3 on Windows via GtkSharp.Dependencies this is redundant. -->
<SkipGtkInstall>true</SkipGtkInstall>