mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2026-02-19 23:31:07 +00:00
Compare commits
41 Commits
begone/osx
...
setup-wiza
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7dd718d6f | ||
|
|
6ee7957574 | ||
|
|
bf62531802 | ||
|
|
17be50ea80 | ||
|
|
ec50a1ec3e | ||
|
|
5a20047e5e | ||
|
|
f9fed4cf4d | ||
|
|
2970dcd3c7 | ||
|
|
4be6cb2fa1 | ||
|
|
c90d2af9cd | ||
|
|
13ff9cb162 | ||
|
|
b35ba58831 | ||
|
|
e12a77d4a3 | ||
|
|
804a4e0bcb | ||
|
|
94870eafaa | ||
|
|
7e6cc31866 | ||
|
|
3b25c43abf | ||
|
|
1804dd031b | ||
|
|
211498e060 | ||
|
|
4bdee89288 | ||
|
|
d8a6364cca | ||
|
|
2f794794c6 | ||
|
|
1d6c2426df | ||
|
|
6cd03f15fa | ||
|
|
3fe7600382 | ||
|
|
dc2aa837b3 | ||
|
|
133ac41425 | ||
|
|
fd2ecee479 | ||
|
|
8f529d17a8 | ||
|
|
884d0f526c | ||
|
|
c5b325bde2 | ||
|
|
8ab851ead8 | ||
|
|
5a060cf451 | ||
|
|
9b0fa3bf6d | ||
|
|
325e13a490 | ||
|
|
e202cccc6e | ||
|
|
e0ed8f56ea | ||
|
|
46b2fb92d7 | ||
|
|
8563e7d4dc | ||
|
|
ee10cbf735 | ||
|
|
b033adbde7 |
@@ -44,7 +44,7 @@
|
||||
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.126" />
|
||||
<PackageVersion Include="Ryujinx.UpdateClient" Version="1.0.44" />
|
||||
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="1.0.44" />
|
||||
<PackageVersion Include="Gommon" Version="2.8.0.1" />
|
||||
<PackageVersion Include="Gommon" Version="2.8.0.2" />
|
||||
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
|
||||
<PackageVersion Include="Sep" Version="0.11.1" />
|
||||
<PackageVersion Include="shaderc.net" Version="0.1.0" />
|
||||
@@ -59,4 +59,4 @@
|
||||
<PackageVersion Include="System.Management" Version="9.0.2" />
|
||||
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -24841,6 +24841,781 @@
|
||||
"zh_CN": "如果您在设置中有某些 LDN 口令则可加入此游戏。",
|
||||
"zh_TW": "你只能加入與 LDN 網路密碼片語 (passphrase) 設定相同的遊戲。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardOpen",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Setup Wizard",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardActionBack",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Back",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardActionNext",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Next",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardFirstPageTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Welcome to Ryubing!",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardFirstPageContent",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Ryubing is a fork of the discontinued Nintendo Switch emulator, Ryujinx.\n\nThis setup wizard will guide you through the necessary steps needed for Ryubing to play your Switch games on PC.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardFirstPageAction",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Start Setup",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardKeysPageTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Key Files",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardKeysPageDescription",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Please select the folder containing your prod/title .keys files:",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardKeysPageFolderPopupTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Please select the folder containing your prod/title .keys files",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardKeysPageHelpText",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Not sure how to get your keys?",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardKeysPageSkipText",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Skipped setting up keys as you already have a valid key installation and did not choose a folder to install from.\nClick '{0}' if you wish to reinstall your keys.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardFirmwarePageTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Switch Firmware",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardFirmwarePageDescription",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Please select the folder or .zip/.xci containing your dumped Nintendo Switch firmware:",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardFirmwarePageFolderPopupTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Please select the folder containing your dumped & extracted Switch firmware.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardFirmwarePageFilePopupTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Please select the file containing your dumped Switch firmware.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardFirmwarePageFileBrowse",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Select .zip or .xci",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardFirmwarePageFolderBrowse",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Select Extracted Folder",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardFirmwarePageInstallSuccessNotificationTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Firmware installed",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardFirmwarePageInstallSuccessNotificationText",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Installed firmware version {0}.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardFirmwarePageInstallFailNotificationTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Firmware not installed",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardFirmwarePageInstallFailNotificationText",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "It seems some error occurred when trying to install the firmware at path '{0}'.\nDid that folder contain a firmware dump?",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardFirmwarePageHelpText",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Not sure how to get your firmware off of your Switch?",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardFirmwarePageSkipText",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Skipped setting up firmware as you already have a valid firmware installation and did not choose a folder or file to install from.\nClick '{0}' if you wish to overwrite your firmware.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardGameDirsPageTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Game, Update, and DLC Paths",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardGameDirsPageDescription",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "{0} can be pointed at any number of folders to look for your games, updates, and DLC content.\nAt least one folder must be specified in game directories before continuing.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardGameDirsPageNoFoldersSelectedError",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "At least one folder for games must be selected; otherwise the UI will be empty.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardGameDirsPageHelpText",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Not sure how to get your games, updates, and/or DLC onto your PC?",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardFinalPageTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Setup Complete",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardFinalPageDescription",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Your installation of Ryubing (aka Ryujinx) has been completed.\n\nIf you require assistance, feel free to join our Discord server and ask for help,\nafter verifying your possession of a modded Nintendo Switch.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardFinalPageAction",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Finish",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SetupWizardHelpLinkButton",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Click here to view a guide.",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,11 @@ namespace Ryujinx.Common
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_queue.CompleteAdding();
|
||||
try
|
||||
{
|
||||
_queue.CompleteAdding();
|
||||
} catch (ObjectDisposedException) {}
|
||||
|
||||
_cts.Cancel();
|
||||
_workerThread.Join();
|
||||
|
||||
|
||||
@@ -31,6 +31,11 @@ namespace Ryujinx.Common.Configuration
|
||||
public static string KeysDirPath { get; private set; }
|
||||
public static string KeysDirPathUser { get; }
|
||||
|
||||
public static string GetKeysDir() =>
|
||||
Mode is LaunchMode.UserProfile && Directory.Exists(KeysDirPathUser)
|
||||
? KeysDirPathUser
|
||||
: KeysDirPath;
|
||||
|
||||
public static string LogsDirPath { get; private set; }
|
||||
|
||||
public const string DefaultNandDir = "bis";
|
||||
|
||||
@@ -13,6 +13,15 @@ namespace Ryujinx.Common
|
||||
public const string SetupGuideWikiUrl =
|
||||
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Setup-&-Configuration-Guide";
|
||||
|
||||
public const string DumpKeysWikiUrl =
|
||||
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Dumping/Keys";
|
||||
|
||||
public const string DumpFirmwareWikiUrl =
|
||||
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Dumping/Firmware";
|
||||
|
||||
public const string DumpContentWikiUrl =
|
||||
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Dumping/Games,-Updates-&-DLC";
|
||||
|
||||
public const string MultiplayerWikiUrl =
|
||||
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Multiplayer-(LDN-Local-Wireless)-Guide";
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ using System;
|
||||
|
||||
namespace Ryujinx.HLE.Exceptions
|
||||
{
|
||||
class InvalidFirmwarePackageException : Exception
|
||||
public class InvalidFirmwarePackageException : Exception
|
||||
{
|
||||
public InvalidFirmwarePackageException(string message) : base(message) { }
|
||||
}
|
||||
|
||||
@@ -487,7 +487,7 @@ namespace Ryujinx.HLE.FileSystem
|
||||
|
||||
if (keyPaths.Length is 0)
|
||||
throw new FileNotFoundException($"Directory '{keysSource}' contained no '.keys' files.");
|
||||
|
||||
|
||||
foreach (string filePath in keyPaths)
|
||||
{
|
||||
try
|
||||
@@ -548,6 +548,9 @@ namespace Ryujinx.HLE.FileSystem
|
||||
new DirectoryInfo(registeredDirectory).Delete(true);
|
||||
}
|
||||
|
||||
if (!Directory.Exists(temporaryDirectory))
|
||||
return; // nothing to move
|
||||
|
||||
Directory.Move(temporaryDirectory, registeredDirectory);
|
||||
|
||||
LoadEntries();
|
||||
|
||||
@@ -219,6 +219,8 @@ namespace Ryujinx.HLE.FileSystem
|
||||
FileSystemServerInitializer.InitializeWithConfig(fsServerClient, fsServer, fsServerConfig);
|
||||
}
|
||||
|
||||
public bool HasKeySet { get; private set; }
|
||||
|
||||
public void ReloadKeySet()
|
||||
{
|
||||
KeySet ??= KeySet.CreateDefaultKeySet();
|
||||
@@ -228,12 +230,19 @@ namespace Ryujinx.HLE.FileSystem
|
||||
string consoleKeyFile = null;
|
||||
string devKeyFile = null;
|
||||
|
||||
if (AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile)
|
||||
{
|
||||
LoadSetAtPath(AppDataManager.KeysDirPathUser);
|
||||
}
|
||||
LoadSetAtPath(AppDataManager.GetKeysDir());
|
||||
|
||||
LoadSetAtPath(AppDataManager.KeysDirPath);
|
||||
HasKeySet = (prodKeyFile != null && titleKeyFile != null) || prodKeyFile != null;
|
||||
|
||||
|
||||
ExternalKeyReader.ReadKeyFile(
|
||||
KeySet,
|
||||
prodKeyFile,
|
||||
devKeyFile,
|
||||
titleKeyFile,
|
||||
consoleKeyFile);
|
||||
|
||||
return;
|
||||
|
||||
void LoadSetAtPath(string basePath)
|
||||
{
|
||||
@@ -262,8 +271,6 @@ namespace Ryujinx.HLE.FileSystem
|
||||
devKeyFile = localDevKeyFile;
|
||||
}
|
||||
}
|
||||
|
||||
ExternalKeyReader.ReadKeyFile(KeySet, prodKeyFile, devKeyFile, titleKeyFile, consoleKeyFile, null);
|
||||
}
|
||||
|
||||
public void ImportTickets(IFileSystem fs)
|
||||
|
||||
@@ -334,7 +334,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption
|
||||
|
||||
private static string GetKeyRetailBinPath()
|
||||
{
|
||||
return Path.Combine(AppDataManager.KeysDirPath, "key_retail.bin");
|
||||
return Path.Combine(AppDataManager.GetKeysDir(), "key_retail.bin");
|
||||
}
|
||||
|
||||
public static bool HasAmiiboKeyFile => File.Exists(GetKeyRetailBinPath());
|
||||
|
||||
@@ -264,7 +264,7 @@ namespace Ryujinx.Ava.Common
|
||||
{
|
||||
Dispatcher.UIThread.Post(waitingDialog.Close);
|
||||
|
||||
NotificationHelper.ShowInformation(
|
||||
RyujinxNotificationManager.ShowInformation(
|
||||
RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle),
|
||||
$"{titleName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}");
|
||||
}
|
||||
@@ -380,7 +380,7 @@ namespace Ryujinx.Ava.Common
|
||||
{
|
||||
Dispatcher.UIThread.Post(waitingDialog.Close);
|
||||
|
||||
NotificationHelper.ShowInformation(
|
||||
RyujinxNotificationManager.ShowInformation(
|
||||
RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle),
|
||||
$"{updateName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}");
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace Ryujinx.Ava.Common.Locale
|
||||
{ LocaleKeys.RyujinxConfirm, [RyujinxApp.FullAppName] },
|
||||
{ LocaleKeys.RyujinxUpdater, [RyujinxApp.FullAppName] },
|
||||
{ LocaleKeys.RyujinxRebooter, [RyujinxApp.FullAppName] },
|
||||
{ LocaleKeys.SetupWizardGameDirsPageDescription, [RyujinxApp.FullAppName] },
|
||||
{ LocaleKeys.CompatibilityListSearchBoxWatermarkWithCount, [CompatibilityDatabase.Entries.Length] },
|
||||
{ LocaleKeys.CompatibilityListTitle, [CompatibilityDatabase.Entries.Length] }
|
||||
});
|
||||
|
||||
32
src/Ryujinx/Common/UIImages.cs
Normal file
32
src/Ryujinx/Common/UIImages.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform;
|
||||
using Gommon;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.Common
|
||||
{
|
||||
// ReSharper disable once InconsistentNaming
|
||||
// UiImages is ugly, so no
|
||||
public static class UIImages
|
||||
{
|
||||
public const string LogoPathFormat = "resm:Ryujinx.Assets.UIImages.Logo_{0}_{1}.png?assembly=Ryujinx";
|
||||
public const string IconPathFormat = "resm:Ryujinx.Assets.UIImages.Icon_{0}.png?assembly=Ryujinx";
|
||||
|
||||
public static Bitmap LoadBitmap(string uri)
|
||||
=> new(AssetLoader.Open(new Uri(uri)));
|
||||
|
||||
public static Bitmap GetIconByName(string iconName)
|
||||
=> LoadBitmap(IconPathFormat.Format(iconName));
|
||||
|
||||
public static Bitmap GetLogoByNameAndTheme(string iconName, bool isDarkTheme) =>
|
||||
LoadBitmap(LogoPathFormat.Format(iconName,
|
||||
isDarkTheme
|
||||
? "Dark"
|
||||
: "Light"
|
||||
)
|
||||
);
|
||||
|
||||
public static Bitmap GetLogoByNameAndVariant(string iconName, string theme)
|
||||
=> LoadBitmap(LogoPathFormat.Format(iconName, theme));
|
||||
}
|
||||
}
|
||||
@@ -156,12 +156,9 @@ namespace Ryujinx.Headless
|
||||
option.UserProfile = profile.Name;
|
||||
|
||||
// Check if keys exists.
|
||||
if (!File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys")))
|
||||
if (!File.Exists(Path.Combine(AppDataManager.GetKeysDir(), "prod.keys")))
|
||||
{
|
||||
if (!(AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && File.Exists(Path.Combine(AppDataManager.KeysDirPathUser, "prod.keys"))))
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, "Keys not found");
|
||||
}
|
||||
Logger.Error?.Print(LogClass.Application, "Keys not found");
|
||||
}
|
||||
|
||||
ReloadConfig();
|
||||
|
||||
@@ -32,6 +32,8 @@ namespace Ryujinx.Ava
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
public static bool IsFirstStart { get; set; }
|
||||
|
||||
public static double WindowScaleFactor { get; set; }
|
||||
public static double DesktopScaleFactor { get; set; } = 1.0;
|
||||
public static string Version { get; private set; }
|
||||
@@ -187,12 +189,9 @@ namespace Ryujinx.Ava
|
||||
DriverUtilities.InitDriverConfig(ConfigurationState.Instance.Graphics.BackendThreading == BackendThreading.Off);
|
||||
|
||||
// Check if keys exists.
|
||||
if (!File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys")))
|
||||
if (!File.Exists(Path.Combine(AppDataManager.GetKeysDir(), "prod.keys")))
|
||||
{
|
||||
if (!(AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && File.Exists(Path.Combine(AppDataManager.KeysDirPathUser, "prod.keys"))))
|
||||
{
|
||||
MainWindow.ShowKeyErrorOnLoad = true;
|
||||
}
|
||||
MainWindow.ShowKeyErrorOnLoad = true;
|
||||
}
|
||||
|
||||
if (CommandLineState.LaunchPathArg != null)
|
||||
@@ -221,7 +220,6 @@ namespace Ryujinx.Ava
|
||||
|
||||
public static void ReloadConfig(bool isRunGameWithCustomConfig = false)
|
||||
{
|
||||
|
||||
string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName);
|
||||
string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName);
|
||||
|
||||
@@ -247,6 +245,7 @@ namespace Ryujinx.Ava
|
||||
|
||||
ConfigurationState.Instance.LoadDefault();
|
||||
ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
|
||||
IsFirstStart = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -262,6 +261,8 @@ namespace Ryujinx.Ava
|
||||
|
||||
ConfigurationFileFormat.RenameInvalidConfigFile(ConfigurationPath);
|
||||
|
||||
IsFirstStart = true;
|
||||
|
||||
ConfigurationState.Instance.LoadDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Notifications;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public static class ControlExtensions
|
||||
{
|
||||
public static RyujinxNotificationManager CreateNotificationManager(
|
||||
this Window window,
|
||||
NotificationPosition visiblePosition = NotificationPosition.BottomRight,
|
||||
int maxItems = RyujinxNotificationManager.MaxNotifications,
|
||||
Thickness? margin = null
|
||||
) => new(window, visiblePosition, maxItems, margin);
|
||||
|
||||
extension(Control ctrl)
|
||||
{
|
||||
public int GridRow
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Notifications;
|
||||
using Avalonia.Threading;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Common;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public static class NotificationHelper
|
||||
{
|
||||
private const int MaxNotifications = 4;
|
||||
private const int NotificationDelayInMs = 5000;
|
||||
|
||||
private static WindowNotificationManager _notificationManager;
|
||||
|
||||
private static readonly BlockingCollection<Notification> _notifications = new();
|
||||
|
||||
public static void SetNotificationManager(Window host)
|
||||
{
|
||||
_notificationManager = new WindowNotificationManager(host)
|
||||
{
|
||||
Position = NotificationPosition.BottomRight,
|
||||
MaxItems = MaxNotifications,
|
||||
Margin = new Thickness(0, 0, 15, 40),
|
||||
};
|
||||
|
||||
Lazy<AsyncWorkQueue<Notification>> maybeAsyncWorkQueue = new(
|
||||
() => new AsyncWorkQueue<Notification>(notification =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_notificationManager.Show(notification);
|
||||
});
|
||||
},
|
||||
"UI.NotificationThread",
|
||||
_notifications),
|
||||
LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
|
||||
_notificationManager.TemplateApplied += (sender, args) =>
|
||||
{
|
||||
// NOTE: Force creation of the AsyncWorkQueue.
|
||||
_ = maybeAsyncWorkQueue.Value;
|
||||
};
|
||||
|
||||
host.Closing += (sender, args) =>
|
||||
{
|
||||
if (maybeAsyncWorkQueue.IsValueCreated)
|
||||
{
|
||||
maybeAsyncWorkQueue.Value.Dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static void Show(string title, string text, NotificationType type, bool waitingExit = false, Action onClick = null, Action onClose = null)
|
||||
{
|
||||
TimeSpan delay = waitingExit ? TimeSpan.FromMilliseconds(0) : TimeSpan.FromMilliseconds(NotificationDelayInMs);
|
||||
|
||||
_notifications.Add(new Notification(title, text, type, delay, onClick, onClose));
|
||||
}
|
||||
|
||||
public static void ShowError(string message) =>
|
||||
ShowError(
|
||||
LocaleManager.Instance[LocaleKeys.DialogErrorTitle],
|
||||
$"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}"
|
||||
);
|
||||
|
||||
public static void ShowInformation(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) =>
|
||||
Show(
|
||||
title,
|
||||
text,
|
||||
NotificationType.Information,
|
||||
waitingExit,
|
||||
onClick,
|
||||
onClose);
|
||||
|
||||
public static void ShowSuccess(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) =>
|
||||
Show(
|
||||
title,
|
||||
text,
|
||||
NotificationType.Success,
|
||||
waitingExit,
|
||||
onClick,
|
||||
onClose);
|
||||
|
||||
public static void ShowWarning(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) =>
|
||||
Show(
|
||||
title,
|
||||
text,
|
||||
NotificationType.Warning,
|
||||
waitingExit,
|
||||
onClick,
|
||||
onClose);
|
||||
|
||||
public static void ShowError(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) =>
|
||||
Show(
|
||||
title,
|
||||
text,
|
||||
NotificationType.Error,
|
||||
waitingExit,
|
||||
onClick,
|
||||
onClose);
|
||||
}
|
||||
}
|
||||
179
src/Ryujinx/UI/Helpers/RyujinxNotificationManager.cs
Normal file
179
src/Ryujinx/UI/Helpers/RyujinxNotificationManager.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Notifications;
|
||||
using Avalonia.Threading;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Common;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public class RyujinxNotificationManager
|
||||
{
|
||||
public static RyujinxNotificationManager Shared { get; set; }
|
||||
|
||||
public const int MaxNotifications = 4;
|
||||
private const int NotificationDelayInMs = 5000;
|
||||
|
||||
private readonly WindowNotificationManager _notificationManager;
|
||||
|
||||
private readonly BlockingCollection<Notification> _notifications = new();
|
||||
|
||||
public RyujinxNotificationManager(Window host,
|
||||
NotificationPosition visiblePosition = NotificationPosition.BottomRight,
|
||||
int maxItems = MaxNotifications,
|
||||
Thickness? margin = null)
|
||||
{
|
||||
_notificationManager = new WindowNotificationManager(host)
|
||||
{
|
||||
Position = visiblePosition,
|
||||
MaxItems = maxItems,
|
||||
Margin = margin ?? new Thickness(0, 0, 15, 40)
|
||||
};
|
||||
|
||||
Lazy<AsyncWorkQueue<Notification>> maybeAsyncWorkQueue = new(
|
||||
() => new AsyncWorkQueue<Notification>(notification =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_notificationManager.Show(notification);
|
||||
});
|
||||
},
|
||||
"UI.NotificationThread",
|
||||
_notifications),
|
||||
LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
|
||||
_notificationManager.TemplateApplied += (sender, args) =>
|
||||
{
|
||||
// NOTE: Force creation of the AsyncWorkQueue.
|
||||
_ = maybeAsyncWorkQueue.Value;
|
||||
};
|
||||
|
||||
host.Closing += (sender, args) =>
|
||||
{
|
||||
if (maybeAsyncWorkQueue.IsValueCreated)
|
||||
{
|
||||
maybeAsyncWorkQueue.Value.Dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static void Show(string title, string text, NotificationType type, bool waitingExit = false,
|
||||
Action onClick = null, Action onClose = null)
|
||||
=> Shared?.Send(title, text, type, waitingExit, onClick, onClose);
|
||||
|
||||
public void Send(string title, string text, NotificationType type, bool waitingExit = false,
|
||||
Action onClick = null, Action onClose = null)
|
||||
{
|
||||
TimeSpan delay = waitingExit
|
||||
? TimeSpan.FromMilliseconds(0)
|
||||
: TimeSpan.FromMilliseconds(NotificationDelayInMs);
|
||||
|
||||
_notifications.Add(new Notification(title, text, type, delay, onClick, onClose));
|
||||
}
|
||||
|
||||
#region Instance notification senders
|
||||
|
||||
public void Information(string title, string text, bool waitingExit = false, Action onClick = null,
|
||||
Action onClose = null) =>
|
||||
Send(
|
||||
title,
|
||||
text,
|
||||
NotificationType.Information,
|
||||
waitingExit,
|
||||
onClick,
|
||||
onClose);
|
||||
|
||||
public void Success(string title, string text, bool waitingExit = false, Action onClick = null,
|
||||
Action onClose = null) =>
|
||||
Send(
|
||||
title,
|
||||
text,
|
||||
NotificationType.Success,
|
||||
waitingExit,
|
||||
onClick,
|
||||
onClose);
|
||||
|
||||
public void Warning(string title, string text, bool waitingExit = false, Action onClick = null,
|
||||
Action onClose = null) =>
|
||||
Send(
|
||||
title,
|
||||
text,
|
||||
NotificationType.Warning,
|
||||
waitingExit,
|
||||
onClick,
|
||||
onClose);
|
||||
|
||||
public void Error(string title, string text, bool waitingExit = false, Action onClick = null,
|
||||
Action onClose = null) =>
|
||||
Send(
|
||||
title,
|
||||
text,
|
||||
NotificationType.Error,
|
||||
waitingExit,
|
||||
onClick,
|
||||
onClose);
|
||||
|
||||
public void Error(string message, bool waitingExit = false) =>
|
||||
Error(
|
||||
LocaleManager.Instance[LocaleKeys.DialogErrorTitle],
|
||||
$"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}",
|
||||
waitingExit: waitingExit
|
||||
);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static notification senders
|
||||
|
||||
public static void ShowInformation(string title, string text, bool waitingExit = false, Action onClick = null,
|
||||
Action onClose = null) =>
|
||||
Show(
|
||||
title,
|
||||
text,
|
||||
NotificationType.Information,
|
||||
waitingExit,
|
||||
onClick,
|
||||
onClose);
|
||||
|
||||
public static void ShowSuccess(string title, string text, bool waitingExit = false, Action onClick = null,
|
||||
Action onClose = null) =>
|
||||
Show(
|
||||
title,
|
||||
text,
|
||||
NotificationType.Success,
|
||||
waitingExit,
|
||||
onClick,
|
||||
onClose);
|
||||
|
||||
public static void ShowWarning(string title, string text, bool waitingExit = false, Action onClick = null,
|
||||
Action onClose = null) =>
|
||||
Show(
|
||||
title,
|
||||
text,
|
||||
NotificationType.Warning,
|
||||
waitingExit,
|
||||
onClick,
|
||||
onClose);
|
||||
|
||||
public static void ShowError(string title, string text, bool waitingExit = false, Action onClick = null,
|
||||
Action onClose = null) =>
|
||||
Show(
|
||||
title,
|
||||
text,
|
||||
NotificationType.Error,
|
||||
waitingExit,
|
||||
onClick,
|
||||
onClose);
|
||||
|
||||
public static void ShowError(string message, bool waitingExit = false) =>
|
||||
ShowError(
|
||||
LocaleManager.Instance[LocaleKeys.DialogErrorTitle],
|
||||
$"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}",
|
||||
waitingExit: waitingExit
|
||||
);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
172
src/Ryujinx/UI/Models/FirmwareAvatarCache.cs
Normal file
172
src/Ryujinx/UI/Models/FirmwareAvatarCache.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
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.ViewModels;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
public class FirmwareAvatarCache : BaseModel, IReadOnlyDictionary<string, byte[]>
|
||||
{
|
||||
private readonly Dictionary<string, byte[]> _backing = new();
|
||||
|
||||
public FirmwareAvatarCache(ContentManager contentManager, VirtualFileSystem virtualFileSystem)
|
||||
{
|
||||
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 UniqueRef<IFile> file = new();
|
||||
|
||||
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;
|
||||
|
||||
SKImage avatarImage = SKImage.FromPixelCopy(new SKImageInfo(256, 256, SKColorType.Rgba8888, SKAlphaType.Premul), DecompressYaz0(stream));
|
||||
|
||||
using (SKData data = avatarImage.Encode(SKEncodedImageFormat.Png, 100))
|
||||
{
|
||||
data.SaveTo(streamPng);
|
||||
}
|
||||
|
||||
_backing[item.FullPath] = streamPng.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<ProfileImageModel> CreateProfileImageModels()
|
||||
=> this.Select(x => new ProfileImageModel(x.Key, x.Value));
|
||||
|
||||
private static byte[] DecompressYaz0(MemoryStream 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.ReadExactly(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;
|
||||
}
|
||||
|
||||
#region dictionary impl
|
||||
|
||||
IEnumerator<KeyValuePair<string, byte[]>> IEnumerable<KeyValuePair<string, byte[]>>.GetEnumerator()
|
||||
{
|
||||
return (_backing as IEnumerable<KeyValuePair<string, byte[]>>).GetEnumerator();
|
||||
}
|
||||
|
||||
public IEnumerator GetEnumerator()
|
||||
{
|
||||
return ((IEnumerable)_backing).GetEnumerator();
|
||||
}
|
||||
|
||||
public int Count => _backing.Count;
|
||||
public bool ContainsKey(string key) => _backing.ContainsKey(key);
|
||||
|
||||
public bool TryGetValue(string key, out byte[] value) => _backing.TryGetValue(key, out value);
|
||||
|
||||
public byte[] this[string key] => _backing[key];
|
||||
|
||||
public IEnumerable<string> Keys => _backing.Keys;
|
||||
public IEnumerable<byte[]> Values => _backing.Values;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Ava.Utilities;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Ava.UI.SetupWizard;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<UserControl 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:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:pages="clr-namespace:Ryujinx.Ava.UI.SetupWizard.Pages"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:DataType="pages:SetupFinishedPageContext"
|
||||
x:Class="Ryujinx.Ava.UI.SetupWizard.Pages.SetupFinishedPage">
|
||||
<Grid
|
||||
ColumnDefinitions="*"
|
||||
RowDefinitions="*,Auto"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch">
|
||||
<Border
|
||||
Margin="15"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
CornerRadius="5"
|
||||
Background="{DynamicResource AppListBackgroundColor}">
|
||||
<TextBlock Margin="15" Text="{ext:Locale SetupWizardFinalPageDescription}" TextAlignment="Center" TextWrapping="Wrap" />
|
||||
</Border>
|
||||
<Button Grid.Row="1"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Center"
|
||||
MinWidth="45"
|
||||
MinHeight="32"
|
||||
Padding="8"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
CornerRadius="5"
|
||||
Tag="https://discord.gg/PEuzjrFXUA"
|
||||
ToolTip.Tip="{ext:Locale AboutDiscordUrlTooltipMessage}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<Image Source="{Binding OwningWizard.DiscordLogo}" />
|
||||
<TextBlock Text="Discord"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,22 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Common.Helper;
|
||||
|
||||
namespace Ryujinx.Ava.UI.SetupWizard.Pages
|
||||
{
|
||||
public partial class SetupFinishedPage : RyujinxControl<SetupFinishedPageContext>
|
||||
{
|
||||
public SetupFinishedPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void Button_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button { Tag: string url })
|
||||
OpenHelper.OpenUrl(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Gommon;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
|
||||
namespace Ryujinx.Ava.UI.SetupWizard.Pages
|
||||
{
|
||||
public class SetupFinishedPageContext() : SetupWizardPageContext(LocaleKeys.SetupWizardFinalPageTitle)
|
||||
{
|
||||
public override LocaleKeys ActionContent => LocaleKeys.SetupWizardFinalPageAction;
|
||||
|
||||
// informative step; this implementation is not called.
|
||||
public override Result CompleteStep() => Result.Success;
|
||||
}
|
||||
}
|
||||
27
src/Ryujinx/UI/SetupWizard/Pages/Fw/SetupFirmwarePage.axaml
Normal file
27
src/Ryujinx/UI/SetupWizard/Pages/Fw/SetupFirmwarePage.axaml
Normal file
@@ -0,0 +1,27 @@
|
||||
<UserControl 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:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:pages="clr-namespace:Ryujinx.Ava.UI.SetupWizard.Pages"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:DataType="pages:SetupFirmwarePageContext"
|
||||
x:Class="Ryujinx.Ava.UI.SetupWizard.Pages.SetupFirmwarePage">
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale SetupWizardFirmwarePageDescription}"/>
|
||||
<Grid ColumnDefinitions="*" RowDefinitions="*,Auto">
|
||||
<TextBox Name="FirmwarePathField" Margin="0, 10, 0, 5" Text="{Binding FirmwareSourcePath}" IsReadOnly="True" />
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="3.5">
|
||||
<Button
|
||||
Content="{ext:Locale SetupWizardFirmwarePageFolderBrowse}"
|
||||
Command="{Binding BrowseFolderCommand}"
|
||||
CommandParameter="{Binding #FirmwarePathField}"/>
|
||||
<Button
|
||||
Content="{ext:Locale SetupWizardFirmwarePageFileBrowse}"
|
||||
Command="{Binding BrowseFileCommand}"
|
||||
CommandParameter="{Binding #FirmwarePathField}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
|
||||
namespace Ryujinx.Ava.UI.SetupWizard.Pages
|
||||
{
|
||||
public partial class SetupFirmwarePage : RyujinxControl<SetupFirmwarePageContext>
|
||||
{
|
||||
public SetupFirmwarePage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
167
src/Ryujinx/UI/SetupWizard/Pages/Fw/SetupFirmwarePageContext.cs
Normal file
167
src/Ryujinx/UI/SetupWizard/Pages/Fw/SetupFirmwarePageContext.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Platform.Storage;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Gommon;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.Utilities;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.SetupWizard.Pages
|
||||
{
|
||||
public partial class SetupFirmwarePageContext() : SetupWizardPageContext(LocaleKeys.SetupWizardFirmwarePageTitle)
|
||||
{
|
||||
[ObservableProperty] public partial string FirmwareSourcePath { get; set; }
|
||||
|
||||
[RelayCommand]
|
||||
private static async Task BrowseFile(TextBox tb)
|
||||
{
|
||||
Optional<IStorageFile> result =
|
||||
await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFilePickerAsync(
|
||||
new FilePickerOpenOptions
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.SetupWizardFirmwarePageFilePopupTitle],
|
||||
FileTypeFilter = new List<FilePickerFileType>
|
||||
{
|
||||
new(LocaleManager.Instance[LocaleKeys.FileDialogAllTypes])
|
||||
{
|
||||
Patterns = ["*.xci", "*.zip"],
|
||||
AppleUniformTypeIdentifiers = ["com.ryujinx.xci", "public.zip-archive"],
|
||||
MimeTypes = ["application/x-nx-xci", "application/zip"],
|
||||
},
|
||||
new("XCI")
|
||||
{
|
||||
Patterns = ["*.xci"],
|
||||
AppleUniformTypeIdentifiers = ["com.ryujinx.xci"],
|
||||
MimeTypes = ["application/x-nx-xci"],
|
||||
},
|
||||
new("ZIP")
|
||||
{
|
||||
Patterns = ["*.zip"],
|
||||
AppleUniformTypeIdentifiers = ["public.zip-archive"],
|
||||
MimeTypes = ["application/zip"],
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (result.TryGet(out IStorageFile firmwareFile))
|
||||
{
|
||||
tb.Text = firmwareFile.TryGetLocalPath();
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private static async Task BrowseFolder(TextBox tb)
|
||||
{
|
||||
Optional<IStorageFolder> result =
|
||||
await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFolderPickerAsync(
|
||||
new FolderPickerOpenOptions
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.SetupWizardFirmwarePageFolderPopupTitle]
|
||||
});
|
||||
|
||||
if (result.TryGet(out IStorageFolder firmwareFolder))
|
||||
{
|
||||
tb.Text = firmwareFolder.TryGetLocalPath();
|
||||
}
|
||||
}
|
||||
|
||||
public override object CreateHelpContent()
|
||||
{
|
||||
Grid grid = new()
|
||||
{
|
||||
RowDefinitions = [new(GridLength.Auto), new(GridLength.Auto)],
|
||||
HorizontalAlignment = HorizontalAlignment.Center
|
||||
};
|
||||
|
||||
grid.Children.Add(new TextBlock
|
||||
{
|
||||
Text = LocaleManager.Instance[LocaleKeys.SetupWizardFirmwarePageHelpText],
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
GridRow = 0
|
||||
});
|
||||
|
||||
grid.Children.Add(new HyperlinkButton
|
||||
{
|
||||
Content = LocaleManager.Instance[LocaleKeys.SetupWizardHelpLinkButton],
|
||||
NavigateUri = new Uri(SharedConstants.DumpFirmwareWikiUrl),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
GridRow = 1
|
||||
});
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
public override Result CompleteStep()
|
||||
{
|
||||
if (string.IsNullOrEmpty(FirmwareSourcePath) && RyujinxSetupWizard.HasFirmware)
|
||||
{
|
||||
NotificationManager.Information(
|
||||
title: LocaleManager.Instance[LocaleKeys.RyujinxInfo],
|
||||
text: LocaleManager.GetFormatted(
|
||||
LocaleKeys.SetupWizardFirmwarePageSkipText,
|
||||
LocaleManager.Instance[LocaleKeys.SetupWizardActionBack]
|
||||
)
|
||||
);
|
||||
return Result.Success; // This handles the user selecting no file/dir and just hitting Next.
|
||||
}
|
||||
|
||||
if (!Directory.Exists(FirmwareSourcePath))
|
||||
return Result.Fail;
|
||||
|
||||
try
|
||||
{
|
||||
RyujinxApp.MainWindow.ContentManager.InstallFirmware(FirmwareSourcePath);
|
||||
SystemVersion installedFwVer = RyujinxApp.MainWindow.ContentManager.GetCurrentFirmwareVersion();
|
||||
if (installedFwVer != null)
|
||||
{
|
||||
NotificationManager.Information(
|
||||
LocaleManager.Instance[LocaleKeys.SetupWizardFirmwarePageInstallSuccessNotificationTitle],
|
||||
LocaleManager.GetFormatted(
|
||||
LocaleKeys.SetupWizardFirmwarePageInstallSuccessNotificationTitle,
|
||||
installedFwVer.VersionString
|
||||
)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
NotificationManager.Error(
|
||||
LocaleManager.Instance[LocaleKeys.SetupWizardFirmwarePageInstallFailNotificationTitle],
|
||||
LocaleManager.GetFormatted(
|
||||
LocaleKeys.SetupWizardFirmwarePageInstallFailNotificationText,
|
||||
FirmwareSourcePath
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
RyujinxApp.MainWindow.ViewModel.RefreshFirmwareStatus(installedFwVer, allowNullVersion: true);
|
||||
|
||||
// Purge Applet Cache.
|
||||
|
||||
DirectoryInfo miiEditorCacheFolder = new(
|
||||
Path.Combine(AppDataManager.GamesDirPath, "0100000000001009", "cache")
|
||||
);
|
||||
|
||||
if (miiEditorCacheFolder.Exists)
|
||||
{
|
||||
miiEditorCacheFolder.Delete(true);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
NotificationManager.Error(e.Message, waitingExit: true);
|
||||
return Result.Fail;
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<UserControl 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:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:pages="clr-namespace:Ryujinx.UI.SetupWizard.Pages"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Ryujinx.UI.SetupWizard.Pages.SetupGameDirsPage"
|
||||
x:DataType="pages:SetupGameDirsPageContext">
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
Spacing="10"
|
||||
Orientation="Vertical" HorizontalAlignment="Stretch">
|
||||
<TextBlock Foreground="{DynamicResource SecondaryTextColor}" Text="{ext:Locale SetupWizardGameDirsPageDescription}" />
|
||||
<TextBlock Classes="h1" Text="{ext:Locale SettingsTabGeneralGameDirectories}" />
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<ListBox
|
||||
Name="GameDirsList"
|
||||
MinHeight="120"
|
||||
ItemsSource="{Binding GameDirs}">
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Padding" Value="10" />
|
||||
<Setter Property="Background" Value="{DynamicResource ListBoxBackground}" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
</ListBox>
|
||||
<Grid HorizontalAlignment="Stretch" ColumnDefinitions="*,Auto,Auto">
|
||||
<TextBox
|
||||
Name="GameDirPathBox"
|
||||
Margin="0"
|
||||
Watermark="{ext:Locale AddGameDirBoxTooltip}"
|
||||
VerticalAlignment="Stretch" />
|
||||
<Button
|
||||
Name="AddGameDirButton"
|
||||
Grid.Column="1"
|
||||
MinWidth="90"
|
||||
Margin="10,0,0,0">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
Text="{ext:Locale SettingsTabGeneralAdd}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="RemoveGameDirButton"
|
||||
Grid.Column="2"
|
||||
MinWidth="90"
|
||||
Margin="5,0,0,0"
|
||||
Click="RemoveGameDirButton_OnClick">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
Text="{ext:Locale SettingsTabGeneralRemove}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<StackPanel Orientation="Vertical" Spacing="5">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Classes="h1" Text="{ext:Locale SettingsTabGeneralAutoloadDirectories}" />
|
||||
</StackPanel>
|
||||
<TextBlock Foreground="{DynamicResource SecondaryTextColor}"
|
||||
Text="{ext:Locale SettingsTabGeneralAutoloadNote}" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<ListBox
|
||||
Name="AutoloadDirsList"
|
||||
MinHeight="100"
|
||||
ItemsSource="{Binding UpdateAndDlcDirs}">
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Padding" Value="10" />
|
||||
<Setter Property="Background" Value="{DynamicResource ListBoxBackground}" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
</ListBox>
|
||||
<Grid HorizontalAlignment="Stretch" ColumnDefinitions="*,Auto,Auto">
|
||||
<TextBox
|
||||
Name="AutoloadDirPathBox"
|
||||
Margin="0"
|
||||
Watermark="{ext:Locale AddGameDirBoxTooltip}"
|
||||
VerticalAlignment="Stretch" />
|
||||
<Button
|
||||
Name="AddAutoloadDirButton"
|
||||
Grid.Column="1"
|
||||
MinWidth="90"
|
||||
Margin="10,0,0,0">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
Text="{ext:Locale SettingsTabGeneralAdd}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="RemoveAutoloadDirButton"
|
||||
Grid.Column="2"
|
||||
MinWidth="90"
|
||||
Margin="5,0,0,0"
|
||||
Click="RemoveAutoloadDirButton_OnClick">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
Text="{ext:Locale SettingsTabGeneralRemove}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,80 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Ryujinx.Ava;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.Utilities;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.UI.SetupWizard.Pages
|
||||
{
|
||||
public partial class SetupGameDirsPage : RyujinxControl<SetupGameDirsPageContext>
|
||||
{
|
||||
public SetupGameDirsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
AddGameDirButton.Command =
|
||||
Commands.Create(() => AddDirButton(GameDirPathBox, ViewModel.GameDirs));
|
||||
AddAutoloadDirButton.Command =
|
||||
Commands.Create(() => AddDirButton(AutoloadDirPathBox, ViewModel.UpdateAndDlcDirs));
|
||||
}
|
||||
|
||||
private async Task AddDirButton(TextBox addDirBox, ObservableCollection<string> directories)
|
||||
{
|
||||
string path = addDirBox.Text;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !directories.Contains(path))
|
||||
{
|
||||
directories.Add(path);
|
||||
|
||||
addDirBox.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
Gommon.Optional<IStorageFolder> folder = await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFolderPickerAsync();
|
||||
|
||||
if (folder.HasValue)
|
||||
{
|
||||
directories.Add(folder.Value.Path.LocalPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveGameDirButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
int oldIndex = GameDirsList.SelectedIndex;
|
||||
|
||||
foreach (string path in new List<string>(GameDirsList.SelectedItems.Cast<string>()))
|
||||
{
|
||||
ViewModel.GameDirs.Remove(path);
|
||||
}
|
||||
|
||||
if (GameDirsList.ItemCount > 0)
|
||||
{
|
||||
GameDirsList.SelectedIndex = oldIndex < GameDirsList.ItemCount ? oldIndex : 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveAutoloadDirButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
int oldIndex = AutoloadDirsList.SelectedIndex;
|
||||
|
||||
foreach (string path in new List<string>(AutoloadDirsList.SelectedItems.Cast<string>()))
|
||||
{
|
||||
ViewModel.UpdateAndDlcDirs.Remove(path);
|
||||
}
|
||||
|
||||
if (AutoloadDirsList.ItemCount > 0)
|
||||
{
|
||||
AutoloadDirsList.SelectedIndex = oldIndex < AutoloadDirsList.ItemCount ? oldIndex : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System.Collections.ObjectModel;
|
||||
using Gommon;
|
||||
using Ryujinx.Ava;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Systems.Configuration;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.SetupWizard;
|
||||
using Ryujinx.Common;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.UI.SetupWizard.Pages
|
||||
{
|
||||
public partial class SetupGameDirsPageContext() : SetupWizardPageContext(LocaleKeys.SetupWizardGameDirsPageTitle)
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<string> GameDirs { get; set; }
|
||||
= new(ConfigurationState.Instance.UI.GameDirs);
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<string> UpdateAndDlcDirs { get; set; }
|
||||
= new(ConfigurationState.Instance.UI.AutoloadDirs);
|
||||
|
||||
public override Result CompleteStep()
|
||||
{
|
||||
if (GameDirs.Count is 0)
|
||||
{
|
||||
NotificationManager.Error(LocaleManager.Instance[LocaleKeys.SetupWizardGameDirsPageNoFoldersSelectedError]);
|
||||
return Result.Fail;
|
||||
}
|
||||
|
||||
OwningWizard.ModifyConfig(config =>
|
||||
{
|
||||
config.UI.GameDirs.Value = GameDirs.ToList();
|
||||
config.UI.AutoloadDirs.Value = UpdateAndDlcDirs.ToList();
|
||||
});
|
||||
|
||||
RyujinxApp.MainWindow.LoadApplications();
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
public override object CreateHelpContent()
|
||||
{
|
||||
Grid grid = new()
|
||||
{
|
||||
RowDefinitions = [new(GridLength.Auto), new(GridLength.Auto)],
|
||||
HorizontalAlignment = HorizontalAlignment.Center
|
||||
};
|
||||
|
||||
grid.Children.Add(new TextBlock
|
||||
{
|
||||
Text = LocaleManager.Instance[LocaleKeys.SetupWizardGameDirsPageHelpText],
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
GridRow = 0
|
||||
});
|
||||
|
||||
grid.Children.Add(new HyperlinkButton
|
||||
{
|
||||
Content = LocaleManager.Instance[LocaleKeys.SetupWizardHelpLinkButton],
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
NavigateUri = new Uri(SharedConstants.DumpContentWikiUrl),
|
||||
GridRow = 1
|
||||
});
|
||||
|
||||
return grid;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPage.axaml
Normal file
22
src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPage.axaml
Normal file
@@ -0,0 +1,22 @@
|
||||
<UserControl 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:pages="clr-namespace:Ryujinx.Ava.UI.SetupWizard.Pages"
|
||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Ryujinx.Ava.UI.SetupWizard.Pages.SetupKeysPage"
|
||||
x:DataType="pages:SetupKeysPageContext">
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale SetupWizardKeysPageDescription}" Margin="0,0,0,10"/>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBox Name="KeysFolderPathField" Text="{Binding KeysFolderPath}" IsReadOnly="True" />
|
||||
<Button Grid.Column="1"
|
||||
Content="..."
|
||||
Command="{Binding BrowseCommand}"
|
||||
CommandParameter="{Binding #KeysFolderPathField}"
|
||||
Margin="5,0,0,0"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
|
||||
13
src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPage.axaml.cs
Normal file
13
src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPage.axaml.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
|
||||
namespace Ryujinx.Ava.UI.SetupWizard.Pages
|
||||
{
|
||||
public partial class SetupKeysPage : RyujinxControl<SetupKeysPageContext>
|
||||
{
|
||||
public SetupKeysPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
130
src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPageContext.cs
Normal file
130
src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPageContext.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Platform.Storage;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using DynamicData;
|
||||
using Gommon;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.Utilities;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.Exceptions;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.SetupWizard.Pages
|
||||
{
|
||||
public partial class SetupKeysPageContext() : SetupWizardPageContext(LocaleKeys.SetupWizardKeysPageTitle)
|
||||
{
|
||||
public override object CreateHelpContent()
|
||||
{
|
||||
Grid grid = new()
|
||||
{
|
||||
RowDefinitions = [new(GridLength.Auto), new(GridLength.Auto)],
|
||||
HorizontalAlignment = HorizontalAlignment.Center
|
||||
};
|
||||
|
||||
grid.Children.Add(new TextBlock
|
||||
{
|
||||
Text = LocaleManager.Instance[LocaleKeys.SetupWizardKeysPageHelpText],
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
GridRow = 0
|
||||
});
|
||||
|
||||
grid.Children.Add(new HyperlinkButton
|
||||
{
|
||||
Content = LocaleManager.Instance[LocaleKeys.SetupWizardHelpLinkButton],
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
NavigateUri = new Uri(SharedConstants.DumpKeysWikiUrl),
|
||||
GridRow = 1
|
||||
});
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
[ObservableProperty] public partial string KeysFolderPath { get; set; }
|
||||
|
||||
[RelayCommand]
|
||||
private static async Task Browse(TextBox tb)
|
||||
{
|
||||
Optional<IStorageFolder> result =
|
||||
await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFolderPickerAsync(
|
||||
new FolderPickerOpenOptions
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.SetupWizardKeysPageFolderPopupTitle]
|
||||
});
|
||||
|
||||
if (result.TryGet(out IStorageFolder keyFolder))
|
||||
{
|
||||
tb.Text = keyFolder.TryGetLocalPath();
|
||||
}
|
||||
}
|
||||
|
||||
public override Result CompleteStep()
|
||||
{
|
||||
if (string.IsNullOrEmpty(KeysFolderPath) && RyujinxApp.MainWindow.VirtualFileSystem.HasKeySet)
|
||||
{
|
||||
NotificationManager.Information(
|
||||
title: LocaleManager.Instance[LocaleKeys.RyujinxInfo],
|
||||
text: LocaleManager.GetFormatted(
|
||||
LocaleKeys.SetupWizardKeysPageSkipText,
|
||||
LocaleManager.Instance[LocaleKeys.SetupWizardActionBack]
|
||||
));
|
||||
return Result.Success; // This handles the user selecting no folder and just hitting Next.
|
||||
}
|
||||
|
||||
if (!Directory.Exists(KeysFolderPath))
|
||||
return Result.Fail;
|
||||
|
||||
try
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, $"Installing keys from {KeysFolderPath}");
|
||||
|
||||
ContentManager.InstallKeys(KeysFolderPath, AppDataManager.GetKeysDir());
|
||||
|
||||
NotificationManager.Information(
|
||||
title: LocaleManager.Instance[LocaleKeys.RyujinxInfo],
|
||||
text: LocaleManager.Instance[LocaleKeys.DialogKeysInstallerKeysInstallSuccessMessage]);
|
||||
}
|
||||
catch (InvalidFirmwarePackageException ifwpe)
|
||||
{
|
||||
NotificationManager.Error(ifwpe.Message, waitingExit: true);
|
||||
return Result.Failure(NoKeysFoundInFolder.Shared);
|
||||
}
|
||||
catch (MissingKeyException ex)
|
||||
{
|
||||
NotificationManager.Error(ex.ToString(), waitingExit: true);
|
||||
return Result.Failure(NoKeysFoundInFolder.Shared);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
string message = ex.Message;
|
||||
if (ex is FormatException)
|
||||
{
|
||||
message = LocaleManager.Instance.UpdateAndGetDynamicValue(
|
||||
LocaleKeys.DialogKeysInstallerKeysNotFoundErrorMessage, KeysFolderPath);
|
||||
}
|
||||
|
||||
NotificationManager.Error(message, waitingExit: true);
|
||||
|
||||
return Result.Failure(new MessageError(message));
|
||||
}
|
||||
finally
|
||||
{
|
||||
RyujinxApp.MainWindow.VirtualFileSystem.ReloadKeySet();
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
||||
public struct NoKeysFoundInFolder : IErrorState
|
||||
{
|
||||
public static readonly NoKeysFoundInFolder Shared = new();
|
||||
}
|
||||
}
|
||||
3
src/Ryujinx/UI/SetupWizard/README.md
Normal file
3
src/Ryujinx/UI/SetupWizard/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ryubing Setup Wizard
|
||||
|
||||
Directly modified from the code found [here](https://github.com/TKMM-Team/Tkmm/tree/master/src/Tkmm/Wizard).
|
||||
82
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.Steps.cs
Normal file
82
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.Steps.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using Ryujinx.Ava.UI.SetupWizard.Pages;
|
||||
using Ryujinx.UI.SetupWizard.Pages;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.SetupWizard
|
||||
{
|
||||
public partial class RyujinxSetupWizard
|
||||
{
|
||||
private async ValueTask<bool> SetupKeys()
|
||||
{
|
||||
if (_overwrite || !RyujinxApp.MainWindow.VirtualFileSystem.HasKeySet)
|
||||
{
|
||||
Retry:
|
||||
bool result = await NextPage<SetupKeysPage, SetupKeysPageContext>(out SetupKeysPageContext keyContext)
|
||||
.Show();
|
||||
|
||||
if (!result)
|
||||
return false;
|
||||
|
||||
if (!keyContext.CompleteStep())
|
||||
goto Retry;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async ValueTask<bool> SetupFirmware()
|
||||
{
|
||||
if (_overwrite || !HasFirmware)
|
||||
{
|
||||
if (!RyujinxApp.MainWindow.VirtualFileSystem.HasKeySet)
|
||||
{
|
||||
NotificationManager.Error("Keys still seem to not be installed. Please try again.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Retry:
|
||||
bool result =
|
||||
await NextPage<SetupFirmwarePage, SetupFirmwarePageContext>(out SetupFirmwarePageContext fwContext)
|
||||
.Show();
|
||||
|
||||
if (!result)
|
||||
return false;
|
||||
|
||||
if (!fwContext.CompleteStep())
|
||||
goto Retry;
|
||||
|
||||
OnPropertyChanged(nameof(HasFirmware));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async ValueTask<bool> SetupGameDirs()
|
||||
{
|
||||
|
||||
if (!HasFirmware)
|
||||
{
|
||||
NotificationManager.Error("Firmware still seems to not be installed. Please try again.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Retry:
|
||||
bool result =
|
||||
await NextPage<SetupGameDirsPage, SetupGameDirsPageContext>(out SetupGameDirsPageContext gdContext)
|
||||
.Show();
|
||||
|
||||
if (!result)
|
||||
return false;
|
||||
|
||||
if (!gdContext.CompleteStep())
|
||||
goto Retry;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private ValueTask<bool> Finish()
|
||||
=> NextPage<SetupFinishedPage, SetupFinishedPageContext>(out _)
|
||||
.WithHelpButtonVisible(false)
|
||||
.Show();
|
||||
}
|
||||
}
|
||||
144
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.cs
Normal file
144
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Notifications;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Ryujinx.Ava.Common;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Systems.Configuration;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.SetupWizard
|
||||
{
|
||||
public partial class RyujinxSetupWizard : BaseModel, IDisposable
|
||||
{
|
||||
private bool _configWasModified;
|
||||
|
||||
private readonly RyujinxSetupWizardWindow _window;
|
||||
private readonly bool _overwrite;
|
||||
|
||||
public void SetWindowTitle(string titleText)
|
||||
{
|
||||
_window.Title = titleText;
|
||||
ToolTip.SetTip(_window.RyuLogo, titleText);
|
||||
}
|
||||
|
||||
public RyujinxSetupWizard(RyujinxSetupWizardWindow wizardWindow, bool overwriteMode)
|
||||
{
|
||||
_window = wizardWindow;
|
||||
_overwrite = overwriteMode;
|
||||
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle);
|
||||
RyujinxApp.ThemeChanged += Ryujinx_ThemeChanged;
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateLogoTheme("Dark");
|
||||
}
|
||||
}
|
||||
|
||||
private SetupWizardPage FirstPage() => new(_window.WizardPresenter, this, isFirstPage: true);
|
||||
|
||||
private SetupWizardPage NextPage() => new(_window.WizardPresenter, this);
|
||||
|
||||
private SetupWizardPage NextPage<TControl, TContext>(out TContext boundContext)
|
||||
where TControl : RyujinxControl<TContext>, new()
|
||||
where TContext : SetupWizardPageContext, new()
|
||||
=> NextPage()
|
||||
.WithContent<TControl, TContext>(out boundContext)
|
||||
.WithTitle(boundContext.Title)
|
||||
.WithActionContent(boundContext.ActionContent);
|
||||
|
||||
public static bool HasFirmware => RyujinxApp.MainWindow.ContentManager.GetCurrentFirmwareVersion() != null;
|
||||
|
||||
public RyujinxNotificationManager NotificationManager { get; private set; }
|
||||
|
||||
internal void ModifyConfig(Action<ConfigurationState> modifier)
|
||||
{
|
||||
modifier(ConfigurationState.Instance);
|
||||
_configWasModified = true;
|
||||
}
|
||||
|
||||
public async Task Start()
|
||||
{
|
||||
NotificationManager = _window.CreateNotificationManager(
|
||||
// I wanted to do bottom center but that...literally just shows top center? Okay.
|
||||
// Fuck it, weird window height hack to do it instead.
|
||||
// 120 is not exact, just a random number. Looks fine though.
|
||||
NotificationPosition.TopCenter,
|
||||
margin: new Thickness(0, _window.Height - 135, 0, 0)
|
||||
);
|
||||
|
||||
RyujinxSetupWizardWindow.IsOpen = true;
|
||||
Start:
|
||||
await FirstPage()
|
||||
.WithTitle(LocaleKeys.SetupWizardFirstPageTitle)
|
||||
.WithContent(LocaleKeys.SetupWizardFirstPageContent)
|
||||
.WithActionContent(LocaleKeys.SetupWizardFirstPageAction)
|
||||
.Show();
|
||||
// result is unhandled as the first page cannot display anything other than the next button.
|
||||
// back does not need to be handled
|
||||
|
||||
Keys:
|
||||
if (!await SetupKeys())
|
||||
goto Start;
|
||||
|
||||
Firmware:
|
||||
if (!await SetupFirmware())
|
||||
goto Keys;
|
||||
|
||||
GameDirs:
|
||||
if (!await SetupGameDirs())
|
||||
goto Firmware;
|
||||
|
||||
if (!await Finish())
|
||||
goto GameDirs;
|
||||
|
||||
Return:
|
||||
if (_configWasModified)
|
||||
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.GlobalConfigurationPath);
|
||||
|
||||
NotificationManager = null;
|
||||
_window.Close();
|
||||
RyujinxSetupWizardWindow.IsOpen = false;
|
||||
}
|
||||
|
||||
#region Discord logo stuff
|
||||
|
||||
[ObservableProperty] public partial Bitmap DiscordLogo { get; set; }
|
||||
|
||||
private void Ryujinx_ThemeChanged()
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle));
|
||||
}
|
||||
|
||||
private void UpdateLogoTheme(string theme)
|
||||
{
|
||||
bool isDarkTheme = theme == "Dark" ||
|
||||
(theme == "Auto" && RyujinxApp.DetectSystemTheme() == ThemeVariant.Dark);
|
||||
|
||||
DiscordLogo = UIImages
|
||||
.GetLogoByNameAndTheme("Discord", isDarkTheme)
|
||||
.CreateScaledBitmap(new PixelSize(32, 24));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
RyujinxApp.ThemeChanged -= Ryujinx_ThemeChanged;
|
||||
|
||||
DiscordLogo.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
18
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizardWindow.axaml
Normal file
18
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizardWindow.axaml
Normal file
@@ -0,0 +1,18 @@
|
||||
<windows:StyleableAppWindow 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:windows="clr-namespace:Ryujinx.Ava.UI.Windows"
|
||||
xmlns:setupWizard="clr-namespace:Ryujinx.Ava.UI.SetupWizard"
|
||||
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
|
||||
CanResize="False"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Ryujinx.Ava.UI.SetupWizard.RyujinxSetupWizardWindow"
|
||||
x:DataType="setupWizard:RyujinxSetupWizard">
|
||||
<Grid VerticalAlignment="Stretch" HorizontalAlignment="Stretch" RowDefinitions="Auto,*">
|
||||
<Grid Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Name="FlushControls">
|
||||
<controls:RyujinxLogo Name="RyuLogo"/>
|
||||
</Grid>
|
||||
<ContentPresenter Grid.Row="1" Name="WizardPresenter"/>
|
||||
</Grid>
|
||||
</windows:StyleableAppWindow>
|
||||
90
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizardWindow.axaml.cs
Normal file
90
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizardWindow.axaml.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using Avalonia.Controls;
|
||||
using Ryujinx.Ava.Systems.Configuration;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.SetupWizard
|
||||
{
|
||||
public partial class RyujinxSetupWizardWindow : StyleableAppWindow
|
||||
{
|
||||
public static bool IsOpen { get; set; }
|
||||
|
||||
public RyujinxSetupWizardWindow() : base(useCustomTitleBar: true)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
FlushControls.IsVisible = !ConfigurationState.Instance.ShowOldUI;
|
||||
}
|
||||
}
|
||||
|
||||
public static Task ShowAsync(bool overwriteMode, Window owner = null)
|
||||
{
|
||||
if (!CanShowSetupWizard)
|
||||
return Task.CompletedTask;
|
||||
|
||||
Task windowTask = ShowAsync(
|
||||
CreateWindow(out RyujinxSetupWizard wiz, overwriteMode),
|
||||
owner
|
||||
);
|
||||
_ = wiz.Start();
|
||||
return windowTask.ContinueWith(_ => wiz.Dispose());
|
||||
}
|
||||
|
||||
public static RyujinxSetupWizardWindow CreateWindow(out RyujinxSetupWizard setupWizard, bool overwriteMode = false)
|
||||
{
|
||||
RyujinxSetupWizardWindow window = new();
|
||||
window.DataContext = setupWizard = new RyujinxSetupWizard(window, overwriteMode);
|
||||
window.Height = 700;
|
||||
window.Width = 825;
|
||||
return window;
|
||||
}
|
||||
|
||||
public static bool CanShowSetupWizard =>
|
||||
!File.Exists(Path.Combine(AppDataManager.BaseDirPath, ".DoNotShowSetupWizard"));
|
||||
|
||||
public static bool DisableSetupWizard()
|
||||
{
|
||||
if (!CanShowSetupWizard)
|
||||
return false; //cannot disable; file exists, so it's already disabled.
|
||||
|
||||
string disableFile = Path.Combine(AppDataManager.BaseDirPath, ".DoNotShowSetupWizard");
|
||||
|
||||
try
|
||||
{
|
||||
File.Create(disableFile, 0).Dispose();
|
||||
File.SetAttributes(disableFile, File.GetAttributes(disableFile) | FileAttributes.Hidden);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error?.PrintStack(LogClass.Application, e.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool EnableSetupWizard()
|
||||
{
|
||||
if (CanShowSetupWizard)
|
||||
return false; //cannot enable; file does not exist, so it's already enabled.
|
||||
|
||||
string disableFile = Path.Combine(AppDataManager.BaseDirPath, ".DoNotShowSetupWizard");
|
||||
|
||||
try
|
||||
{
|
||||
File.Delete(disableFile);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error?.PrintStack(LogClass.Application, e.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
94
src/Ryujinx/UI/SetupWizard/SetupWizardPage.Builder.cs
Normal file
94
src/Ryujinx/UI/SetupWizard/SetupWizardPage.Builder.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
|
||||
namespace Ryujinx.Ava.UI.SetupWizard
|
||||
{
|
||||
public partial class SetupWizardPage
|
||||
{
|
||||
public SetupWizardPage WithTitle(LocaleKeys title) => WithTitle(LocaleManager.Instance[title]);
|
||||
|
||||
public SetupWizardPage WithTitle(string title)
|
||||
{
|
||||
Title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SetupWizardPage WithContent(LocaleKeys content) => WithContent(LocaleManager.Instance[content]);
|
||||
|
||||
public SetupWizardPage WithContent(object? content)
|
||||
{
|
||||
if (content is StyledElement { Parent: ContentControl parent })
|
||||
{
|
||||
parent.Content = null;
|
||||
}
|
||||
|
||||
Content = content;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SetupWizardPage WithHelpContent(LocaleKeys content) =>
|
||||
WithHelpContent(LocaleManager.Instance[content]);
|
||||
|
||||
public SetupWizardPage WithHelpContent(object? content)
|
||||
{
|
||||
if (content is string str)
|
||||
{
|
||||
TextBlock tb = new()
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextAlignment = TextAlignment.Center,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
FontSize = 20.0,
|
||||
Text = str
|
||||
};
|
||||
|
||||
tb.Classes.Add("h1");
|
||||
|
||||
content = tb;
|
||||
}
|
||||
|
||||
HelpContent = content;
|
||||
HasHelpContent = content != null;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SetupWizardPage WithContent<TControl>(object? context = null) where TControl : Control, new()
|
||||
{
|
||||
Content = new TControl { DataContext = context };
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public SetupWizardPage WithContent<TControl, TContext>(out TContext boundContext)
|
||||
where TControl : RyujinxControl<TContext>, new()
|
||||
where TContext : SetupWizardPageContext, new()
|
||||
{
|
||||
boundContext = new() { OwningWizard = ownerWizard };
|
||||
|
||||
if (boundContext.CreateHelpContent() is { } content)
|
||||
WithHelpContent(content);
|
||||
|
||||
return WithContent<TControl>(boundContext);
|
||||
}
|
||||
|
||||
public SetupWizardPage WithActionContent(LocaleKeys content) =>
|
||||
WithActionContent(LocaleManager.Instance[content]);
|
||||
|
||||
public SetupWizardPage WithActionContent(object? content)
|
||||
{
|
||||
ActionContent = content;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SetupWizardPage WithHelpButtonVisible(bool visible)
|
||||
{
|
||||
ShowHelpButton = visible;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src/Ryujinx/UI/SetupWizard/SetupWizardPage.cs
Normal file
67
src/Ryujinx/UI/SetupWizard/SetupWizardPage.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using Avalonia.Controls.Presenters;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.SetupWizard
|
||||
{
|
||||
public partial class SetupWizardPage(
|
||||
ContentPresenter contentPresenter,
|
||||
RyujinxSetupWizard ownerWizard,
|
||||
bool isFirstPage = false) : BaseModel
|
||||
{
|
||||
private bool? _result;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
public bool IsFirstPage => isFirstPage;
|
||||
|
||||
public RyujinxSetupWizard Parent => ownerWizard;
|
||||
|
||||
[ObservableProperty] public partial string? Title { get; set; }
|
||||
|
||||
[ObservableProperty] public partial object? Content { get; set; }
|
||||
|
||||
[ObservableProperty] public partial object? HelpContent { get; set; }
|
||||
|
||||
[ObservableProperty] public partial bool HasHelpContent { get; set; }
|
||||
|
||||
[ObservableProperty] public partial bool ShowHelpButton { get; set; } = true;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial object? ActionContent { get; set; } = LocaleManager.Instance[LocaleKeys.SetupWizardActionNext];
|
||||
|
||||
[RelayCommand]
|
||||
private void MoveBack()
|
||||
{
|
||||
_result = false;
|
||||
_cts.Cancel();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void MoveNext()
|
||||
{
|
||||
_result = true;
|
||||
_cts.Cancel();
|
||||
}
|
||||
|
||||
public async ValueTask<bool> Show()
|
||||
{
|
||||
contentPresenter.Content = new SetupWizardPageView { ViewModel = this };
|
||||
ownerWizard.SetWindowTitle(Title);
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(-1, _cts.Token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
return _result ?? false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/Ryujinx/UI/SetupWizard/SetupWizardPageContext.cs
Normal file
35
src/Ryujinx/UI/SetupWizard/SetupWizardPageContext.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Gommon;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
|
||||
|
||||
namespace Ryujinx.Ava.UI.SetupWizard
|
||||
{
|
||||
public abstract class SetupWizardPageContext(LocaleKeys title) : BaseModel
|
||||
{
|
||||
public RyujinxSetupWizard OwningWizard
|
||||
{
|
||||
get;
|
||||
init
|
||||
{
|
||||
field = value;
|
||||
NotificationManager = field.NotificationManager;
|
||||
}
|
||||
}
|
||||
|
||||
public RyujinxNotificationManager NotificationManager { get; private init; }
|
||||
|
||||
public LocaleKeys Title => title;
|
||||
|
||||
public virtual LocaleKeys ActionContent => LocaleKeys.SetupWizardActionNext;
|
||||
|
||||
// ReSharper disable once UnusedMemberInSuper.Global
|
||||
// it's used implicitly as we use this type as a where guard for generics for WithContent<TControl, TContext>,
|
||||
// it also ensures all context types implement completion
|
||||
public abstract Result CompleteStep();
|
||||
|
||||
#nullable enable
|
||||
public virtual object? CreateHelpContent() => null;
|
||||
}
|
||||
}
|
||||
92
src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml
Normal file
92
src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml
Normal file
@@ -0,0 +1,92 @@
|
||||
<UserControl 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:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:fa="using:Projektanker.Icons.Avalonia"
|
||||
xmlns:wiz="using:Ryujinx.Ava.UI.SetupWizard"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:DataType="wiz:SetupWizardPage"
|
||||
x:Class="Ryujinx.Ava.UI.SetupWizard.SetupWizardPageView">
|
||||
<Grid RowDefinitions="*,Auto" Margin="60">
|
||||
<ScrollViewer>
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<TextBlock Grid.Row="0"
|
||||
TextWrapping="WrapWithOverflow"
|
||||
FontSize="46"
|
||||
Text="{Binding Title}" />
|
||||
<ContentPresenter Grid.Row="1"
|
||||
Content="{Binding}"
|
||||
IsVisible="{Binding !#InfoToggle.IsChecked}"
|
||||
TextWrapping="WrapWithOverflow"
|
||||
Margin="0,15,0,0">
|
||||
<ContentPresenter.DataTemplates>
|
||||
<DataTemplate DataType="{x:Type wiz:SetupWizardPage}">
|
||||
<ContentControl Content="{Binding Content}" VerticalAlignment="Stretch"/>
|
||||
</DataTemplate>
|
||||
</ContentPresenter.DataTemplates>
|
||||
</ContentPresenter>
|
||||
|
||||
<Grid Grid.Row="1"
|
||||
ColumnDefinitions="*" RowDefinitions="*,Auto"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch"
|
||||
IsVisible="{Binding #InfoToggle.IsChecked}">
|
||||
<Border
|
||||
Margin="15"
|
||||
IsVisible="{Binding HasHelpContent}"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="5"
|
||||
Background="{DynamicResource AppListBackgroundColor}">
|
||||
<ContentPresenter Content="{Binding}"
|
||||
Margin="5"
|
||||
TextWrapping="WrapWithOverflow" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<ContentPresenter.DataTemplates>
|
||||
<DataTemplate DataType="{x:Type wiz:SetupWizardPage}">
|
||||
<ContentControl Content="{Binding HelpContent}" />
|
||||
</DataTemplate>
|
||||
</ContentPresenter.DataTemplates>
|
||||
</ContentPresenter>
|
||||
</Border>
|
||||
<Button Grid.Row="1"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Center"
|
||||
MinWidth="45"
|
||||
MinHeight="32"
|
||||
MaxWidth="45"
|
||||
MaxHeight="32"
|
||||
Padding="8"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
CornerRadius="5"
|
||||
Tag="https://discord.gg/PEuzjrFXUA"
|
||||
ToolTip.Tip="{ext:Locale AboutDiscordUrlTooltipMessage}">
|
||||
<Image Source="{Binding Parent.DiscordLogo}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
|
||||
<Grid ColumnDefinitions="Auto,Auto,*" Grid.Row="1">
|
||||
<ToggleButton Name="InfoToggle" Padding="6" IsVisible="{Binding ShowHelpButton}">
|
||||
<fa:Icon Value="fa-solid fa-circle-info" />
|
||||
</ToggleButton>
|
||||
|
||||
<Button IsVisible="{Binding !IsFirstPage}"
|
||||
Grid.Column="1"
|
||||
Content="{ext:Locale SetupWizardActionBack}"
|
||||
Margin="10,0,0,0"
|
||||
Command="{Binding MoveBackCommand}" />
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Grid.Column="2">
|
||||
<Button Content="{Binding ActionContent}"
|
||||
Command="{Binding MoveNextCommand}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
23
src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml.cs
Normal file
23
src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
|
||||
using Ryujinx.Common.Helper;
|
||||
|
||||
namespace Ryujinx.Ava.UI.SetupWizard
|
||||
{
|
||||
public partial class SetupWizardPageView : RyujinxControl<SetupWizardPage>
|
||||
{
|
||||
public SetupWizardPageView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void Button_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button { Tag: string url })
|
||||
OpenHelper.OpenUrl(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Gommon;
|
||||
using Ryujinx.Ava.Common;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Systems.Configuration;
|
||||
using System;
|
||||
@@ -36,21 +37,17 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
Dispatcher.UIThread.Post(() => UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value));
|
||||
}
|
||||
|
||||
private const string LogoPathFormat = "resm:Ryujinx.Assets.UIImages.Logo_{0}_{1}.png?assembly=Ryujinx";
|
||||
|
||||
private void UpdateLogoTheme(string theme)
|
||||
{
|
||||
bool isDarkTheme = theme == "Dark" ||
|
||||
(theme == "Auto" && RyujinxApp.DetectSystemTheme() == ThemeVariant.Dark);
|
||||
|
||||
string themeName = isDarkTheme ? "Dark" : "Light";
|
||||
|
||||
DiscordLogo = LoadBitmap(LogoPathFormat.Format("Discord", themeName));
|
||||
GitLabLogo = LoadBitmap(LogoPathFormat.Format("GitLab", themeName));
|
||||
DiscordLogo = UIImages.GetLogoByNameAndTheme("Discord", isDarkTheme)
|
||||
.CreateScaledBitmap(new PixelSize(32, 24));
|
||||
GitLabLogo = UIImages.GetLogoByNameAndTheme("GitLab", isDarkTheme)
|
||||
.CreateScaledBitmap(new PixelSize(32, 31));
|
||||
}
|
||||
|
||||
private static Bitmap LoadBitmap(string uri) => new(Avalonia.Platform.AssetLoader.Open(new Uri(uri)));
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
RyujinxApp.ThemeChanged -= Ryujinx_ThemeChanged;
|
||||
|
||||
@@ -45,6 +45,7 @@ using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
|
||||
using Ryujinx.HLE.UI;
|
||||
using Ryujinx.Input.HLE;
|
||||
using Ryujinx.Ava.UI.SetupWizard;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -870,7 +871,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
private void RefreshGrid()
|
||||
{
|
||||
IObservableList<ApplicationData> appsList = Applications.ToObservableChangeSet()
|
||||
_ = Applications.ToObservableChangeSet()
|
||||
.Filter(Filter)
|
||||
.Sort(GetComparer())
|
||||
.Bind(out ReadOnlyObservableCollection<ApplicationData> apps)
|
||||
@@ -1013,16 +1014,11 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleKeysInstallation(string filename)
|
||||
public async Task HandleKeysInstallation(string filename)
|
||||
{
|
||||
try
|
||||
{
|
||||
string systemDirectory = AppDataManager.KeysDirPath;
|
||||
if (AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile &&
|
||||
Directory.Exists(AppDataManager.KeysDirPathUser))
|
||||
{
|
||||
systemDirectory = AppDataManager.KeysDirPathUser;
|
||||
}
|
||||
string systemDirectory = AppDataManager.GetKeysDir();
|
||||
|
||||
string dialogTitle =
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogKeysInstallerKeysInstallTitle);
|
||||
@@ -1376,8 +1372,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
Patterns = ["*.zip"],
|
||||
AppleUniformTypeIdentifiers = ["public.zip-archive"],
|
||||
MimeTypes = ["application/zip"],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (result.HasValue)
|
||||
@@ -1757,12 +1753,19 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
public static void UpdateGameMetadata(string titleId, TimeSpan playTime)
|
||||
=> ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime));
|
||||
|
||||
public void RefreshFirmwareStatus()
|
||||
/// <remarks>
|
||||
/// By default, this method will try to retrieve the installed FW version if the version parameter is null.
|
||||
/// <paramref name="allowNullVersion"/> forces this method to accept null and not re-lookup
|
||||
/// in the case you want to deliberately cause an update with a missing firmware version;
|
||||
///
|
||||
/// i.e., in the setup wizard.
|
||||
/// </remarks>
|
||||
public void RefreshFirmwareStatus(SystemVersion version = null, bool allowNullVersion = false)
|
||||
{
|
||||
SystemVersion version = null;
|
||||
try
|
||||
{
|
||||
version = ContentManager.GetCurrentFirmwareVersion();
|
||||
if (!allowNullVersion)
|
||||
version ??= ContentManager.GetCurrentFirmwareVersion();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -1947,14 +1950,14 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
if (ConfigurationState.Instance.Debug.DebuggerSuspendOnStart)
|
||||
{
|
||||
NotificationHelper.ShowInformation(
|
||||
RyujinxNotificationManager.ShowInformation(
|
||||
LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckSuspendOnStartTitle],
|
||||
LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckSuspendOnStartMessage]);
|
||||
}
|
||||
|
||||
if (ConfigurationState.Instance.Debug.EnableGdbStub)
|
||||
{
|
||||
NotificationHelper.ShowInformation(
|
||||
RyujinxNotificationManager.ShowInformation(
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.NotificationLaunchCheckGdbStubTitle, ConfigurationState.Instance.Debug.GdbStubPort.Value),
|
||||
LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckGdbStubMessage]);
|
||||
}
|
||||
@@ -1973,7 +1976,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
_ => LocaleKeys.SettingsTabSystemDramSize4GiB,
|
||||
};
|
||||
|
||||
NotificationHelper.ShowWarning(
|
||||
RyujinxNotificationManager.ShowWarning(
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(
|
||||
LocaleKeys.NotificationLaunchCheckDramSizeTitle,
|
||||
LocaleManager.Instance[memoryConfigurationLocaleKey]
|
||||
|
||||
@@ -1,46 +1,36 @@
|
||||
using Avalonia.Media;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
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 DynamicData;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using Color = Avalonia.Media.Color;
|
||||
using Image = SkiaSharp.SKImage;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
public partial class UserFirmwareAvatarSelectorViewModel : BaseModel
|
||||
{
|
||||
private static readonly Dictionary<string, byte[]> _avatarStore = new();
|
||||
private static FirmwareAvatarCache _avatarCache;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ProfileImageModel> Images { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Color BackgroundColor { get; set; } = Colors.White;
|
||||
|
||||
public Color BackgroundColor
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged();
|
||||
ChangeImageBackground();
|
||||
}
|
||||
} = Colors.White;
|
||||
|
||||
public UserFirmwareAvatarSelectorViewModel()
|
||||
{
|
||||
Images = [];
|
||||
|
||||
LoadImagesFromStore();
|
||||
PropertyChanged += (_, args) =>
|
||||
{
|
||||
if (args.PropertyName == nameof(BackgroundColor))
|
||||
ChangeImageBackground();
|
||||
};
|
||||
}
|
||||
|
||||
public int SelectedIndex
|
||||
@@ -65,10 +55,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
Images.Clear();
|
||||
|
||||
foreach (KeyValuePair<string, byte[]> image in _avatarStore)
|
||||
{
|
||||
Images.Add(new ProfileImageModel(image.Key, image.Value));
|
||||
}
|
||||
Images.AddRange(_avatarCache.CreateProfileImageModels());
|
||||
}
|
||||
|
||||
private void ChangeImageBackground()
|
||||
@@ -81,127 +68,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem)
|
||||
{
|
||||
if (_avatarStore.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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 UniqueRef<IFile> file = new();
|
||||
|
||||
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.FromPixelCopy(new SKImageInfo(256, 256, SKColorType.Rgba8888, SKAlphaType.Premul), DecompressYaz0(stream));
|
||||
|
||||
using (SKData data = avatarImage.Encode(SKEncodedImageFormat.Png, 100))
|
||||
{
|
||||
data.SaveTo(streamPng);
|
||||
}
|
||||
|
||||
_avatarStore.Add(item.FullPath, streamPng.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] DecompressYaz0(MemoryStream 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.ReadExactly(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;
|
||||
_avatarCache ??= new FirmwareAvatarCache(contentManager, virtualFileSystem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace Ryujinx.Ava.UI.Views.Dialog
|
||||
|
||||
await clipboard.SetTextAsync(appData.IdString);
|
||||
|
||||
NotificationHelper.ShowInformation(
|
||||
RyujinxNotificationManager.ShowInformation(
|
||||
"Copied Title ID",
|
||||
$"{appData.Name} ({appData.IdString})");
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
|
||||
xmlns:common="clr-namespace:Ryujinx.Common;assembly=Ryujinx.Common"
|
||||
xmlns:setupWizard="clr-namespace:Ryujinx.Ava.UI.SetupWizard"
|
||||
x:DataType="viewModels:MainWindowViewModel"
|
||||
x:Class="Ryujinx.Ava.UI.Views.Main.MainMenuBarView">
|
||||
<Design.DataContext>
|
||||
@@ -244,6 +245,11 @@
|
||||
Header="{ext:Locale LdnGameListOpen}"
|
||||
Icon="{ext:Icon fa-solid fa-people-group}"
|
||||
IsEnabled="{Binding IsRyuLdnEnabled}"/>
|
||||
<MenuItem
|
||||
Name="SetupWizardMenuItem"
|
||||
Header="{ext:Locale SetupWizardOpen}"
|
||||
Icon="{ext:Icon fa-solid fa-wand-sparkles}"
|
||||
IsEnabled="{x:Static setupWizard:RyujinxSetupWizardWindow.CanShowSetupWizard}"/>
|
||||
<Separator />
|
||||
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarHelpFaqAndGuides}" Icon="{ext:Icon fa-solid fa-question}" >
|
||||
<MenuItem
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Threading;
|
||||
using Gommon;
|
||||
@@ -10,6 +11,7 @@ using Ryujinx.Ava.Systems.AppLibrary;
|
||||
using Ryujinx.Ava.Systems.Configuration;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.SetupWizard;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ava.UI.Views.Dialog;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
@@ -30,6 +32,7 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
{
|
||||
public MainWindow Window { get; private set; }
|
||||
|
||||
|
||||
public MainMenuBarView()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -50,6 +53,9 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
AboutWindowMenuItem.Command = Commands.Create(AboutView.Show);
|
||||
CompatibilityListMenuItem.Command = Commands.Create(() => CompatibilityListWindow.Show());
|
||||
LdnGameListMenuItem.Command = Commands.Create(() => LdnGamesListWindow.Show());
|
||||
SetupWizardMenuItem.Command = Commands.Create(() =>
|
||||
RyujinxSetupWizardWindow.ShowAsync(overwriteMode: !PollShiftPressed())
|
||||
);
|
||||
|
||||
UpdateMenuItem.Command = MainWindowViewModel.UpdateCommand;
|
||||
|
||||
@@ -62,9 +68,42 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
WindowSize1440PMenuItem.Command =
|
||||
WindowSize2160PMenuItem.Command = Commands.Create<string>(ChangeWindowSize);
|
||||
|
||||
KeyDown += OnKeyDown;
|
||||
KeyUp += OnKeyUp;
|
||||
|
||||
LocaleManager.Instance.LocaleChanged += OnLocaleChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// KeyUp is not reliably invoked (or invoked at all, seemingly) when a window showing up causes the main menu bar to view,
|
||||
/// as shift is technically raised when that control is no longer the foreground control.
|
||||
///
|
||||
/// This stores <see cref="IsShiftPressed"/> to a temp variable, sets <see cref="IsShiftPressed"/> to false (if it is true), then returns the temp variable.
|
||||
/// </summary>
|
||||
private bool PollShiftPressed()
|
||||
{
|
||||
bool temp = IsShiftPressed;
|
||||
if (temp)
|
||||
IsShiftPressed = false;
|
||||
return temp;
|
||||
}
|
||||
|
||||
private bool IsShiftPressed { get; set; }
|
||||
|
||||
private void OnKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key is Key.LeftShift or Key.RightShift && !IsShiftPressed)
|
||||
//down is called even for keys that have been held for a while, aka key repeats.
|
||||
//the check for shift being pressed prevents setting the variable every time the down event is received, if shift is already known to be pressed.
|
||||
IsShiftPressed = true;
|
||||
}
|
||||
|
||||
private void OnKeyUp(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key is Key.LeftShift or Key.RightShift)
|
||||
IsShiftPressed = false;
|
||||
}
|
||||
|
||||
private void OnLocaleChanged()
|
||||
{
|
||||
ChangeLanguageMenuItem.ItemsSource = GenerateLanguageMenuItems();
|
||||
@@ -145,11 +184,13 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
}
|
||||
else
|
||||
{
|
||||
bool customConfigExists = File.Exists(Program.GetDirGameUserConfig(ViewModel.SelectedApplication.IdString));
|
||||
bool customConfigExists =
|
||||
File.Exists(Program.GetDirGameUserConfig(ViewModel.SelectedApplication.IdString));
|
||||
|
||||
if (!ViewModel.IsGameRunning || !customConfigExists)
|
||||
{
|
||||
await Window.SettingsWindow.ShowDialog(Window); // The game is not running, or if the user configuration does not exist
|
||||
await Window.SettingsWindow
|
||||
.ShowDialog(Window); // The game is not running, or if the user configuration does not exist
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -173,7 +214,8 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
if (!MiiApplet.CanStart(out ApplicationData appData, out BlitStruct<ApplicationControlProperty> nacpData))
|
||||
return;
|
||||
|
||||
await ViewModel.LoadApplication(appData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen, nacpData);
|
||||
await ViewModel.LoadApplication(appData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen,
|
||||
nacpData);
|
||||
}
|
||||
|
||||
public async Task OpenCheatManagerForCurrentApp()
|
||||
@@ -181,7 +223,8 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
if (!ViewModel.IsGameRunning)
|
||||
return;
|
||||
|
||||
string name = ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties.Title[(int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage].NameString.ToString();
|
||||
string name = ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties
|
||||
.Title[(int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage].NameString.ToString();
|
||||
|
||||
await StyleableAppWindow.ShowAsync(
|
||||
new CheatWindow(
|
||||
@@ -210,18 +253,24 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
{
|
||||
ViewModel.AreMimeTypesRegistered = FileAssociationHelper.Install();
|
||||
if (ViewModel.AreMimeTypesRegistered)
|
||||
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesSuccessMessage], string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
|
||||
await ContentDialogHelper.CreateInfoDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesSuccessMessage], string.Empty,
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
|
||||
else
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesErrorMessage]);
|
||||
await ContentDialogHelper.CreateErrorDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesErrorMessage]);
|
||||
}
|
||||
|
||||
private async Task UninstallFileTypes()
|
||||
{
|
||||
ViewModel.AreMimeTypesRegistered = !FileAssociationHelper.Uninstall();
|
||||
if (!ViewModel.AreMimeTypesRegistered)
|
||||
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesSuccessMessage], string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
|
||||
await ContentDialogHelper.CreateInfoDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesSuccessMessage], string.Empty,
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
|
||||
else
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesErrorMessage]);
|
||||
await ContentDialogHelper.CreateErrorDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesErrorMessage]);
|
||||
}
|
||||
|
||||
private void ChangeWindowSize(string resolution)
|
||||
@@ -233,7 +282,7 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
|
||||
// Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024)
|
||||
double barsHeight = ((Window.StatusBarHeight + Window.MenuBarHeight) +
|
||||
(ConfigurationState.Instance.ShowOldUI ? (int)Window.TitleBar.Height : 0));
|
||||
(ConfigurationState.Instance.ShowOldUI ? (int)Window.TitleBar.Height : 0));
|
||||
|
||||
double windowWidthScaled = (resolutionWidth * Program.WindowScaleFactor);
|
||||
double windowHeightScaled = ((resolutionHeight + barsHeight) * Program.WindowScaleFactor);
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace Ryujinx.Ava.UI.Views.Misc
|
||||
|
||||
await clipboard.SetTextAsync(appData.IdString);
|
||||
|
||||
NotificationHelper.ShowInformation(
|
||||
RyujinxNotificationManager.ShowInformation(
|
||||
"Copied Title ID",
|
||||
$"{appData.Name} ({appData.IdString})");
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
<viewModels:CompatibilityViewModel />
|
||||
</window:StyleableAppWindow.DataContext>
|
||||
<Grid RowDefinitions="Auto,Auto,*">
|
||||
|
||||
<!-- UI FlushControls -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto,Auto,Auto" Name="FlushControls">
|
||||
<controls:RyujinxLogo
|
||||
|
||||
@@ -30,6 +30,7 @@ using Ryujinx.HLE.HOS;
|
||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
using Ryujinx.Input.HLE;
|
||||
using Ryujinx.Input.SDL3;
|
||||
using Ryujinx.Ava.UI.SetupWizard;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -134,11 +135,18 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
|
||||
NotificationHelper.SetNotificationManager(this);
|
||||
RyujinxNotificationManager.Shared = new RyujinxNotificationManager(this);
|
||||
|
||||
Executor.ExecuteBackgroundAsync(async () =>
|
||||
{
|
||||
await ShowIntelMacWarningAsync();
|
||||
await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
await ShowIntelMacWarningAsync();
|
||||
|
||||
if (Program.IsFirstStart)
|
||||
await RyujinxSetupWizardWindow.ShowAsync(overwriteMode: false, this);
|
||||
});
|
||||
|
||||
if (CommandLineState.FirmwareToInstallPathArg.TryGet(out FilePath fwPath))
|
||||
{
|
||||
if (fwPath is { ExistsAsFile: true, Extension: "xci" or "zip" } || fwPath.ExistsAsDirectory)
|
||||
@@ -150,6 +158,8 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
else
|
||||
Logger.Notice.Print(LogClass.UI, "Invalid firmware type provided. Path must be a directory, or a .zip or .xci file.");
|
||||
}
|
||||
|
||||
await CheckLaunchState();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -399,7 +409,7 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (!RyujinxSetupWizardWindow.IsOpen)
|
||||
{
|
||||
ShowKeyErrorOnLoad = false;
|
||||
|
||||
@@ -538,8 +548,6 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
LoadApplications();
|
||||
}
|
||||
|
||||
_ = CheckLaunchState();
|
||||
}
|
||||
|
||||
private void SetMainContent(Control content = null)
|
||||
|
||||
Reference in New Issue
Block a user