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/ModManagerViewModel.cs | 336 +++++++++++++++++++++++
 1 file changed, 336 insertions(+)
 create mode 100644 src/Ryujinx/UI/ViewModels/ModManagerViewModel.cs

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

diff --git a/src/Ryujinx/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/ModManagerViewModel.cs
new file mode 100644
index 00000000..8321bf89
--- /dev/null
+++ b/src/Ryujinx/UI/ViewModels/ModManagerViewModel.cs
@@ -0,0 +1,336 @@
+using Avalonia;
+using Avalonia.Collections;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Platform.Storage;
+using Avalonia.Threading;
+using DynamicData;
+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.HOS;
+using System;
+using System.IO;
+using System.Linq;
+
+namespace Ryujinx.Ava.UI.ViewModels
+{
+    public class ModManagerViewModel : BaseModel
+    {
+        private readonly string _modJsonPath;
+
+        private AvaloniaList<ModModel> _mods = new();
+        private AvaloniaList<ModModel> _views = new();
+        private AvaloniaList<ModModel> _selectedMods = new();
+
+        private string _search;
+        private readonly ulong _applicationId;
+        private readonly IStorageProvider _storageProvider;
+
+        private static readonly ModMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
+        public AvaloniaList<ModModel> Mods
+        {
+            get => _mods;
+            set
+            {
+                _mods = value;
+                OnPropertyChanged();
+                OnPropertyChanged(nameof(ModCount));
+                Sort();
+            }
+        }
+
+        public AvaloniaList<ModModel> Views
+        {
+            get => _views;
+            set
+            {
+                _views = value;
+                OnPropertyChanged();
+            }
+        }
+
+        public AvaloniaList<ModModel> SelectedMods
+        {
+            get => _selectedMods;
+            set
+            {
+                _selectedMods = value;
+                OnPropertyChanged();
+            }
+        }
+
+        public string Search
+        {
+            get => _search;
+            set
+            {
+                _search = value;
+                OnPropertyChanged();
+                Sort();
+            }
+        }
+
+        public string ModCount
+        {
+            get => string.Format(LocaleManager.Instance[LocaleKeys.ModWindowHeading], Mods.Count);
+        }
+
+        public ModManagerViewModel(ulong applicationId)
+        {
+            _applicationId = applicationId;
+
+            _modJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationId.ToString("x16"), "mods.json");
+
+            if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+            {
+                _storageProvider = desktop.MainWindow.StorageProvider;
+            }
+
+            LoadMods(applicationId);
+        }
+
+        private void LoadMods(ulong applicationId)
+        {
+            Mods.Clear();
+            SelectedMods.Clear();
+
+            string[] modsBasePaths = [ModLoader.GetSdModsBasePath(), ModLoader.GetModsBasePath()];
+
+            foreach (var path in modsBasePaths)
+            {
+                var inSd = path == ModLoader.GetSdModsBasePath();
+                var modCache = new ModLoader.ModCache();
+
+                ModLoader.QueryContentsDir(modCache, new DirectoryInfo(Path.Combine(path, "contents")), applicationId);
+
+                foreach (var mod in modCache.RomfsDirs)
+                {
+                    var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled, inSd);
+                    if (Mods.All(x => x.Path != mod.Path.Parent.FullName))
+                    {
+                        Mods.Add(modModel);
+                    }
+                }
+
+                foreach (var mod in modCache.RomfsContainers)
+                {
+                    Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled, inSd));
+                }
+
+                foreach (var mod in modCache.ExefsDirs)
+                {
+                    var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled, inSd);
+                    if (Mods.All(x => x.Path != mod.Path.Parent.FullName))
+                    {
+                        Mods.Add(modModel);
+                    }
+                }
+
+                foreach (var mod in modCache.ExefsContainers)
+                {
+                    Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled, inSd));
+                }
+            }
+
+            Sort();
+        }
+
+        public void Sort()
+        {
+            Mods.AsObservableChangeSet()
+                .Filter(Filter)
+                .Bind(out var view).AsObservableList();
+
+            _views.Clear();
+            _views.AddRange(view);
+
+            SelectedMods = new(Views.Where(x => x.Enabled));
+
+            OnPropertyChanged(nameof(ModCount));
+            OnPropertyChanged(nameof(Views));
+            OnPropertyChanged(nameof(SelectedMods));
+        }
+
+        private bool Filter(object arg)
+        {
+            if (arg is ModModel content)
+            {
+                return string.IsNullOrWhiteSpace(_search) || content.Name.ToLower().Contains(_search.ToLower());
+            }
+
+            return false;
+        }
+
+        public void Save()
+        {
+            ModMetadata modData = new();
+
+            foreach (ModModel mod in Mods)
+            {
+                modData.Mods.Add(new Mod
+                {
+                    Name = mod.Name,
+                    Path = mod.Path,
+                    Enabled = SelectedMods.Contains(mod),
+                });
+            }
+
+            JsonHelper.SerializeToFile(_modJsonPath, modData, _serializerContext.ModMetadata);
+        }
+
+        public void Delete(ModModel model)
+        {
+            var isSubdir = true;
+            var pathToDelete = model.Path;
+            var basePath = model.InSd ? ModLoader.GetSdModsBasePath() : ModLoader.GetModsBasePath();
+            var modsDir = ModLoader.GetApplicationDir(basePath, _applicationId.ToString("x16"));
+
+            if (new DirectoryInfo(model.Path).Parent?.FullName == modsDir)
+            {
+                isSubdir = false;
+            }
+
+            if (isSubdir)
+            {
+                var parentDir = String.Empty;
+
+                foreach (var dir in Directory.GetDirectories(modsDir, "*", SearchOption.TopDirectoryOnly))
+                {
+                    if (Directory.GetDirectories(dir, "*", SearchOption.AllDirectories).Contains(model.Path))
+                    {
+                        parentDir = dir;
+                        break;
+                    }
+                }
+
+                if (parentDir == String.Empty)
+                {
+                    Dispatcher.UIThread.Post(async () =>
+                    {
+                        await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(
+                            LocaleKeys.DialogModDeleteNoParentMessage,
+                            model.Path));
+                    });
+                    return;
+                }
+            }
+
+            Logger.Info?.Print(LogClass.Application, $"Deleting mod at \"{pathToDelete}\"");
+            Directory.Delete(pathToDelete, true);
+
+            Mods.Remove(model);
+            OnPropertyChanged(nameof(ModCount));
+            Sort();
+        }
+
+        private void AddMod(DirectoryInfo directory)
+        {
+            string[] directories;
+
+            try
+            {
+                directories = Directory.GetDirectories(directory.ToString(), "*", SearchOption.AllDirectories);
+            }
+            catch (Exception exception)
+            {
+                Dispatcher.UIThread.Post(async () =>
+                {
+                    await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(
+                        LocaleKeys.DialogLoadFileErrorMessage,
+                        exception.ToString(),
+                        directory));
+                });
+                return;
+            }
+
+            var destinationDir = ModLoader.GetApplicationDir(ModLoader.GetSdModsBasePath(), _applicationId.ToString("x16"));
+
+            // TODO: More robust checking for valid mod folders
+            var isDirectoryValid = true;
+
+            if (directories.Length == 0)
+            {
+                isDirectoryValid = false;
+            }
+
+            if (!isDirectoryValid)
+            {
+                Dispatcher.UIThread.Post(async () =>
+                {
+                    await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogModInvalidMessage]);
+                });
+                return;
+            }
+
+            foreach (var dir in directories)
+            {
+                string dirToCreate = dir.Replace(directory.Parent.ToString(), destinationDir);
+
+                // Mod already exists
+                if (Directory.Exists(dirToCreate))
+                {
+                    Dispatcher.UIThread.Post(async () =>
+                    {
+                        await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(
+                            LocaleKeys.DialogLoadFileErrorMessage,
+                            LocaleManager.Instance[LocaleKeys.DialogModAlreadyExistsMessage],
+                            dirToCreate));
+                    });
+
+                    return;
+                }
+
+                Directory.CreateDirectory(dirToCreate);
+            }
+
+            var files = Directory.GetFiles(directory.ToString(), "*", SearchOption.AllDirectories);
+
+            foreach (var file in files)
+            {
+                File.Copy(file, file.Replace(directory.Parent.ToString(), destinationDir), true);
+            }
+
+            LoadMods(_applicationId);
+        }
+
+        public async void Add()
+        {
+            var result = await _storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
+            {
+                Title = LocaleManager.Instance[LocaleKeys.SelectModDialogTitle],
+                AllowMultiple = true,
+            });
+
+            foreach (var folder in result)
+            {
+                AddMod(new DirectoryInfo(folder.Path.LocalPath));
+            }
+        }
+
+        public void DeleteAll()
+        {
+            foreach (var mod in Mods)
+            {
+                Delete(mod);
+            }
+
+            Mods.Clear();
+            OnPropertyChanged(nameof(ModCount));
+            Sort();
+        }
+
+        public void EnableAll()
+        {
+            SelectedMods = new(Mods);
+        }
+
+        public void DisableAll()
+        {
+            SelectedMods.Clear();
+        }
+    }
+}
-- 
cgit v1.2.3-70-g09d2