diff --git a/Emerald.CoreX/Core.cs b/Emerald.CoreX/Core.cs index ea42b9ad..c3f712fd 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 = []; @@ -50,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) @@ -90,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); } @@ -101,7 +106,6 @@ public void SaveGames() } } - /// /// Initializes the Core with the given Minecraft path and retrieves the list of available vanilla Minecraft versions. /// @@ -114,6 +118,7 @@ public async Task InitializeAndRefresh(MinecraftPath? basePath = null) isIndeterminate: true, isCancellable: true ); + IsRefreshing = true; try { GameOptions = settingsService.Get("BaseGameOptions", Models.GameSettings.FromMLaunchOption(new())); @@ -136,8 +141,9 @@ public async Task InitializeAndRefresh(MinecraftPath? basePath = null) var l = await Launcher.GetAllVersionsAsync(not.CancellationToken.Value); VanillaVersions.Clear(); - VanillaVersions.AddRange(l.Select(x => new Versions.Version() { Metadata = x, BasedOn = x.Name, ReleaseType = x.Type })); + 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) { @@ -150,7 +156,16 @@ public async Task InitializeAndRefresh(MinecraftPath? basePath = null) _notify.Complete(not.Id, false, ex.Message, ex); Initialized = false; } - _logger.LogInformation("Loaded {count} vanilla versions", VanillaVersions.Count); + finally + { + foreach (var game in Games) + { + game.CreateMCLauncher(IsOfflineMode); + } + _logger.LogInformation("Loaded {count} vanilla versions", VanillaVersions.Count); + IsRefreshing = false; + VersionsRefreshed?.Invoke(this, new()); + } } /// @@ -165,7 +180,6 @@ public async Task InstallGame(Game game, bool showFileprog = false) try { - _logger.LogInformation("Installing game {version}", version.BasedOn); if(game == null) @@ -178,7 +192,8 @@ await game.InstallVersion( isOffline: IsOfflineMode, showFileProgress: showFileprog ); - + + SaveGames(); } catch (Exception ex) { @@ -194,9 +209,9 @@ public void AddGame(Versions.Version version) var path = Path.Combine( BasePath.BasePath, GamesFolderName, version.DisplayName); - var game = new Game(new(path), GameOptions, version); + Games.Add(game); SaveGames(); diff --git a/Emerald.CoreX/Game.cs b/Emerald.CoreX/Game.cs index 03668597..cb960ce7 100644 --- a/Emerald.CoreX/Game.cs +++ b/Emerald.CoreX/Game.cs @@ -40,16 +40,8 @@ public Game(MinecraftPath path, Models.GameSettings options, Versions.Version ve _logger.LogInformation("Game instance created with path: {Path} and options: {Options}", path, options); } - /// - /// Installs the specified Minecraft version, including downloading necessary files - /// and handling both online and offline modes. - /// - /// Indicates whether the installation is performed in offline mode, bypassing online resources. - /// Determines whether detailed file progress information is displayed during the installation process. - /// A task that represents the asynchronous installation operation. - public async Task InstallVersion(bool isOffline = false, bool showFileProgress = false) + public void CreateMCLauncher(bool isOffline) { - _logger.LogInformation("Starting InstallVersion with isOffline: {IsOffline}, showFileProgress: {ShowFileProgress}", isOffline, showFileProgress); var param = MinecraftLauncherParameters.CreateDefault(Path); if (isOffline) @@ -59,8 +51,20 @@ public async Task InstallVersion(bool isOffline = false, bool showFileProgress = } Launcher = new MinecraftLauncher(param); + } + /// + /// Installs the specified Minecraft version, including downloading necessary files + /// and handling both online and offline modes. + /// + /// Indicates whether the installation is performed in offline mode, bypassing online resources. + /// Determines whether detailed file progress information is displayed during the installation process. + /// A task that represents the asynchronous installation operation. + public async Task InstallVersion(bool isOffline = false, bool showFileProgress = false) + { + _logger.LogInformation("Starting InstallVersion with isOffline: {IsOffline}, showFileProgress: {ShowFileProgress}", isOffline, showFileProgress); + CreateMCLauncher(isOffline); - var not = _notify.Create( + var not = _notify.Create( "Initializing Version", $"Initializing {Version.Type} version {Version.DisplayName}", 0, @@ -101,6 +105,19 @@ public async Task InstallVersion(bool isOffline = false, bool showFileProgress = } } + Version.RealVersion = ver; + + 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); await Launcher.InstallAsync( diff --git a/Emerald.CoreX/Helpers/BaseSettingsService.cs b/Emerald.CoreX/Helpers/BaseSettingsService.cs index c0bf42eb..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; @@ -15,91 +13,86 @@ public class BaseSettingsService : IBaseSettingsService WriteIndented = true }; + private readonly string _settingsFolder; + public event EventHandler? APINoMatch; 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", "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) - { - SaveToFileAsync(key, value).Wait(); - } - 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 { SaveToFileAsync(key, value).Wait(); } - 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 LoadFromFileAsync(key, defaultVal).GetAwaiter().GetResult(); - } - 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); - // fallback: if in-memory load failed, try file once - if (!loadFromFile) + _logger.LogError(ex, "Error loading key '{Key}' from file.", key); + + // If load fails, try to persist the default so the file is corrected for next time + try { - try { return LoadFromFileAsync(key, defaultVal).GetAwaiter().GetResult(); } - 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 async Task SaveToFileAsync(string key, T value) + private void SaveToFile(string key, T value) { - string fileName = $"{key}.json"; - StorageFile file = await ApplicationData.Current.LocalFolder.CreateFileAsync( - fileName, CreationCollisionOption.ReplaceExisting); - + string filePath = Path.Combine(_settingsFolder, $"{key}.json"); string json = JsonSerializer.Serialize(value, _jsonOptions); - await FileIO.WriteTextAsync(file, json); + File.WriteAllText(filePath, json); } - private async Task LoadFromFileAsync(string key, T defaultVal) + 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 fileName = $"{key}.json"; - StorageFile file = await ApplicationData.Current.LocalFolder.GetFileAsync(fileName); - string json = await FileIO.ReadTextAsync(file); + string json = File.ReadAllText(filePath); return JsonSerializer.Deserialize(json) ?? defaultVal; } - catch (FileNotFoundException) + catch (JsonException jsonEx) { + _logger.LogError(jsonEx, "Corrupted JSON for key '{Key}'. Returning default.", key); return defaultVal; } } 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.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.CoreX/Versions/Version.cs b/Emerald.CoreX/Versions/Version.cs index cefc62ad..a038f18e 100644 --- a/Emerald.CoreX/Versions/Version.cs +++ b/Emerald.CoreX/Versions/Version.cs @@ -26,9 +26,10 @@ public class Version public string? ModVersion { get; set; } - public CmlLib.Core.VersionMetadata.IVersionMetadata? Metadata { get; set; } + public string? RealVersion { get; set; } + public DateTime ReleaseTime { get;set; } - public string DisplayName; //This Should be unique among all versions + public string DisplayName { get; set; } //This Should be unique among all versions public override bool Equals(object? obj) { @@ -39,12 +40,11 @@ public override bool Equals(object? obj) BasedOn == other.BasedOn && ReleaseType == other.ReleaseType && ModVersion == other.ModVersion && - Equals(Metadata, other.Metadata) && DisplayName == other.DisplayName; } public override int GetHashCode() { - return HashCode.Combine(Type, BasedOn, ReleaseType, ModVersion, Metadata, DisplayName); + return HashCode.Combine(Type, BasedOn, ReleaseType, ModVersion, DisplayName); } } diff --git a/Emerald/App.xaml.cs b/Emerald/App.xaml.cs index 6d2b1b7c..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,12 +58,12 @@ private void ConfigureServices(IServiceCollection services) //Accounts services.AddSingleton(); - //Notifications services.AddTransient(); //ViewModels services.AddTransient(); + services.AddTransient(); } protected override void OnLaunched(LaunchActivatedEventArgs args) @@ -106,14 +105,14 @@ protected override void OnLaunched(LaunchActivatedEventArgs args) Ioc.Default.ConfigureServices(Host.Services); SS = Ioc.Default.GetService(); - this.Log().LogInformation("New Instance was created. Logs are being saved at: {logPath}",logPath); - var nf = Ioc.Default.GetService(); - nf.Info("test title","test content"); - nf.Error("test error", "error", new(0, 1, 0), new Exception("test error")); - nf.Create("test","testprog", isIndeterminate: true); + //load settings, SS.LoadData(); + var ac = Ioc.Default.GetService(); + + ac.InitializeAsync("dfeccda7-604a-4895-b409-9d35f1679b5d"); // Public client ID + // Do not repeat app initialization when the Window already has content, // just ensure that the window is active if (MainWindow.Content is not Frame rootFrame) @@ -209,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/Emerald.csproj b/Emerald/Emerald.csproj index f3c7e191..b54874f8 100644 --- a/Emerald/Emerald.csproj +++ b/Emerald/Emerald.csproj @@ -141,4 +141,16 @@ + + + MSBuild:Compile + + + + + + MSBuild:Compile + + + diff --git a/Emerald/Helpers/Extensions.cs b/Emerald/Helpers/Extensions.cs index 541fd2bc..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 { @@ -71,7 +70,7 @@ public static string KiloFormat(this int num) return num.ToString("#,0"); } - public static ContentDialog ToContentDialog(this UIElement content, string title, string closebtnText = null, ContentDialogButton defaultButton = ContentDialogButton.Close, bool addScrollBar = true) + public static ContentDialog ToContentDialog(this UIElement content, string title, string closebtnText = null, ContentDialogButton defaultButton = ContentDialogButton.Close, bool addScrollBar = true, string PrimaryButtonText = null) { ContentDialog dialog = new() { @@ -79,6 +78,7 @@ public static ContentDialog ToContentDialog(this UIElement content, string title Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style, Title = title, CloseButtonText = closebtnText, + PrimaryButtonText = PrimaryButtonText, DefaultButton = defaultButton, Content = addScrollBar ? new ScrollViewer() { @@ -143,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 fca45657..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", @@ -98,6 +97,14 @@ void InitializeNavView() SolidFontIconGlyph = "\xE7BF", IsSelected = false }); + NavView.MenuItems.Add(new SquareNavigationViewItem("Accounts".Localize()) + { + Thumbnail = "ms-appx:///Assets/NavigationViewIcons/store.png", + Tag = "Accounts", + FontIconGlyph = "\xE7BF", + SolidFontIconGlyph = "\xE7BF", + IsSelected = false + }); NavView.MenuItems.Add(new SquareNavigationViewItem("News".Localize()) { Thumbnail = "ms-appx:///Assets/NavigationViewIcons/news.png", @@ -137,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) { @@ -178,18 +183,19 @@ private void Navigate(SquareNavigationViewItem itm) case "Home": NavigateOnce(typeof(GamesPage)); break; + case "Accounts": + NavigateOnce(typeof(AccountsPage)); + break; default: NavigateOnce(typeof(SettingsPage)); break; } (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/AccountsPageViewModel.cs b/Emerald/ViewModels/AccountsPageViewModel.cs new file mode 100644 index 00000000..8be6d9c4 --- /dev/null +++ b/Emerald/ViewModels/AccountsPageViewModel.cs @@ -0,0 +1,113 @@ +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Emerald.CoreX.Models; +using Emerald.CoreX.Notifications; +using Emerald.CoreX.Services; +using Microsoft.Extensions.Logging; +using System; + +namespace Emerald.ViewModels; + +public partial class AccountsPageViewModel : ObservableObject +{ + private readonly IAccountService _accountService; + private readonly INotificationService _notificationService; + private readonly ILogger _logger; + + [ObservableProperty] + private bool _isLoading; + + [ObservableProperty] + private string _offlineUsername = string.Empty; + + public ObservableCollection Accounts => _accountService.Accounts; + + public AccountsPageViewModel(IAccountService accountService, INotificationService notificationService, ILogger logger) + { + _accountService = accountService; + _notificationService = notificationService; + _logger = logger; + } + + [RelayCommand] + private async Task InitializeAsync() + { + if (Accounts.Count > 0) return; // Already loaded + + IsLoading = true; + try + { + await _accountService.LoadAllAccountsAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to load accounts."); + _notificationService.Error("AccountLoadError", "Could not load accounts.", ex: ex); + } + finally + { + IsLoading = false; + } + } + + [RelayCommand] + private async Task AddMicrosoftAccountAsync() + { + IsLoading = true; + try + { + await _accountService.SignInMicrosoftAccountAsync(); + _notificationService.Info("AccountAdded", "Microsoft account added successfully!"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to sign in with Microsoft account."); + _notificationService.Error("SignInError", "Failed to add Microsoft account.", ex: ex); + } + finally + { + IsLoading = false; + } + } + + [RelayCommand] + private void AddOfflineAccount() + { + if (string.IsNullOrWhiteSpace(OfflineUsername)) + { + _notificationService.Warning("InvalidUsername", "Offline username cannot be empty."); + return; + } + + try + { + _accountService.CreateOfflineAccount(OfflineUsername); + _notificationService.Info("AccountAdded", $"Offline account '{OfflineUsername}' created."); + OfflineUsername = string.Empty; // Clear for next use + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create offline account."); + _notificationService.Error("CreateOfflineError", "Could not create offline account.", ex: ex); + } + } + + [RelayCommand] + private async Task RemoveAccountAsync(EAccount? account) + { + if (account is null) return; + + try + { + await _accountService.RemoveAccountAsync(account); + _notificationService.Info("AccountRemoved", $"Account '{account.Name}' has been removed."); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to remove account."); + _notificationService.Error("RemoveAccountError", "Could not remove the account.", ex: ex); + } + } +} diff --git a/Emerald/ViewModels/GamesPageViewModel.cs b/Emerald/ViewModels/GamesPageViewModel.cs index 6a9c84ec..a73daece 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; @@ -169,12 +168,35 @@ private void UpdateFilteredAvailableVersions() } FilteredAvailableVersions.Clear(); - foreach (var version in filtered.OrderByDescending(v => v.Metadata?.ReleaseTime ?? DateTime.MinValue)) + foreach (var version in filtered.OrderByDescending(v => v?.ReleaseTime ?? DateTime.MinValue)) { FilteredAvailableVersions.Add(version); } } + 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(); + UpdateAvailableVersions(); UpdateFilteredGames(); } catch (Exception ex) @@ -227,23 +230,14 @@ 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; } } - [RelayCommand] private async Task LoadModLoadersAsync() { @@ -304,7 +298,6 @@ private async Task CreateGameAsync() BasedOn = SelectedVersion.BasedOn, Type = SelectedModLoaderType, ReleaseType = SelectedVersion.ReleaseType, - Metadata = SelectedVersion.Metadata, ModVersion = modVer }; @@ -354,7 +347,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 new file mode 100644 index 00000000..ce0e1290 --- /dev/null +++ b/Emerald/Views/AccountsPage.xaml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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); + } + } +} diff --git a/Emerald/Views/GamesPage.xaml b/Emerald/Views/GamesPage.xaml index fcfa58e1..ba081ac8 100644 --- a/Emerald/Views/GamesPage.xaml +++ b/Emerald/Views/GamesPage.xaml @@ -69,13 +69,13 @@ Text="Open Folder" /> diff --git a/Emerald/Views/GamesPage.xaml.cs b/Emerald/Views/GamesPage.xaml.cs index ebc1fdda..3b403dca 100644 --- a/Emerald/Views/GamesPage.xaml.cs +++ b/Emerald/Views/GamesPage.xaml.cs @@ -132,7 +132,6 @@ private async void ManageSettings_Click(object sender, RoutedEventArgs e) } } - private async void OpenFolder_Click(object sender, RoutedEventArgs e) { if (sender is MenuFlyoutItem item && item.Tag is Game game) @@ -161,7 +160,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)); } } @@ -172,4 +171,20 @@ private void LaunchGame_Click(object sender, RoutedEventArgs e) _ = ViewModel.LaunchGameCommand.ExecuteAsync(game); } } + + private void RemoveGame_Click(object sender, RoutedEventArgs e) + { + if (sender is MenuFlyoutItem item && item.Tag is Game game) + { + ViewModel.RemoveGameCommand.Execute(game); + } + } + + private void RemoveGameWFiles_Click(object sender, RoutedEventArgs e) + { + if (sender is MenuFlyoutItem item && item.Tag is Game game) + { + _ = 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(); diff --git a/global.json b/global.json index 456efb61..46feddff 100644 --- a/global.json +++ b/global.json @@ -1,8 +1,8 @@ { "msbuild-sdks": { - "Uno.Sdk": "6.2.39" + "Uno.Sdk": "6.4.24" }, "sdk": { "allowPrerelease": false } -} +} \ No newline at end of file