From ec6cb0abb4b7669895b6e96fd7581c93b5abd691 Mon Sep 17 00:00:00 2001
From: Mary Guillemard <mary@mary.zone>
Date: Sat, 2 Mar 2024 12:51:05 +0100
Subject: 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>
---
 src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs | 249 ++++++++++++++++++++++
 1 file changed, 249 insertions(+)
 create mode 100644 src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs

(limited to 'src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs')

diff --git a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs
new file mode 100644
index 00000000..5989ce09
--- /dev/null
+++ b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs
@@ -0,0 +1,249 @@
+using Avalonia;
+using Avalonia.Collections;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Platform.Storage;
+using Avalonia.Threading;
+using LibHac.Common;
+using LibHac.Fs;
+using LibHac.Fs.Fsa;
+using LibHac.FsSystem;
+using LibHac.Ns;
+using LibHac.Tools.FsSystem;
+using LibHac.Tools.FsSystem.NcaUtils;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.UI.Helpers;
+using Ryujinx.Ava.UI.Models;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.UI.App.Common;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Path = System.IO.Path;
+using SpanHelpers = LibHac.Common.SpanHelpers;
+
+namespace Ryujinx.Ava.UI.ViewModels
+{
+    public class TitleUpdateViewModel : BaseModel
+    {
+        public TitleUpdateMetadata TitleUpdateWindowData;
+        public readonly string TitleUpdateJsonPath;
+        private VirtualFileSystem VirtualFileSystem { get; }
+        private ulong TitleId { get; }
+
+        private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
+        private AvaloniaList<object> _views = new();
+        private object _selectedUpdate;
+
+        private static readonly TitleUpdateMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
+        public AvaloniaList<TitleUpdateModel> TitleUpdates
+        {
+            get => _titleUpdates;
+            set
+            {
+                _titleUpdates = value;
+                OnPropertyChanged();
+            }
+        }
+
+        public AvaloniaList<object> Views
+        {
+            get => _views;
+            set
+            {
+                _views = value;
+                OnPropertyChanged();
+            }
+        }
+
+        public object SelectedUpdate
+        {
+            get => _selectedUpdate;
+            set
+            {
+                _selectedUpdate = value;
+                OnPropertyChanged();
+            }
+        }
+
+        public IStorageProvider StorageProvider;
+
+        public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId)
+        {
+            VirtualFileSystem = virtualFileSystem;
+
+            TitleId = titleId;
+
+            if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+            {
+                StorageProvider = desktop.MainWindow.StorageProvider;
+            }
+
+            TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json");
+
+            try
+            {
+                TitleUpdateWindowData = JsonHelper.DeserializeFromFile(TitleUpdateJsonPath, _serializerContext.TitleUpdateMetadata);
+            }
+            catch
+            {
+                Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {TitleId} at {TitleUpdateJsonPath}");
+
+                TitleUpdateWindowData = new TitleUpdateMetadata
+                {
+                    Selected = "",
+                    Paths = new List<string>(),
+                };
+
+                Save();
+            }
+
+            LoadUpdates();
+        }
+
+        private void LoadUpdates()
+        {
+            foreach (string path in TitleUpdateWindowData.Paths)
+            {
+                AddUpdate(path);
+            }
+
+            TitleUpdateModel selected = TitleUpdates.FirstOrDefault(x => x.Path == TitleUpdateWindowData.Selected, null);
+
+            SelectedUpdate = selected;
+
+            // NOTE: Save the list again to remove leftovers.
+            Save();
+            SortUpdates();
+        }
+
+        public void SortUpdates()
+        {
+            var list = TitleUpdates.ToList();
+
+            list.Sort((first, second) =>
+            {
+                if (string.IsNullOrEmpty(first.Control.DisplayVersionString.ToString()))
+                {
+                    return -1;
+                }
+
+                if (string.IsNullOrEmpty(second.Control.DisplayVersionString.ToString()))
+                {
+                    return 1;
+                }
+
+                return Version.Parse(first.Control.DisplayVersionString.ToString()).CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
+            });
+
+            Views.Clear();
+            Views.Add(new BaseModel());
+            Views.AddRange(list);
+
+            if (SelectedUpdate == null)
+            {
+                SelectedUpdate = Views[0];
+            }
+            else if (!TitleUpdates.Contains(SelectedUpdate))
+            {
+                if (Views.Count > 1)
+                {
+                    SelectedUpdate = Views[1];
+                }
+                else
+                {
+                    SelectedUpdate = Views[0];
+                }
+            }
+        }
+
+        private void AddUpdate(string path)
+        {
+            if (File.Exists(path) && TitleUpdates.All(x => x.Path != path))
+            {
+                using FileStream file = new(path, FileMode.Open, FileAccess.Read);
+
+                try
+                {
+                    var pfs = new PartitionFileSystem();
+                    pfs.Initialize(file.AsStorage()).ThrowIfFailure();
+                    (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, pfs, TitleId.ToString("x16"), 0);
+
+                    if (controlNca != null && patchNca != null)
+                    {
+                        ApplicationControlProperty controlData = new();
+
+                        using UniqueRef<IFile> nacpFile = new();
+
+                        controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
+                        nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
+
+                        TitleUpdates.Add(new TitleUpdateModel(controlData, path));
+                    }
+                    else
+                    {
+                        Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
+                    }
+                }
+                catch (Exception ex)
+                {
+                    Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadFileErrorMessage, ex.Message, path)));
+                }
+            }
+        }
+
+        public void RemoveUpdate(TitleUpdateModel update)
+        {
+            TitleUpdates.Remove(update);
+
+            SortUpdates();
+        }
+
+        public async Task Add()
+        {
+            var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
+            {
+                AllowMultiple = true,
+                FileTypeFilter = new List<FilePickerFileType>
+                {
+                    new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
+                    {
+                        Patterns = new[] { "*.nsp" },
+                        AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nsp" },
+                        MimeTypes = new[] { "application/x-nx-nsp" },
+                    },
+                },
+            });
+
+            foreach (var file in result)
+            {
+                AddUpdate(file.Path.LocalPath);
+            }
+
+            SortUpdates();
+        }
+
+        public void Save()
+        {
+            TitleUpdateWindowData.Paths.Clear();
+            TitleUpdateWindowData.Selected = "";
+
+            foreach (TitleUpdateModel update in TitleUpdates)
+            {
+                TitleUpdateWindowData.Paths.Add(update.Path);
+
+                if (update == SelectedUpdate)
+                {
+                    TitleUpdateWindowData.Selected = update.Path;
+                }
+            }
+
+            JsonHelper.SerializeToFile(TitleUpdateJsonPath, TitleUpdateWindowData, _serializerContext.TitleUpdateMetadata);
+        }
+    }
+}
-- 
cgit v1.2.3-70-g09d2