Browse Source

infra: Make Avalonia the default UI (#6375)

* misc: Move Ryujinx project to Ryujinx.Gtk3

This breaks release CI for now but that's fine.

Signed-off-by: Mary Guillemard <mary@mary.zone>

* misc: Move Ryujinx.Ava project to Ryujinx

This breaks CI for now, but it's fine.

Signed-off-by: Mary Guillemard <mary@mary.zone>

* infra: Make Avalonia the default UI

Should fix CI after the previous changes.

GTK3 isn't build by the release job anymore, only by PR CI.

This also ensure that the test-ava update package is still generated to
allow update from the old testing channel.

Signed-off-by: Mary Guillemard <mary@mary.zone>

* Fix missing copy in create_app_bundle.sh

Signed-off-by: Mary Guillemard <mary@mary.zone>

* Fix syntax error

Signed-off-by: Mary Guillemard <mary@mary.zone>

---------

Signed-off-by: Mary Guillemard <mary@mary.zone>
Mary Guillemard 2 năm trước cách đây
mục cha
commit
ec6cb0abb4
100 tập tin đã thay đổi với 1019 bổ sung937 xóa
  1. 4 4
      .editorconfig
  2. 1 1
      .github/labeler.yml
  3. 11 11
      .github/workflows/build.yml
  4. 5 5
      .github/workflows/nightly_pr_comment.yml
  5. 7 6
      .github/workflows/release.yml
  6. 2 2
      Ryujinx.sln
  7. 0 4
      distribution/linux/Ryujinx.sh
  8. 2 2
      distribution/macos/create_app_bundle.sh
  9. 12 5
      distribution/macos/create_macos_build_ava.sh
  10. 0 237
      src/Ryujinx.Ava/Program.cs
  11. 0 167
      src/Ryujinx.Ava/Ryujinx.Ava.csproj
  12. 0 0
      src/Ryujinx.Gtk3/Input/GTK3/GTK3Keyboard.cs
  13. 0 0
      src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs
  14. 0 0
      src/Ryujinx.Gtk3/Input/GTK3/GTK3MappingHelper.cs
  15. 0 0
      src/Ryujinx.Gtk3/Input/GTK3/GTK3Mouse.cs
  16. 0 0
      src/Ryujinx.Gtk3/Input/GTK3/GTK3MouseDriver.cs
  17. 2 2
      src/Ryujinx.Gtk3/Modules/Updater/UpdateDialog.cs
  18. 0 0
      src/Ryujinx.Gtk3/Modules/Updater/UpdateDialog.glade
  19. 170 312
      src/Ryujinx.Gtk3/Modules/Updater/Updater.cs
  20. 378 0
      src/Ryujinx.Gtk3/Program.cs
  21. 104 0
      src/Ryujinx.Gtk3/Ryujinx.Gtk3.csproj
  22. 0 0
      src/Ryujinx.Gtk3/Ryujinx.ico
  23. 1 1
      src/Ryujinx.Gtk3/UI/Applet/ErrorAppletDialog.cs
  24. 0 0
      src/Ryujinx.Gtk3/UI/Applet/GtkDynamicTextInputHandler.cs
  25. 0 0
      src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs
  26. 0 0
      src/Ryujinx.Gtk3/UI/Applet/GtkHostUITheme.cs
  27. 0 0
      src/Ryujinx.Gtk3/UI/Applet/SwkbdAppletDialog.cs
  28. 0 0
      src/Ryujinx.Gtk3/UI/Helper/MetalHelper.cs
  29. 0 0
      src/Ryujinx.Gtk3/UI/Helper/SortHelper.cs
  30. 0 0
      src/Ryujinx.Gtk3/UI/Helper/ThemeHelper.cs
  31. 1 1
      src/Ryujinx.Gtk3/UI/MainWindow.cs
  32. 0 0
      src/Ryujinx.Gtk3/UI/MainWindow.glade
  33. 0 0
      src/Ryujinx.Gtk3/UI/OpenGLRenderer.cs
  34. 0 0
      src/Ryujinx.Gtk3/UI/OpenToolkitBindingsContext.cs
  35. 0 0
      src/Ryujinx.Gtk3/UI/RendererWidgetBase.cs
  36. 0 0
      src/Ryujinx.Gtk3/UI/SPBOpenGLContext.cs
  37. 0 0
      src/Ryujinx.Gtk3/UI/StatusUpdatedEventArgs.cs
  38. 0 0
      src/Ryujinx.Gtk3/UI/VulkanRenderer.cs
  39. 0 0
      src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.Designer.cs
  40. 1 1
      src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.cs
  41. 0 0
      src/Ryujinx.Gtk3/UI/Widgets/GtkDialog.cs
  42. 0 0
      src/Ryujinx.Gtk3/UI/Widgets/GtkInputDialog.cs
  43. 1 1
      src/Ryujinx.Gtk3/UI/Widgets/ProfileDialog.cs
  44. 0 0
      src/Ryujinx.Gtk3/UI/Widgets/ProfileDialog.glade
  45. 0 0
      src/Ryujinx.Gtk3/UI/Widgets/RawInputToTextEntry.cs
  46. 0 0
      src/Ryujinx.Gtk3/UI/Widgets/UserErrorDialog.cs
  47. 0 0
      src/Ryujinx.Gtk3/UI/Windows/AboutWindow.Designer.cs
  48. 0 0
      src/Ryujinx.Gtk3/UI/Windows/AboutWindow.cs
  49. 0 0
      src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.Designer.cs
  50. 0 0
      src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.cs
  51. 0 0
      src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs
  52. 1 1
      src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs
  53. 0 0
      src/Ryujinx.Gtk3/UI/Windows/CheatWindow.glade
  54. 1 1
      src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.cs
  55. 0 0
      src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.glade
  56. 1 1
      src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs
  57. 0 0
      src/Ryujinx.Gtk3/UI/Windows/DlcWindow.glade
  58. 1 1
      src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.cs
  59. 0 0
      src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.glade
  60. 1 1
      src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs
  61. 0 0
      src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.glade
  62. 0 0
      src/Ryujinx.Gtk3/UI/Windows/UserProfilesManagerWindow.Designer.cs
  63. 0 0
      src/Ryujinx.Gtk3/UI/Windows/UserProfilesManagerWindow.cs
  64. 0 0
      src/Ryujinx/App.axaml
  65. 0 0
      src/Ryujinx/App.axaml.cs
  66. 0 0
      src/Ryujinx/AppHost.cs
  67. 0 0
      src/Ryujinx/Assets/Fonts/SegoeFluentIcons.ttf
  68. 0 0
      src/Ryujinx/Assets/Icons/Controller_JoyConLeft.svg
  69. 0 0
      src/Ryujinx/Assets/Icons/Controller_JoyConPair.svg
  70. 0 0
      src/Ryujinx/Assets/Icons/Controller_JoyConRight.svg
  71. 0 0
      src/Ryujinx/Assets/Icons/Controller_ProCon.svg
  72. 0 0
      src/Ryujinx/Assets/Locales/de_DE.json
  73. 0 0
      src/Ryujinx/Assets/Locales/el_GR.json
  74. 0 0
      src/Ryujinx/Assets/Locales/en_US.json
  75. 0 0
      src/Ryujinx/Assets/Locales/es_ES.json
  76. 0 0
      src/Ryujinx/Assets/Locales/fr_FR.json
  77. 0 0
      src/Ryujinx/Assets/Locales/he_IL.json
  78. 0 0
      src/Ryujinx/Assets/Locales/it_IT.json
  79. 0 0
      src/Ryujinx/Assets/Locales/ja_JP.json
  80. 0 0
      src/Ryujinx/Assets/Locales/ko_KR.json
  81. 0 0
      src/Ryujinx/Assets/Locales/pl_PL.json
  82. 0 0
      src/Ryujinx/Assets/Locales/pt_BR.json
  83. 0 0
      src/Ryujinx/Assets/Locales/ru_RU.json
  84. 0 0
      src/Ryujinx/Assets/Locales/tr_TR.json
  85. 0 0
      src/Ryujinx/Assets/Locales/uk_UA.json
  86. 0 0
      src/Ryujinx/Assets/Locales/zh_CN.json
  87. 0 0
      src/Ryujinx/Assets/Locales/zh_TW.json
  88. 0 0
      src/Ryujinx/Assets/Styles/Styles.xaml
  89. 0 0
      src/Ryujinx/Assets/Styles/Themes.xaml
  90. 0 0
      src/Ryujinx/Common/ApplicationHelper.cs
  91. 0 0
      src/Ryujinx/Common/ApplicationSort.cs
  92. 0 0
      src/Ryujinx/Common/KeyboardHotkeyState.cs
  93. 0 0
      src/Ryujinx/Common/Locale/LocaleExtension.cs
  94. 1 1
      src/Ryujinx/Common/Locale/LocaleManager.cs
  95. 0 0
      src/Ryujinx/Input/AvaloniaKeyboard.cs
  96. 0 0
      src/Ryujinx/Input/AvaloniaKeyboardDriver.cs
  97. 0 0
      src/Ryujinx/Input/AvaloniaKeyboardMappingHelper.cs
  98. 0 0
      src/Ryujinx/Input/AvaloniaMouse.cs
  99. 0 0
      src/Ryujinx/Input/AvaloniaMouseDriver.cs
  100. 311 169
      src/Ryujinx/Modules/Updater/Updater.cs

+ 4 - 4
.editorconfig

@@ -259,12 +259,12 @@ dotnet_diagnostic.CA1861.severity = none
 # Disable "Prefer using 'string.Equals(string, StringComparison)' to perform a case-insensitive comparison, but keep in mind that this might cause subtle changes in behavior, so make sure to conduct thorough testing after applying the suggestion, or if culturally sensitive comparison is not required, consider using 'StringComparison.OrdinalIgnoreCase'"
 dotnet_diagnostic.CA1862.severity = none
 
-[src/Ryujinx.HLE/HOS/Services/**.cs]
-# Disable "mark members as static" rule for services
+[src/Ryujinx/UI/ViewModels/**.cs]
+# Disable "mark members as static" rule for ViewModels
 dotnet_diagnostic.CA1822.severity = none
 
-[src/Ryujinx.Ava/UI/ViewModels/**.cs]
-# Disable "mark members as static" rule for ViewModels
+[src/Ryujinx.HLE/HOS/Services/**.cs]
+# Disable "mark members as static" rule for services
 dotnet_diagnostic.CA1822.severity = none
 
 [src/Ryujinx.Tests/Cpu/*.cs]

+ 1 - 1
.github/labeler.yml

@@ -20,7 +20,7 @@ gpu:
 
 gui:
 - changed-files:
-  - any-glob-to-any-file: ['src/Ryujinx/**', 'src/Ryujinx.UI.Common/**', 'src/Ryujinx.UI.LocaleGenerator/**', 'src/Ryujinx.Ava/**']
+  - any-glob-to-any-file: ['src/Ryujinx/**', 'src/Ryujinx.UI.Common/**', 'src/Ryujinx.UI.LocaleGenerator/**', 'src/Ryujinx.Gtk3/**']
 
 horizon:
 - changed-files:

+ 11 - 11
.github/workflows/build.yml

@@ -67,15 +67,15 @@ jobs:
         run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true
         if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest'
 
-      - name: Publish Ryujinx.Ava
-        run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish_ava -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Ava --self-contained true
+      - name: Publish Ryujinx.Gtk3
+        run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish_gtk -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Gtk3 --self-contained true
         if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest'
 
       - name: Set executable bit
         run: |
           chmod +x ./publish/Ryujinx ./publish/Ryujinx.sh
           chmod +x ./publish_sdl2_headless/Ryujinx.Headless.SDL2 ./publish_sdl2_headless/Ryujinx.sh
-          chmod +x ./publish_ava/Ryujinx.Ava ./publish_ava/Ryujinx.sh
+          chmod +x ./publish_gtk/Ryujinx.Gtk3 ./publish_gtk/Ryujinx.sh
         if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
 
       - name: Upload Ryujinx artifact
@@ -92,11 +92,11 @@ jobs:
           path: publish_sdl2_headless
         if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest'
 
-      - name: Upload Ryujinx.Ava artifact
+      - name: Upload Ryujinx.Gtk3 artifact
         uses: actions/upload-artifact@v4
         with:
-          name: ava-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}
-          path: publish_ava
+          name: gtk-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}
+          path: publish_gtk
         if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest'
 
   build_macos:
@@ -140,19 +140,19 @@ jobs:
         shell: bash
         if: github.event_name == 'pull_request'
 
-      - name: Publish macOS Ryujinx.Ava
+      - name: Publish macOS Ryujinx
         run: |
-          ./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish_ava ./distribution/macos/entitlements.xml "${{ env.RYUJINX_BASE_VERSION }}" "${{ steps.git_short_hash.outputs.result }}" "${{ matrix.configuration }}" "-p:ExtraDefineConstants=DISABLE_UPDATER"
+          ./distribution/macos/create_macos_build_ava.sh . publish_tmp publish ./distribution/macos/entitlements.xml "${{ env.RYUJINX_BASE_VERSION }}" "${{ steps.git_short_hash.outputs.result }}" "${{ matrix.configuration }}" "-p:ExtraDefineConstants=DISABLE_UPDATER"
 
       - name: Publish macOS Ryujinx.Headless.SDL2
         run: |
           ./distribution/macos/create_macos_build_headless.sh . publish_tmp_headless publish_headless ./distribution/macos/entitlements.xml "${{ env.RYUJINX_BASE_VERSION }}" "${{ steps.git_short_hash.outputs.result }}" "${{ matrix.configuration }}" "-p:ExtraDefineConstants=DISABLE_UPDATER"
 
-      - name: Upload Ryujinx.Ava artifact
+      - name: Upload Ryujinx artifact
         uses: actions/upload-artifact@v4
         with:
-          name: ava-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal
-          path: "publish_ava/*.tar.gz"
+          name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal
+          path: "publish/*.tar.gz"
         if: github.event_name == 'pull_request'
 
       - name: Upload Ryujinx.Headless.SDL2 artifact

+ 5 - 5
.github/workflows/nightly_pr_comment.yml

@@ -39,24 +39,24 @@ jobs:
               return core.error(`No artifacts found`);
             }
             let body = `Download the artifacts for this pull request:\n`;
-            let hidden_avalonia_artifacts = `\n\n <details><summary>Experimental GUI (Avalonia)</summary>\n`;
+            let hidden_gtk_artifacts = `\n\n <details><summary>Old GUI (GTK3)</summary>\n`;
             let hidden_headless_artifacts = `\n\n <details><summary>GUI-less (SDL2)</summary>\n`;
             let hidden_debug_artifacts = `\n\n <details><summary>Only for Developers</summary>\n`;
             for (const art of artifacts) {
               if(art.name.includes('Debug')) {
                 hidden_debug_artifacts += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
-              } else if(art.name.includes('ava-ryujinx')) {
-                hidden_avalonia_artifacts += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
+              } else if(art.name.includes('gtk-ryujinx')) {
+                hidden_gtk_artifacts += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
               } else if(art.name.includes('sdl2-ryujinx-headless')) {
                 hidden_headless_artifacts += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
               } else {
                 body += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
               }
             }
-            hidden_avalonia_artifacts += `\n</details>`;
+            hidden_gtk_artifacts += `\n</details>`;
             hidden_headless_artifacts += `\n</details>`;
             hidden_debug_artifacts += `\n</details>`;
-            body += hidden_avalonia_artifacts;
+            body += hidden_gtk_artifacts;
             body += hidden_headless_artifacts;
             body += hidden_debug_artifacts;
 

+ 7 - 6
.github/workflows/release.yml

@@ -86,14 +86,13 @@ jobs:
 
       - name: Publish
         run: |
-          dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_gtk/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained true
+          dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_ava/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained true
           dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Headless.SDL2 --self-contained true
-          dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_ava/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Ava --self-contained true
 
       - name: Packing Windows builds
         if: matrix.platform.os == 'windows-latest'
         run: |
-          pushd publish_gtk
+          pushd publish_ava
           7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish
           popd
 
@@ -102,6 +101,7 @@ jobs:
           popd
 
           pushd publish_ava
+          mv publish/Ryujinx.exe publish/Ryujinx.Ava.exe
           7z a ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish
           popd
         shell: bash
@@ -109,7 +109,7 @@ jobs:
       - name: Packing Linux builds
         if: matrix.platform.os == 'ubuntu-latest'
         run: |
-          pushd publish_gtk
+          pushd publish_ava
           chmod +x publish/Ryujinx.sh publish/Ryujinx
           tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish
           popd
@@ -120,6 +120,7 @@ jobs:
           popd
 
           pushd publish_ava
+          mv publish/Ryujinx publish/Ryujinx.Ava
           chmod +x publish/Ryujinx.sh publish/Ryujinx.Ava
           tar -czvf ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish
           popd
@@ -183,10 +184,10 @@ jobs:
           sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
         shell: bash
 
-      - name: Publish macOS Ryujinx.Ava
+      - name: Publish macOS Ryujinx
         run: |
           ./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish_ava ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release
-          
+
       - name: Publish macOS Ryujinx.Headless.SDL2
         run: |
           ./distribution/macos/create_macos_build_headless.sh . publish_tmp_headless publish_headless ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release

+ 2 - 2
Ryujinx.sln

@@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio Version 17
 VisualStudioVersion = 17.1.32228.430
 MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx", "src\Ryujinx\Ryujinx.csproj", "{074045D4-3ED2-4711-9169-E385F2BFB5A0}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Gtk3", "src\Ryujinx.Gtk3\Ryujinx.Gtk3.csproj", "{074045D4-3ED2-4711-9169-E385F2BFB5A0}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Tests", "src\Ryujinx.Tests\Ryujinx.Tests.csproj", "{EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}"
 EndProject
@@ -69,7 +69,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Headless.SDL2", "sr
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Nvdec.FFmpeg", "src\Ryujinx.Graphics.Nvdec.FFmpeg\Ryujinx.Graphics.Nvdec.FFmpeg.csproj", "{BEE1C184-C9A4-410B-8DFC-FB74D5C93AEB}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Ava", "src\Ryujinx.Ava\Ryujinx.Ava.csproj", "{7C1B2721-13DA-4B62-B046-C626605ECCE6}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx", "src\Ryujinx\Ryujinx.csproj", "{7C1B2721-13DA-4B62-B046-C626605ECCE6}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.UI.Common", "src\Ryujinx.UI.Common\Ryujinx.UI.Common.csproj", "{BA161CA0-CD65-4E6E-B644-51C8D1E542DC}"
 EndProject

+ 0 - 4
distribution/linux/Ryujinx.sh

@@ -6,10 +6,6 @@ if [ -f "$SCRIPT_DIR/Ryujinx.Headless.SDL2" ]; then
     RYUJINX_BIN="Ryujinx.Headless.SDL2"
 fi
 
-if [ -f "$SCRIPT_DIR/Ryujinx.Ava" ]; then
-    RYUJINX_BIN="Ryujinx.Ava"
-fi
-
 if [ -f "$SCRIPT_DIR/Ryujinx" ]; then
     RYUJINX_BIN="Ryujinx"
 fi

+ 2 - 2
distribution/macos/create_app_bundle.sh

@@ -14,8 +14,8 @@ mkdir "$APP_BUNDLE_DIRECTORY/Contents/Frameworks"
 mkdir "$APP_BUNDLE_DIRECTORY/Contents/MacOS"
 mkdir "$APP_BUNDLE_DIRECTORY/Contents/Resources"
 
-# Copy executables first
-cp "$PUBLISH_DIRECTORY/Ryujinx.Ava" "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
+# Copy executable and nsure executable can be executed
+cp "$PUBLISH_DIRECTORY/Ryujinx" "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
 chmod u+x "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
 
 # Then all libraries

+ 12 - 5
distribution/macos/create_macos_build_ava.sh

@@ -22,9 +22,9 @@ EXTRA_ARGS=$8
 
 if [ "$VERSION" == "1.1.0" ];
 then
-  RELEASE_TAR_FILE_NAME=test-ava-ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.app.tar
+  RELEASE_TAR_FILE_NAME=ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.app.tar
 else
-  RELEASE_TAR_FILE_NAME=test-ava-ryujinx-$VERSION-macos_universal.app.tar
+  RELEASE_TAR_FILE_NAME=ryujinx-$VERSION-macos_universal.app.tar
 fi
 
 ARM64_APP_BUNDLE="$TEMP_DIRECTORY/output_arm64/Ryujinx.app"
@@ -38,9 +38,9 @@ mkdir -p "$TEMP_DIRECTORY"
 DOTNET_COMMON_ARGS=(-p:DebugType=embedded -p:Version="$VERSION" -p:SourceRevisionId="$SOURCE_REVISION_ID" --self-contained true $EXTRA_ARGS)
 
 dotnet restore
-dotnet build -c "$CONFIGURATION" src/Ryujinx.Ava
-dotnet publish -c "$CONFIGURATION" -r osx-arm64 -o "$TEMP_DIRECTORY/publish_arm64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx.Ava
-dotnet publish -c "$CONFIGURATION" -r osx-x64 -o "$TEMP_DIRECTORY/publish_x64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx.Ava
+dotnet build -c "$CONFIGURATION" src/Ryujinx
+dotnet publish -c "$CONFIGURATION" -r osx-arm64 -o "$TEMP_DIRECTORY/publish_arm64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx
+dotnet publish -c "$CONFIGURATION" -r osx-x64 -o "$TEMP_DIRECTORY/publish_x64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx
 
 # Get rid of the support library for ARMeilleure for x64 (that's only for arm64)
 rm -rf "$TEMP_DIRECTORY/publish_x64/libarmeilleure-jitsupport.dylib"
@@ -108,6 +108,13 @@ tar --exclude "Ryujinx.app/Contents/MacOS/Ryujinx" -cvf "$RELEASE_TAR_FILE_NAME"
 python3 "$BASE_DIR/distribution/misc/add_tar_exec.py" "$RELEASE_TAR_FILE_NAME" "Ryujinx.app/Contents/MacOS/Ryujinx" "Ryujinx.app/Contents/MacOS/Ryujinx"
 gzip -9 < "$RELEASE_TAR_FILE_NAME" > "$RELEASE_TAR_FILE_NAME.gz"
 rm "$RELEASE_TAR_FILE_NAME"
+
+# Create legacy update package for Avalonia to not left behind old testers.
+if [ "$VERSION" != "1.1.0" ];
+then
+    cp $RELEASE_TAR_FILE_NAME.gz test-ava-ryujinx-$VERSION-macos_universal.app.tar.gz
+fi
+
 popd
 
 echo "Done"

+ 0 - 237
src/Ryujinx.Ava/Program.cs

@@ -1,237 +0,0 @@
-using Avalonia;
-using Avalonia.Threading;
-using Ryujinx.Ava.UI.Helpers;
-using Ryujinx.Ava.UI.Windows;
-using Ryujinx.Common;
-using Ryujinx.Common.Configuration;
-using Ryujinx.Common.GraphicsDriver;
-using Ryujinx.Common.Logging;
-using Ryujinx.Common.SystemInterop;
-using Ryujinx.Modules;
-using Ryujinx.SDL2.Common;
-using Ryujinx.UI.Common;
-using Ryujinx.UI.Common.Configuration;
-using Ryujinx.UI.Common.Helper;
-using Ryujinx.UI.Common.SystemInfo;
-using System;
-using System.IO;
-using System.Runtime.InteropServices;
-using System.Threading.Tasks;
-
-namespace Ryujinx.Ava
-{
-    internal partial class Program
-    {
-        public static double WindowScaleFactor { get; set; }
-        public static double DesktopScaleFactor { get; set; } = 1.0;
-        public static string Version { get; private set; }
-        public static string ConfigurationPath { get; private set; }
-        public static bool PreviewerDetached { get; private set; }
-
-        [LibraryImport("user32.dll", SetLastError = true)]
-        public static partial int MessageBoxA(IntPtr hWnd, [MarshalAs(UnmanagedType.LPStr)] string text, [MarshalAs(UnmanagedType.LPStr)] string caption, uint type);
-
-        private const uint MbIconwarning = 0x30;
-
-        public static void Main(string[] args)
-        {
-            Version = ReleaseInformation.Version;
-
-            if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134))
-            {
-                _ = MessageBoxA(IntPtr.Zero, "You are running an outdated version of Windows.\n\nStarting on June 1st 2022, Ryujinx will only support Windows 10 1803 and newer.\n", $"Ryujinx {Version}", MbIconwarning);
-            }
-
-            PreviewerDetached = true;
-
-            Initialize(args);
-
-            LoggerAdapter.Register();
-
-            BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
-        }
-
-        public static AppBuilder BuildAvaloniaApp()
-        {
-            return AppBuilder.Configure<App>()
-                .UsePlatformDetect()
-                .With(new X11PlatformOptions
-                {
-                    EnableMultiTouch = true,
-                    EnableIme = true,
-                    EnableInputFocusProxy = Environment.GetEnvironmentVariable("XDG_CURRENT_DESKTOP") == "gamescope",
-                    RenderingMode = new[] { X11RenderingMode.Glx, X11RenderingMode.Software },
-                })
-                .With(new Win32PlatformOptions
-                {
-                    WinUICompositionBackdropCornerRadius = 8.0f,
-                    RenderingMode = new[] { Win32RenderingMode.AngleEgl, Win32RenderingMode.Software },
-                })
-                .UseSkia();
-        }
-
-        private static void Initialize(string[] args)
-        {
-            // Parse arguments
-            CommandLineState.ParseArguments(args);
-
-            // Delete backup files after updating.
-            Task.Run(Updater.CleanupUpdate);
-
-            Console.Title = $"Ryujinx Console {Version}";
-
-            // Hook unhandled exception and process exit events.
-            AppDomain.CurrentDomain.UnhandledException += (sender, e) => ProcessUnhandledException(e.ExceptionObject as Exception, e.IsTerminating);
-            AppDomain.CurrentDomain.ProcessExit += (sender, e) => Exit();
-
-            // Setup base data directory.
-            AppDataManager.Initialize(CommandLineState.BaseDirPathArg);
-
-            // Initialize the configuration.
-            ConfigurationState.Initialize();
-
-            // Initialize the logger system.
-            LoggerModule.Initialize();
-
-            // Initialize Discord integration.
-            DiscordIntegrationModule.Initialize();
-
-            // Initialize SDL2 driver
-            SDL2Driver.MainThreadDispatcher = action => Dispatcher.UIThread.InvokeAsync(action, DispatcherPriority.Input);
-
-            ReloadConfig();
-
-            WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor();
-
-            // Logging system information.
-            PrintSystemInfo();
-
-            // Enable OGL multithreading on the driver, when available.
-            DriverUtilities.ToggleOGLThreading(ConfigurationState.Instance.Graphics.BackendThreading == BackendThreading.Off);
-
-            // Check if keys exists.
-            if (!File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys")))
-            {
-                if (!(AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && File.Exists(Path.Combine(AppDataManager.KeysDirPathUser, "prod.keys"))))
-                {
-                    MainWindow.ShowKeyErrorOnLoad = true;
-                }
-            }
-
-            if (CommandLineState.LaunchPathArg != null)
-            {
-                MainWindow.DeferLoadApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg);
-            }
-        }
-
-        public static void ReloadConfig()
-        {
-            string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName);
-            string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName);
-
-            // Now load the configuration as the other subsystems are now registered
-            if (File.Exists(localConfigurationPath))
-            {
-                ConfigurationPath = localConfigurationPath;
-            }
-            else if (File.Exists(appDataConfigurationPath))
-            {
-                ConfigurationPath = appDataConfigurationPath;
-            }
-
-            if (ConfigurationPath == null)
-            {
-                // No configuration, we load the default values and save it to disk
-                ConfigurationPath = appDataConfigurationPath;
-
-                ConfigurationState.Instance.LoadDefault();
-                ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
-            }
-            else
-            {
-                if (ConfigurationFileFormat.TryLoad(ConfigurationPath, out ConfigurationFileFormat configurationFileFormat))
-                {
-                    ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath);
-                }
-                else
-                {
-                    ConfigurationState.Instance.LoadDefault();
-
-                    Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location {ConfigurationPath}");
-                }
-            }
-
-            // Check if graphics backend was overridden
-            if (CommandLineState.OverrideGraphicsBackend != null)
-            {
-                if (CommandLineState.OverrideGraphicsBackend.ToLower() == "opengl")
-                {
-                    ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.OpenGl;
-                }
-                else if (CommandLineState.OverrideGraphicsBackend.ToLower() == "vulkan")
-                {
-                    ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.Vulkan;
-                }
-            }
-
-            // Check if docked mode was overriden.
-            if (CommandLineState.OverrideDockedMode.HasValue)
-            {
-                ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value;
-            }
-
-            // Check if HideCursor was overridden.
-            if (CommandLineState.OverrideHideCursor is not null)
-            {
-                ConfigurationState.Instance.HideCursor.Value = CommandLineState.OverrideHideCursor!.ToLower() switch
-                {
-                    "never" => HideCursorMode.Never,
-                    "onidle" => HideCursorMode.OnIdle,
-                    "always" => HideCursorMode.Always,
-                    _ => ConfigurationState.Instance.HideCursor.Value,
-                };
-            }
-        }
-
-        private static void PrintSystemInfo()
-        {
-            Logger.Notice.Print(LogClass.Application, $"Ryujinx Version: {Version}");
-            SystemInfo.Gather().Print();
-
-            Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {(Logger.GetEnabledLevels().Count == 0 ? "<None>" : string.Join(", ", Logger.GetEnabledLevels()))}");
-
-            if (AppDataManager.Mode == AppDataManager.LaunchMode.Custom)
-            {
-                Logger.Notice.Print(LogClass.Application, $"Launch Mode: Custom Path {AppDataManager.BaseDirPath}");
-            }
-            else
-            {
-                Logger.Notice.Print(LogClass.Application, $"Launch Mode: {AppDataManager.Mode}");
-            }
-        }
-
-        private static void ProcessUnhandledException(Exception ex, bool isTerminating)
-        {
-            string message = $"Unhandled exception caught: {ex}";
-
-            Logger.Error?.PrintMsg(LogClass.Application, message);
-
-            if (Logger.Error == null)
-            {
-                Logger.Notice.PrintMsg(LogClass.Application, message);
-            }
-
-            if (isTerminating)
-            {
-                Exit();
-            }
-        }
-
-        public static void Exit()
-        {
-            DiscordIntegrationModule.Exit();
-
-            Logger.Shutdown();
-        }
-    }
-}

+ 0 - 167
src/Ryujinx.Ava/Ryujinx.Ava.csproj

@@ -1,167 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-  <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
-    <RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
-    <OutputType>Exe</OutputType>
-    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
-    <Version>1.0.0-dirty</Version>
-    <DefineConstants Condition=" '$(ExtraDefineConstants)' != '' ">$(DefineConstants);$(ExtraDefineConstants)</DefineConstants>
-    <SigningCertificate Condition=" '$(SigningCertificate)' == '' ">-</SigningCertificate>
-    <RootNamespace>Ryujinx.Ava</RootNamespace>
-    <ApplicationIcon>Ryujinx.ico</ApplicationIcon>
-    <TieredPGO>true</TieredPGO>
-    <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
-    <ApplicationManifest>app.manifest</ApplicationManifest>
-  </PropertyGroup>
-
-  <Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="$([MSBuild]::IsOSPlatform('OSX'))">
-    <Exec Command="codesign --entitlements '$(ProjectDir)..\..\distribution\macos\entitlements.xml' -f --deep -s $(SigningCertificate) '$(TargetDir)$(TargetName)'" />
-  </Target>
-
-  <PropertyGroup Condition="'$(RuntimeIdentifier)' != ''">
-    <PublishSingleFile>true</PublishSingleFile>
-    <TrimmerSingleWarn>false</TrimmerSingleWarn>
-    <PublishTrimmed>true</PublishTrimmed>
-    <TrimMode>partial</TrimMode>
-  </PropertyGroup>
-
-  <!--
-    FluentAvalonia, used in the Avalonia UI, requires a workaround for the json serializer used internally when using .NET 8+ System.Text.Json.
-    See:
-      https://github.com/amwx/FluentAvalonia/issues/481
-      https://devblogs.microsoft.com/dotnet/system-text-json-in-dotnet-8/
-  -->
-  <PropertyGroup>
-    <JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
-  </PropertyGroup>
-
-  <ItemGroup>
-    <PackageReference Include="Avalonia" />
-    <PackageReference Include="Avalonia.Desktop" />
-    <PackageReference Include="Avalonia.Diagnostics" Condition="'$(Configuration)'=='Debug'" />
-    <PackageReference Include="Avalonia.Controls.DataGrid" />
-    <PackageReference Include="Avalonia.Markup.Xaml.Loader" />
-    <PackageReference Include="Avalonia.Svg" />
-    <PackageReference Include="Avalonia.Svg.Skia" />
-    <PackageReference Include="DynamicData" />
-    <PackageReference Include="FluentAvaloniaUI" />
-
-    <PackageReference Include="OpenTK.Core" />
-    <PackageReference Include="Ryujinx.Audio.OpenAL.Dependencies" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64' AND '$(RuntimeIdentifier)' != 'osx-arm64'" />
-    <PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies" />
-    <PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'win-x64'" />
-    <PackageReference Include="Silk.NET.Vulkan" />
-    <PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
-    <PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
-    <PackageReference Include="SPB" />
-    <PackageReference Include="SharpZipLib" />
-    <PackageReference Include="SixLabors.ImageSharp" />
-
-    <!--NOTE: DO NOT REMOVE, THIS IS REQUIRED AS A RESULT OF A TRIMMING ISSUE IN AVALONIA -->
-    <PackageReference Include="System.Drawing.Common" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <ProjectReference Include="..\Ryujinx.Audio.Backends.SDL2\Ryujinx.Audio.Backends.SDL2.csproj" />
-    <ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
-    <ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
-    <ProjectReference Include="..\Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj" />
-    <ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />
-    <ProjectReference Include="..\Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj" />
-    <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
-    <ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
-    <ProjectReference Include="..\ARMeilleure\ARMeilleure.csproj" />
-    <ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
-    <ProjectReference Include="..\Ryujinx.Graphics.Gpu\Ryujinx.Graphics.Gpu.csproj" />
-    <ProjectReference Include="..\Ryujinx.UI.Common\Ryujinx.UI.Common.csproj" />
-    <ProjectReference Include="..\Ryujinx.UI.LocaleGenerator\Ryujinx.UI.LocaleGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <Content Include="..\..\distribution\windows\alsoft.ini" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64'">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-      <TargetPath>alsoft.ini</TargetPath>
-    </Content>
-    <Content Include="..\..\distribution\legal\THIRDPARTY.md">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-      <TargetPath>THIRDPARTY.md</TargetPath>
-    </Content>
-    <Content Include="..\..\LICENSE.txt">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-      <TargetPath>LICENSE.txt</TargetPath>
-    </Content>
-  </ItemGroup>
-
-  <ItemGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64' OR '$(RuntimeIdentifier)' == 'linux-arm64'">
-    <Content Include="..\..\distribution\linux\Ryujinx.sh">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-    </Content>
-    <Content Include="..\..\distribution\linux\mime\Ryujinx.xml">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-      <TargetPath>mime\Ryujinx.xml</TargetPath>
-    </Content>
-  </ItemGroup>
-
-  <ItemGroup>
-    <AvaloniaResource Include="UI\**\*.xaml">
-      <SubType>Designer</SubType>
-    </AvaloniaResource>
-    <AvaloniaResource Include="Assets\Fonts\SegoeFluentIcons.ttf" />
-    <AvaloniaResource Include="Assets\Styles\Themes.xaml">
-      <Generator>MSBuild:Compile</Generator>
-    </AvaloniaResource>
-    <AvaloniaResource Include="Assets\Styles\Styles.xaml" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <None Remove="Assets\Locales\el_GR.json" />
-    <None Remove="Assets\Locales\en_US.json" />
-    <None Remove="Assets\Locales\es_ES.json" />
-    <None Remove="Assets\Locales\fr_FR.json" />
-    <None Remove="Assets\Locales\he_IL.json" />
-    <None Remove="Assets\Locales\de_DE.json" />
-    <None Remove="Assets\Locales\it_IT.json" />
-    <None Remove="Assets\Locales\ja_JP.json" />
-    <None Remove="Assets\Locales\ko_KR.json" />
-    <None Remove="Assets\Locales\pl_PL.json" />
-    <None Remove="Assets\Locales\pt_BR.json" />
-    <None Remove="Assets\Locales\ru_RU.json" />
-    <None Remove="Assets\Locales\tr_TR.json" />
-    <None Remove="Assets\Locales\uk_UA.json" />
-    <None Remove="Assets\Locales\zh_CN.json" />
-    <None Remove="Assets\Locales\zh_TW.json" />
-    <None Remove="Assets\Styles\Styles.xaml" />
-    <None Remove="Assets\Styles\Themes.xaml" />
-    <None Remove="Assets\Icons\Controller_JoyConLeft.svg" />
-    <None Remove="Assets\Icons\Controller_JoyConPair.svg" />
-    <None Remove="Assets\Icons\Controller_JoyConRight.svg" />
-    <None Remove="Assets\Icons\Controller_ProCon.svg" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <EmbeddedResource Include="Assets\Locales\el_GR.json" />
-    <EmbeddedResource Include="Assets\Locales\en_US.json" />
-    <EmbeddedResource Include="Assets\Locales\es_ES.json" />
-    <EmbeddedResource Include="Assets\Locales\fr_FR.json" />
-    <EmbeddedResource Include="Assets\Locales\he_IL.json" />
-    <EmbeddedResource Include="Assets\Locales\de_DE.json" />
-    <EmbeddedResource Include="Assets\Locales\it_IT.json" />
-    <EmbeddedResource Include="Assets\Locales\ja_JP.json" />
-    <EmbeddedResource Include="Assets\Locales\ko_KR.json" />
-    <EmbeddedResource Include="Assets\Locales\pl_PL.json" />
-    <EmbeddedResource Include="Assets\Locales\pt_BR.json" />
-    <EmbeddedResource Include="Assets\Locales\ru_RU.json" />
-    <EmbeddedResource Include="Assets\Locales\tr_TR.json" />
-    <EmbeddedResource Include="Assets\Locales\uk_UA.json" />
-    <EmbeddedResource Include="Assets\Locales\zh_CN.json" />
-    <EmbeddedResource Include="Assets\Locales\zh_TW.json" />
-    <EmbeddedResource Include="Assets\Styles\Styles.xaml" />
-    <EmbeddedResource Include="Assets\Icons\Controller_JoyConLeft.svg" />
-    <EmbeddedResource Include="Assets\Icons\Controller_JoyConPair.svg" />
-    <EmbeddedResource Include="Assets\Icons\Controller_JoyConRight.svg" />
-    <EmbeddedResource Include="Assets\Icons\Controller_ProCon.svg" />
-  </ItemGroup>
-  <ItemGroup>
-    <AdditionalFiles Include="Assets\Locales\en_US.json" />
-  </ItemGroup>
-</Project>

+ 0 - 0
src/Ryujinx/Input/GTK3/GTK3Keyboard.cs → src/Ryujinx.Gtk3/Input/GTK3/GTK3Keyboard.cs


+ 0 - 0
src/Ryujinx/Input/GTK3/GTK3KeyboardDriver.cs → src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs


+ 0 - 0
src/Ryujinx/Input/GTK3/GTK3MappingHelper.cs → src/Ryujinx.Gtk3/Input/GTK3/GTK3MappingHelper.cs


+ 0 - 0
src/Ryujinx/Input/GTK3/GTK3Mouse.cs → src/Ryujinx.Gtk3/Input/GTK3/GTK3Mouse.cs


+ 0 - 0
src/Ryujinx/Input/GTK3/GTK3MouseDriver.cs → src/Ryujinx.Gtk3/Input/GTK3/GTK3MouseDriver.cs


+ 2 - 2
src/Ryujinx/Modules/Updater/UpdateDialog.cs → src/Ryujinx.Gtk3/Modules/Updater/UpdateDialog.cs

@@ -25,7 +25,7 @@ namespace Ryujinx.Modules
         private readonly string _buildUrl;
         private bool _restartQuery;
 
-        public UpdateDialog(MainWindow mainWindow, Version newVersion, string buildUrl) : this(new Builder("Ryujinx.Modules.Updater.UpdateDialog.glade"), mainWindow, newVersion, buildUrl) { }
+        public UpdateDialog(MainWindow mainWindow, Version newVersion, string buildUrl) : this(new Builder("Ryujinx.Gtk3.Modules.Updater.UpdateDialog.glade"), mainWindow, newVersion, buildUrl) { }
 
         private UpdateDialog(Builder builder, MainWindow mainWindow, Version newVersion, string buildUrl) : base(builder.GetRawOwnedObject("UpdateDialog"))
         {
@@ -34,7 +34,7 @@ namespace Ryujinx.Modules
             _mainWindow = mainWindow;
             _buildUrl = buildUrl;
 
-            Icon = new Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_Ryujinx.png");
+            Icon = new Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Gtk3.UI.Common.Resources.Logo_Ryujinx.png");
             MainText.Text = "Do you want to update Ryujinx to the latest version?";
             SecondaryText.Text = $"{Program.Version} -> {newVersion}";
 

+ 0 - 0
src/Ryujinx/Modules/Updater/UpdateDialog.glade → src/Ryujinx.Gtk3/Modules/Updater/UpdateDialog.glade


+ 170 - 312
src/Ryujinx.Ava/Modules/Updater/Updater.cs → src/Ryujinx.Gtk3/Modules/Updater/Updater.cs

@@ -1,75 +1,93 @@
-using Avalonia.Controls;
-using Avalonia.Threading;
-using FluentAvalonia.UI.Controls;
+using Gtk;
 using ICSharpCode.SharpZipLib.GZip;
 using ICSharpCode.SharpZipLib.Tar;
 using ICSharpCode.SharpZipLib.Zip;
-using Ryujinx.Ava;
-using Ryujinx.Ava.Common.Locale;
-using Ryujinx.Ava.UI.Helpers;
 using Ryujinx.Common;
 using Ryujinx.Common.Logging;
 using Ryujinx.Common.Utilities;
-using Ryujinx.UI.Common.Helper;
+using Ryujinx.UI;
 using Ryujinx.UI.Common.Models.Github;
+using Ryujinx.UI.Widgets;
 using System;
 using System.Collections.Generic;
-using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Net;
 using System.Net.Http;
 using System.Net.NetworkInformation;
-using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
-using System.Runtime.Versioning;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 
 namespace Ryujinx.Modules
 {
-    internal static class Updater
+    public static class Updater
     {
         private const string GitHubApiUrl = "https://api.github.com";
-        private static readonly GithubReleasesJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+        private const int ConnectionCount = 4;
+
+        internal static bool Running;
 
         private static readonly string _homeDir = AppDomain.CurrentDomain.BaseDirectory;
         private static readonly string _updateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update");
         private static readonly string _updatePublishDir = Path.Combine(_updateDir, "publish");
-        private const int ConnectionCount = 4;
 
         private static string _buildVer;
         private static string _platformExt;
         private static string _buildUrl;
         private static long _buildSize;
-        private static bool _updateSuccessful;
-        private static bool _running;
 
-        private static readonly string[] _windowsDependencyDirs = Array.Empty<string>();
+        private static readonly GithubReleasesJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
+        // On Windows, GtkSharp.Dependencies adds these extra dirs that must be cleaned during updates.
+        private static readonly string[] _windowsDependencyDirs = { "bin", "etc", "lib", "share" };
 
-        public static async Task BeginParse(Window mainWindow, bool showVersionUpToDate)
+        private static HttpClient ConstructHttpClient()
         {
-            if (_running)
+            HttpClient result = new();
+
+            // Required by GitHub to interact with APIs.
+            result.DefaultRequestHeaders.Add("User-Agent", "Ryujinx-Updater/1.0.0");
+
+            return result;
+        }
+
+        public static async Task BeginParse(MainWindow mainWindow, bool showVersionUpToDate)
+        {
+            if (Running)
             {
                 return;
             }
 
-            _running = true;
+            Running = true;
+            mainWindow.UpdateMenuItem.Sensitive = false;
+
+            int artifactIndex = -1;
 
             // Detect current platform
             if (OperatingSystem.IsMacOS())
             {
-                _platformExt = "macos_universal.app.tar.gz";
+                _platformExt = "osx_x64.zip";
+                artifactIndex = 1;
             }
             else if (OperatingSystem.IsWindows())
             {
                 _platformExt = "win_x64.zip";
+                artifactIndex = 2;
             }
             else if (OperatingSystem.IsLinux())
             {
                 var arch = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "arm64" : "x64";
                 _platformExt = $"linux_{arch}.tar.gz";
+                artifactIndex = 0;
+            }
+
+            if (artifactIndex == -1)
+            {
+                GtkDialog.CreateErrorDialog("Your platform is not supported!");
+
+                return;
             }
 
             Version newVersion;
@@ -81,14 +99,9 @@ namespace Ryujinx.Modules
             }
             catch
             {
+                GtkDialog.CreateWarningDialog("Failed to convert the current Ryujinx version.", "Cancelling Update!");
                 Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!");
 
-                await ContentDialogHelper.CreateWarningDialog(
-                    LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage],
-                    LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
-
-                _running = false;
-
                 return;
             }
 
@@ -96,15 +109,16 @@ namespace Ryujinx.Modules
             try
             {
                 using HttpClient jsonClient = ConstructHttpClient();
-
                 string buildInfoUrl = $"{GitHubApiUrl}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest";
+
+                // Fetch latest build information
                 string fetchedJson = await jsonClient.GetStringAsync(buildInfoUrl);
                 var fetched = JsonHelper.Deserialize(fetchedJson, _serializerContext.GithubReleasesJsonResponse);
                 _buildVer = fetched.Name;
 
                 foreach (var asset in fetched.Assets)
                 {
-                    if (asset.Name.StartsWith("test-ava-ryujinx") && asset.Name.EndsWith(_platformExt))
+                    if (asset.Name.StartsWith("gtk-ryujinx") && asset.Name.EndsWith(_platformExt))
                     {
                         _buildUrl = asset.BrowserDownloadUrl;
 
@@ -112,13 +126,9 @@ namespace Ryujinx.Modules
                         {
                             if (showVersionUpToDate)
                             {
-                                await ContentDialogHelper.CreateUpdaterInfoDialog(
-                                    LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
-                                    "");
+                                GtkDialog.CreateUpdaterInfoDialog("You are already using the latest version of Ryujinx!", "");
                             }
 
-                            _running = false;
-
                             return;
                         }
 
@@ -126,29 +136,20 @@ namespace Ryujinx.Modules
                     }
                 }
 
-                // If build not done, assume no new update are available.
-                if (_buildUrl is null)
+                if (_buildUrl == null)
                 {
                     if (showVersionUpToDate)
                     {
-                        await ContentDialogHelper.CreateUpdaterInfoDialog(
-                            LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
-                            "");
+                        GtkDialog.CreateUpdaterInfoDialog("You are already using the latest version of Ryujinx!", "");
                     }
 
-                    _running = false;
-
                     return;
                 }
             }
             catch (Exception exception)
             {
                 Logger.Error?.Print(LogClass.Application, exception.Message);
-
-                await ContentDialogHelper.CreateErrorDialog(
-                    LocaleManager.Instance[LocaleKeys.DialogUpdaterFailedToGetVersionMessage]);
-
-                _running = false;
+                GtkDialog.CreateErrorDialog("An error occurred when trying to get release information from GitHub Release. This can be caused if a new release is being compiled by GitHub Actions. Try again in a few minutes.");
 
                 return;
             }
@@ -159,13 +160,8 @@ namespace Ryujinx.Modules
             }
             catch
             {
-                Logger.Error?.Print(LogClass.Application, "Failed to convert the received Ryujinx version from Github!");
-
-                await ContentDialogHelper.CreateWarningDialog(
-                    LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage],
-                    LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
-
-                _running = false;
+                GtkDialog.CreateWarningDialog("Failed to convert the received Ryujinx version from GitHub Release.", "Cancelling Update!");
+                Logger.Error?.Print(LogClass.Application, "Failed to convert the received Ryujinx version from GitHub Release!");
 
                 return;
             }
@@ -174,12 +170,11 @@ namespace Ryujinx.Modules
             {
                 if (showVersionUpToDate)
                 {
-                    await ContentDialogHelper.CreateUpdaterInfoDialog(
-                        LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
-                        "");
+                    GtkDialog.CreateUpdaterInfoDialog("You are already using the latest version of Ryujinx!", "");
                 }
 
-                _running = false;
+                Running = false;
+                mainWindow.UpdateMenuItem.Sensitive = true;
 
                 return;
             }
@@ -202,39 +197,13 @@ namespace Ryujinx.Modules
                 _buildSize = -1;
             }
 
-            await Dispatcher.UIThread.InvokeAsync(async () =>
-            {
-                // Show a message asking the user if they want to update
-                var shouldUpdate = await ContentDialogHelper.CreateChoiceDialog(
-                    LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
-                    LocaleManager.Instance[LocaleKeys.RyujinxUpdaterMessage],
-                    $"{Program.Version} -> {newVersion}");
-
-                if (shouldUpdate)
-                {
-                    await UpdateRyujinx(mainWindow, _buildUrl);
-                }
-                else
-                {
-                    _running = false;
-                }
-            });
-        }
-
-        private static HttpClient ConstructHttpClient()
-        {
-            HttpClient result = new();
-
-            // Required by GitHub to interact with APIs.
-            result.DefaultRequestHeaders.Add("User-Agent", "Ryujinx-Updater/1.0.0");
-
-            return result;
+            // Show a message asking the user if they want to update
+            UpdateDialog updateDialog = new(mainWindow, newVersion, _buildUrl);
+            updateDialog.Show();
         }
 
-        private static async Task UpdateRyujinx(Window parent, string downloadUrl)
+        public static void UpdateRyujinx(UpdateDialog updateDialog, string downloadUrl)
         {
-            _updateSuccessful = false;
-
             // Empty update dir, although it shouldn't ever have anything inside it
             if (Directory.Exists(_updateDir))
             {
@@ -245,93 +214,22 @@ namespace Ryujinx.Modules
 
             string updateFile = Path.Combine(_updateDir, "update.bin");
 
-            TaskDialog taskDialog = new()
-            {
-                Header = LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
-                SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterDownloading],
-                IconSource = new SymbolIconSource { Symbol = Symbol.Download },
-                ShowProgressBar = true,
-                XamlRoot = parent,
-            };
+            // Download the update .zip
+            updateDialog.MainText.Text = "Downloading Update...";
+            updateDialog.ProgressBar.Value = 0;
+            updateDialog.ProgressBar.MaxValue = 100;
 
-            taskDialog.Opened += (s, e) =>
+            if (_buildSize >= 0)
             {
-                if (_buildSize >= 0)
-                {
-                    DoUpdateWithMultipleThreads(taskDialog, downloadUrl, updateFile);
-                }
-                else
-                {
-                    DoUpdateWithSingleThread(taskDialog, downloadUrl, updateFile);
-                }
-            };
-
-            await taskDialog.ShowAsync(true);
-
-            if (_updateSuccessful)
+                DoUpdateWithMultipleThreads(updateDialog, downloadUrl, updateFile);
+            }
+            else
             {
-                bool shouldRestart = true;
-
-                if (!OperatingSystem.IsMacOS())
-                {
-                    shouldRestart = await ContentDialogHelper.CreateChoiceDialog(LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
-                        LocaleManager.Instance[LocaleKeys.DialogUpdaterCompleteMessage],
-                        LocaleManager.Instance[LocaleKeys.DialogUpdaterRestartMessage]);
-                }
-
-                if (shouldRestart)
-                {
-                    List<string> arguments = CommandLineState.Arguments.ToList();
-                    string executableDirectory = AppDomain.CurrentDomain.BaseDirectory;
-
-                    // On macOS we perform the update at relaunch.
-                    if (OperatingSystem.IsMacOS())
-                    {
-                        string baseBundlePath = Path.GetFullPath(Path.Combine(executableDirectory, "..", ".."));
-                        string newBundlePath = Path.Combine(_updateDir, "Ryujinx.app");
-                        string updaterScriptPath = Path.Combine(newBundlePath, "Contents", "Resources", "updater.sh");
-                        string currentPid = Environment.ProcessId.ToString();
-
-                        arguments.InsertRange(0, new List<string> { updaterScriptPath, baseBundlePath, newBundlePath, currentPid });
-                        Process.Start("/bin/bash", arguments);
-                    }
-                    else
-                    {
-                        // Find the process name.
-                        string ryuName = Path.GetFileName(Environment.ProcessPath);
-
-                        // Some operating systems can see the renamed executable, so strip off the .ryuold if found.
-                        if (ryuName.EndsWith(".ryuold"))
-                        {
-                            ryuName = ryuName[..^7];
-                        }
-
-                        // Fallback if the executable could not be found.
-                        if (!Path.Exists(Path.Combine(executableDirectory, ryuName)))
-                        {
-                            ryuName = OperatingSystem.IsWindows() ? "Ryujinx.Ava.exe" : "Ryujinx.Ava";
-                        }
-
-                        ProcessStartInfo processStart = new(ryuName)
-                        {
-                            UseShellExecute = true,
-                            WorkingDirectory = executableDirectory,
-                        };
-
-                        foreach (string argument in CommandLineState.Arguments)
-                        {
-                            processStart.ArgumentList.Add(argument);
-                        }
-
-                        Process.Start(processStart);
-                    }
-
-                    Environment.Exit(0);
-                }
+                DoUpdateWithSingleThread(updateDialog, downloadUrl, updateFile);
             }
         }
 
-        private static void DoUpdateWithMultipleThreads(TaskDialog taskDialog, string downloadUrl, string updateFile)
+        private static void DoUpdateWithMultipleThreads(UpdateDialog updateDialog, string downloadUrl, string updateFile)
         {
             // Multi-Threaded Updater
             long chunkSize = _buildSize / ConnectionCount;
@@ -355,7 +253,6 @@ namespace Ryujinx.Modules
                 // TODO: WebClient is obsolete and need to be replaced with a more complex logic using HttpClient.
                 using WebClient client = new();
 #pragma warning restore SYSLIB0014
-
                 webClients.Add(client);
 
                 if (i == ConnectionCount - 1)
@@ -375,7 +272,7 @@ namespace Ryujinx.Modules
                     Interlocked.Exchange(ref progressPercentage[index], args.ProgressPercentage);
                     Interlocked.Add(ref totalProgressPercentage, args.ProgressPercentage);
 
-                    taskDialog.SetProgressBarState(totalProgressPercentage / ConnectionCount, TaskDialogProgressState.Normal);
+                    updateDialog.ProgressBar.Value = totalProgressPercentage / ConnectionCount;
                 };
 
                 client.DownloadDataCompleted += (_, args) =>
@@ -386,8 +283,6 @@ namespace Ryujinx.Modules
                     {
                         webClients[index].Dispose();
 
-                        taskDialog.Hide();
-
                         return;
                     }
 
@@ -405,24 +300,18 @@ namespace Ryujinx.Modules
 
                         File.WriteAllBytes(updateFile, mergedFileBytes);
 
-                        // On macOS, ensure that we remove the quarantine bit to prevent Gatekeeper from blocking execution.
-                        if (OperatingSystem.IsMacOS())
-                        {
-                            using Process xattrProcess = Process.Start("xattr", new List<string> { "-d", "com.apple.quarantine", updateFile });
-
-                            xattrProcess.WaitForExit();
-                        }
-
                         try
                         {
-                            InstallUpdate(taskDialog, updateFile);
+                            InstallUpdate(updateDialog, updateFile);
                         }
                         catch (Exception e)
                         {
                             Logger.Warning?.Print(LogClass.Application, e.Message);
                             Logger.Warning?.Print(LogClass.Application, "Multi-Threaded update failed, falling back to single-threaded updater.");
 
-                            DoUpdateWithSingleThread(taskDialog, downloadUrl, updateFile);
+                            DoUpdateWithSingleThread(updateDialog, downloadUrl, updateFile);
+
+                            return;
                         }
                     }
                 };
@@ -441,14 +330,14 @@ namespace Ryujinx.Modules
                         webClient.CancelAsync();
                     }
 
-                    DoUpdateWithSingleThread(taskDialog, downloadUrl, updateFile);
+                    DoUpdateWithSingleThread(updateDialog, downloadUrl, updateFile);
 
                     return;
                 }
             }
         }
 
-        private static void DoUpdateWithSingleThreadWorker(TaskDialog taskDialog, string downloadUrl, string updateFile)
+        private static void DoUpdateWithSingleThreadWorker(UpdateDialog updateDialog, string downloadUrl, string updateFile)
         {
             using HttpClient client = new();
             // We do not want to timeout while downloading
@@ -474,165 +363,151 @@ namespace Ryujinx.Modules
 
                 byteWritten += readSize;
 
-                taskDialog.SetProgressBarState(GetPercentage(byteWritten, totalBytes), TaskDialogProgressState.Normal);
-
+                updateDialog.ProgressBar.Value = ((double)byteWritten / totalBytes) * 100;
                 updateFileStream.Write(buffer, 0, readSize);
             }
 
-            InstallUpdate(taskDialog, updateFile);
-        }
-
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        private static double GetPercentage(double value, double max)
-        {
-            return max == 0 ? 0 : value / max * 100;
+            InstallUpdate(updateDialog, updateFile);
         }
 
-        private static void DoUpdateWithSingleThread(TaskDialog taskDialog, string downloadUrl, string updateFile)
+        private static void DoUpdateWithSingleThread(UpdateDialog updateDialog, string downloadUrl, string updateFile)
         {
-            Thread worker = new(() => DoUpdateWithSingleThreadWorker(taskDialog, downloadUrl, updateFile))
+            Thread worker = new(() => DoUpdateWithSingleThreadWorker(updateDialog, downloadUrl, updateFile))
             {
                 Name = "Updater.SingleThreadWorker",
             };
-
             worker.Start();
         }
 
-        [SupportedOSPlatform("linux")]
-        [SupportedOSPlatform("macos")]
-        private static void ExtractTarGzipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath)
+        private static async void InstallUpdate(UpdateDialog updateDialog, string updateFile)
         {
-            using Stream inStream = File.OpenRead(archivePath);
-            using GZipInputStream gzipStream = new(inStream);
-            using TarInputStream tarStream = new(gzipStream, Encoding.ASCII);
-
-            TarEntry tarEntry;
+            // Extract Update
+            updateDialog.MainText.Text = "Extracting Update...";
+            updateDialog.ProgressBar.Value = 0;
 
-            while ((tarEntry = tarStream.GetNextEntry()) is not null)
+            if (OperatingSystem.IsLinux())
             {
-                if (tarEntry.IsDirectory)
+                using Stream inStream = File.OpenRead(updateFile);
+                using Stream gzipStream = new GZipInputStream(inStream);
+                using TarInputStream tarStream = new(gzipStream, Encoding.ASCII);
+                updateDialog.ProgressBar.MaxValue = inStream.Length;
+
+                await Task.Run(() =>
                 {
-                    continue;
-                }
+                    TarEntry tarEntry;
 
-                string outPath = Path.Combine(outputDirectoryPath, tarEntry.Name);
+                    if (!OperatingSystem.IsWindows())
+                    {
+                        while ((tarEntry = tarStream.GetNextEntry()) != null)
+                        {
+                            if (tarEntry.IsDirectory)
+                            {
+                                continue;
+                            }
 
-                Directory.CreateDirectory(Path.GetDirectoryName(outPath));
+                            string outPath = Path.Combine(_updateDir, tarEntry.Name);
 
-                using FileStream outStream = File.OpenWrite(outPath);
-                tarStream.CopyEntryContents(outStream);
+                            Directory.CreateDirectory(Path.GetDirectoryName(outPath));
 
-                File.SetUnixFileMode(outPath, (UnixFileMode)tarEntry.TarHeader.Mode);
-                File.SetLastWriteTime(outPath, DateTime.SpecifyKind(tarEntry.ModTime, DateTimeKind.Utc));
+                            using FileStream outStream = File.OpenWrite(outPath);
+                            tarStream.CopyEntryContents(outStream);
 
-                Dispatcher.UIThread.Post(() =>
-                {
-                    if (tarEntry is null)
-                    {
-                        return;
-                    }
+                            File.SetUnixFileMode(outPath, (UnixFileMode)tarEntry.TarHeader.Mode);
+                            File.SetLastWriteTime(outPath, DateTime.SpecifyKind(tarEntry.ModTime, DateTimeKind.Utc));
 
-                    taskDialog.SetProgressBarState(GetPercentage(tarEntry.Size, inStream.Length), TaskDialogProgressState.Normal);
-                });
-            }
-        }
+                            TarEntry entry = tarEntry;
 
-        private static void ExtractZipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath)
-        {
-            using Stream inStream = File.OpenRead(archivePath);
-            using ZipFile zipFile = new(inStream);
+                            Application.Invoke(delegate
+                            {
+                                updateDialog.ProgressBar.Value += entry.Size;
+                            });
+                        }
+                    }
+                });
 
-            double count = 0;
-            foreach (ZipEntry zipEntry in zipFile)
+                updateDialog.ProgressBar.Value = inStream.Length;
+            }
+            else
             {
-                count++;
-                if (zipEntry.IsDirectory)
-                {
-                    continue;
-                }
+                using Stream inStream = File.OpenRead(updateFile);
+                using ZipFile zipFile = new(inStream);
+                updateDialog.ProgressBar.MaxValue = zipFile.Count;
 
-                string outPath = Path.Combine(outputDirectoryPath, zipEntry.Name);
+                await Task.Run(() =>
+                {
+                    foreach (ZipEntry zipEntry in zipFile)
+                    {
+                        if (zipEntry.IsDirectory)
+                        {
+                            continue;
+                        }
 
-                Directory.CreateDirectory(Path.GetDirectoryName(outPath));
+                        string outPath = Path.Combine(_updateDir, zipEntry.Name);
 
-                using Stream zipStream = zipFile.GetInputStream(zipEntry);
-                using FileStream outStream = File.OpenWrite(outPath);
+                        Directory.CreateDirectory(Path.GetDirectoryName(outPath));
 
-                zipStream.CopyTo(outStream);
+                        using Stream zipStream = zipFile.GetInputStream(zipEntry);
+                        using FileStream outStream = File.OpenWrite(outPath);
+                        zipStream.CopyTo(outStream);
 
-                File.SetLastWriteTime(outPath, DateTime.SpecifyKind(zipEntry.DateTime, DateTimeKind.Utc));
+                        File.SetLastWriteTime(outPath, DateTime.SpecifyKind(zipEntry.DateTime, DateTimeKind.Utc));
 
-                Dispatcher.UIThread.Post(() =>
-                {
-                    taskDialog.SetProgressBarState(GetPercentage(count, zipFile.Count), TaskDialogProgressState.Normal);
+                        Application.Invoke(delegate
+                        {
+                            updateDialog.ProgressBar.Value++;
+                        });
+                    }
                 });
             }
-        }
-
-        private static void InstallUpdate(TaskDialog taskDialog, string updateFile)
-        {
-            // Extract Update
-            taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterExtracting];
-            taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
-
-            if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
-            {
-                ExtractTarGzipFile(taskDialog, updateFile, _updateDir);
-            }
-            else if (OperatingSystem.IsWindows())
-            {
-                ExtractZipFile(taskDialog, updateFile, _updateDir);
-            }
-            else
-            {
-                throw new NotSupportedException();
-            }
 
             // Delete downloaded zip
             File.Delete(updateFile);
 
             List<string> allFiles = EnumerateFilesToDelete().ToList();
 
-            taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterRenaming];
-            taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
+            updateDialog.MainText.Text = "Renaming Old Files...";
+            updateDialog.ProgressBar.Value = 0;
+            updateDialog.ProgressBar.MaxValue = allFiles.Count;
 
-            // NOTE: On macOS, replacement is delayed to the restart phase.
-            if (!OperatingSystem.IsMacOS())
+            // Replace old files
+            await Task.Run(() =>
             {
-                // Replace old files
-                double count = 0;
                 foreach (string file in allFiles)
                 {
-                    count++;
                     try
                     {
                         File.Move(file, file + ".ryuold");
 
-                        Dispatcher.UIThread.InvokeAsync(() =>
+                        Application.Invoke(delegate
                         {
-                            taskDialog.SetProgressBarState(GetPercentage(count, allFiles.Count), TaskDialogProgressState.Normal);
+                            updateDialog.ProgressBar.Value++;
                         });
                     }
                     catch
                     {
-                        Logger.Warning?.Print(LogClass.Application, LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.UpdaterRenameFailed, file));
+                        Logger.Warning?.Print(LogClass.Application, "Updater was unable to rename file: " + file);
                     }
                 }
 
-                Dispatcher.UIThread.InvokeAsync(() =>
+                Application.Invoke(delegate
                 {
-                    taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterAddingFiles];
-                    taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
+                    updateDialog.MainText.Text = "Adding New Files...";
+                    updateDialog.ProgressBar.Value = 0;
+                    updateDialog.ProgressBar.MaxValue = Directory.GetFiles(_updatePublishDir, "*", SearchOption.AllDirectories).Length;
                 });
 
-                MoveAllFilesOver(_updatePublishDir, _homeDir, taskDialog);
+                MoveAllFilesOver(_updatePublishDir, _homeDir, updateDialog);
+            });
 
-                Directory.Delete(_updateDir, true);
-            }
+            Directory.Delete(_updateDir, true);
 
-            _updateSuccessful = true;
+            updateDialog.MainText.Text = "Update Complete!";
+            updateDialog.SecondaryText.Text = "Do you want to restart Ryujinx now?";
+            updateDialog.Modal = true;
 
-            taskDialog.Hide();
+            updateDialog.ProgressBar.Hide();
+            updateDialog.YesButton.Show();
+            updateDialog.NoButton.Show();
         }
 
         public static bool CanUpdate(bool showWarnings)
@@ -642,11 +517,7 @@ namespace Ryujinx.Modules
             {
                 if (showWarnings)
                 {
-                    Dispatcher.UIThread.InvokeAsync(() =>
-                        ContentDialogHelper.CreateWarningDialog(
-                            LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetMessage],
-                            LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetSubMessage])
-                    );
+                    GtkDialog.CreateWarningDialog("You are not connected to the Internet!", "Please verify that you have a working Internet connection!");
                 }
 
                 return false;
@@ -656,11 +527,7 @@ namespace Ryujinx.Modules
             {
                 if (showWarnings)
                 {
-                    Dispatcher.UIThread.InvokeAsync(() =>
-                        ContentDialogHelper.CreateWarningDialog(
-                            LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildMessage],
-                            LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage])
-                    );
+                    GtkDialog.CreateWarningDialog("You cannot update a Dirty build of Ryujinx!", "Please download Ryujinx at https://ryujinx.org/ if you are looking for a supported version.");
                 }
 
                 return false;
@@ -672,19 +539,11 @@ namespace Ryujinx.Modules
             {
                 if (ReleaseInformation.IsFlatHubBuild)
                 {
-                    Dispatcher.UIThread.InvokeAsync(() =>
-                        ContentDialogHelper.CreateWarningDialog(
-                            LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle],
-                            LocaleManager.Instance[LocaleKeys.DialogUpdaterFlatpakNotSupportedMessage])
-                    );
+                    GtkDialog.CreateWarningDialog("Updater Disabled!", "Please update Ryujinx via FlatHub.");
                 }
                 else
                 {
-                    Dispatcher.UIThread.InvokeAsync(() =>
-                        ContentDialogHelper.CreateWarningDialog(
-                            LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle],
-                            LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage])
-                    );
+                    GtkDialog.CreateWarningDialog("Updater Disabled!", "Please download Ryujinx at https://ryujinx.org/ if you are looking for a supported version.");
                 }
             }
 
@@ -698,7 +557,7 @@ namespace Ryujinx.Modules
             var files = Directory.EnumerateFiles(_homeDir); // All files directly in base dir.
 
             // Determine and exclude user files only when the updater is running, not when cleaning old files
-            if (_running && !OperatingSystem.IsMacOS())
+            if (Running)
             {
                 // Compare the loose files in base directory against the loose files from the incoming update, and store foreign ones in a user list.
                 var oldFiles = Directory.EnumerateFiles(_homeDir, "*", SearchOption.TopDirectoryOnly).Select(Path.GetFileName);
@@ -724,9 +583,8 @@ namespace Ryujinx.Modules
             return files.Where(f => !new FileInfo(f).Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System));
         }
 
-        private static void MoveAllFilesOver(string root, string dest, TaskDialog taskDialog)
+        private static void MoveAllFilesOver(string root, string dest, UpdateDialog dialog)
         {
-            int total = Directory.GetFiles(root, "*", SearchOption.AllDirectories).Length;
             foreach (string directory in Directory.GetDirectories(root))
             {
                 string dirName = Path.GetFileName(directory);
@@ -736,28 +594,28 @@ namespace Ryujinx.Modules
                     Directory.CreateDirectory(Path.Combine(dest, dirName));
                 }
 
-                MoveAllFilesOver(directory, Path.Combine(dest, dirName), taskDialog);
+                MoveAllFilesOver(directory, Path.Combine(dest, dirName), dialog);
             }
 
-            double count = 0;
             foreach (string file in Directory.GetFiles(root))
             {
-                count++;
-
                 File.Move(file, Path.Combine(dest, Path.GetFileName(file)), true);
 
-                Dispatcher.UIThread.InvokeAsync(() =>
+                Application.Invoke(delegate
                 {
-                    taskDialog.SetProgressBarState(GetPercentage(count, total), TaskDialogProgressState.Normal);
+                    dialog.ProgressBar.Value++;
                 });
             }
         }
 
         public static void CleanupUpdate()
         {
-            foreach (string file in Directory.GetFiles(_homeDir, "*.ryuold", SearchOption.AllDirectories))
+            foreach (string file in EnumerateFilesToDelete())
             {
-                File.Delete(file);
+                if (Path.GetExtension(file).EndsWith(".ryuold"))
+                {
+                    File.Delete(file);
+                }
             }
         }
     }

+ 378 - 0
src/Ryujinx.Gtk3/Program.cs

@@ -0,0 +1,378 @@
+using Gtk;
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.GraphicsDriver;
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.SystemInterop;
+using Ryujinx.Modules;
+using Ryujinx.SDL2.Common;
+using Ryujinx.UI;
+using Ryujinx.UI.Common;
+using Ryujinx.UI.Common.Configuration;
+using Ryujinx.UI.Common.Helper;
+using Ryujinx.UI.Common.SystemInfo;
+using Ryujinx.UI.Widgets;
+using SixLabors.ImageSharp.Formats.Jpeg;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+
+namespace Ryujinx
+{
+    partial class Program
+    {
+        public static double WindowScaleFactor { get; private set; }
+
+        public static string Version { get; private set; }
+
+        public static string ConfigurationPath { get; set; }
+
+        public static string CommandLineProfile { get; set; }
+
+        private const string X11LibraryName = "libX11";
+
+        [LibraryImport(X11LibraryName)]
+        private static partial int XInitThreads();
+
+        [LibraryImport("user32.dll", SetLastError = true)]
+        public static partial int MessageBoxA(IntPtr hWnd, [MarshalAs(UnmanagedType.LPStr)] string text, [MarshalAs(UnmanagedType.LPStr)] string caption, uint type);
+
+        [LibraryImport("libc", SetLastError = true)]
+        private static partial int setenv([MarshalAs(UnmanagedType.LPStr)] string name, [MarshalAs(UnmanagedType.LPStr)] string value, int overwrite);
+
+        private const uint MbIconWarning = 0x30;
+
+        static Program()
+        {
+            if (OperatingSystem.IsLinux())
+            {
+                NativeLibrary.SetDllImportResolver(typeof(Program).Assembly, (name, assembly, path) =>
+                {
+                    if (name != X11LibraryName)
+                    {
+                        return IntPtr.Zero;
+                    }
+
+                    if (!NativeLibrary.TryLoad("libX11.so.6", assembly, path, out IntPtr result))
+                    {
+                        if (!NativeLibrary.TryLoad("libX11.so", assembly, path, out result))
+                        {
+                            return IntPtr.Zero;
+                        }
+                    }
+
+                    return result;
+                });
+            }
+        }
+
+        static void Main(string[] args)
+        {
+            Version = ReleaseInformation.Version;
+
+            if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134))
+            {
+                MessageBoxA(IntPtr.Zero, "You are running an outdated version of Windows.\n\nStarting on June 1st 2022, Ryujinx will only support Windows 10 1803 and newer.\n", $"Ryujinx {Version}", MbIconWarning);
+            }
+
+            // Parse arguments
+            CommandLineState.ParseArguments(args);
+
+            // Hook unhandled exception and process exit events.
+            GLib.ExceptionManager.UnhandledException += (GLib.UnhandledExceptionArgs e) => ProcessUnhandledException(e.ExceptionObject as Exception, e.IsTerminating);
+            AppDomain.CurrentDomain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) => ProcessUnhandledException(e.ExceptionObject as Exception, e.IsTerminating);
+            AppDomain.CurrentDomain.ProcessExit += (object sender, EventArgs e) => Exit();
+
+            // Make process DPI aware for proper window sizing on high-res screens.
+            ForceDpiAware.Windows();
+            WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor();
+
+            // Delete backup files after updating.
+            Task.Run(Updater.CleanupUpdate);
+
+            Console.Title = $"Ryujinx Console {Version}";
+
+            // NOTE: GTK3 doesn't init X11 in a multi threaded way.
+            // This ends up causing race condition and abort of XCB when a context is created by SPB (even if SPB do call XInitThreads).
+            if (OperatingSystem.IsLinux())
+            {
+                if (XInitThreads() == 0)
+                {
+                    throw new NotSupportedException("Failed to initialize multi-threading support.");
+                }
+
+                Environment.SetEnvironmentVariable("GDK_BACKEND", "x11");
+                setenv("GDK_BACKEND", "x11", 1);
+            }
+
+            if (OperatingSystem.IsMacOS())
+            {
+                string baseDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);
+                string resourcesDataDir;
+
+                if (Path.GetFileName(baseDirectory) == "MacOS")
+                {
+                    resourcesDataDir = Path.Combine(Directory.GetParent(baseDirectory).FullName, "Resources");
+                }
+                else
+                {
+                    resourcesDataDir = baseDirectory;
+                }
+
+                static void SetEnvironmentVariableNoCaching(string key, string value)
+                {
+                    int res = setenv(key, value, 1);
+                    Debug.Assert(res != -1);
+                }
+
+                // On macOS, GTK3 needs XDG_DATA_DIRS to be set, otherwise it will try searching for "gschemas.compiled" in system directories.
+                SetEnvironmentVariableNoCaching("XDG_DATA_DIRS", Path.Combine(resourcesDataDir, "share"));
+
+                // On macOS, GTK3 needs GDK_PIXBUF_MODULE_FILE to be set, otherwise it will try searching for "loaders.cache" in system directories.
+                SetEnvironmentVariableNoCaching("GDK_PIXBUF_MODULE_FILE", Path.Combine(resourcesDataDir, "lib", "gdk-pixbuf-2.0", "2.10.0", "loaders.cache"));
+
+                SetEnvironmentVariableNoCaching("GTK_IM_MODULE_FILE", Path.Combine(resourcesDataDir, "lib", "gtk-3.0", "3.0.0", "immodules.cache"));
+            }
+
+            string systemPath = Environment.GetEnvironmentVariable("Path", EnvironmentVariableTarget.Machine);
+            Environment.SetEnvironmentVariable("Path", $"{Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin")};{systemPath}");
+
+            // Setup base data directory.
+            AppDataManager.Initialize(CommandLineState.BaseDirPathArg);
+
+            // Initialize the configuration.
+            ConfigurationState.Initialize();
+
+            // Initialize the logger system.
+            LoggerModule.Initialize();
+
+            // Initialize Discord integration.
+            DiscordIntegrationModule.Initialize();
+
+            // Initialize SDL2 driver
+            SDL2Driver.MainThreadDispatcher = action =>
+            {
+                Application.Invoke(delegate
+                {
+                    action();
+                });
+            };
+
+            // Sets ImageSharp Jpeg Encoder Quality.
+            SixLabors.ImageSharp.Configuration.Default.ImageFormatsManager.SetEncoder(JpegFormat.Instance, new JpegEncoder()
+            {
+                Quality = 100,
+            });
+
+            string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName);
+            string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName);
+
+            // Now load the configuration as the other subsystems are now registered
+            ConfigurationPath = File.Exists(localConfigurationPath)
+                ? localConfigurationPath
+                : File.Exists(appDataConfigurationPath)
+                    ? appDataConfigurationPath
+                    : null;
+
+            if (ConfigurationPath == null)
+            {
+                // No configuration, we load the default values and save it to disk
+                ConfigurationPath = appDataConfigurationPath;
+
+                ConfigurationState.Instance.LoadDefault();
+                ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
+            }
+            else
+            {
+                if (ConfigurationFileFormat.TryLoad(ConfigurationPath, out ConfigurationFileFormat configurationFileFormat))
+                {
+                    ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath);
+                }
+                else
+                {
+                    ConfigurationState.Instance.LoadDefault();
+
+                    Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location {ConfigurationPath}");
+                }
+            }
+
+            // Check if graphics backend was overridden.
+            if (CommandLineState.OverrideGraphicsBackend != null)
+            {
+                if (CommandLineState.OverrideGraphicsBackend.ToLower() == "opengl")
+                {
+                    ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.OpenGl;
+                }
+                else if (CommandLineState.OverrideGraphicsBackend.ToLower() == "vulkan")
+                {
+                    ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.Vulkan;
+                }
+            }
+
+            // Check if HideCursor was overridden.
+            if (CommandLineState.OverrideHideCursor is not null)
+            {
+                ConfigurationState.Instance.HideCursor.Value = CommandLineState.OverrideHideCursor!.ToLower() switch
+                {
+                    "never" => HideCursorMode.Never,
+                    "onidle" => HideCursorMode.OnIdle,
+                    "always" => HideCursorMode.Always,
+                    _ => ConfigurationState.Instance.HideCursor.Value,
+                };
+            }
+
+            // Check if docked mode was overridden.
+            if (CommandLineState.OverrideDockedMode.HasValue)
+            {
+                ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value;
+            }
+
+            // Logging system information.
+            PrintSystemInfo();
+
+            // Enable OGL multithreading on the driver, when available.
+            BackendThreading threadingMode = ConfigurationState.Instance.Graphics.BackendThreading;
+            DriverUtilities.ToggleOGLThreading(threadingMode == BackendThreading.Off);
+
+            // Initialize Gtk.
+            Application.Init();
+
+            // Check if keys exists.
+            bool hasSystemProdKeys = File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys"));
+            bool hasCommonProdKeys = AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && File.Exists(Path.Combine(AppDataManager.KeysDirPathUser, "prod.keys"));
+            if (!hasSystemProdKeys && !hasCommonProdKeys)
+            {
+                UserErrorDialog.CreateUserErrorDialog(UserError.NoKeys);
+            }
+
+            // Show the main window UI.
+            MainWindow mainWindow = new();
+            mainWindow.Show();
+
+            if (OperatingSystem.IsLinux())
+            {
+                int currentVmMaxMapCount = LinuxHelper.VmMaxMapCount;
+
+                if (LinuxHelper.VmMaxMapCount < LinuxHelper.RecommendedVmMaxMapCount)
+                {
+                    Logger.Warning?.Print(LogClass.Application, $"The value of vm.max_map_count is lower than {LinuxHelper.RecommendedVmMaxMapCount}. ({currentVmMaxMapCount})");
+
+                    if (LinuxHelper.PkExecPath is not null)
+                    {
+                        var buttonTexts = new Dictionary<int, string>()
+                        {
+                            { 0, "Yes, until the next restart" },
+                            { 1, "Yes, permanently" },
+                            { 2, "No" },
+                        };
+
+                        ResponseType response = GtkDialog.CreateCustomDialog(
+                            "Ryujinx - Low limit for memory mappings detected",
+                            $"Would you like to increase the value of vm.max_map_count to {LinuxHelper.RecommendedVmMaxMapCount}?",
+                            "Some games might try to create more memory mappings than currently allowed. " +
+                            "Ryujinx will crash as soon as this limit gets exceeded.",
+                            buttonTexts,
+                            MessageType.Question);
+
+                        int rc;
+
+                        switch ((int)response)
+                        {
+                            case 0:
+                                rc = LinuxHelper.RunPkExec($"echo {LinuxHelper.RecommendedVmMaxMapCount} > {LinuxHelper.VmMaxMapCountPath}");
+                                if (rc == 0)
+                                {
+                                    Logger.Info?.Print(LogClass.Application, $"vm.max_map_count set to {LinuxHelper.VmMaxMapCount} until the next restart.");
+                                }
+                                else
+                                {
+                                    Logger.Error?.Print(LogClass.Application, $"Unable to change vm.max_map_count. Process exited with code: {rc}");
+                                }
+                                break;
+                            case 1:
+                                rc = LinuxHelper.RunPkExec($"echo \"vm.max_map_count = {LinuxHelper.RecommendedVmMaxMapCount}\" > {LinuxHelper.SysCtlConfigPath} && sysctl -p {LinuxHelper.SysCtlConfigPath}");
+                                if (rc == 0)
+                                {
+                                    Logger.Info?.Print(LogClass.Application, $"vm.max_map_count set to {LinuxHelper.VmMaxMapCount}. Written to config: {LinuxHelper.SysCtlConfigPath}");
+                                }
+                                else
+                                {
+                                    Logger.Error?.Print(LogClass.Application, $"Unable to write new value for vm.max_map_count to config. Process exited with code: {rc}");
+                                }
+                                break;
+                        }
+                    }
+                    else
+                    {
+                        GtkDialog.CreateWarningDialog(
+                            "Max amount of memory mappings is lower than recommended.",
+                            $"The current value of vm.max_map_count ({currentVmMaxMapCount}) is lower than {LinuxHelper.RecommendedVmMaxMapCount}." +
+                            "Some games might try to create more memory mappings than currently allowed. " +
+                            "Ryujinx will crash as soon as this limit gets exceeded.\n\n" +
+                            "You might want to either manually increase the limit or install pkexec, which allows Ryujinx to assist with that.");
+                    }
+                }
+            }
+
+            if (CommandLineState.LaunchPathArg != null)
+            {
+                mainWindow.RunApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg);
+            }
+
+            if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false))
+            {
+                Updater.BeginParse(mainWindow, false).ContinueWith(task =>
+                {
+                    Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}");
+                }, TaskContinuationOptions.OnlyOnFaulted);
+            }
+
+            Application.Run();
+        }
+
+        private static void PrintSystemInfo()
+        {
+            Logger.Notice.Print(LogClass.Application, $"Ryujinx Version: {Version}");
+            SystemInfo.Gather().Print();
+
+            var enabledLogs = Logger.GetEnabledLevels();
+            Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {(enabledLogs.Count == 0 ? "<None>" : string.Join(", ", enabledLogs))}");
+
+            if (AppDataManager.Mode == AppDataManager.LaunchMode.Custom)
+            {
+                Logger.Notice.Print(LogClass.Application, $"Launch Mode: Custom Path {AppDataManager.BaseDirPath}");
+            }
+            else
+            {
+                Logger.Notice.Print(LogClass.Application, $"Launch Mode: {AppDataManager.Mode}");
+            }
+        }
+
+        private static void ProcessUnhandledException(Exception ex, bool isTerminating)
+        {
+            string message = $"Unhandled exception caught: {ex}";
+
+            Logger.Error?.PrintMsg(LogClass.Application, message);
+
+            if (Logger.Error == null)
+            {
+                Logger.Notice.PrintMsg(LogClass.Application, message);
+            }
+
+            if (isTerminating)
+            {
+                Exit();
+            }
+        }
+
+        public static void Exit()
+        {
+            DiscordIntegrationModule.Exit();
+
+            Logger.Shutdown();
+        }
+    }
+}

+ 104 - 0
src/Ryujinx.Gtk3/Ryujinx.Gtk3.csproj

@@ -0,0 +1,104 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net8.0</TargetFramework>
+    <RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
+    <OutputType>Exe</OutputType>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <Version>1.0.0-dirty</Version>
+    <DefineConstants Condition=" '$(ExtraDefineConstants)' != '' ">$(DefineConstants);$(ExtraDefineConstants)</DefineConstants>
+    <!-- As we already provide GTK3 on Windows via GtkSharp.Dependencies this is redundant. -->
+    <SkipGtkInstall>true</SkipGtkInstall>
+    <TieredPGO>true</TieredPGO>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(RuntimeIdentifier)' != ''">
+    <PublishSingleFile>true</PublishSingleFile>
+    <TrimmerSingleWarn>false</TrimmerSingleWarn>
+    <PublishTrimmed>true</PublishTrimmed>
+    <TrimMode>partial</TrimMode>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Ryujinx.GtkSharp" />
+    <PackageReference Include="GtkSharp.Dependencies" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64' AND '$(RuntimeIdentifier)' != 'osx-arm64'" />
+    <PackageReference Include="GtkSharp.Dependencies.osx" Condition="'$(RuntimeIdentifier)' == 'osx-x64' OR '$(RuntimeIdentifier)' == 'osx-arm64'" />
+    <PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies" />
+    <PackageReference Include="Ryujinx.Audio.OpenAL.Dependencies" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64' AND '$(RuntimeIdentifier)' != 'osx-arm64'" />
+    <PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'win-x64'" />
+    <PackageReference Include="OpenTK.Core" />
+    <PackageReference Include="OpenTK.Graphics" />
+    <PackageReference Include="SPB" />
+    <PackageReference Include="SharpZipLib" />
+    <PackageReference Include="SixLabors.ImageSharp" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
+    <ProjectReference Include="..\Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj" />
+    <ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />
+    <ProjectReference Include="..\Ryujinx.Audio.Backends.SDL2\Ryujinx.Audio.Backends.SDL2.csproj" />
+    <ProjectReference Include="..\Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj" />
+    <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
+    <ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
+    <ProjectReference Include="..\ARMeilleure\ARMeilleure.csproj" />
+    <ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
+    <ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
+    <ProjectReference Include="..\Ryujinx.Graphics.Gpu\Ryujinx.Graphics.Gpu.csproj" />
+    <ProjectReference Include="..\Ryujinx.UI.Common\Ryujinx.UI.Common.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Content Include="..\..\distribution\windows\alsoft.ini" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64'">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+      <TargetPath>alsoft.ini</TargetPath>
+    </Content>
+    <Content Include="..\..\distribution\legal\THIRDPARTY.md">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+      <TargetPath>THIRDPARTY.md</TargetPath>
+    </Content>
+    <Content Include="..\..\LICENSE.txt">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+      <TargetPath>LICENSE.txt</TargetPath>
+    </Content>
+  </ItemGroup>
+
+  <ItemGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64' OR '$(RuntimeIdentifier)' == 'linux-arm64'">
+    <Content Include="..\..\distribution\linux\Ryujinx.sh">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </Content>
+    <Content Include="..\..\distribution\linux\mime\Ryujinx.xml">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+      <TargetPath>mime\Ryujinx.xml</TargetPath>
+    </Content>
+  </ItemGroup>
+
+  <!-- Due to .net core 3.1 embedded resource loading -->
+  <PropertyGroup>
+    <EmbeddedResourceUseDependentUponConvention>false</EmbeddedResourceUseDependentUponConvention>
+    <ApplicationIcon>Ryujinx.ico</ApplicationIcon>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <None Remove="UI\MainWindow.glade" />
+    <None Remove="UI\Widgets\ProfileDialog.glade" />
+    <None Remove="UI\Windows\CheatWindow.glade" />
+    <None Remove="UI\Windows\ControllerWindow.glade" />
+    <None Remove="UI\Windows\DlcWindow.glade" />
+    <None Remove="UI\Windows\SettingsWindow.glade" />
+    <None Remove="UI\Windows\TitleUpdateWindow.glade" />
+    <None Remove="Modules\Updater\UpdateDialog.glade" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <EmbeddedResource Include="UI\MainWindow.glade" />
+    <EmbeddedResource Include="UI\Widgets\ProfileDialog.glade" />
+    <EmbeddedResource Include="UI\Windows\CheatWindow.glade" />
+    <EmbeddedResource Include="UI\Windows\ControllerWindow.glade" />
+    <EmbeddedResource Include="UI\Windows\DlcWindow.glade" />
+    <EmbeddedResource Include="UI\Windows\SettingsWindow.glade" />
+    <EmbeddedResource Include="UI\Windows\TitleUpdateWindow.glade" />
+    <EmbeddedResource Include="Modules\Updater\UpdateDialog.glade" />
+  </ItemGroup>
+
+</Project>

+ 0 - 0
src/Ryujinx.Ava/Ryujinx.ico → src/Ryujinx.Gtk3/Ryujinx.ico


+ 1 - 1
src/Ryujinx/UI/Applet/ErrorAppletDialog.cs → src/Ryujinx.Gtk3/UI/Applet/ErrorAppletDialog.cs

@@ -8,7 +8,7 @@ namespace Ryujinx.UI.Applet
     {
         public ErrorAppletDialog(Window parentWindow, DialogFlags dialogFlags, MessageType messageType, string[] buttons) : base(parentWindow, dialogFlags, messageType, ButtonsType.None, null)
         {
-            Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_Ryujinx.png");
+            Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Gtk3.UI.Common.Resources.Logo_Ryujinx.png");
 
             int responseId = 0;
 

+ 0 - 0
src/Ryujinx/UI/Applet/GtkDynamicTextInputHandler.cs → src/Ryujinx.Gtk3/UI/Applet/GtkDynamicTextInputHandler.cs


+ 0 - 0
src/Ryujinx/UI/Applet/GtkHostUIHandler.cs → src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs


+ 0 - 0
src/Ryujinx/UI/Applet/GtkHostUITheme.cs → src/Ryujinx.Gtk3/UI/Applet/GtkHostUITheme.cs


+ 0 - 0
src/Ryujinx/UI/Applet/SwkbdAppletDialog.cs → src/Ryujinx.Gtk3/UI/Applet/SwkbdAppletDialog.cs


+ 0 - 0
src/Ryujinx/UI/Helper/MetalHelper.cs → src/Ryujinx.Gtk3/UI/Helper/MetalHelper.cs


+ 0 - 0
src/Ryujinx/UI/Helper/SortHelper.cs → src/Ryujinx.Gtk3/UI/Helper/SortHelper.cs


+ 0 - 0
src/Ryujinx/UI/Helper/ThemeHelper.cs → src/Ryujinx.Gtk3/UI/Helper/ThemeHelper.cs


+ 1 - 1
src/Ryujinx/UI/MainWindow.cs → src/Ryujinx.Gtk3/UI/MainWindow.cs

@@ -143,7 +143,7 @@ namespace Ryujinx.UI
 
 #pragma warning restore CS0649, IDE0044, CS0169, IDE0051
 
-        public MainWindow() : this(new Builder("Ryujinx.UI.MainWindow.glade")) { }
+        public MainWindow() : this(new Builder("Ryujinx.Gtk3.UI.MainWindow.glade")) { }
 
         private MainWindow(Builder builder) : base(builder.GetRawOwnedObject("_mainWin"))
         {

+ 0 - 0
src/Ryujinx/UI/MainWindow.glade → src/Ryujinx.Gtk3/UI/MainWindow.glade


+ 0 - 0
src/Ryujinx/UI/OpenGLRenderer.cs → src/Ryujinx.Gtk3/UI/OpenGLRenderer.cs


+ 0 - 0
src/Ryujinx/UI/OpenToolkitBindingsContext.cs → src/Ryujinx.Gtk3/UI/OpenToolkitBindingsContext.cs


+ 0 - 0
src/Ryujinx/UI/RendererWidgetBase.cs → src/Ryujinx.Gtk3/UI/RendererWidgetBase.cs


+ 0 - 0
src/Ryujinx/UI/SPBOpenGLContext.cs → src/Ryujinx.Gtk3/UI/SPBOpenGLContext.cs


+ 0 - 0
src/Ryujinx/UI/StatusUpdatedEventArgs.cs → src/Ryujinx.Gtk3/UI/StatusUpdatedEventArgs.cs


+ 0 - 0
src/Ryujinx/UI/VulkanRenderer.cs → src/Ryujinx.Gtk3/UI/VulkanRenderer.cs


+ 0 - 0
src/Ryujinx/UI/Widgets/GameTableContextMenu.Designer.cs → src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.Designer.cs


+ 1 - 1
src/Ryujinx/UI/Widgets/GameTableContextMenu.cs → src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.cs

@@ -189,7 +189,7 @@ namespace Ryujinx.UI.Widgets
                         _dialog = new MessageDialog(null, DialogFlags.DestroyWithParent, MessageType.Info, ButtonsType.Cancel, null)
                         {
                             Title = "Ryujinx - NCA Section Extractor",
-                            Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_Ryujinx.png"),
+                            Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Gtk3.UI.Common.Resources.Logo_Ryujinx.png"),
                             SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_titleFilePath)}...",
                             WindowPosition = WindowPosition.Center,
                         };

+ 0 - 0
src/Ryujinx/UI/Widgets/GtkDialog.cs → src/Ryujinx.Gtk3/UI/Widgets/GtkDialog.cs


+ 0 - 0
src/Ryujinx/UI/Widgets/GtkInputDialog.cs → src/Ryujinx.Gtk3/UI/Widgets/GtkInputDialog.cs


+ 1 - 1
src/Ryujinx/UI/Widgets/ProfileDialog.cs → src/Ryujinx.Gtk3/UI/Widgets/ProfileDialog.cs

@@ -15,7 +15,7 @@ namespace Ryujinx.UI.Widgets
         [GUI] Label _errorMessage;
 #pragma warning restore CS0649, IDE0044
 
-        public ProfileDialog() : this(new Builder("Ryujinx.UI.Widgets.ProfileDialog.glade")) { }
+        public ProfileDialog() : this(new Builder("Ryujinx.Gtk3.UI.Widgets.ProfileDialog.glade")) { }
 
         private ProfileDialog(Builder builder) : base(builder.GetRawOwnedObject("_profileDialog"))
         {

+ 0 - 0
src/Ryujinx/UI/Widgets/ProfileDialog.glade → src/Ryujinx.Gtk3/UI/Widgets/ProfileDialog.glade


+ 0 - 0
src/Ryujinx/UI/Widgets/RawInputToTextEntry.cs → src/Ryujinx.Gtk3/UI/Widgets/RawInputToTextEntry.cs


+ 0 - 0
src/Ryujinx/UI/Widgets/UserErrorDialog.cs → src/Ryujinx.Gtk3/UI/Widgets/UserErrorDialog.cs


+ 0 - 0
src/Ryujinx/UI/Windows/AboutWindow.Designer.cs → src/Ryujinx.Gtk3/UI/Windows/AboutWindow.Designer.cs


+ 0 - 0
src/Ryujinx/UI/Windows/AboutWindow.cs → src/Ryujinx.Gtk3/UI/Windows/AboutWindow.cs


+ 0 - 0
src/Ryujinx/UI/Windows/AmiiboWindow.Designer.cs → src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.Designer.cs


+ 0 - 0
src/Ryujinx/UI/Windows/AmiiboWindow.cs → src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.cs


+ 0 - 0
src/Ryujinx/UI/Windows/AvatarWindow.cs → src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs


+ 1 - 1
src/Ryujinx/UI/Windows/CheatWindow.cs → src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs

@@ -22,7 +22,7 @@ namespace Ryujinx.UI.Windows
         [GUI] Button _saveButton;
 #pragma warning restore CS0649, IDE0044
 
-        public CheatWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : this(new Builder("Ryujinx.UI.Windows.CheatWindow.glade"), virtualFileSystem, titleId, titleName, titlePath) { }
+        public CheatWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : this(new Builder("Ryujinx.Gtk3.UI.Windows.CheatWindow.glade"), virtualFileSystem, titleId, titleName, titlePath) { }
 
         private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : base(builder.GetRawOwnedObject("_cheatWindow"))
         {

+ 0 - 0
src/Ryujinx/UI/Windows/CheatWindow.glade → src/Ryujinx.Gtk3/UI/Windows/CheatWindow.glade


+ 1 - 1
src/Ryujinx/UI/Windows/ControllerWindow.cs → src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.cs

@@ -117,7 +117,7 @@ namespace Ryujinx.UI.Windows
 
         private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
 
-        public ControllerWindow(MainWindow mainWindow, PlayerIndex controllerId) : this(mainWindow, new Builder("Ryujinx.UI.Windows.ControllerWindow.glade"), controllerId) { }
+        public ControllerWindow(MainWindow mainWindow, PlayerIndex controllerId) : this(mainWindow, new Builder("Ryujinx.Gtk3.UI.Windows.ControllerWindow.glade"), controllerId) { }
 
         private ControllerWindow(MainWindow mainWindow, Builder builder, PlayerIndex controllerId) : base(builder.GetRawOwnedObject("_controllerWin"))
         {

+ 0 - 0
src/Ryujinx/UI/Windows/ControllerWindow.glade → src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.glade


+ 1 - 1
src/Ryujinx/UI/Windows/DlcWindow.cs → src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs

@@ -32,7 +32,7 @@ namespace Ryujinx.UI.Windows
         [GUI] TreeSelection _dlcTreeSelection;
 #pragma warning restore CS0649, IDE0044
 
-        public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.UI.Windows.DlcWindow.glade"), virtualFileSystem, titleId, titleName) { }
+        public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Gtk3.UI.Windows.DlcWindow.glade"), virtualFileSystem, titleId, titleName) { }
 
         private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_dlcWindow"))
         {

+ 0 - 0
src/Ryujinx/UI/Windows/DlcWindow.glade → src/Ryujinx.Gtk3/UI/Windows/DlcWindow.glade


+ 1 - 1
src/Ryujinx/UI/Windows/SettingsWindow.cs → src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.cs

@@ -120,7 +120,7 @@ namespace Ryujinx.UI.Windows
 
 #pragma warning restore CS0649, IDE0044
 
-        public SettingsWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this(parent, new Builder("Ryujinx.UI.Windows.SettingsWindow.glade"), virtualFileSystem, contentManager) { }
+        public SettingsWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this(parent, new Builder("Ryujinx.Gtk3.UI.Windows.SettingsWindow.glade"), virtualFileSystem, contentManager) { }
 
         private SettingsWindow(MainWindow parent, Builder builder, VirtualFileSystem virtualFileSystem, ContentManager contentManager) : base(builder.GetRawOwnedObject("_settingsWin"))
         {

+ 0 - 0
src/Ryujinx/UI/Windows/SettingsWindow.glade → src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.glade


+ 1 - 1
src/Ryujinx/UI/Windows/TitleUpdateWindow.cs → src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs

@@ -38,7 +38,7 @@ namespace Ryujinx.UI.Windows
         [GUI] RadioButton _noUpdateRadioButton;
 #pragma warning restore CS0649, IDE0044
 
-        public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.UI.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { }
+        public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Gtk3.UI.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { }
 
         private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_titleUpdateWindow"))
         {

+ 0 - 0
src/Ryujinx/UI/Windows/TitleUpdateWindow.glade → src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.glade


+ 0 - 0
src/Ryujinx/UI/Windows/UserProfilesManagerWindow.Designer.cs → src/Ryujinx.Gtk3/UI/Windows/UserProfilesManagerWindow.Designer.cs


+ 0 - 0
src/Ryujinx/UI/Windows/UserProfilesManagerWindow.cs → src/Ryujinx.Gtk3/UI/Windows/UserProfilesManagerWindow.cs


+ 0 - 0
src/Ryujinx.Ava/App.axaml → src/Ryujinx/App.axaml


+ 0 - 0
src/Ryujinx.Ava/App.axaml.cs → src/Ryujinx/App.axaml.cs


+ 0 - 0
src/Ryujinx.Ava/AppHost.cs → src/Ryujinx/AppHost.cs


+ 0 - 0
src/Ryujinx.Ava/Assets/Fonts/SegoeFluentIcons.ttf → src/Ryujinx/Assets/Fonts/SegoeFluentIcons.ttf


+ 0 - 0
src/Ryujinx.Ava/Assets/Icons/Controller_JoyConLeft.svg → src/Ryujinx/Assets/Icons/Controller_JoyConLeft.svg


+ 0 - 0
src/Ryujinx.Ava/Assets/Icons/Controller_JoyConPair.svg → src/Ryujinx/Assets/Icons/Controller_JoyConPair.svg


+ 0 - 0
src/Ryujinx.Ava/Assets/Icons/Controller_JoyConRight.svg → src/Ryujinx/Assets/Icons/Controller_JoyConRight.svg


+ 0 - 0
src/Ryujinx.Ava/Assets/Icons/Controller_ProCon.svg → src/Ryujinx/Assets/Icons/Controller_ProCon.svg


+ 0 - 0
src/Ryujinx.Ava/Assets/Locales/de_DE.json → src/Ryujinx/Assets/Locales/de_DE.json


+ 0 - 0
src/Ryujinx.Ava/Assets/Locales/el_GR.json → src/Ryujinx/Assets/Locales/el_GR.json


+ 0 - 0
src/Ryujinx.Ava/Assets/Locales/en_US.json → src/Ryujinx/Assets/Locales/en_US.json


+ 0 - 0
src/Ryujinx.Ava/Assets/Locales/es_ES.json → src/Ryujinx/Assets/Locales/es_ES.json


+ 0 - 0
src/Ryujinx.Ava/Assets/Locales/fr_FR.json → src/Ryujinx/Assets/Locales/fr_FR.json


+ 0 - 0
src/Ryujinx.Ava/Assets/Locales/he_IL.json → src/Ryujinx/Assets/Locales/he_IL.json


+ 0 - 0
src/Ryujinx.Ava/Assets/Locales/it_IT.json → src/Ryujinx/Assets/Locales/it_IT.json


+ 0 - 0
src/Ryujinx.Ava/Assets/Locales/ja_JP.json → src/Ryujinx/Assets/Locales/ja_JP.json


+ 0 - 0
src/Ryujinx.Ava/Assets/Locales/ko_KR.json → src/Ryujinx/Assets/Locales/ko_KR.json


+ 0 - 0
src/Ryujinx.Ava/Assets/Locales/pl_PL.json → src/Ryujinx/Assets/Locales/pl_PL.json


+ 0 - 0
src/Ryujinx.Ava/Assets/Locales/pt_BR.json → src/Ryujinx/Assets/Locales/pt_BR.json


+ 0 - 0
src/Ryujinx.Ava/Assets/Locales/ru_RU.json → src/Ryujinx/Assets/Locales/ru_RU.json


+ 0 - 0
src/Ryujinx.Ava/Assets/Locales/tr_TR.json → src/Ryujinx/Assets/Locales/tr_TR.json


+ 0 - 0
src/Ryujinx.Ava/Assets/Locales/uk_UA.json → src/Ryujinx/Assets/Locales/uk_UA.json


+ 0 - 0
src/Ryujinx.Ava/Assets/Locales/zh_CN.json → src/Ryujinx/Assets/Locales/zh_CN.json


+ 0 - 0
src/Ryujinx.Ava/Assets/Locales/zh_TW.json → src/Ryujinx/Assets/Locales/zh_TW.json


+ 0 - 0
src/Ryujinx.Ava/Assets/Styles/Styles.xaml → src/Ryujinx/Assets/Styles/Styles.xaml


+ 0 - 0
src/Ryujinx.Ava/Assets/Styles/Themes.xaml → src/Ryujinx/Assets/Styles/Themes.xaml


+ 0 - 0
src/Ryujinx.Ava/Common/ApplicationHelper.cs → src/Ryujinx/Common/ApplicationHelper.cs


+ 0 - 0
src/Ryujinx.Ava/Common/ApplicationSort.cs → src/Ryujinx/Common/ApplicationSort.cs


+ 0 - 0
src/Ryujinx.Ava/Common/KeyboardHotkeyState.cs → src/Ryujinx/Common/KeyboardHotkeyState.cs


+ 0 - 0
src/Ryujinx.Ava/Common/Locale/LocaleExtension.cs → src/Ryujinx/Common/Locale/LocaleExtension.cs


+ 1 - 1
src/Ryujinx.Ava/Common/Locale/LocaleManager.cs → src/Ryujinx/Common/Locale/LocaleManager.cs

@@ -143,7 +143,7 @@ namespace Ryujinx.Ava.Common.Locale
         private static Dictionary<LocaleKeys, string> LoadJsonLanguage(string languageCode = DefaultLanguageCode)
         {
             var localeStrings = new Dictionary<LocaleKeys, string>();
-            string languageJson = EmbeddedResources.ReadAllText($"Ryujinx.Ava/Assets/Locales/{languageCode}.json");
+            string languageJson = EmbeddedResources.ReadAllText($"Ryujinx/Assets/Locales/{languageCode}.json");
             var strings = JsonHelper.Deserialize(languageJson, CommonJsonContext.Default.StringDictionary);
 
             foreach (var item in strings)

+ 0 - 0
src/Ryujinx.Ava/Input/AvaloniaKeyboard.cs → src/Ryujinx/Input/AvaloniaKeyboard.cs


+ 0 - 0
src/Ryujinx.Ava/Input/AvaloniaKeyboardDriver.cs → src/Ryujinx/Input/AvaloniaKeyboardDriver.cs


+ 0 - 0
src/Ryujinx.Ava/Input/AvaloniaKeyboardMappingHelper.cs → src/Ryujinx/Input/AvaloniaKeyboardMappingHelper.cs


+ 0 - 0
src/Ryujinx.Ava/Input/AvaloniaMouse.cs → src/Ryujinx/Input/AvaloniaMouse.cs


+ 0 - 0
src/Ryujinx.Ava/Input/AvaloniaMouseDriver.cs → src/Ryujinx/Input/AvaloniaMouseDriver.cs


+ 311 - 169
src/Ryujinx/Modules/Updater/Updater.cs

@@ -1,93 +1,75 @@
-using Gtk;
+using Avalonia.Controls;
+using Avalonia.Threading;
+using FluentAvalonia.UI.Controls;
 using ICSharpCode.SharpZipLib.GZip;
 using ICSharpCode.SharpZipLib.Tar;
 using ICSharpCode.SharpZipLib.Zip;
+using Ryujinx.Ava;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.UI.Helpers;
 using Ryujinx.Common;
 using Ryujinx.Common.Logging;
 using Ryujinx.Common.Utilities;
-using Ryujinx.UI;
+using Ryujinx.UI.Common.Helper;
 using Ryujinx.UI.Common.Models.Github;
-using Ryujinx.UI.Widgets;
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Net;
 using System.Net.Http;
 using System.Net.NetworkInformation;
+using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 
 namespace Ryujinx.Modules
 {
-    public static class Updater
+    internal static class Updater
     {
         private const string GitHubApiUrl = "https://api.github.com";
-        private const int ConnectionCount = 4;
-
-        internal static bool Running;
+        private static readonly GithubReleasesJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
 
         private static readonly string _homeDir = AppDomain.CurrentDomain.BaseDirectory;
         private static readonly string _updateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update");
         private static readonly string _updatePublishDir = Path.Combine(_updateDir, "publish");
+        private const int ConnectionCount = 4;
 
         private static string _buildVer;
         private static string _platformExt;
         private static string _buildUrl;
         private static long _buildSize;
+        private static bool _updateSuccessful;
+        private static bool _running;
 
-        private static readonly GithubReleasesJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
-
-        // On Windows, GtkSharp.Dependencies adds these extra dirs that must be cleaned during updates.
-        private static readonly string[] _windowsDependencyDirs = { "bin", "etc", "lib", "share" };
+        private static readonly string[] _windowsDependencyDirs = Array.Empty<string>();
 
-        private static HttpClient ConstructHttpClient()
+        public static async Task BeginParse(Window mainWindow, bool showVersionUpToDate)
         {
-            HttpClient result = new();
-
-            // Required by GitHub to interact with APIs.
-            result.DefaultRequestHeaders.Add("User-Agent", "Ryujinx-Updater/1.0.0");
-
-            return result;
-        }
-
-        public static async Task BeginParse(MainWindow mainWindow, bool showVersionUpToDate)
-        {
-            if (Running)
+            if (_running)
             {
                 return;
             }
 
-            Running = true;
-            mainWindow.UpdateMenuItem.Sensitive = false;
-
-            int artifactIndex = -1;
+            _running = true;
 
             // Detect current platform
             if (OperatingSystem.IsMacOS())
             {
-                _platformExt = "osx_x64.zip";
-                artifactIndex = 1;
+                _platformExt = "macos_universal.app.tar.gz";
             }
             else if (OperatingSystem.IsWindows())
             {
                 _platformExt = "win_x64.zip";
-                artifactIndex = 2;
             }
             else if (OperatingSystem.IsLinux())
             {
                 var arch = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "arm64" : "x64";
                 _platformExt = $"linux_{arch}.tar.gz";
-                artifactIndex = 0;
-            }
-
-            if (artifactIndex == -1)
-            {
-                GtkDialog.CreateErrorDialog("Your platform is not supported!");
-
-                return;
             }
 
             Version newVersion;
@@ -99,9 +81,14 @@ namespace Ryujinx.Modules
             }
             catch
             {
-                GtkDialog.CreateWarningDialog("Failed to convert the current Ryujinx version.", "Cancelling Update!");
                 Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!");
 
+                await ContentDialogHelper.CreateWarningDialog(
+                    LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage],
+                    LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
+
+                _running = false;
+
                 return;
             }
 
@@ -109,9 +96,8 @@ namespace Ryujinx.Modules
             try
             {
                 using HttpClient jsonClient = ConstructHttpClient();
-                string buildInfoUrl = $"{GitHubApiUrl}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest";
 
-                // Fetch latest build information
+                string buildInfoUrl = $"{GitHubApiUrl}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest";
                 string fetchedJson = await jsonClient.GetStringAsync(buildInfoUrl);
                 var fetched = JsonHelper.Deserialize(fetchedJson, _serializerContext.GithubReleasesJsonResponse);
                 _buildVer = fetched.Name;
@@ -126,9 +112,13 @@ namespace Ryujinx.Modules
                         {
                             if (showVersionUpToDate)
                             {
-                                GtkDialog.CreateUpdaterInfoDialog("You are already using the latest version of Ryujinx!", "");
+                                await ContentDialogHelper.CreateUpdaterInfoDialog(
+                                    LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
+                                    "");
                             }
 
+                            _running = false;
+
                             return;
                         }
 
@@ -136,20 +126,29 @@ namespace Ryujinx.Modules
                     }
                 }
 
-                if (_buildUrl == null)
+                // If build not done, assume no new update are available.
+                if (_buildUrl is null)
                 {
                     if (showVersionUpToDate)
                     {
-                        GtkDialog.CreateUpdaterInfoDialog("You are already using the latest version of Ryujinx!", "");
+                        await ContentDialogHelper.CreateUpdaterInfoDialog(
+                            LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
+                            "");
                     }
 
+                    _running = false;
+
                     return;
                 }
             }
             catch (Exception exception)
             {
                 Logger.Error?.Print(LogClass.Application, exception.Message);
-                GtkDialog.CreateErrorDialog("An error occurred when trying to get release information from GitHub Release. This can be caused if a new release is being compiled by GitHub Actions. Try again in a few minutes.");
+
+                await ContentDialogHelper.CreateErrorDialog(
+                    LocaleManager.Instance[LocaleKeys.DialogUpdaterFailedToGetVersionMessage]);
+
+                _running = false;
 
                 return;
             }
@@ -160,8 +159,13 @@ namespace Ryujinx.Modules
             }
             catch
             {
-                GtkDialog.CreateWarningDialog("Failed to convert the received Ryujinx version from GitHub Release.", "Cancelling Update!");
-                Logger.Error?.Print(LogClass.Application, "Failed to convert the received Ryujinx version from GitHub Release!");
+                Logger.Error?.Print(LogClass.Application, "Failed to convert the received Ryujinx version from Github!");
+
+                await ContentDialogHelper.CreateWarningDialog(
+                    LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage],
+                    LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
+
+                _running = false;
 
                 return;
             }
@@ -170,11 +174,12 @@ namespace Ryujinx.Modules
             {
                 if (showVersionUpToDate)
                 {
-                    GtkDialog.CreateUpdaterInfoDialog("You are already using the latest version of Ryujinx!", "");
+                    await ContentDialogHelper.CreateUpdaterInfoDialog(
+                        LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
+                        "");
                 }
 
-                Running = false;
-                mainWindow.UpdateMenuItem.Sensitive = true;
+                _running = false;
 
                 return;
             }
@@ -197,13 +202,39 @@ namespace Ryujinx.Modules
                 _buildSize = -1;
             }
 
-            // Show a message asking the user if they want to update
-            UpdateDialog updateDialog = new(mainWindow, newVersion, _buildUrl);
-            updateDialog.Show();
+            await Dispatcher.UIThread.InvokeAsync(async () =>
+            {
+                // Show a message asking the user if they want to update
+                var shouldUpdate = await ContentDialogHelper.CreateChoiceDialog(
+                    LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
+                    LocaleManager.Instance[LocaleKeys.RyujinxUpdaterMessage],
+                    $"{Program.Version} -> {newVersion}");
+
+                if (shouldUpdate)
+                {
+                    await UpdateRyujinx(mainWindow, _buildUrl);
+                }
+                else
+                {
+                    _running = false;
+                }
+            });
+        }
+
+        private static HttpClient ConstructHttpClient()
+        {
+            HttpClient result = new();
+
+            // Required by GitHub to interact with APIs.
+            result.DefaultRequestHeaders.Add("User-Agent", "Ryujinx-Updater/1.0.0");
+
+            return result;
         }
 
-        public static void UpdateRyujinx(UpdateDialog updateDialog, string downloadUrl)
+        private static async Task UpdateRyujinx(Window parent, string downloadUrl)
         {
+            _updateSuccessful = false;
+
             // Empty update dir, although it shouldn't ever have anything inside it
             if (Directory.Exists(_updateDir))
             {
@@ -214,22 +245,93 @@ namespace Ryujinx.Modules
 
             string updateFile = Path.Combine(_updateDir, "update.bin");
 
-            // Download the update .zip
-            updateDialog.MainText.Text = "Downloading Update...";
-            updateDialog.ProgressBar.Value = 0;
-            updateDialog.ProgressBar.MaxValue = 100;
+            TaskDialog taskDialog = new()
+            {
+                Header = LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
+                SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterDownloading],
+                IconSource = new SymbolIconSource { Symbol = Symbol.Download },
+                ShowProgressBar = true,
+                XamlRoot = parent,
+            };
 
-            if (_buildSize >= 0)
+            taskDialog.Opened += (s, e) =>
             {
-                DoUpdateWithMultipleThreads(updateDialog, downloadUrl, updateFile);
-            }
-            else
+                if (_buildSize >= 0)
+                {
+                    DoUpdateWithMultipleThreads(taskDialog, downloadUrl, updateFile);
+                }
+                else
+                {
+                    DoUpdateWithSingleThread(taskDialog, downloadUrl, updateFile);
+                }
+            };
+
+            await taskDialog.ShowAsync(true);
+
+            if (_updateSuccessful)
             {
-                DoUpdateWithSingleThread(updateDialog, downloadUrl, updateFile);
+                bool shouldRestart = true;
+
+                if (!OperatingSystem.IsMacOS())
+                {
+                    shouldRestart = await ContentDialogHelper.CreateChoiceDialog(LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
+                        LocaleManager.Instance[LocaleKeys.DialogUpdaterCompleteMessage],
+                        LocaleManager.Instance[LocaleKeys.DialogUpdaterRestartMessage]);
+                }
+
+                if (shouldRestart)
+                {
+                    List<string> arguments = CommandLineState.Arguments.ToList();
+                    string executableDirectory = AppDomain.CurrentDomain.BaseDirectory;
+
+                    // On macOS we perform the update at relaunch.
+                    if (OperatingSystem.IsMacOS())
+                    {
+                        string baseBundlePath = Path.GetFullPath(Path.Combine(executableDirectory, "..", ".."));
+                        string newBundlePath = Path.Combine(_updateDir, "Ryujinx.app");
+                        string updaterScriptPath = Path.Combine(newBundlePath, "Contents", "Resources", "updater.sh");
+                        string currentPid = Environment.ProcessId.ToString();
+
+                        arguments.InsertRange(0, new List<string> { updaterScriptPath, baseBundlePath, newBundlePath, currentPid });
+                        Process.Start("/bin/bash", arguments);
+                    }
+                    else
+                    {
+                        // Find the process name.
+                        string ryuName = Path.GetFileName(Environment.ProcessPath);
+
+                        // Some operating systems can see the renamed executable, so strip off the .ryuold if found.
+                        if (ryuName.EndsWith(".ryuold"))
+                        {
+                            ryuName = ryuName[..^7];
+                        }
+
+                        // Fallback if the executable could not be found.
+                        if (!Path.Exists(Path.Combine(executableDirectory, ryuName)))
+                        {
+                            ryuName = OperatingSystem.IsWindows() ? "Ryujinx.exe" : "Ryujinx";
+                        }
+
+                        ProcessStartInfo processStart = new(ryuName)
+                        {
+                            UseShellExecute = true,
+                            WorkingDirectory = executableDirectory,
+                        };
+
+                        foreach (string argument in CommandLineState.Arguments)
+                        {
+                            processStart.ArgumentList.Add(argument);
+                        }
+
+                        Process.Start(processStart);
+                    }
+
+                    Environment.Exit(0);
+                }
             }
         }
 
-        private static void DoUpdateWithMultipleThreads(UpdateDialog updateDialog, string downloadUrl, string updateFile)
+        private static void DoUpdateWithMultipleThreads(TaskDialog taskDialog, string downloadUrl, string updateFile)
         {
             // Multi-Threaded Updater
             long chunkSize = _buildSize / ConnectionCount;
@@ -253,6 +355,7 @@ namespace Ryujinx.Modules
                 // TODO: WebClient is obsolete and need to be replaced with a more complex logic using HttpClient.
                 using WebClient client = new();
 #pragma warning restore SYSLIB0014
+
                 webClients.Add(client);
 
                 if (i == ConnectionCount - 1)
@@ -272,7 +375,7 @@ namespace Ryujinx.Modules
                     Interlocked.Exchange(ref progressPercentage[index], args.ProgressPercentage);
                     Interlocked.Add(ref totalProgressPercentage, args.ProgressPercentage);
 
-                    updateDialog.ProgressBar.Value = totalProgressPercentage / ConnectionCount;
+                    taskDialog.SetProgressBarState(totalProgressPercentage / ConnectionCount, TaskDialogProgressState.Normal);
                 };
 
                 client.DownloadDataCompleted += (_, args) =>
@@ -283,6 +386,8 @@ namespace Ryujinx.Modules
                     {
                         webClients[index].Dispose();
 
+                        taskDialog.Hide();
+
                         return;
                     }
 
@@ -300,18 +405,24 @@ namespace Ryujinx.Modules
 
                         File.WriteAllBytes(updateFile, mergedFileBytes);
 
+                        // On macOS, ensure that we remove the quarantine bit to prevent Gatekeeper from blocking execution.
+                        if (OperatingSystem.IsMacOS())
+                        {
+                            using Process xattrProcess = Process.Start("xattr", new List<string> { "-d", "com.apple.quarantine", updateFile });
+
+                            xattrProcess.WaitForExit();
+                        }
+
                         try
                         {
-                            InstallUpdate(updateDialog, updateFile);
+                            InstallUpdate(taskDialog, updateFile);
                         }
                         catch (Exception e)
                         {
                             Logger.Warning?.Print(LogClass.Application, e.Message);
                             Logger.Warning?.Print(LogClass.Application, "Multi-Threaded update failed, falling back to single-threaded updater.");
 
-                            DoUpdateWithSingleThread(updateDialog, downloadUrl, updateFile);
-
-                            return;
+                            DoUpdateWithSingleThread(taskDialog, downloadUrl, updateFile);
                         }
                     }
                 };
@@ -330,14 +441,14 @@ namespace Ryujinx.Modules
                         webClient.CancelAsync();
                     }
 
-                    DoUpdateWithSingleThread(updateDialog, downloadUrl, updateFile);
+                    DoUpdateWithSingleThread(taskDialog, downloadUrl, updateFile);
 
                     return;
                 }
             }
         }
 
-        private static void DoUpdateWithSingleThreadWorker(UpdateDialog updateDialog, string downloadUrl, string updateFile)
+        private static void DoUpdateWithSingleThreadWorker(TaskDialog taskDialog, string downloadUrl, string updateFile)
         {
             using HttpClient client = new();
             // We do not want to timeout while downloading
@@ -363,151 +474,165 @@ namespace Ryujinx.Modules
 
                 byteWritten += readSize;
 
-                updateDialog.ProgressBar.Value = ((double)byteWritten / totalBytes) * 100;
+                taskDialog.SetProgressBarState(GetPercentage(byteWritten, totalBytes), TaskDialogProgressState.Normal);
+
                 updateFileStream.Write(buffer, 0, readSize);
             }
 
-            InstallUpdate(updateDialog, updateFile);
+            InstallUpdate(taskDialog, updateFile);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static double GetPercentage(double value, double max)
+        {
+            return max == 0 ? 0 : value / max * 100;
         }
 
-        private static void DoUpdateWithSingleThread(UpdateDialog updateDialog, string downloadUrl, string updateFile)
+        private static void DoUpdateWithSingleThread(TaskDialog taskDialog, string downloadUrl, string updateFile)
         {
-            Thread worker = new(() => DoUpdateWithSingleThreadWorker(updateDialog, downloadUrl, updateFile))
+            Thread worker = new(() => DoUpdateWithSingleThreadWorker(taskDialog, downloadUrl, updateFile))
             {
                 Name = "Updater.SingleThreadWorker",
             };
+
             worker.Start();
         }
 
-        private static async void InstallUpdate(UpdateDialog updateDialog, string updateFile)
+        [SupportedOSPlatform("linux")]
+        [SupportedOSPlatform("macos")]
+        private static void ExtractTarGzipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath)
         {
-            // Extract Update
-            updateDialog.MainText.Text = "Extracting Update...";
-            updateDialog.ProgressBar.Value = 0;
+            using Stream inStream = File.OpenRead(archivePath);
+            using GZipInputStream gzipStream = new(inStream);
+            using TarInputStream tarStream = new(gzipStream, Encoding.ASCII);
 
-            if (OperatingSystem.IsLinux())
-            {
-                using Stream inStream = File.OpenRead(updateFile);
-                using Stream gzipStream = new GZipInputStream(inStream);
-                using TarInputStream tarStream = new(gzipStream, Encoding.ASCII);
-                updateDialog.ProgressBar.MaxValue = inStream.Length;
+            TarEntry tarEntry;
 
-                await Task.Run(() =>
+            while ((tarEntry = tarStream.GetNextEntry()) is not null)
+            {
+                if (tarEntry.IsDirectory)
                 {
-                    TarEntry tarEntry;
-
-                    if (!OperatingSystem.IsWindows())
-                    {
-                        while ((tarEntry = tarStream.GetNextEntry()) != null)
-                        {
-                            if (tarEntry.IsDirectory)
-                            {
-                                continue;
-                            }
-
-                            string outPath = Path.Combine(_updateDir, tarEntry.Name);
+                    continue;
+                }
 
-                            Directory.CreateDirectory(Path.GetDirectoryName(outPath));
+                string outPath = Path.Combine(outputDirectoryPath, tarEntry.Name);
 
-                            using FileStream outStream = File.OpenWrite(outPath);
-                            tarStream.CopyEntryContents(outStream);
+                Directory.CreateDirectory(Path.GetDirectoryName(outPath));
 
-                            File.SetUnixFileMode(outPath, (UnixFileMode)tarEntry.TarHeader.Mode);
-                            File.SetLastWriteTime(outPath, DateTime.SpecifyKind(tarEntry.ModTime, DateTimeKind.Utc));
+                using FileStream outStream = File.OpenWrite(outPath);
+                tarStream.CopyEntryContents(outStream);
 
-                            TarEntry entry = tarEntry;
+                File.SetUnixFileMode(outPath, (UnixFileMode)tarEntry.TarHeader.Mode);
+                File.SetLastWriteTime(outPath, DateTime.SpecifyKind(tarEntry.ModTime, DateTimeKind.Utc));
 
-                            Application.Invoke(delegate
-                            {
-                                updateDialog.ProgressBar.Value += entry.Size;
-                            });
-                        }
+                Dispatcher.UIThread.Post(() =>
+                {
+                    if (tarEntry is null)
+                    {
+                        return;
                     }
-                });
 
-                updateDialog.ProgressBar.Value = inStream.Length;
+                    taskDialog.SetProgressBarState(GetPercentage(tarEntry.Size, inStream.Length), TaskDialogProgressState.Normal);
+                });
             }
-            else
-            {
-                using Stream inStream = File.OpenRead(updateFile);
-                using ZipFile zipFile = new(inStream);
-                updateDialog.ProgressBar.MaxValue = zipFile.Count;
+        }
 
-                await Task.Run(() =>
+        private static void ExtractZipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath)
+        {
+            using Stream inStream = File.OpenRead(archivePath);
+            using ZipFile zipFile = new(inStream);
+
+            double count = 0;
+            foreach (ZipEntry zipEntry in zipFile)
+            {
+                count++;
+                if (zipEntry.IsDirectory)
                 {
-                    foreach (ZipEntry zipEntry in zipFile)
-                    {
-                        if (zipEntry.IsDirectory)
-                        {
-                            continue;
-                        }
+                    continue;
+                }
 
-                        string outPath = Path.Combine(_updateDir, zipEntry.Name);
+                string outPath = Path.Combine(outputDirectoryPath, zipEntry.Name);
 
-                        Directory.CreateDirectory(Path.GetDirectoryName(outPath));
+                Directory.CreateDirectory(Path.GetDirectoryName(outPath));
 
-                        using Stream zipStream = zipFile.GetInputStream(zipEntry);
-                        using FileStream outStream = File.OpenWrite(outPath);
-                        zipStream.CopyTo(outStream);
+                using Stream zipStream = zipFile.GetInputStream(zipEntry);
+                using FileStream outStream = File.OpenWrite(outPath);
 
-                        File.SetLastWriteTime(outPath, DateTime.SpecifyKind(zipEntry.DateTime, DateTimeKind.Utc));
+                zipStream.CopyTo(outStream);
 
-                        Application.Invoke(delegate
-                        {
-                            updateDialog.ProgressBar.Value++;
-                        });
-                    }
+                File.SetLastWriteTime(outPath, DateTime.SpecifyKind(zipEntry.DateTime, DateTimeKind.Utc));
+
+                Dispatcher.UIThread.Post(() =>
+                {
+                    taskDialog.SetProgressBarState(GetPercentage(count, zipFile.Count), TaskDialogProgressState.Normal);
                 });
             }
+        }
+
+        private static void InstallUpdate(TaskDialog taskDialog, string updateFile)
+        {
+            // Extract Update
+            taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterExtracting];
+            taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
+
+            if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+            {
+                ExtractTarGzipFile(taskDialog, updateFile, _updateDir);
+            }
+            else if (OperatingSystem.IsWindows())
+            {
+                ExtractZipFile(taskDialog, updateFile, _updateDir);
+            }
+            else
+            {
+                throw new NotSupportedException();
+            }
 
             // Delete downloaded zip
             File.Delete(updateFile);
 
             List<string> allFiles = EnumerateFilesToDelete().ToList();
 
-            updateDialog.MainText.Text = "Renaming Old Files...";
-            updateDialog.ProgressBar.Value = 0;
-            updateDialog.ProgressBar.MaxValue = allFiles.Count;
+            taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterRenaming];
+            taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
 
-            // Replace old files
-            await Task.Run(() =>
+            // NOTE: On macOS, replacement is delayed to the restart phase.
+            if (!OperatingSystem.IsMacOS())
             {
+                // Replace old files
+                double count = 0;
                 foreach (string file in allFiles)
                 {
+                    count++;
                     try
                     {
                         File.Move(file, file + ".ryuold");
 
-                        Application.Invoke(delegate
+                        Dispatcher.UIThread.InvokeAsync(() =>
                         {
-                            updateDialog.ProgressBar.Value++;
+                            taskDialog.SetProgressBarState(GetPercentage(count, allFiles.Count), TaskDialogProgressState.Normal);
                         });
                     }
                     catch
                     {
-                        Logger.Warning?.Print(LogClass.Application, "Updater was unable to rename file: " + file);
+                        Logger.Warning?.Print(LogClass.Application, LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.UpdaterRenameFailed, file));
                     }
                 }
 
-                Application.Invoke(delegate
+                Dispatcher.UIThread.InvokeAsync(() =>
                 {
-                    updateDialog.MainText.Text = "Adding New Files...";
-                    updateDialog.ProgressBar.Value = 0;
-                    updateDialog.ProgressBar.MaxValue = Directory.GetFiles(_updatePublishDir, "*", SearchOption.AllDirectories).Length;
+                    taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterAddingFiles];
+                    taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
                 });
 
-                MoveAllFilesOver(_updatePublishDir, _homeDir, updateDialog);
-            });
+                MoveAllFilesOver(_updatePublishDir, _homeDir, taskDialog);
 
-            Directory.Delete(_updateDir, true);
+                Directory.Delete(_updateDir, true);
+            }
 
-            updateDialog.MainText.Text = "Update Complete!";
-            updateDialog.SecondaryText.Text = "Do you want to restart Ryujinx now?";
-            updateDialog.Modal = true;
+            _updateSuccessful = true;
 
-            updateDialog.ProgressBar.Hide();
-            updateDialog.YesButton.Show();
-            updateDialog.NoButton.Show();
+            taskDialog.Hide();
         }
 
         public static bool CanUpdate(bool showWarnings)
@@ -517,7 +642,11 @@ namespace Ryujinx.Modules
             {
                 if (showWarnings)
                 {
-                    GtkDialog.CreateWarningDialog("You are not connected to the Internet!", "Please verify that you have a working Internet connection!");
+                    Dispatcher.UIThread.InvokeAsync(() =>
+                        ContentDialogHelper.CreateWarningDialog(
+                            LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetMessage],
+                            LocaleManager.Instance[LocaleKeys.DialogUpdaterNoInternetSubMessage])
+                    );
                 }
 
                 return false;
@@ -527,7 +656,11 @@ namespace Ryujinx.Modules
             {
                 if (showWarnings)
                 {
-                    GtkDialog.CreateWarningDialog("You cannot update a Dirty build of Ryujinx!", "Please download Ryujinx at https://ryujinx.org/ if you are looking for a supported version.");
+                    Dispatcher.UIThread.InvokeAsync(() =>
+                        ContentDialogHelper.CreateWarningDialog(
+                            LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildMessage],
+                            LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage])
+                    );
                 }
 
                 return false;
@@ -539,11 +672,19 @@ namespace Ryujinx.Modules
             {
                 if (ReleaseInformation.IsFlatHubBuild)
                 {
-                    GtkDialog.CreateWarningDialog("Updater Disabled!", "Please update Ryujinx via FlatHub.");
+                    Dispatcher.UIThread.InvokeAsync(() =>
+                        ContentDialogHelper.CreateWarningDialog(
+                            LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle],
+                            LocaleManager.Instance[LocaleKeys.DialogUpdaterFlatpakNotSupportedMessage])
+                    );
                 }
                 else
                 {
-                    GtkDialog.CreateWarningDialog("Updater Disabled!", "Please download Ryujinx at https://ryujinx.org/ if you are looking for a supported version.");
+                    Dispatcher.UIThread.InvokeAsync(() =>
+                        ContentDialogHelper.CreateWarningDialog(
+                            LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle],
+                            LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage])
+                    );
                 }
             }
 
@@ -557,7 +698,7 @@ namespace Ryujinx.Modules
             var files = Directory.EnumerateFiles(_homeDir); // All files directly in base dir.
 
             // Determine and exclude user files only when the updater is running, not when cleaning old files
-            if (Running)
+            if (_running && !OperatingSystem.IsMacOS())
             {
                 // Compare the loose files in base directory against the loose files from the incoming update, and store foreign ones in a user list.
                 var oldFiles = Directory.EnumerateFiles(_homeDir, "*", SearchOption.TopDirectoryOnly).Select(Path.GetFileName);
@@ -583,8 +724,9 @@ namespace Ryujinx.Modules
             return files.Where(f => !new FileInfo(f).Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System));
         }
 
-        private static void MoveAllFilesOver(string root, string dest, UpdateDialog dialog)
+        private static void MoveAllFilesOver(string root, string dest, TaskDialog taskDialog)
         {
+            int total = Directory.GetFiles(root, "*", SearchOption.AllDirectories).Length;
             foreach (string directory in Directory.GetDirectories(root))
             {
                 string dirName = Path.GetFileName(directory);
@@ -594,28 +736,28 @@ namespace Ryujinx.Modules
                     Directory.CreateDirectory(Path.Combine(dest, dirName));
                 }
 
-                MoveAllFilesOver(directory, Path.Combine(dest, dirName), dialog);
+                MoveAllFilesOver(directory, Path.Combine(dest, dirName), taskDialog);
             }
 
+            double count = 0;
             foreach (string file in Directory.GetFiles(root))
             {
+                count++;
+
                 File.Move(file, Path.Combine(dest, Path.GetFileName(file)), true);
 
-                Application.Invoke(delegate
+                Dispatcher.UIThread.InvokeAsync(() =>
                 {
-                    dialog.ProgressBar.Value++;
+                    taskDialog.SetProgressBarState(GetPercentage(count, total), TaskDialogProgressState.Normal);
                 });
             }
         }
 
         public static void CleanupUpdate()
         {
-            foreach (string file in EnumerateFilesToDelete())
+            foreach (string file in Directory.GetFiles(_homeDir, "*.ryuold", SearchOption.AllDirectories))
             {
-                if (Path.GetExtension(file).EndsWith(".ryuold"))
-                {
-                    File.Delete(file);
-                }
+                File.Delete(file);
             }
         }
     }

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác