From d4d60460eb44a6eea13d27dfc5f80b82ef661a54 Mon Sep 17 00:00:00 2001 From: NoobNotFound <82730163+NoobNotFound@users.noreply.github.com> Date: Thu, 25 Sep 2025 01:44:06 +0530 Subject: [PATCH 01/15] Refactor Core and enhance game management features Refactored the `Core` class to introduce `SavedGame` and `SavedGameCollection` for managing saved games. Added `LoadGames`, `SaveGames`, and `RemoveGame` methods, improving game lifecycle management. Updated `InstallGame` to handle `Game` objects and save state changes. Enhanced the `Game` class with a `FromTuple` method, improved error handling, and added offline mode checks. Improved `BaseSettingsService` to support file-based storage with `SaveToFileAsync` and `LoadFromFileAsync`. Updated `Set` and `Get` methods to handle file-based settings. Refined mod loader installers (`Fabric`, `Forge`, `Quilt`, `ModLoaderRouter`) with better error handling and offline support. Upgraded the solution to Visual Studio 18 and removed unused build configurations. Centralized settings keys in the new `SettingsKeys` class. Enhanced logging and exception handling across the codebase for better debugging and user notifications. --- Emerald.CoreX/Core.cs | 145 +++++++++++++++++-- Emerald.CoreX/Game.cs | 30 +++- Emerald.CoreX/Helpers/BaseSettingsService.cs | 81 +++++++++-- Emerald.CoreX/Helpers/SettingsKeys.cs | 12 ++ Emerald.CoreX/Installers/Fabric.cs | 2 +- Emerald.CoreX/Installers/Forge.cs | 4 +- Emerald.CoreX/Installers/ModLoaderRouter.cs | 2 +- Emerald.CoreX/Installers/Quilt.cs | 2 +- Emerald.CoreX/Versions/Version.cs | 2 +- Emerald.sln | 56 +------ 10 files changed, 241 insertions(+), 95 deletions(-) create mode 100644 Emerald.CoreX/Helpers/SettingsKeys.cs diff --git a/Emerald.CoreX/Core.cs b/Emerald.CoreX/Core.cs index 2f461eb0..57956d5d 100644 --- a/Emerald.CoreX/Core.cs +++ b/Emerald.CoreX/Core.cs @@ -1,14 +1,23 @@ -using Microsoft.Extensions.Logging; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; using CmlLib.Core; +using CmlLib.Core.Utils; using CmlLib.Core.VersionMetadata; -using Emerald.CoreX.Notifications; -using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; using Emerald.CoreX.Helpers; +using Emerald.CoreX.Notifications; using Emerald.Services; +using Microsoft.Extensions.Logging; namespace Emerald.CoreX; -public class Core(ILogger _logger, INotificationService _notify, BaseSettingsService settingsService) +public record SavedGame(string Path, Versions.Version Version, Models.GameSettings GameOptions); + +public record SavedGameCollection(string BasePath, SavedGame[] Games); + +public partial class Core(ILogger _logger, INotificationService _notify, BaseSettingsService settingsService) : ObservableObject { + public const string GamesFolderName = "EmeraldGames"; public MinecraftLauncher Launcher { get; set; } public bool IsRunning { get; set; } = false; @@ -19,14 +28,80 @@ public class Core(ILogger _logger, INotificationService _notify, BaseSetti public readonly ObservableCollection Games = new(); - private bool initialized = false; + [ObservableProperty] + private bool _initialized = false; public Models.GameSettings GameOptions = new(); - public async Task Refresh() + + private SavedGameCollection[] SavedgamesWithPaths = []; + + public void LoadGames() + { + if (BasePath == null) + { + _logger.LogWarning("Cannot load games, BasePath is not set"); + throw new InvalidOperationException("Cannot load games, BasePath is not set"); + } + + var gamesFolder = Path.Combine(BasePath.BasePath, GamesFolderName); + if (!Path.Exists(gamesFolder)) + { + _logger.LogInformation("Games folder does not exist, creating..."); + Directory.CreateDirectory(gamesFolder); + } + + SavedgamesWithPaths = settingsService.Get(SettingsKeys.SavedGames, [], true); + + var collection = SavedgamesWithPaths.FirstOrDefault(x => x.BasePath == BasePath.BasePath); + if (collection == null) + { + _logger.LogInformation("Saved games paths does not contain any games"); + return; + } + + foreach (var sg in collection.Games) + { + try + { + var game = Game.FromTuple((sg.Path, sg.Version, sg.GameOptions)); + Games.Add(game); + } + catch (Exception ex) + { + _logger.LogWarning("Failed to load game from {dir}: {ex}", sg.Path, ex.Message); + _notify.Error("FailedToLoadGame", $"Failed to load game from {sg.Path}", ex: ex); + } + } + + _logger.LogInformation("Loaded {count} games from", Games.Count); + } + public void SaveGames() { + _logger.LogInformation("Saving {count} games", Games.Count); + + var toSave = Games.Select(x => + new SavedGame(x.Path.BasePath, x.Version, x.Options) + ).ToArray(); + + try + { + var list = SavedgamesWithPaths.ToList(); + list.RemoveAll(x => x.BasePath == BasePath.BasePath); + list.Add(new SavedGameCollection(BasePath.BasePath, toSave)); + + SavedgamesWithPaths = list.ToArray(); + settingsService.Set(SettingsKeys.SavedGames, SavedgamesWithPaths, true); + _logger.LogInformation("Saved {count} games", toSave.Length); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to save games"); + throw; + } } + /// /// Initializes the Core with the given Minecraft path and retrieves the list of available vanilla Minecraft versions. /// @@ -44,7 +119,7 @@ public async Task InitializeAndRefresh(MinecraftPath? basePath = null) GameOptions = settingsService.Get("BaseGameOptions", Models.GameSettings.FromMLaunchOption(new())); _logger.LogInformation("Trying to load vanilla minecraft versions from servers"); - if (!initialized && basePath == null) + if (!Initialized && basePath == null) { _logger.LogInformation("Minecraft Path must be set on first initialize"); throw new InvalidOperationException("Minecraft Path must be set on first initialize"); @@ -54,8 +129,9 @@ public async Task InitializeAndRefresh(MinecraftPath? basePath = null) Launcher = new MinecraftLauncher(basePath); BasePath = basePath; } - - initialized = true; + + LoadGames(); + Initialized = true; var l = await Launcher.GetAllVersionsAsync(not.CancellationToken.Value); @@ -72,7 +148,7 @@ public async Task InitializeAndRefresh(MinecraftPath? basePath = null) { _logger.LogCritical(ex, "Failed to load vanilla minecraft versions: {ex}", ex.Message); _notify.Complete(not.Id, false, ex.Message, ex); - initialized = false; + Initialized = false; } _logger.LogInformation("Loaded {count} vanilla versions", VanillaVersions.Count); } @@ -83,14 +159,14 @@ public async Task InitializeAndRefresh(MinecraftPath? basePath = null) /// The version of the game to be installed. Must exist in the collection of games. /// Specifies whether to display file progress during installation. /// A task that represents the asynchronous operation of installing the game version. -public async Task InstallGame(Versions.Version version, bool showFileprog = false) +public async Task InstallGame(Game game, bool showFileprog = false) { - + var version = game.Version; + try { - _logger.LogInformation("Installing game {version}", version.BasedOn); - var game = Games.Where(x => x.Equals(version)).First(); + _logger.LogInformation("Installing game {version}", version.BasedOn); if(game == null) { @@ -116,11 +192,13 @@ public void AddGame(Versions.Version version) { _logger.LogInformation("Adding game {version}", version.BasedOn); - var path = Path.Combine( BasePath.BasePath, version.DisplayName); + var path = Path.Combine( BasePath.BasePath, GamesFolderName, version.DisplayName); + + var game = new Game(new(path), GameOptions, version); - var game = new Game(new(path), GameOptions); Games.Add(game); + SaveGames(); var not = _notify.Info( "AddedGame", @@ -137,4 +215,39 @@ public void AddGame(Versions.Version version) ); } } + + public void RemoveGame(Game game, bool deleteFolder = false) + { + try + { + _logger.LogInformation("Removing game {version}", game.Version.BasedOn); + if (!Games.Contains(game)) + { + _logger.LogWarning("Game {version} not found in collection", game.Version.BasedOn); + throw new NullReferenceException($"Game {game.Version.BasedOn} not found in collection"); + } + Games.Remove(game); + SaveGames(); + + if (deleteFolder && Path.Exists(game.Path.BasePath)) + { + _logger.LogInformation("Deleting game folder {path}", game.Path.BasePath); + Directory.Delete(game.Path.BasePath, true); + } + + var not = _notify.Info( + "RemovedGame", + $"{game.Version.DisplayName} based on {game.Version.BasedOn} {game.Version.Type}" + ); + } + catch (Exception ex) + { + _logger.LogWarning("Failed to remove game {version}: {ex}", game.Version.BasedOn, ex.Message); + _notify.Error( + "FailedToRemoveGame", + $"Failed to remove game {game.Version.DisplayName} based on {game.Version.BasedOn} {game.Version.Type}", + ex: ex + ); + } + } } diff --git a/Emerald.CoreX/Game.cs b/Emerald.CoreX/Game.cs index 31cec9b9..ac8639e4 100644 --- a/Emerald.CoreX/Game.cs +++ b/Emerald.CoreX/Game.cs @@ -11,6 +11,9 @@ namespace Emerald.CoreX; public class Game { + public static Game FromTuple((string Path, Versions.Version version, Models.GameSettings Options) t) + => new(new MinecraftPath(t.Path), t.Options, t.version); + private readonly ILogger _logger; private readonly Notifications.INotificationService _notify; @@ -26,13 +29,14 @@ public class Game /// Represents a Game instance, responsible for managing the installation, configuration, /// and launching of Minecraft versions. /// - public Game(MinecraftPath path, Models.GameSettings options) + public Game(MinecraftPath path, Models.GameSettings options, Versions.Version version) { _notify = Ioc.Default.GetService(); _logger = this.Log(); Launcher = new MinecraftLauncher(); Path = path; Options = options; + Version = version; _logger.LogInformation("Game instance created with path: {Path} and options: {Options}", path, options); } @@ -71,24 +75,33 @@ public async Task InstallVersion(bool isOffline = false, bool showFileProgress = try { - string ver = await Ioc.Default.GetService().RouteAndInitializeAsync(Path, Version); + string? ver = await Ioc.Default.GetService().RouteAndInitializeAsync(Path, Version); _logger.LogInformation("Version initialization completed. Version: {Version}", ver); if (ver == null) { - var vers = await Launcher.GetAllVersionsAsync(); - ver = vers.First(x => x.Name.ToLower().Contains(Version.BasedOn.ToLower())).Name; - _logger.LogWarning("Version {VersionType} {ModVersion} {BasedOn} not found. Using {FallbackVersion} instead.", Version.Type, Version.ModVersion, Version.BasedOn, ver); + _logger.LogWarning("Version {VersionType} {ModVersion} {BasedOn} not found.", Version.Type, Version.ModVersion, Version.BasedOn); - _notify.Update( + _notify.Complete( not.Id, - message: $"Version {Version.Type} {Version.ModVersion} {Version.BasedOn} not found. Using {ver} instead.", - isIndeterminate: false + message: $"Version {Version.Type} {Version.ModVersion} {Version.BasedOn} not found. Check your internet connection.", + success: false ); return; } + if (isOffline) //checking if verison actually exists + { + + var vers = await Launcher.GetAllVersionsAsync(); + var mver = vers.Where(x => x.Name == ver).First(); + if (mver == null) + { + _logger.LogWarning("Version {Version} not found in offline mode. Can't proceed installation.", ver); + throw new NullReferenceException($"Version {ver} not found in offline mode. Can't proceed installation."); + } + } (string Files, string bytes, double prog) prog = (string.Empty, string.Empty, 0); @@ -125,6 +138,7 @@ await Launcher.InstallAsync( catch (Exception ex) { _logger.LogError(ex, "An error occurred during version installation."); + _notify.Complete(not.Id, false, "Installation Failed", ex); } } diff --git a/Emerald.CoreX/Helpers/BaseSettingsService.cs b/Emerald.CoreX/Helpers/BaseSettingsService.cs index f0649da2..71ba2279 100644 --- a/Emerald.CoreX/Helpers/BaseSettingsService.cs +++ b/Emerald.CoreX/Helpers/BaseSettingsService.cs @@ -1,8 +1,7 @@ using Microsoft.Extensions.Logging; using System.Text.Json; using System; -using System.Collections.Generic; -using System.Linq; +using System.IO; using System.Threading.Tasks; using Windows.Storage; @@ -11,6 +10,10 @@ namespace Emerald.Services; public class BaseSettingsService { private readonly ILogger _logger; + private readonly JsonSerializerOptions _jsonOptions = new() + { + WriteIndented = true // Makes file easier to read/debug + }; public event EventHandler? APINoMatch; @@ -18,27 +21,43 @@ public BaseSettingsService(ILogger logger) { _logger = logger; } - public void Set(string key, T value) + + public void Set(string key, T value, bool storeInFile = false) { try { - string json = JsonSerializer.Serialize(value); - ApplicationData.Current.LocalSettings.Values[key] = json; + if (storeInFile) + { + SaveToFileAsync(key, value).Wait(); + } + else + { + string json = JsonSerializer.Serialize(value, _jsonOptions); + ApplicationData.Current.LocalSettings.Values[key] = json; + } } catch (Exception ex) { - _logger.LogError(ex, "Error deserializing key '{Key}' from settings", key); + _logger.LogError(ex, "Error saving key '{Key}' to settings", key); } } - public T Get(string key, T defaultVal) + public T Get(string key, T defaultVal, bool loadFromFile = false) { try { - string json = ApplicationData.Current.LocalSettings.Values[key] as string; - if (!string.IsNullOrWhiteSpace(json)) + if (loadFromFile) + { + return LoadFromFileAsync(key, defaultVal).GetAwaiter().GetResult(); + } + else { - return JsonSerializer.Deserialize(json); + if (ApplicationData.Current.LocalSettings.Values.TryGetValue(key, out object? value) + && value is string json + && !string.IsNullOrWhiteSpace(json)) + { + return JsonSerializer.Deserialize(json) ?? defaultVal; + } } } catch (Exception ex) @@ -47,8 +66,48 @@ public T Get(string key, T defaultVal) } // Save default value if deserialization fails or key is missing - Set(key, defaultVal); + Set(key, defaultVal, storeInFile: loadFromFile); return defaultVal; } + + private async Task SaveToFileAsync(string key, T value) + { + try + { + string fileName = $"{key}.json"; + StorageFile file = await ApplicationData.Current.LocalFolder.CreateFileAsync( + fileName, CreationCollisionOption.ReplaceExisting); + + string json = JsonSerializer.Serialize(value, _jsonOptions); + await FileIO.WriteTextAsync(file, json); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error writing key '{Key}' to file storage", key); + throw; + } + } + + private async Task LoadFromFileAsync(string key, T defaultVal) + { + try + { + string fileName = $"{key}.json"; + StorageFile file = await ApplicationData.Current.LocalFolder.GetFileAsync(fileName); + + string json = await FileIO.ReadTextAsync(file); + return JsonSerializer.Deserialize(json) ?? defaultVal; + } + catch (FileNotFoundException) + { + // Return default if file doesn't exist + return defaultVal; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error reading key '{Key}' from file storage", key); + return defaultVal; + } + } } diff --git a/Emerald.CoreX/Helpers/SettingsKeys.cs b/Emerald.CoreX/Helpers/SettingsKeys.cs new file mode 100644 index 00000000..2b47bfb1 --- /dev/null +++ b/Emerald.CoreX/Helpers/SettingsKeys.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Emerald.CoreX.Helpers; + +public static class SettingsKeys +{ + public const string SavedGames = "SavedGames"; +} diff --git a/Emerald.CoreX/Installers/Fabric.cs b/Emerald.CoreX/Installers/Fabric.cs index 1ecf5987..c368dd5a 100644 --- a/Emerald.CoreX/Installers/Fabric.cs +++ b/Emerald.CoreX/Installers/Fabric.cs @@ -69,7 +69,7 @@ public async Task InstallAsync(MinecraftPath path, string mcversion, str { this.Log().LogWarning("Fabric Loader installation is not supported offline. sending the version name"); _notify.Complete(not.Id, false, "Fabric Loader installation is not supported offline. Passed the version name."); - return FabricInstaller.GetVersionName(mcversion, modversion ?? (await fabricInstaller.GetFirstLoader(modversion)).Version); + return FabricInstaller.GetVersionName(mcversion, modversion ?? (await fabricInstaller.GetFirstLoader(mcversion))?.Version ?? throw new NullReferenceException("No internet and no mod name found.")); } diff --git a/Emerald.CoreX/Installers/Forge.cs b/Emerald.CoreX/Installers/Forge.cs index c6c48a92..5529d644 100644 --- a/Emerald.CoreX/Installers/Forge.cs +++ b/Emerald.CoreX/Installers/Forge.cs @@ -63,10 +63,10 @@ public async Task InstallAsync(MinecraftPath path, string mcversion, str this.Log().LogInformation("Installing Forge Loader for {mcversion}", mcversion); try { - - var forge = new ForgeInstaller(new(path)); + //TODO: check whether ForgeInstaller supports offline installation + string? versionName = null; if (modversion == null) diff --git a/Emerald.CoreX/Installers/ModLoaderRouter.cs b/Emerald.CoreX/Installers/ModLoaderRouter.cs index eeadad64..59a981d1 100644 --- a/Emerald.CoreX/Installers/ModLoaderRouter.cs +++ b/Emerald.CoreX/Installers/ModLoaderRouter.cs @@ -16,7 +16,7 @@ public ModLoaderRouter() Installers = Ioc.Default.GetServices(); } - public async Task RouteAndInitializeAsync(MinecraftPath path, Versions.Version version) + public async Task RouteAndInitializeAsync(MinecraftPath path, Versions.Version version) { if (version.Type == Versions.Type.Vanilla) diff --git a/Emerald.CoreX/Installers/Quilt.cs b/Emerald.CoreX/Installers/Quilt.cs index 33459c96..2a567526 100644 --- a/Emerald.CoreX/Installers/Quilt.cs +++ b/Emerald.CoreX/Installers/Quilt.cs @@ -69,7 +69,7 @@ public async Task InstallAsync(MinecraftPath path, string mcversion, str { this.Log().LogWarning("Fabric Loader installation is not supported offline. sending the version name"); _notify.Complete(not.Id, false, "Fabric Loader installation is not supported offline. Passed the version name."); - return QuiltInstaller.GetVersionName(mcversion, modversion ?? (await QuiltInstaller.GetFirstLoader(modversion)).Version); + return QuiltInstaller.GetVersionName(mcversion, modversion ?? (await QuiltInstaller.GetFirstLoader(mcversion))?.Version ?? throw new NullReferenceException("No internet and no mod name found.")); } diff --git a/Emerald.CoreX/Versions/Version.cs b/Emerald.CoreX/Versions/Version.cs index acde23e9..cefc62ad 100644 --- a/Emerald.CoreX/Versions/Version.cs +++ b/Emerald.CoreX/Versions/Version.cs @@ -28,7 +28,7 @@ public class Version public CmlLib.Core.VersionMetadata.IVersionMetadata? Metadata { get; set; } - public string DisplayName; + public string DisplayName; //This Should be unique among all versions public override bool Equals(object? obj) { diff --git a/Emerald.sln b/Emerald.sln index bb5184e0..cddebe04 100644 --- a/Emerald.sln +++ b/Emerald.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.11.35103.136 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11018.127 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emerald", "Emerald\Emerald.csproj", "{9D3213F4-E514-4E7D-872A-725DB4872436}" EndProject @@ -32,10 +32,6 @@ Global Release|arm64 = Release|arm64 Release|x64 = Release|x64 Release|x86 = Release|x86 - SkipOld|Any CPU = SkipOld|Any CPU - SkipOld|arm64 = SkipOld|arm64 - SkipOld|x64 = SkipOld|x64 - SkipOld|x86 = SkipOld|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {9D3213F4-E514-4E7D-872A-725DB4872436}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -62,18 +58,6 @@ Global {9D3213F4-E514-4E7D-872A-725DB4872436}.Release|x86.ActiveCfg = Release|Any CPU {9D3213F4-E514-4E7D-872A-725DB4872436}.Release|x86.Build.0 = Release|Any CPU {9D3213F4-E514-4E7D-872A-725DB4872436}.Release|x86.Deploy.0 = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|Any CPU.ActiveCfg = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|Any CPU.Build.0 = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|Any CPU.Deploy.0 = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|arm64.ActiveCfg = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|arm64.Build.0 = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|arm64.Deploy.0 = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|x64.ActiveCfg = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|x64.Build.0 = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|x64.Deploy.0 = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|x86.ActiveCfg = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|x86.Build.0 = Release|Any CPU - {9D3213F4-E514-4E7D-872A-725DB4872436}.SkipOld|x86.Deploy.0 = Release|Any CPU {196FD412-FCBA-4266-A75E-5648F6AADDFB}.Debug|Any CPU.ActiveCfg = Debug|x64 {196FD412-FCBA-4266-A75E-5648F6AADDFB}.Debug|Any CPU.Build.0 = Debug|x64 {196FD412-FCBA-4266-A75E-5648F6AADDFB}.Debug|arm64.ActiveCfg = Debug|arm64 @@ -90,14 +74,6 @@ Global {196FD412-FCBA-4266-A75E-5648F6AADDFB}.Release|x64.Build.0 = Release|x64 {196FD412-FCBA-4266-A75E-5648F6AADDFB}.Release|x86.ActiveCfg = Release|x86 {196FD412-FCBA-4266-A75E-5648F6AADDFB}.Release|x86.Build.0 = Release|x86 - {196FD412-FCBA-4266-A75E-5648F6AADDFB}.SkipOld|Any CPU.ActiveCfg = Debug|x64 - {196FD412-FCBA-4266-A75E-5648F6AADDFB}.SkipOld|Any CPU.Build.0 = Debug|x64 - {196FD412-FCBA-4266-A75E-5648F6AADDFB}.SkipOld|arm64.ActiveCfg = Debug|arm64 - {196FD412-FCBA-4266-A75E-5648F6AADDFB}.SkipOld|arm64.Build.0 = Debug|arm64 - {196FD412-FCBA-4266-A75E-5648F6AADDFB}.SkipOld|x64.ActiveCfg = Debug|x64 - {196FD412-FCBA-4266-A75E-5648F6AADDFB}.SkipOld|x64.Build.0 = Debug|x64 - {196FD412-FCBA-4266-A75E-5648F6AADDFB}.SkipOld|x86.ActiveCfg = Debug|x86 - {196FD412-FCBA-4266-A75E-5648F6AADDFB}.SkipOld|x86.Build.0 = Debug|x86 {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.Debug|Any CPU.ActiveCfg = Debug|x64 {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.Debug|Any CPU.Build.0 = Debug|x64 {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.Debug|Any CPU.Deploy.0 = Debug|x64 @@ -122,18 +98,6 @@ Global {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.Release|x86.ActiveCfg = Release|x86 {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.Release|x86.Build.0 = Release|x86 {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.Release|x86.Deploy.0 = Release|x86 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|Any CPU.ActiveCfg = SkipOld|x64 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|Any CPU.Build.0 = SkipOld|x64 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|Any CPU.Deploy.0 = SkipOld|x64 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|arm64.ActiveCfg = SkipOld|arm64 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|arm64.Build.0 = SkipOld|arm64 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|arm64.Deploy.0 = SkipOld|arm64 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|x64.ActiveCfg = SkipOld|x64 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|x64.Build.0 = SkipOld|x64 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|x64.Deploy.0 = SkipOld|x64 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|x86.ActiveCfg = SkipOld|x86 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|x86.Build.0 = SkipOld|x86 - {59A6F3D1-B6E1-48AD-80D3-215DF2791AC1}.SkipOld|x86.Deploy.0 = SkipOld|x86 {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.Debug|Any CPU.Build.0 = Debug|Any CPU {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -150,14 +114,6 @@ Global {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.Release|x64.Build.0 = Release|Any CPU {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.Release|x86.ActiveCfg = Release|Any CPU {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.Release|x86.Build.0 = Release|Any CPU - {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.SkipOld|Any CPU.ActiveCfg = Debug|Any CPU - {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.SkipOld|Any CPU.Build.0 = Debug|Any CPU - {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.SkipOld|arm64.ActiveCfg = Debug|Any CPU - {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.SkipOld|arm64.Build.0 = Debug|Any CPU - {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.SkipOld|x64.ActiveCfg = Debug|Any CPU - {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.SkipOld|x64.Build.0 = Debug|Any CPU - {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.SkipOld|x86.ActiveCfg = Debug|Any CPU - {BE4AEE6B-3F2E-4FFB-940D-3E4916EAE913}.SkipOld|x86.Build.0 = Debug|Any CPU {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.Debug|Any CPU.Build.0 = Debug|Any CPU {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -174,14 +130,6 @@ Global {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.Release|x64.Build.0 = Release|Any CPU {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.Release|x86.ActiveCfg = Release|Any CPU {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.Release|x86.Build.0 = Release|Any CPU - {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.SkipOld|Any CPU.ActiveCfg = Debug|Any CPU - {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.SkipOld|Any CPU.Build.0 = Debug|Any CPU - {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.SkipOld|arm64.ActiveCfg = Debug|Any CPU - {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.SkipOld|arm64.Build.0 = Debug|Any CPU - {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.SkipOld|x64.ActiveCfg = Debug|Any CPU - {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.SkipOld|x64.Build.0 = Debug|Any CPU - {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.SkipOld|x86.ActiveCfg = Debug|Any CPU - {D103EFF9-90EA-49C7-9DD9-636E6056E9C3}.SkipOld|x86.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From c8943567ede38d272b48e81fe92c18cc1d85bba2 Mon Sep 17 00:00:00 2001 From: NoobNotFound <82730163+NoobNotFound@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:50:30 +0530 Subject: [PATCH 02/15] Refactor to use IBaseSettingsService interface Replaced direct dependency on BaseSettingsService with the IBaseSettingsService interface across the codebase to improve flexibility, modularity, and testability. Updated the `Core` class constructor, `SettingsService` constructor, and dependency injection registration in `App.xaml.cs` to use the interface. Introduced a new `IBaseSettingsService` interface defining the contract for `BaseSettingsService`, including methods for storing and retrieving values and an `APINoMatch` event. Refactored `BaseSettingsService` to implement the new interface. Updated `using` directives to remove unused namespaces and add necessary ones. Updated the project file to target .NET 9.0 for desktop applications. Minor non-functional changes were made to the project file. --- Emerald.CoreX/Core.cs | 2 +- Emerald.CoreX/Emerald.CoreX.csproj | 2 +- Emerald.CoreX/Helpers/BaseSettingsService.cs | 5 ++--- Emerald.CoreX/Services/IBaseSettingsService.cs | 13 +++++++++++++ Emerald/App.xaml.cs | 2 +- Emerald/Services/SettingsService.cs | 2 +- 6 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 Emerald.CoreX/Services/IBaseSettingsService.cs diff --git a/Emerald.CoreX/Core.cs b/Emerald.CoreX/Core.cs index 57956d5d..ea42b9ad 100644 --- a/Emerald.CoreX/Core.cs +++ b/Emerald.CoreX/Core.cs @@ -15,7 +15,7 @@ public record SavedGame(string Path, Versions.Version Version, Models.GameSettin public record SavedGameCollection(string BasePath, SavedGame[] Games); -public partial class Core(ILogger _logger, INotificationService _notify, BaseSettingsService settingsService) : ObservableObject +public partial class Core(ILogger _logger, INotificationService _notify, IBaseSettingsService settingsService) : ObservableObject { public const string GamesFolderName = "EmeraldGames"; public MinecraftLauncher Launcher { get; set; } diff --git a/Emerald.CoreX/Emerald.CoreX.csproj b/Emerald.CoreX/Emerald.CoreX.csproj index 4690b3e5..020b44f6 100644 --- a/Emerald.CoreX/Emerald.CoreX.csproj +++ b/Emerald.CoreX/Emerald.CoreX.csproj @@ -1,4 +1,4 @@ - + diff --git a/Emerald.CoreX/Helpers/BaseSettingsService.cs b/Emerald.CoreX/Helpers/BaseSettingsService.cs index 71ba2279..e97faaa5 100644 --- a/Emerald.CoreX/Helpers/BaseSettingsService.cs +++ b/Emerald.CoreX/Helpers/BaseSettingsService.cs @@ -1,13 +1,12 @@ using Microsoft.Extensions.Logging; -using System.Text.Json; using System; -using System.IO; +using System.Text.Json; using System.Threading.Tasks; using Windows.Storage; namespace Emerald.Services; -public class BaseSettingsService +public class BaseSettingsService : IBaseSettingsService { private readonly ILogger _logger; private readonly JsonSerializerOptions _jsonOptions = new() diff --git a/Emerald.CoreX/Services/IBaseSettingsService.cs b/Emerald.CoreX/Services/IBaseSettingsService.cs new file mode 100644 index 00000000..16f6a779 --- /dev/null +++ b/Emerald.CoreX/Services/IBaseSettingsService.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Logging; +using System; + +namespace Emerald.Services; + +public interface IBaseSettingsService +{ + event EventHandler? APINoMatch; + + void Set(string key, T value, bool storeInFile = false); + + T Get(string key, T defaultVal, bool loadFromFile = false); +} diff --git a/Emerald/App.xaml.cs b/Emerald/App.xaml.cs index eb9a60fa..480b7a4f 100644 --- a/Emerald/App.xaml.cs +++ b/Emerald/App.xaml.cs @@ -43,7 +43,7 @@ private void ConfigureServices(IServiceCollection services) //Settings services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); //Notifications services.AddSingleton(); diff --git a/Emerald/Services/SettingsService.cs b/Emerald/Services/SettingsService.cs index 3328d2a9..1f85b9d4 100644 --- a/Emerald/Services/SettingsService.cs +++ b/Emerald/Services/SettingsService.cs @@ -8,7 +8,7 @@ using Windows.Storage; namespace Emerald.Services; -public class SettingsService(BaseSettingsService _baseService, ILogger _logger) +public class SettingsService(IBaseSettingsService _baseService, ILogger _logger) { public Helpers.Settings.JSON.Settings Settings { get; private set; } From bef51cf113527cbf383050425dcc9196faefc759 Mon Sep 17 00:00:00 2001 From: codefactor-io Date: Fri, 26 Sep 2025 10:32:03 +0000 Subject: [PATCH 03/15] [CodeFactor] Apply fixes --- Emerald.CoreX/Game.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Emerald.CoreX/Game.cs b/Emerald.CoreX/Game.cs index ac8639e4..1d8da55f 100644 --- a/Emerald.CoreX/Game.cs +++ b/Emerald.CoreX/Game.cs @@ -80,7 +80,6 @@ public async Task InstallVersion(bool isOffline = false, bool showFileProgress = if (ver == null) { - _logger.LogWarning("Version {VersionType} {ModVersion} {BasedOn} not found.", Version.Type, Version.ModVersion, Version.BasedOn); _notify.Complete( @@ -93,7 +92,6 @@ public async Task InstallVersion(bool isOffline = false, bool showFileProgress = } if (isOffline) //checking if verison actually exists { - var vers = await Launcher.GetAllVersionsAsync(); var mver = vers.Where(x => x.Name == ver).First(); if (mver == null) From c047c5640a4abc9ad5466a07df82da8d7be3e836 Mon Sep 17 00:00:00 2001 From: NoobNotFound <82730163+NoobNotFound@users.noreply.github.com> Date: Fri, 26 Sep 2025 20:53:38 +0530 Subject: [PATCH 04/15] Refactor and enhance Minecraft settings UI Refactored `Extensions.Localize` to improve logging and fallback behavior. Enhanced `GameSettings` with new properties and `[NotifyPropertyChangedFor]` attributes. Introduced `MinecraftSettingsUC` user control to encapsulate Minecraft-related settings, replacing inline UI in `GeneralPage`. Simplified `ArgumentsListView` by removing `ArgTemplate` and directly binding to `ObservableCollection`. Improved dependency injection by registering `IBaseSettingsService` in `App.xaml.cs`. Upgraded `Uno.Sdk` to `6.2.39` in `global.json`. Removed legacy code and improved UI structure in `ArgumentsListView` and `MinecraftSettingsUC`. Enhanced logging and localization across the application. --- Emerald.CoreX/Helpers/Extensions.cs | 8 +- Emerald.CoreX/Models/GameSettings.cs | 7 +- Emerald/App.xaml.cs | 3 +- Emerald/Emerald.csproj | 6 + Emerald/Helpers/Extensions.cs | 30 ---- .../MarkupExtensions/LocalizeString.cs | 1 + Emerald/Helpers/MessageBox.cs | 1 + Emerald/Helpers/Settings/JSON.cs | 1 + Emerald/MainPage.xaml.cs | 1 + Emerald/UserControls/ArgumentsListView.xaml | 71 +++----- .../UserControls/ArgumentsListView.xaml.cs | 91 +++------- Emerald/UserControls/MinecraftSettingsUC.xaml | 163 ++++++++++++++++++ .../UserControls/MinecraftSettingsUC.xaml.cs | 115 ++++++++++++ Emerald/Views/Settings/AppearancePage.xaml.cs | 1 + Emerald/Views/Settings/GeneralPage.xaml | 150 +--------------- Emerald/Views/Settings/GeneralPage.xaml.cs | 41 +---- global.json | 4 +- 17 files changed, 362 insertions(+), 332 deletions(-) create mode 100644 Emerald/UserControls/MinecraftSettingsUC.xaml create mode 100644 Emerald/UserControls/MinecraftSettingsUC.xaml.cs diff --git a/Emerald.CoreX/Helpers/Extensions.cs b/Emerald.CoreX/Helpers/Extensions.cs index 58abe91b..f0195728 100644 --- a/Emerald.CoreX/Helpers/Extensions.cs +++ b/Emerald.CoreX/Helpers/Extensions.cs @@ -135,11 +135,15 @@ public static string Localize(this string resourceKey) return cached; } - string s = Windows.ApplicationModel.Resources.ResourceLoader + string? s = Windows.ApplicationModel.Resources.ResourceLoader .GetForViewIndependentUse() .GetString(resourceKey); + if (string.IsNullOrEmpty(s)) - throw new NullReferenceException("ResourceLoader.GetString returned empty/null"); + { + _logger.LogWarning("ResourceLoader.GetString returned empty/null, returning defaultkey"); + return resourceKey; + } cachedResources.AddOrUpdate(resourceKey, s, (_, _) => s); diff --git a/Emerald.CoreX/Models/GameSettings.cs b/Emerald.CoreX/Models/GameSettings.cs index 7da297d7..5c00645d 100644 --- a/Emerald.CoreX/Models/GameSettings.cs +++ b/Emerald.CoreX/Models/GameSettings.cs @@ -9,6 +9,7 @@ using CmlLib.Core.ProcessBuilder; using Emerald.CoreX.Helpers; using CommunityToolkit.Mvvm.ComponentModel; +using System.Runtime.Serialization; namespace Emerald.CoreX.Models; public partial class GameSettings : ObservableObject @@ -16,7 +17,8 @@ public partial class GameSettings : ObservableObject [JsonIgnore] public double MaxRAMinGB => Math.Round((MaximumRamMb / 1024.00), 2); - + + [NotifyPropertyChangedFor(nameof(MaxRAMinGB))] [ObservableProperty] private int _maximumRamMb; @@ -29,12 +31,15 @@ public partial class GameSettings : ObservableObject [ObservableProperty] private bool _isDemo; + [NotifyPropertyChangedFor(nameof(ScreenSizeStatus))] [ObservableProperty] private int _screenWidth; + [NotifyPropertyChangedFor(nameof(ScreenSizeStatus))] [ObservableProperty] private int _screenHeight; + [NotifyPropertyChangedFor(nameof(ScreenSizeStatus))] [ObservableProperty] private bool _fullScreen; diff --git a/Emerald/App.xaml.cs b/Emerald/App.xaml.cs index 480b7a4f..a93125d7 100644 --- a/Emerald/App.xaml.cs +++ b/Emerald/App.xaml.cs @@ -8,6 +8,7 @@ using System; using Emerald.Helpers; using CommunityToolkit.Mvvm.DependencyInjection; +using Emerald.CoreX.Helpers; namespace Emerald; public partial class App : Application @@ -43,7 +44,7 @@ private void ConfigureServices(IServiceCollection services) //Settings services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); //Notifications services.AddSingleton(); diff --git a/Emerald/Emerald.csproj b/Emerald/Emerald.csproj index 96edc158..2d43a499 100644 --- a/Emerald/Emerald.csproj +++ b/Emerald/Emerald.csproj @@ -120,4 +120,10 @@ + + + MSBuild:Compile + + + diff --git a/Emerald/Helpers/Extensions.cs b/Emerald/Helpers/Extensions.cs index 00fdd6bb..541fd2bc 100644 --- a/Emerald/Helpers/Extensions.cs +++ b/Emerald/Helpers/Extensions.cs @@ -143,36 +143,6 @@ public static string ToMD5(this string s) return sb.ToString(); } - public static string Localize(this string resourceKey) - { - var _logger = Ioc.Default.GetService>(); - try - { - _logger.LogDebug("Localizing {resourceKey}", resourceKey); - - if (cachedResources.TryGetValue(resourceKey, out string cached) && !string.IsNullOrEmpty(cached)) - { - _logger.LogDebug("Found cached {resourceKey} in cache", resourceKey); - return cached; - } - - string s = Windows.ApplicationModel.Resources.ResourceLoader - .GetForViewIndependentUse() - .GetString(resourceKey); - if (string.IsNullOrEmpty(s)) - throw new NullReferenceException("ResourceLoader.GetString returned empty/null"); - - cachedResources.AddOrUpdate(resourceKey, s, (_, _) => s); - - _logger.LogDebug("Localized {resourceKey} to {s}", resourceKey, s); - return string.IsNullOrEmpty(s) ? resourceKey : s; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to localize {resourceKey}", resourceKey); - return resourceKey; - } - } //public static string Localize(this Core.Localized resourceKey) => // resourceKey.ToString().Localize(); diff --git a/Emerald/Helpers/MarkupExtensions/LocalizeString.cs b/Emerald/Helpers/MarkupExtensions/LocalizeString.cs index 495182dc..e45bca8b 100644 --- a/Emerald/Helpers/MarkupExtensions/LocalizeString.cs +++ b/Emerald/Helpers/MarkupExtensions/LocalizeString.cs @@ -1,5 +1,6 @@ using Microsoft.UI.Xaml.Markup; using CommunityToolkit.Mvvm; +using Emerald.CoreX.Helpers; namespace Emerald.Helpers; [MarkupExtensionReturnType(ReturnType = typeof(string))] diff --git a/Emerald/Helpers/MessageBox.cs b/Emerald/Helpers/MessageBox.cs index a40ef08b..e5c54ed1 100644 --- a/Emerald/Helpers/MessageBox.cs +++ b/Emerald/Helpers/MessageBox.cs @@ -1,5 +1,6 @@ using CommonServiceLocator; using CommunityToolkit.Mvvm.DependencyInjection; +using Emerald.CoreX.Helpers; using Emerald.Helpers; using Emerald.Helpers.Enums; using Microsoft.UI; diff --git a/Emerald/Helpers/Settings/JSON.cs b/Emerald/Helpers/Settings/JSON.cs index f04fb80b..1aa91bd8 100644 --- a/Emerald/Helpers/Settings/JSON.cs +++ b/Emerald/Helpers/Settings/JSON.cs @@ -10,6 +10,7 @@ using CmlLib.Core.ProcessBuilder; using Emerald.CoreX.Models; using Emerald.CoreX.Store.Modrinth; +using Emerald.CoreX.Helpers; namespace Emerald.Helpers.Settings.JSON; public class JSON : Models.Model diff --git a/Emerald/MainPage.xaml.cs b/Emerald/MainPage.xaml.cs index b31f5ae9..3c015bc6 100644 --- a/Emerald/MainPage.xaml.cs +++ b/Emerald/MainPage.xaml.cs @@ -1,6 +1,7 @@ using System.Security.Cryptography.X509Certificates; using CommonServiceLocator; using CommunityToolkit.Mvvm.DependencyInjection; +using Emerald.CoreX.Helpers; using Emerald.CoreX.Installers; using Emerald.Helpers; using Emerald.Models; diff --git a/Emerald/UserControls/ArgumentsListView.xaml b/Emerald/UserControls/ArgumentsListView.xaml index ca819bcd..31249d27 100644 --- a/Emerald/UserControls/ArgumentsListView.xaml +++ b/Emerald/UserControls/ArgumentsListView.xaml @@ -2,61 +2,42 @@ x:Class="Emerald.UserControls.ArgumentsListView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:local="using:Emerald.UserControls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignHeight="300" - xmlns:models="using:Emerald.Models" d:DesignWidth="400"> + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - + Text="{x:Bind Mode=TwoWay}" /> + + + + diff --git a/Emerald/UserControls/ArgumentsListView.xaml.cs b/Emerald/UserControls/ArgumentsListView.xaml.cs index a8b6622a..13386def 100644 --- a/Emerald/UserControls/ArgumentsListView.xaml.cs +++ b/Emerald/UserControls/ArgumentsListView.xaml.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Runtime.InteropServices.WindowsRuntime; -using Windows.Foundation; -using Windows.Foundation.Collections; +using CommunityToolkit.Mvvm.DependencyInjection; +using Emerald.CoreX.Helpers; +using Emerald.CoreX.Models; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; @@ -12,83 +14,42 @@ using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Navigation; -using System.Collections.ObjectModel; -using Emerald.Models; -using CommonServiceLocator; -using CommunityToolkit.Mvvm.DependencyInjection; - - +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.Storage.Pickers; namespace Emerald.UserControls; - -//Copied from Emerald.UWP public sealed partial class ArgumentsListView : UserControl { - private int count = 0; - public ObservableCollection Args { get; set; } - - public ArgumentsListView() - { - InitializeComponent(); - view.ItemsSource = Source; - UpdateSource(); - } - - private ObservableCollection Source = new(); - private void btnAdd_Click(object sender, RoutedEventArgs e) + public ObservableCollection Args { - count++; - var r = new ArgTemplate { Arg = "", Count = count }; - Source.Add(r); - UpdateMainSource(); - view.SelectedItem = r; - } - public void UpdateSource() - { - Source.Clear(); - if (Args != null) - { - foreach (var item in Args) - { - count++; - var r = new ArgTemplate { Arg = item, Count = count }; - r.PropertyChanged += (_, _) => - { - UpdateMainSource(); - }; - Source.Add(r); - } - } - btnRemove.IsEnabled = Source.Any(); + get => (ObservableCollection)GetValue(ArgsProperty); + set => SetValue(ArgsProperty, value); } - private void UpdateMainSource() - { - Args.Clear(); - Args.AddRange(Source.Select(x=> x.Arg)); - } - private void btnRemove_Click(object sender, RoutedEventArgs e) - { - foreach (var item in view.SelectedItems) - { - Source.Remove((ArgTemplate)item); - } - UpdateMainSource(); - } + public static readonly DependencyProperty ArgsProperty = + DependencyProperty.Register( + nameof(Args), + typeof(ObservableCollection), + typeof(ArgumentsListView), + new PropertyMetadata(new ObservableCollection()) + ); - private void TextBox_PointerPressed(object sender, PointerRoutedEventArgs e) + public ArgumentsListView() { - view.SelectedIndex = Source.IndexOf(Source.FirstOrDefault(x => x.Count == ((sender as FrameworkElement).DataContext as ArgTemplate).Count)); + InitializeComponent(); + view.ItemsSource = Args; } - private void TextBox_TextChanged(object sender, TextChangedEventArgs e) + private void btnAdd_Click(object sender, RoutedEventArgs e) { - view.SelectedIndex = Source.IndexOf(Source.FirstOrDefault(x => x.Count == ((sender as FrameworkElement).DataContext as ArgTemplate).Count)); - UpdateMainSource(); + Args.Add(string.Empty); + view.SelectedIndex = Args.Count - 1; } - private void TextBox_GotFocus(object sender, RoutedEventArgs e) + private void btnRemove_Click(object sender, RoutedEventArgs e) { - view.SelectedIndex = Source.IndexOf(Source.FirstOrDefault(x => x.Count == ((sender as FrameworkElement).DataContext as ArgTemplate).Count)); + foreach (var selected in view.SelectedItems.Cast().ToList()) + Args.Remove(selected); } private void view_SelectionChanged(object sender, SelectionChangedEventArgs e) diff --git a/Emerald/UserControls/MinecraftSettingsUC.xaml b/Emerald/UserControls/MinecraftSettingsUC.xaml new file mode 100644 index 00000000..cac1e5ef --- /dev/null +++ b/Emerald/UserControls/MinecraftSettingsUC.xaml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + GB + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Emerald/UserControls/MinecraftSettingsUC.xaml.cs b/Emerald/UserControls/MinecraftSettingsUC.xaml.cs new file mode 100644 index 00000000..be6f88d5 --- /dev/null +++ b/Emerald/UserControls/MinecraftSettingsUC.xaml.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using CommunityToolkit.Mvvm.DependencyInjection; +using Emerald.CoreX.Helpers; +using Emerald.CoreX.Models; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.Storage.Pickers; + +// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236 + +namespace Emerald.UserControls; +public sealed partial class MinecraftSettingsUC : UserControl +{ + public bool ShowMainPathEditor + { + get { return (bool)GetValue(ShowMainPathEditorProperty); } + set { SetValue(ShowMainPathEditorProperty, value); } + } + + // Using a DependencyProperty as the backing store for ShowMainPathEditor. This enables animation, styling, binding, etc... + public static readonly DependencyProperty ShowMainPathEditorProperty = + DependencyProperty.Register("ShowMainPathEditor", typeof(bool), typeof(MinecraftSettingsUC), new PropertyMetadata(null)); + + public GameSettings GameSettings + { + get { return (GameSettings)GetValue(GameSettingsProperty); } + set { SetValue(GameSettingsProperty, value); } + } + + // Using a DependencyProperty as the backing store for GameSettings. This enables animation, styling, binding, etc... + public static readonly DependencyProperty GameSettingsProperty = + DependencyProperty.Register("GameSettings", typeof(GameSettings), typeof(MinecraftSettingsUC), new PropertyMetadata(null)); + + + private readonly Services.SettingsService SS; + public MinecraftSettingsUC() + { + SS = Ioc.Default.GetService(); + this.InitializeComponent(); + } + + private async void btnChangeMPath_Click(object sender, RoutedEventArgs e) + { + this.Log().LogInformation("Choosing MC path"); + string path; + + var fop = new FolderPicker + { + CommitButtonText = "Select".Localize() + }; + fop.FileTypeFilter.Add("*"); + + if (DirectResoucres.Platform == "Windows") + WinRT.Interop.InitializeWithWindow.Initialize(fop, WinRT.Interop.WindowNative.GetWindowHandle(App.Current.MainWindow)); + + var f = await fop.PickSingleFolderAsync(); + + if (f != null) + path = f.Path; + else + { + this.Log().LogInformation("User did not select a MC path"); + return; + } + + this.Log().LogInformation("New Minecraft path: {path}", path); + SS.Settings.Minecraft.Path = path; + + await Ioc.Default.GetService().InitializeAndRefresh(new(path)); + } + + private void AdjustRam(int delta) + { + int newValue = GameSettings.MaximumRamMb + delta; + + GameSettings.MaximumRamMb = Math.Clamp( + newValue, + DirectResoucres.MinRAM, + DirectResoucres.MaxRAM + ); + } + + private void btnRamPlus_Click(object sender, RoutedEventArgs e) => + AdjustRam(64); + + private void btnRamMinus_Click(object sender, RoutedEventArgs e) => + AdjustRam(-64); + + private void btnAutoRAM_Click(object sender, RoutedEventArgs e) + { + int sysMax = DirectResoucres.MaxRAM; + + int recommended = sysMax switch + { + <= 4096 => DirectResoucres.MinRAM, // low-memory PCs + <= 8192 => sysMax / 3, // mid-range + <= 16384 => sysMax / 2, // standard gaming rigs + _ => (int)(sysMax * 0.65) // high RAM → ~65% + }; + + GameSettings.MaximumRamMb = recommended; + } + +} diff --git a/Emerald/Views/Settings/AppearancePage.xaml.cs b/Emerald/Views/Settings/AppearancePage.xaml.cs index 0d751b58..efc491ac 100644 --- a/Emerald/Views/Settings/AppearancePage.xaml.cs +++ b/Emerald/Views/Settings/AppearancePage.xaml.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices.WindowsRuntime; using CommonServiceLocator; using CommunityToolkit.Mvvm.DependencyInjection; +using Emerald.CoreX.Helpers; using Emerald.Helpers; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; diff --git a/Emerald/Views/Settings/GeneralPage.xaml b/Emerald/Views/Settings/GeneralPage.xaml index 3803e660..9820b4e0 100644 --- a/Emerald/Views/Settings/GeneralPage.xaml +++ b/Emerald/Views/Settings/GeneralPage.xaml @@ -1,149 +1,7 @@  - - - - - - - - - - - - - - GB - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + mc:Ignorable="d" xmlns:helpers="using:Emerald.Helpers" + xmlns:cn="using:CommunityToolkit.WinUI.Controls" xmlns:uc="using:Emerald.UserControls"> + + diff --git a/Emerald/Views/Settings/GeneralPage.xaml.cs b/Emerald/Views/Settings/GeneralPage.xaml.cs index efbe333c..3aee34b9 100644 --- a/Emerald/Views/Settings/GeneralPage.xaml.cs +++ b/Emerald/Views/Settings/GeneralPage.xaml.cs @@ -34,46 +34,7 @@ public GeneralPage() { SS = Ioc.Default.GetService(); this.InitializeComponent(); + MCUC.GameSettings = SS.Settings.GameSettings; } - private async void btnChangeMPath_Click(object sender, RoutedEventArgs e) - { - this.Log().LogInformation("Choosing MC path"); - string path; - - var fop = new FolderPicker - { - CommitButtonText = "Select".Localize() - }; - fop.FileTypeFilter.Add("*"); - - if(DirectResoucres.Platform == "Windows") - WinRT.Interop.InitializeWithWindow.Initialize(fop, WinRT.Interop.WindowNative.GetWindowHandle(App.Current.MainWindow)); - - var f = await fop.PickSingleFolderAsync(); - - if (f != null) - path = f.Path; - else - { - this.Log().LogInformation("User did not select a MC path"); - return; - } - - this.Log().LogInformation("New Minecraft path: {path}",path); - SS.Settings.Minecraft.Path = path; - - await Ioc.Default.GetService().InitializeAndRefresh(new(path)); - } - - private void btnRamPlus_Click(object sender, RoutedEventArgs e) => - SS.Settings.GameSettings.MaximumRamMb = SS.Settings.Minecraft.RAM + (SS.Settings.Minecraft.RAM >= DirectResoucres.MaxRAM ? 0 : (DirectResoucres.MaxRAM - SS.Settings.GameSettings.MaximumRamMb >= 50 ? 50 : DirectResoucres.MaxRAM - SS.Settings.GameSettings.MaximumRamMb)); - - - private void btnRamMinus_Click(object sender, RoutedEventArgs e) => - SS.Settings.Minecraft.RAM = SS.Settings.GameSettings.MaximumRamMb - (SS.Settings.GameSettings.MaximumRamMb <= DirectResoucres.MinRAM ? 0 : 50); - - - private void btnAutoRAM_Click(object sender, RoutedEventArgs e) => - SS.Settings.Minecraft.RAM = DirectResoucres.MaxRAM / 2; } diff --git a/global.json b/global.json index 8623b085..456efb61 100644 --- a/global.json +++ b/global.json @@ -1,8 +1,8 @@ { "msbuild-sdks": { - "Uno.Sdk": "6.0.96" + "Uno.Sdk": "6.2.39" }, "sdk": { "allowPrerelease": false } -} \ No newline at end of file +} From a70388095c68bec87282f026668bc52b4d1fff7e Mon Sep 17 00:00:00 2001 From: NoobNotFound Date: Sat, 27 Sep 2025 20:50:05 +0530 Subject: [PATCH 05/15] Refactor localization keys to avoid a bug Replaces string-based argument handling with a LaunchArg model and synchronizes external/internal collections in ArgumentsListView. Updates all localization markup extensions to use KeyName instead of Name. Refactors MinecraftSettingsUC to use ShowMainSettings and adds copy path functionality. Updates target frameworks to net9.0-desktop and net9.0-windows10.0.26100. Cleans up code and improves bindings and UI logic across settings and controls. --- .vscode/launch.json | 3 +- .vscode/tasks.json | 4 +- Emerald.CoreX/Emerald.CoreX.csproj | 1 - Emerald/Emerald.csproj | 3 +- .../MarkupExtensions/LocalizeString.cs | 4 +- Emerald/Models/ArgTemplate.cs | 9 +- Emerald/UserControls/ArgumentsListView.xaml | 32 +++--- .../UserControls/ArgumentsListView.xaml.cs | 100 ++++++++++++++---- Emerald/UserControls/MinecraftSettingsUC.xaml | 89 +++++++++------- .../UserControls/MinecraftSettingsUC.xaml.cs | 93 ++++++++-------- .../UserControls/NotificationListControl.xaml | 12 +-- Emerald/Views/Settings/AppearancePage.xaml | 38 +++---- Emerald/Views/Settings/GeneralPage.xaml | 2 +- Emerald/Views/Settings/SettingsPage.xaml | 6 +- 14 files changed, 235 insertions(+), 161 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 1d6e6504..5c5560ed 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,7 @@ // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md "version": "0.2.0", "configurations": [ + { "name": "Uno Platform Mobile", "type": "Uno", @@ -20,7 +21,7 @@ "request": "launch", "preLaunchTask": "build-desktop", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/Emerald/bin/Debug/net8.0-desktop/Emerald.dll", + "program": "${workspaceFolder}/Emerald/bin/Debug/net9.0-desktop/Emerald.dll", "args": [], "launchSettingsProfile": "Emerald (Desktop)", "env": { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ac5be062..7e63da46 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -9,7 +9,7 @@ "build", "${workspaceFolder}/Emerald/Emerald.csproj", "/property:GenerateFullPaths=true", - "/property:TargetFramework=net8.0-desktop", + "/property:TargetFramework=net9.0-desktop", "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" @@ -22,7 +22,7 @@ "publish", "${workspaceFolder}/Emerald/Emerald.csproj", "/property:GenerateFullPaths=true", - "/property:TargetFramework=net8.0-desktop", + "/property:TargetFramework=net9.0-desktop", "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" diff --git a/Emerald.CoreX/Emerald.CoreX.csproj b/Emerald.CoreX/Emerald.CoreX.csproj index 020b44f6..3b7a82c1 100644 --- a/Emerald.CoreX/Emerald.CoreX.csproj +++ b/Emerald.CoreX/Emerald.CoreX.csproj @@ -5,7 +5,6 @@ net9.0-desktop; $(TargetFrameworks);net9.0-windows10.0.22621 - $(TargetFrameworks);net9.0-maccatalyst true Library diff --git a/Emerald/Emerald.csproj b/Emerald/Emerald.csproj index 2d43a499..85b99e2a 100644 --- a/Emerald/Emerald.csproj +++ b/Emerald/Emerald.csproj @@ -3,8 +3,7 @@ net9.0-desktop; - $(TargetFrameworks);net9.0-windows10.0.22621 - $(TargetFrameworks);net9.0-maccatalyst + $(TargetFrameworks);net9.0-windows10.0.26100 Exe true diff --git a/Emerald/Helpers/MarkupExtensions/LocalizeString.cs b/Emerald/Helpers/MarkupExtensions/LocalizeString.cs index e45bca8b..8812d857 100644 --- a/Emerald/Helpers/MarkupExtensions/LocalizeString.cs +++ b/Emerald/Helpers/MarkupExtensions/LocalizeString.cs @@ -6,8 +6,8 @@ namespace Emerald.Helpers; [MarkupExtensionReturnType(ReturnType = typeof(string))] public sealed class Localize : MarkupExtension { - public string Name { get; set; } + public string KeyName { get; set; } protected override object ProvideValue() - => Name.Localize(); + => KeyName.Localize(); } diff --git a/Emerald/Models/ArgTemplate.cs b/Emerald/Models/ArgTemplate.cs index ee708800..f43a5e2d 100644 --- a/Emerald/Models/ArgTemplate.cs +++ b/Emerald/Models/ArgTemplate.cs @@ -1,13 +1,8 @@ -using CommunityToolkit.Mvvm.ComponentModel; - namespace Emerald.Models; -//Copied from Emerald.UWP [ObservableObject] -public partial class ArgTemplate +public partial class LaunchArg { [ObservableProperty] - private string arg; - - public int Count { get; set; } + public string value = string.Empty; } diff --git a/Emerald/UserControls/ArgumentsListView.xaml b/Emerald/UserControls/ArgumentsListView.xaml index 31249d27..c0022268 100644 --- a/Emerald/UserControls/ArgumentsListView.xaml +++ b/Emerald/UserControls/ArgumentsListView.xaml @@ -1,12 +1,13 @@  + x:Class="Emerald.UserControls.ArgumentsListView" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:models="using:Emerald.Models" + mc:Ignorable="d" + d:DesignHeight="300" + d:DesignWidth="400"> @@ -27,14 +28,15 @@ - + + Margin="-9,-4" + VerticalAlignment="Stretch" + Background="Transparent" + BorderThickness="0" + PlaceholderText="Empty" + Text="{x:Bind Value, Mode=TwoWay}" + GotFocus="TextBox_GotFocus" /> diff --git a/Emerald/UserControls/ArgumentsListView.xaml.cs b/Emerald/UserControls/ArgumentsListView.xaml.cs index 13386def..077f2e02 100644 --- a/Emerald/UserControls/ArgumentsListView.xaml.cs +++ b/Emerald/UserControls/ArgumentsListView.xaml.cs @@ -1,25 +1,16 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.IO; +using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; -using CommunityToolkit.Mvvm.DependencyInjection; -using Emerald.CoreX.Helpers; -using Emerald.CoreX.Models; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Controls.Primitives; -using Microsoft.UI.Xaml.Data; -using Microsoft.UI.Xaml.Input; -using Microsoft.UI.Xaml.Media; -using Microsoft.UI.Xaml.Navigation; -using Windows.Foundation; -using Windows.Foundation.Collections; -using Windows.Storage.Pickers; +using Emerald.Models; + namespace Emerald.UserControls; + public sealed partial class ArgumentsListView : UserControl { + // Public API → strings public ObservableCollection Args { get => (ObservableCollection)GetValue(ArgsProperty); @@ -31,29 +22,96 @@ public ObservableCollection Args nameof(Args), typeof(ObservableCollection), typeof(ArgumentsListView), - new PropertyMetadata(new ObservableCollection()) + new PropertyMetadata(new ObservableCollection(), OnArgsChanged) ); + // Internal collection for binding + private readonly ObservableCollection _internal = new(); + public ArgumentsListView() { InitializeComponent(); - view.ItemsSource = Args; + view.ItemsSource = _internal; + } + + private static void OnArgsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = (ArgumentsListView)d; + + if (e.OldValue is ObservableCollection oldCol) + oldCol.CollectionChanged -= control.ExternalChanged; + + if (e.NewValue is ObservableCollection newCol) + { + newCol.CollectionChanged += control.ExternalChanged; + control.SyncFromExternal(); + } + } + + // Sync external → internal + private void SyncFromExternal() + { + _internal.Clear(); + + if (Args == null) return; + + foreach (var s in Args) + { + var arg = new LaunchArg { Value = s }; + arg.PropertyChanged += InternalArgChanged; + _internal.Add(arg); + } } + // Sync internal → external + private void SyncToExternal() + { + if (Args == null) return; + + Args.CollectionChanged -= ExternalChanged; + Args.Clear(); + foreach (var arg in _internal) + Args.Add(arg.Value); + Args.CollectionChanged += ExternalChanged; + } + + private void InternalArgChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(LaunchArg.Value)) + SyncToExternal(); + } + + private void ExternalChanged(object? sender, NotifyCollectionChangedEventArgs e) => + SyncFromExternal(); + private void btnAdd_Click(object sender, RoutedEventArgs e) { - Args.Add(string.Empty); - view.SelectedIndex = Args.Count - 1; + var newArg = new LaunchArg { Value = string.Empty }; + newArg.PropertyChanged += InternalArgChanged; + _internal.Add(newArg); + view.SelectedIndex = _internal.Count - 1; + SyncToExternal(); } private void btnRemove_Click(object sender, RoutedEventArgs e) { - foreach (var selected in view.SelectedItems.Cast().ToList()) - Args.Remove(selected); + foreach (var selected in view.SelectedItems.Cast().ToList()) + { + selected.PropertyChanged -= InternalArgChanged; + _internal.Remove(selected); + } + + SyncToExternal(); } private void view_SelectionChanged(object sender, SelectionChangedEventArgs e) { btnRemove.IsEnabled = view.SelectedItems.Any(); } + private void TextBox_GotFocus(object sender, RoutedEventArgs e) + { + if (sender is TextBox tb && tb.DataContext is LaunchArg arg) + view.SelectedItem = arg; + } + } diff --git a/Emerald/UserControls/MinecraftSettingsUC.xaml b/Emerald/UserControls/MinecraftSettingsUC.xaml index cac1e5ef..b2c455ad 100644 --- a/Emerald/UserControls/MinecraftSettingsUC.xaml +++ b/Emerald/UserControls/MinecraftSettingsUC.xaml @@ -7,33 +7,46 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" xmlns:helpers="using:Emerald.Helpers" xmlns:cn="using:CommunityToolkit.WinUI.Controls" + xmlns:conv="using:Emerald.Helpers.Converters" d:DesignHeight="300" d:DesignWidth="400"> + + + + + - + - + Header="{helpers:Localize KeyName=MinecraftPath}" + Description="{helpers:Localize KeyName=MinecraftPathDescription}"> + - + + + + + + + + Header="{helpers:Localize KeyName=RAM}" + Description="{helpers:Localize KeyName=RAMDescription}"> @@ -56,17 +69,17 @@ + Description="{helpers:Localize KeyName=DownloadSettingsDescription}"> + Header="{helpers:Localize KeyName=AssetsCheck}"> + Header="{helpers:Localize KeyName=HashCheck}"> @@ -74,44 +87,44 @@ + OffContent="{helpers:Localize KeyName=No}" + OnContent="{helpers:Localize KeyName=Yes}" /> + Text="{helpers:Localize KeyName=AdvancedMinecraftSettings}" /> + OffContent="{helpers:Localize KeyName=No}" + OnContent="{helpers:Localize KeyName=Yes}" /> + OffContent="{helpers:Localize KeyName=No}" + OnContent="{helpers:Localize KeyName=Yes}" /> + Header="{helpers:Localize KeyName=Width}"> + PlaceholderText="{helpers:Localize KeyName=Default}" /> + Header="{helpers:Localize KeyName=Height}"> + PlaceholderText="{helpers:Localize KeyName=Default}" /> + Header="{helpers:Localize KeyName=FullScreen}"> (bool)GetValue(ShowMainSettingsProperty); + set => SetValue(ShowMainSettingsProperty, value); } - // Using a DependencyProperty as the backing store for ShowMainPathEditor. This enables animation, styling, binding, etc... - public static readonly DependencyProperty ShowMainPathEditorProperty = - DependencyProperty.Register("ShowMainPathEditor", typeof(bool), typeof(MinecraftSettingsUC), new PropertyMetadata(null)); + public static readonly DependencyProperty ShowMainSettingsProperty = + DependencyProperty.Register(nameof(ShowMainSettings), typeof(bool), typeof(MinecraftSettingsUC), new PropertyMetadata(false)); public GameSettings GameSettings { - get { return (GameSettings)GetValue(GameSettingsProperty); } - set { SetValue(GameSettingsProperty, value); } + get => (GameSettings)GetValue(GameSettingsProperty); + set => SetValue(GameSettingsProperty, value); } - // Using a DependencyProperty as the backing store for GameSettings. This enables animation, styling, binding, etc... public static readonly DependencyProperty GameSettingsProperty = - DependencyProperty.Register("GameSettings", typeof(GameSettings), typeof(MinecraftSettingsUC), new PropertyMetadata(null)); + DependencyProperty.Register(nameof(GameSettings), typeof(GameSettings), typeof(MinecraftSettingsUC), new PropertyMetadata(null)); + // expose SS as a public property if you bind to it from x:Bind in XAML, otherwise x:Bind may not resolve. + public Services.SettingsService SS { get; } - private readonly Services.SettingsService SS; public MinecraftSettingsUC() { + InitializeComponent(); SS = Ioc.Default.GetService(); - this.InitializeComponent(); } - private async void btnChangeMPath_Click(object sender, RoutedEventArgs e) + // helper method so we can call from multiple handlers + private async Task PickMinecraftFolderAsync() { this.Log().LogInformation("Choosing MC path"); - string path; - var fop = new FolderPicker - { - CommitButtonText = "Select".Localize() - }; + var fop = new FolderPicker { CommitButtonText = "Select".Localize() }; fop.FileTypeFilter.Add("*"); if (DirectResoucres.Platform == "Windows") @@ -66,20 +52,45 @@ private async void btnChangeMPath_Click(object sender, RoutedEventArgs e) var f = await fop.PickSingleFolderAsync(); - if (f != null) - path = f.Path; - else + if (f == null) { this.Log().LogInformation("User did not select a MC path"); return; } + var path = f.Path; this.Log().LogInformation("New Minecraft path: {path}", path); SS.Settings.Minecraft.Path = path; await Ioc.Default.GetService().InitializeAndRefresh(new(path)); } + private async void btnChangeMPath_Click(object sender, RoutedEventArgs e) + { + await PickMinecraftFolderAsync(); + } + + private async void ChangePath_OnClick(object sender, RoutedEventArgs e) + { + // call the same helper instead of calling the handler with nulls + await PickMinecraftFolderAsync(); + } + + private void CopyPath_OnClick(object sender, RoutedEventArgs e) + { + try + { + var path = ShowMainSettings ? SS.Settings.Minecraft.Path : Path.Combine(SS.Settings.Minecraft.Path, CoreX.Core.GamesFolderName); + var dp = new DataPackage(); + dp.SetText(path); + Clipboard.SetContent(dp); + } + catch (Exception ex) + { + this.Log().LogError(ex, "Failed to copy path"); + } + } + private void AdjustRam(int delta) { int newValue = GameSettings.MaximumRamMb + delta; @@ -91,11 +102,8 @@ private void AdjustRam(int delta) ); } - private void btnRamPlus_Click(object sender, RoutedEventArgs e) => - AdjustRam(64); - - private void btnRamMinus_Click(object sender, RoutedEventArgs e) => - AdjustRam(-64); + private void btnRamPlus_Click(object sender, RoutedEventArgs e) => AdjustRam(64); + private void btnRamMinus_Click(object sender, RoutedEventArgs e) => AdjustRam(-64); private void btnAutoRAM_Click(object sender, RoutedEventArgs e) { @@ -103,13 +111,12 @@ private void btnAutoRAM_Click(object sender, RoutedEventArgs e) int recommended = sysMax switch { - <= 4096 => DirectResoucres.MinRAM, // low-memory PCs - <= 8192 => sysMax / 3, // mid-range - <= 16384 => sysMax / 2, // standard gaming rigs - _ => (int)(sysMax * 0.65) // high RAM → ~65% + <= 4096 => DirectResoucres.MinRAM, + <= 8192 => sysMax / 3, + <= 16384 => sysMax / 2, + _ => (int)(sysMax * 0.65) }; GameSettings.MaximumRamMb = recommended; } - } diff --git a/Emerald/UserControls/NotificationListControl.xaml b/Emerald/UserControls/NotificationListControl.xaml index 4604c9d1..3288540b 100644 --- a/Emerald/UserControls/NotificationListControl.xaml +++ b/Emerald/UserControls/NotificationListControl.xaml @@ -40,7 +40,7 @@ - @@ -63,8 +63,8 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Emerald/Views/AccountsPage.xaml.cs b/Emerald/Views/AccountsPage.xaml.cs new file mode 100644 index 00000000..c8b3dfbb --- /dev/null +++ b/Emerald/Views/AccountsPage.xaml.cs @@ -0,0 +1,73 @@ +using System; +using CommunityToolkit.Mvvm.DependencyInjection; +using Emerald.CoreX.Helpers; +using Emerald.CoreX.Models; +using Emerald.Helpers; +using Emerald.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace Emerald.Views; + +public sealed partial class AccountsPage : Page +{ + public AccountsPageViewModel ViewModel { get; } + private TextBox OfflineUserNameTextBox; + public AccountsPage() + { + ViewModel = Ioc.Default.GetService(); + this.InitializeComponent(); + } + + protected override async void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + OfflineUserNameTextBox = new TextBox() + { + Header = "Username".Localize(), + PlaceholderText = "EnterYourDesiredUsername".Localize() + }; + await ViewModel.InitializeCommand.ExecuteAsync(null); + } + + private async void AddOfflineAccount_Click(object sender, RoutedEventArgs e) + { + // Clear previous username before showing + OfflineUserNameTextBox.Text = string.Empty; + var dia = OfflineUserNameTextBox.ToContentDialog("AddOfflineAccount".Localize(),PrimaryButtonText: "Add".Localize(), closebtnText: "Cancel".Localize(),defaultButton: ContentDialogButton.Primary); + + dia.PrimaryButtonClick += AddOfflineAccountDialog_PrimaryButtonClick; + + await dia.ShowAsync(); + + dia.PrimaryButtonClick -= AddOfflineAccountDialog_PrimaryButtonClick; + } + + private void AddOfflineAccountDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) + { + ViewModel.OfflineUsername = OfflineUserNameTextBox.Text.Trim(); + ViewModel.AddOfflineAccountCommand.Execute(null); + } + + private async void RemoveAccount_Click(object sender, RoutedEventArgs e) + { + if ((sender as FrameworkElement)?.Tag is not EAccount account) return; + + var confirmationDialog = new ContentDialog + { + XamlRoot = this.XamlRoot, + Title = "Remove Account", + Content = $"Are you sure you want to remove the account '{account.Name}'? This action cannot be undone.", + PrimaryButtonText = "Remove", + CloseButtonText = "Cancel", + DefaultButton = ContentDialogButton.Close + }; + + var result = await confirmationDialog.ShowAsync(); + if (result == ContentDialogResult.Primary) + { + await ViewModel.RemoveAccountCommand.ExecuteAsync(account); + } + } +} From 7f51eaa5167e1e0f70a1cd8aa8168c75426556ce Mon Sep 17 00:00:00 2001 From: NoobNotFound Date: Mon, 29 Sep 2025 22:29:18 +0530 Subject: [PATCH 11/15] Add RealVersion property and update game launch logic Introduces the RealVersion property to the Version class and ensures it is set during game installation. Updates game launch logic to use RealVersion instead of BasedOn, and saves games after installation. Also adds an inverse BoolToVisibility converter in AccountsPage and changes game installation command execution to run in a background task. --- Emerald.CoreX/Core.cs | 2 ++ Emerald.CoreX/Game.cs | 3 +++ Emerald.CoreX/Versions/Version.cs | 1 + Emerald/ViewModels/GamesPageViewModel.cs | 2 +- Emerald/Views/AccountsPage.xaml | 3 ++- Emerald/Views/GamesPage.xaml.cs | 2 +- 6 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Emerald.CoreX/Core.cs b/Emerald.CoreX/Core.cs index 7a0efe17..9e984baf 100644 --- a/Emerald.CoreX/Core.cs +++ b/Emerald.CoreX/Core.cs @@ -185,6 +185,8 @@ await game.InstallVersion( isOffline: IsOfflineMode, showFileProgress: showFileprog ); + + SaveGames(); } catch (Exception ex) diff --git a/Emerald.CoreX/Game.cs b/Emerald.CoreX/Game.cs index 1e2b60ed..c2bf2116 100644 --- a/Emerald.CoreX/Game.cs +++ b/Emerald.CoreX/Game.cs @@ -95,6 +95,9 @@ public async Task InstallVersion(bool isOffline = false, bool showFileProgress = return; } + + Version.RealVersion = ver; + if (isOffline) //checking if verison actually exists { var vers = await Launcher.GetAllVersionsAsync(); diff --git a/Emerald.CoreX/Versions/Version.cs b/Emerald.CoreX/Versions/Version.cs index 99025472..a038f18e 100644 --- a/Emerald.CoreX/Versions/Version.cs +++ b/Emerald.CoreX/Versions/Version.cs @@ -26,6 +26,7 @@ public class Version public string? ModVersion { get; set; } + public string? RealVersion { get; set; } public DateTime ReleaseTime { get;set; } public string DisplayName { get; set; } //This Should be unique among all versions diff --git a/Emerald/ViewModels/GamesPageViewModel.cs b/Emerald/ViewModels/GamesPageViewModel.cs index 6e8580d6..c5e3a150 100644 --- a/Emerald/ViewModels/GamesPageViewModel.cs +++ b/Emerald/ViewModels/GamesPageViewModel.cs @@ -353,7 +353,7 @@ private async Task LaunchGameAsync(Game? game) return; } var session = await _accountService.AuthenticateAccountAsync(account); - var process = await game.BuildProcess(game.Version.BasedOn, session); + var process = await game.BuildProcess(game.Version.RealVersion, session); process.Start(); _notificationService.Info("GameLaunched", $"Launched {game.Version.DisplayName}"); } diff --git a/Emerald/Views/AccountsPage.xaml b/Emerald/Views/AccountsPage.xaml index d1921715..33c97807 100644 --- a/Emerald/Views/AccountsPage.xaml +++ b/Emerald/Views/AccountsPage.xaml @@ -12,6 +12,7 @@ + @@ -95,7 +96,7 @@ - diff --git a/Emerald/Views/GamesPage.xaml.cs b/Emerald/Views/GamesPage.xaml.cs index 379ea0a5..cc527b44 100644 --- a/Emerald/Views/GamesPage.xaml.cs +++ b/Emerald/Views/GamesPage.xaml.cs @@ -162,7 +162,7 @@ private void InstallGame_Click(object sender, RoutedEventArgs e) { if (sender is Button btn && btn.Tag is Game game) { - _ = ViewModel.InstallGameCommand.ExecuteAsync(game); + Task.Run(() => ViewModel.InstallGameCommand.ExecuteAsync(game)); } } From 1fca7bf79ccd3faa2f17973d9c1259584dfa337d Mon Sep 17 00:00:00 2001 From: NoobNotFound <82730163+NoobNotFound@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:39:52 +0530 Subject: [PATCH 12/15] Refactor refresh logic and improve state handling Refactored the `Core` class to manage refresh state with a new `IsRefreshing` property and `VersionsRefreshed` event. Updated the `GamesPageViewModel` to react to these changes, simplifying state management and reducing redundant code. Added the `UpdateAvailableVersions` method to centralize version filtering logic. Adjusted `BaseSettingsService` to use a more appropriate file path for settings storage. --- Emerald.CoreX/Core.cs | 8 +++ Emerald.CoreX/Helpers/BaseSettingsService.cs | 2 +- Emerald/ViewModels/GamesPageViewModel.cs | 61 +++++++++----------- 3 files changed, 37 insertions(+), 34 deletions(-) diff --git a/Emerald.CoreX/Core.cs b/Emerald.CoreX/Core.cs index 9e984baf..b1f87144 100644 --- a/Emerald.CoreX/Core.cs +++ b/Emerald.CoreX/Core.cs @@ -20,6 +20,8 @@ public partial class Core(ILogger _logger, INotificationService _notify, I public const string GamesFolderName = "EmeraldGames"; public MinecraftLauncher Launcher { get; set; } + public event EventHandler? VersionsRefreshed; + public bool IsRunning { get; set; } = false; public MinecraftPath? BasePath { get; private set; } = null; public bool IsOfflineMode { get; private set; } = false; @@ -31,6 +33,9 @@ public partial class Core(ILogger _logger, INotificationService _notify, I [ObservableProperty] private bool _initialized = false; + [ObservableProperty] + private bool _isRefreshing = false; + public Models.GameSettings GameOptions = new(); private SavedGameCollection[] SavedgamesWithPaths = []; @@ -114,6 +119,7 @@ public async Task InitializeAndRefresh(MinecraftPath? basePath = null) isIndeterminate: true, isCancellable: true ); + IsRefreshing = true; try { GameOptions = settingsService.Get("BaseGameOptions", Models.GameSettings.FromMLaunchOption(new())); @@ -157,6 +163,8 @@ public async Task InitializeAndRefresh(MinecraftPath? basePath = null) game.CreateMCLauncher(IsOfflineMode); } _logger.LogInformation("Loaded {count} vanilla versions", VanillaVersions.Count); + IsRefreshing = false; + VersionsRefreshed?.Invoke(this, new()); } } diff --git a/Emerald.CoreX/Helpers/BaseSettingsService.cs b/Emerald.CoreX/Helpers/BaseSettingsService.cs index ed750cbe..8c4cee14 100644 --- a/Emerald.CoreX/Helpers/BaseSettingsService.cs +++ b/Emerald.CoreX/Helpers/BaseSettingsService.cs @@ -24,7 +24,7 @@ public BaseSettingsService(ILogger logger) _logger = logger; // Use the LocalFolder path as the base folder for file-based settings - _settingsFolder = ApplicationData.Current.LocalFolder.Path; + _settingsFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Emerald"); } public void Set(string key, T value, bool storeInFile = false) diff --git a/Emerald/ViewModels/GamesPageViewModel.cs b/Emerald/ViewModels/GamesPageViewModel.cs index c5e3a150..d1fa0a0f 100644 --- a/Emerald/ViewModels/GamesPageViewModel.cs +++ b/Emerald/ViewModels/GamesPageViewModel.cs @@ -35,8 +35,7 @@ public partial class GamesPageViewModel : ObservableObject [ObservableProperty] private bool _isLoading; - [ObservableProperty] - private bool _isRefreshing; + private bool IsRefreshing => _core.IsRefreshing; [ObservableProperty] private string _searchQuery = string.Empty; @@ -111,10 +110,11 @@ public GamesPageViewModel(Core core, ILogger logger, INotifi FilteredAvailableVersions = new ObservableCollection(); AvailableModLoaders = new ObservableCollection(); - Games.CollectionChanged += (s, e) => UpdateFilteredGames(); + _core.PropertyChanged += (_, _) => this.OnPropertyChanged(); + _core.VersionsRefreshed += (_, _) => UpdateAvailableVersions(); + Games.CollectionChanged += (_, _) => UpdateFilteredGames(); } - // New, simplified navigation commands [RelayCommand] private void GoToNextStep() => AddGameWizardStep++; @@ -124,7 +124,6 @@ public GamesPageViewModel(Core core, ILogger logger, INotifi [RelayCommand] private void StartAddGame() { - // Reset all wizard properties to their default state AddGameWizardStep = 0; NewGameName = string.Empty; SelectedVersion = null; @@ -175,6 +174,29 @@ private void UpdateFilteredAvailableVersions() } } + private void UpdateAvailableVersions() + { + AvailableVersions.Clear(); + foreach (var version in _core.VanillaVersions) + { + AvailableVersions.Add(version); + } + + // Populate release types for filtering + ReleaseTypes.Clear(); + ReleaseTypes.Add("All"); + var distinctTypes = AvailableVersions.Select(v => v.ReleaseType).Distinct().OrderBy(t => t); + foreach (var type in distinctTypes) + { + if (!string.IsNullOrWhiteSpace(type)) + { + ReleaseTypes.Add(type); + } + } + + UpdateFilteredAvailableVersions(); + } + [RelayCommand] private async Task InitializeAsync() { @@ -183,32 +205,13 @@ private async Task InitializeAsync() IsLoading = true; _logger.LogInformation("Initializing GamesPage"); - if (!_core.Initialized) + if (!_core.Initialized && !_core.IsRefreshing) { var path = _settingsService.Settings.Minecraft.Path; var mcPath = path != null ? new MinecraftPath(path) : new(); await _core.InitializeAndRefresh(mcPath); } - AvailableVersions.Clear(); - foreach (var version in _core.VanillaVersions) - { - AvailableVersions.Add(version); - } - - // Populate release types for filtering - ReleaseTypes.Clear(); - ReleaseTypes.Add("All"); - var distinctTypes = AvailableVersions.Select(v => v.ReleaseType).Distinct().OrderBy(t => t); - foreach (var type in distinctTypes) - { - if (!string.IsNullOrWhiteSpace(type)) - { - ReleaseTypes.Add(type); - } - } - - UpdateFilteredAvailableVersions(); UpdateFilteredGames(); } catch (Exception ex) @@ -227,19 +230,11 @@ private async Task RefreshGamesAsync() { try { - IsRefreshing = true; await _core.InitializeAndRefresh(); - UpdateFilteredGames(); - _notificationService.Info("RefreshComplete", "Games list has been refreshed"); } catch (Exception ex) { _logger.LogError(ex, "Failed to refresh games"); - _notificationService.Error("RefreshError", "Failed to refresh games", ex: ex); - } - finally - { - IsRefreshing = false; } } From 5627bb5d7992d91b5917072bd5f1bdd48563e451 Mon Sep 17 00:00:00 2001 From: codefactor-io Date: Thu, 20 Nov 2025 19:52:33 +0000 Subject: [PATCH 13/15] [CodeFactor] Apply fixes --- Emerald.CoreX/Core.cs | 3 --- Emerald.CoreX/Game.cs | 1 - Emerald.CoreX/Installers/Fabric.cs | 2 -- Emerald.CoreX/Installers/Quilt.cs | 2 -- Emerald.CoreX/Models/GameSettings.cs | 3 --- Emerald.CoreX/Services/AccountService.cs | 1 - Emerald/App.xaml.cs | 3 --- Emerald/Helpers/Extensions.cs | 2 -- Emerald/Helpers/MessageBox.cs | 1 - Emerald/Helpers/Settings/JSON.cs | 3 --- Emerald/MainPage.xaml.cs | 5 ----- Emerald/Services/SettingsService.cs | 2 -- Emerald/ViewModels/GamesPageViewModel.cs | 1 - Emerald/Views/GamesPage.xaml.cs | 5 ----- Emerald/Views/Settings/AppearancePage.xaml.cs | 5 ----- 15 files changed, 39 deletions(-) diff --git a/Emerald.CoreX/Core.cs b/Emerald.CoreX/Core.cs index 4e926039..a0c6bf19 100644 --- a/Emerald.CoreX/Core.cs +++ b/Emerald.CoreX/Core.cs @@ -106,7 +106,6 @@ public void SaveGames() } } - /// /// Initializes the Core with the given Minecraft path and retrieves the list of available vanilla Minecraft versions. /// @@ -180,7 +179,6 @@ public async Task InstallGame(Game game, bool showFileprog = false) try { - _logger.LogInformation("Installing game {version}", version.BasedOn); if(game == null) @@ -195,7 +193,6 @@ await game.InstallVersion( ); SaveGames(); - } catch (Exception ex) { diff --git a/Emerald.CoreX/Game.cs b/Emerald.CoreX/Game.cs index e7c26bb2..cb960ce7 100644 --- a/Emerald.CoreX/Game.cs +++ b/Emerald.CoreX/Game.cs @@ -51,7 +51,6 @@ public void CreateMCLauncher(bool isOffline) } Launcher = new MinecraftLauncher(param); - } /// /// Installs the specified Minecraft version, including downloading necessary files diff --git a/Emerald.CoreX/Installers/Fabric.cs b/Emerald.CoreX/Installers/Fabric.cs index c368dd5a..f8bfd956 100644 --- a/Emerald.CoreX/Installers/Fabric.cs +++ b/Emerald.CoreX/Installers/Fabric.cs @@ -62,7 +62,6 @@ public async Task InstallAsync(MinecraftPath path, string mcversion, str this.Log().LogInformation("Installing Fabric Loader for {mcversion}", mcversion); try { - var fabricInstaller = new FabricInstaller(new HttpClient()); if (!online) @@ -72,7 +71,6 @@ public async Task InstallAsync(MinecraftPath path, string mcversion, str return FabricInstaller.GetVersionName(mcversion, modversion ?? (await fabricInstaller.GetFirstLoader(mcversion))?.Version ?? throw new NullReferenceException("No internet and no mod name found.")); } - string? versionName = null; if (modversion == null) diff --git a/Emerald.CoreX/Installers/Quilt.cs b/Emerald.CoreX/Installers/Quilt.cs index 2a567526..5108cabe 100644 --- a/Emerald.CoreX/Installers/Quilt.cs +++ b/Emerald.CoreX/Installers/Quilt.cs @@ -62,7 +62,6 @@ public async Task InstallAsync(MinecraftPath path, string mcversion, str this.Log().LogInformation("Installing Quilt Loader for {mcversion}", mcversion); try { - var QuiltInstaller = new QuiltInstaller(new HttpClient()); if (!online) @@ -72,7 +71,6 @@ public async Task InstallAsync(MinecraftPath path, string mcversion, str return QuiltInstaller.GetVersionName(mcversion, modversion ?? (await QuiltInstaller.GetFirstLoader(mcversion))?.Version ?? throw new NullReferenceException("No internet and no mod name found.")); } - string? versionName = null; if (modversion == null) diff --git a/Emerald.CoreX/Models/GameSettings.cs b/Emerald.CoreX/Models/GameSettings.cs index 938bb9ad..19aecc30 100644 --- a/Emerald.CoreX/Models/GameSettings.cs +++ b/Emerald.CoreX/Models/GameSettings.cs @@ -57,7 +57,6 @@ public partial class GameSettings : ObservableObject [ObservableProperty] private int _serverPort = 25565; - [ObservableProperty] private bool _HashCheck; @@ -69,12 +68,10 @@ public partial class GameSettings : ObservableObject public ObservableCollection JVMArgs { get; set; } = new(); - [JsonIgnore] public string ScreenSizeStatus => FullScreen ? "FullScreen".Localize() : ((ScreenWidth > 0 && ScreenHeight > 0) ? $"{ScreenWidth} × {ScreenHeight}" : "Default".Localize()); - public MLaunchOption ToMLaunchOption() { var opt = new MLaunchOption diff --git a/Emerald.CoreX/Services/AccountService.cs b/Emerald.CoreX/Services/AccountService.cs index 88ad452e..d1dc9a07 100644 --- a/Emerald.CoreX/Services/AccountService.cs +++ b/Emerald.CoreX/Services/AccountService.cs @@ -87,7 +87,6 @@ public async Task LoadAllAccountsAsync() _accounts.AddRange(storedAccounts.Where(acc => acc.Type == AccountType.Offline)); - // Add any new online accounts that aren't in storage foreach (var onlineAccount in onlineAccounts) { diff --git a/Emerald/App.xaml.cs b/Emerald/App.xaml.cs index e3d37024..0761c5b5 100644 --- a/Emerald/App.xaml.cs +++ b/Emerald/App.xaml.cs @@ -27,7 +27,6 @@ public App() TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; } - public Window? MainWindow { get; private set; } protected IHost? Host { get; private set; } private void ConfigureServices(IServiceCollection services) @@ -59,7 +58,6 @@ private void ConfigureServices(IServiceCollection services) //Accounts services.AddSingleton(); - //Notifications services.AddTransient(); @@ -210,7 +208,6 @@ private async void ShowPlatformErrorDialog(string message) { await MessageBox.Show("AppCrash".Localize(), message, Helpers.Enums.MessageBoxButtons.Ok); Application.Current.Exit(); - } catch (Exception ex) { diff --git a/Emerald/Helpers/Extensions.cs b/Emerald/Helpers/Extensions.cs index f66249fe..a938f145 100644 --- a/Emerald/Helpers/Extensions.cs +++ b/Emerald/Helpers/Extensions.cs @@ -36,7 +36,6 @@ public static class Extensions public static int? GetMemoryGB() { - var _logger = Ioc.Default.GetService>(); try { @@ -144,7 +143,6 @@ public static string ToMD5(this string s) return sb.ToString(); } - //public static string Localize(this Core.Localized resourceKey) => // resourceKey.ToString().Localize(); diff --git a/Emerald/Helpers/MessageBox.cs b/Emerald/Helpers/MessageBox.cs index e5c54ed1..3dcbae6e 100644 --- a/Emerald/Helpers/MessageBox.cs +++ b/Emerald/Helpers/MessageBox.cs @@ -175,7 +175,6 @@ public static async Task Show(string text) { var theme = ServiceLocator.IsLocationProviderSet ? - (ElementTheme)Ioc.Default.GetService().Settings.App.Appearance.Theme : ElementTheme.Default; var d = new MessageBox("Information".Localize(), text, MessageBoxButtons.Ok) diff --git a/Emerald/Helpers/Settings/JSON.cs b/Emerald/Helpers/Settings/JSON.cs index 1aa91bd8..10ed0a0e 100644 --- a/Emerald/Helpers/Settings/JSON.cs +++ b/Emerald/Helpers/Settings/JSON.cs @@ -86,8 +86,6 @@ public Minecraft() [JsonIgnore] public double RAMinGB => Math.Round((RAM / 1024.00), 2); - - [ObservableProperty] private string _Path; @@ -359,7 +357,6 @@ public partial class Appearance : JSON [ObservableProperty] private Color? _CustomMicaTintColor; - [ObservableProperty] private int _TintOpacity = 10; diff --git a/Emerald/MainPage.xaml.cs b/Emerald/MainPage.xaml.cs index 9809fae2..02402e80 100644 --- a/Emerald/MainPage.xaml.cs +++ b/Emerald/MainPage.xaml.cs @@ -81,7 +81,6 @@ void TintColor() } void InitializeNavView() { - NavView.MenuItems.Add(new SquareNavigationViewItem("Home".Localize()) { Thumbnail = "ms-appx:///Assets/NavigationViewIcons/home.png", @@ -145,8 +144,6 @@ void InitializeNavView() NavView.Header = new NavViewHeader() { HeaderText = "Home".Localize(), HeaderMargin = GetNavViewHeaderMargin() }; NavView.DisplayModeChanged += (_, _) => (NavView.Header as NavViewHeader).HeaderMargin = GetNavViewHeaderMargin(); Navigate(NavView.SelectedItem as SquareNavigationViewItem); - - } private void MainPage_Loaded(object sender, RoutedEventArgs e) { @@ -195,12 +192,10 @@ private void Navigate(SquareNavigationViewItem itm) } (NavView.Header as NavViewHeader).HeaderText = itm.Tag == "Tasks" ? (NavView.Header as NavViewHeader).HeaderText : itm.Name; (NavView.Header as NavViewHeader).HeaderMargin = GetNavViewHeaderMargin(); - } private void NavigateOnce(Type type) { - if (frame.Content == null || frame.Content.GetType() != type) { frame.Navigate(type, null, new EntranceNavigationTransitionInfo()); diff --git a/Emerald/Services/SettingsService.cs b/Emerald/Services/SettingsService.cs index 1f85b9d4..906dbdb1 100644 --- a/Emerald/Services/SettingsService.cs +++ b/Emerald/Services/SettingsService.cs @@ -10,13 +10,11 @@ namespace Emerald.Services; public class SettingsService(IBaseSettingsService _baseService, ILogger _logger) { - public Helpers.Settings.JSON.Settings Settings { get; private set; } public Helpers.Settings.JSON.Account[] Accounts { get; set; } public event EventHandler? APINoMatch; - public void LoadData() { try diff --git a/Emerald/ViewModels/GamesPageViewModel.cs b/Emerald/ViewModels/GamesPageViewModel.cs index d1fa0a0f..0502493a 100644 --- a/Emerald/ViewModels/GamesPageViewModel.cs +++ b/Emerald/ViewModels/GamesPageViewModel.cs @@ -238,7 +238,6 @@ private async Task RefreshGamesAsync() } } - [RelayCommand] private async Task LoadModLoadersAsync() { diff --git a/Emerald/Views/GamesPage.xaml.cs b/Emerald/Views/GamesPage.xaml.cs index cc527b44..3b403dca 100644 --- a/Emerald/Views/GamesPage.xaml.cs +++ b/Emerald/Views/GamesPage.xaml.cs @@ -129,11 +129,9 @@ private async void ManageSettings_Click(object sender, RoutedEventArgs e) var core = Ioc.Default.GetService(); core.SaveGames(); - } } - private async void OpenFolder_Click(object sender, RoutedEventArgs e) { if (sender is MenuFlyoutItem item && item.Tag is Game game) @@ -180,7 +178,6 @@ private void RemoveGame_Click(object sender, RoutedEventArgs e) { ViewModel.RemoveGameCommand.Execute(game); } - } private void RemoveGameWFiles_Click(object sender, RoutedEventArgs e) @@ -189,7 +186,5 @@ private void RemoveGameWFiles_Click(object sender, RoutedEventArgs e) { _ = ViewModel.RemoveGameWithFilesCommand.ExecuteAsync(game); } - - } } diff --git a/Emerald/Views/Settings/AppearancePage.xaml.cs b/Emerald/Views/Settings/AppearancePage.xaml.cs index efc491ac..c8a86b23 100644 --- a/Emerald/Views/Settings/AppearancePage.xaml.cs +++ b/Emerald/Views/Settings/AppearancePage.xaml.cs @@ -24,7 +24,6 @@ namespace Emerald.Views.Settings; public sealed partial class AppearancePage : Page { - public ObservableCollection TintColorsList { get; } = new() { Color.FromArgb(255, 255, 185, 0), @@ -77,7 +76,6 @@ public sealed partial class AppearancePage : Page Color.FromArgb(255, 126, 115, 95) }; - private readonly Services.SettingsService SS; public AppearancePage() { @@ -116,12 +114,10 @@ private void GVColorList_SelectionChanged(object sender, SelectionChangedEventAr SS.Settings.App.Appearance.CustomMicaTintColor = c; this.Log().Info($"Selected tint color changed to: {c}"); - } private void CustomTintColor_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) { - var c = SS.Settings.App.Appearance.CustomMicaTintColor; var cp = new ColorPicker() { @@ -154,7 +150,6 @@ private void CustomTintColor_Click(object sender, Microsoft.UI.Xaml.RoutedEventA GVColorList.SelectedIndex = TintColorsList.Count - 1; this.Log().Info($"Added new color: {cl}. Updated selected index: {GVColorList.SelectedIndex}"); } - }; _ = d.ShowAsync(); From 11b5f868f898e3660ed6eeeb225ab31848f449c1 Mon Sep 17 00:00:00 2001 From: NoobNotFound <82730163+NoobNotFound@users.noreply.github.com> Date: Fri, 21 Nov 2025 01:34:22 +0530 Subject: [PATCH 14/15] Remove redundant game instantiation in InstallGame The `InstallGame` method in `Core.cs` was updated to remove a duplicate instantiation of the `game` object. Previously, the `game` object was instantiated twice, which was unnecessary. This change eliminates the redundant instantiation, improving code clarity, maintainability, and readability. --- Emerald.CoreX/Core.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Emerald.CoreX/Core.cs b/Emerald.CoreX/Core.cs index a0c6bf19..31ba36aa 100644 --- a/Emerald.CoreX/Core.cs +++ b/Emerald.CoreX/Core.cs @@ -210,7 +210,6 @@ public void AddGame(Versions.Version version) var game = new Game(new(path), GameOptions, version); - var game = new Game(new(path), GameOptions, version); Games.Add(game); SaveGames(); From 9fe547c84692106d472960c6439a14d071b921fa Mon Sep 17 00:00:00 2001 From: NoobNotFound <82730163+NoobNotFound@users.noreply.github.com> Date: Fri, 21 Nov 2025 15:07:09 +0530 Subject: [PATCH 15/15] Refactor settings service to use file-based storage only Removed dual in-memory and file-based storage from BaseSettingsService, now persisting all settings to files under LocalApplicationData/Emerald/Settings. Updated interface and usages to remove file/in-memory flags. Also includes minor UI tweaks in AccountsPage and a bugfix to update available versions after path selection in GamesPageViewModel. --- Emerald.CoreX/Core.cs | 5 +- Emerald.CoreX/Helpers/BaseSettingsService.cs | 71 ++++++++----------- .../Services/IBaseSettingsService.cs | 4 +- Emerald/ViewModels/GamesPageViewModel.cs | 2 +- Emerald/Views/AccountsPage.xaml | 11 ++- global.json | 4 +- 6 files changed, 40 insertions(+), 57 deletions(-) diff --git a/Emerald.CoreX/Core.cs b/Emerald.CoreX/Core.cs index 31ba36aa..c3f712fd 100644 --- a/Emerald.CoreX/Core.cs +++ b/Emerald.CoreX/Core.cs @@ -55,7 +55,7 @@ public void LoadGames() Directory.CreateDirectory(gamesFolder); } - SavedgamesWithPaths = settingsService.Get(SettingsKeys.SavedGames, [], true); + SavedgamesWithPaths = settingsService.Get(SettingsKeys.SavedGames, []); var collection = SavedgamesWithPaths.FirstOrDefault(x => x.BasePath == BasePath.BasePath); if (collection == null) @@ -95,7 +95,7 @@ public void SaveGames() list.Add(new SavedGameCollection(BasePath.BasePath, toSave)); SavedgamesWithPaths = list.ToArray(); - settingsService.Set(SettingsKeys.SavedGames, SavedgamesWithPaths, true); + settingsService.Set(SettingsKeys.SavedGames, SavedgamesWithPaths); _logger.LogInformation("Saved {count} games", toSave.Length); } @@ -143,6 +143,7 @@ public async Task InitializeAndRefresh(MinecraftPath? basePath = null) VanillaVersions.Clear(); VanillaVersions.AddRange(l.Select(x => new Versions.Version() { ReleaseTime = x.ReleaseTime.DateTime, BasedOn = x.Name, ReleaseType = x.Type })); IsOfflineMode = false; + _notify.Complete(not.Id, true); } catch (HttpRequestException) { diff --git a/Emerald.CoreX/Helpers/BaseSettingsService.cs b/Emerald.CoreX/Helpers/BaseSettingsService.cs index 8c4cee14..2b6b4e94 100644 --- a/Emerald.CoreX/Helpers/BaseSettingsService.cs +++ b/Emerald.CoreX/Helpers/BaseSettingsService.cs @@ -2,8 +2,6 @@ using System.Text.Json; using System; using System.IO; -using System.Threading.Tasks; -using Windows.Storage; namespace Emerald.Services; @@ -24,66 +22,49 @@ public BaseSettingsService(ILogger logger) _logger = logger; // Use the LocalFolder path as the base folder for file-based settings - _settingsFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Emerald"); + _settingsFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Emerald", "Settings"); + + // Ensure the directory exists immediately + if (!Directory.Exists(_settingsFolder)) + { + Directory.CreateDirectory(_settingsFolder); + } } - public void Set(string key, T value, bool storeInFile = false) + public void Set(string key, T value) { try { - if (storeInFile) - { - SaveToFile(key, value); - } - else - { - string json = JsonSerializer.Serialize(value, _jsonOptions); - ApplicationData.Current.LocalSettings.Values[key] = json; - } + SaveToFile(key, value); } catch (Exception ex) { - _logger.LogError(ex, "Error saving key '{Key}'", key); - - // fallback: if in-memory save failed, try file once - if (!storeInFile) - { - try { SaveToFile(key, value); } - catch (Exception fileEx) { _logger.LogError(fileEx, "Fallback file save failed for '{Key}'", key); } - } + _logger.LogError(ex, "Error saving key '{Key}' to file.", key); } } - public T Get(string key, T defaultVal, bool loadFromFile = false) + public T Get(string key, T defaultVal) { try { - if (loadFromFile) - { - return LoadFromFile(key, defaultVal); - } - else if (ApplicationData.Current.LocalSettings.Values.TryGetValue(key, out object? value) - && value is string json - && !string.IsNullOrWhiteSpace(json)) - { - return JsonSerializer.Deserialize(json) ?? defaultVal; - } + return LoadFromFile(key, defaultVal); } catch (Exception ex) { - _logger.LogError(ex, "Error loading key '{Key}'", key); + _logger.LogError(ex, "Error loading key '{Key}' from file.", key); - // fallback: if in-memory load failed, try file once - if (!loadFromFile) + // If load fails, try to persist the default so the file is corrected for next time + try { - try { return LoadFromFile(key, defaultVal); } - catch (Exception fileEx) { _logger.LogError(fileEx, "Fallback file load failed for '{Key}'", key); } + Set(key, defaultVal); + } + catch (Exception writeEx) + { + _logger.LogError(writeEx, "Could not write default value for '{Key}' after load failure.", key); } - } - // if all else fails, persist default so next time there's a valid value - Set(key, defaultVal, storeInFile: loadFromFile); - return defaultVal; + return defaultVal; + } } private void SaveToFile(string key, T value) @@ -98,16 +79,20 @@ private T LoadFromFile(string key, T defaultVal) string filePath = Path.Combine(_settingsFolder, $"{key}.json"); if (!File.Exists(filePath)) + { + // If the file doesn't exist, create it with the default value immediately + Set(key, defaultVal); return defaultVal; + } try { string json = File.ReadAllText(filePath); return JsonSerializer.Deserialize(json) ?? defaultVal; } - catch (Exception ex) + catch (JsonException jsonEx) { - _logger.LogError(ex, "Error reading key '{Key}' from file storage", key); + _logger.LogError(jsonEx, "Corrupted JSON for key '{Key}'. Returning default.", key); return defaultVal; } } diff --git a/Emerald.CoreX/Services/IBaseSettingsService.cs b/Emerald.CoreX/Services/IBaseSettingsService.cs index 16f6a779..6ddbcdb2 100644 --- a/Emerald.CoreX/Services/IBaseSettingsService.cs +++ b/Emerald.CoreX/Services/IBaseSettingsService.cs @@ -7,7 +7,7 @@ public interface IBaseSettingsService { event EventHandler? APINoMatch; - void Set(string key, T value, bool storeInFile = false); + void Set(string key, T value); - T Get(string key, T defaultVal, bool loadFromFile = false); + T Get(string key, T defaultVal); } diff --git a/Emerald/ViewModels/GamesPageViewModel.cs b/Emerald/ViewModels/GamesPageViewModel.cs index 0502493a..a73daece 100644 --- a/Emerald/ViewModels/GamesPageViewModel.cs +++ b/Emerald/ViewModels/GamesPageViewModel.cs @@ -211,7 +211,7 @@ private async Task InitializeAsync() var mcPath = path != null ? new MinecraftPath(path) : new(); await _core.InitializeAndRefresh(mcPath); } - + UpdateAvailableVersions(); UpdateFilteredGames(); } catch (Exception ex) diff --git a/Emerald/Views/AccountsPage.xaml b/Emerald/Views/AccountsPage.xaml index 33c97807..ce0e1290 100644 --- a/Emerald/Views/AccountsPage.xaml +++ b/Emerald/Views/AccountsPage.xaml @@ -17,23 +17,20 @@ - - - + - + - +