diff --git a/FluentLauncher.Localization b/FluentLauncher.Localization index 7041e2b7..df051f18 160000 --- a/FluentLauncher.Localization +++ b/FluentLauncher.Localization @@ -1 +1 @@ -Subproject commit 7041e2b748fa459a84932e08898612c1ce652459 +Subproject commit df051f18673ef1b70182fcafaab6d85003fe00b6 diff --git a/Natsurainko.FluentLauncher/App.xaml.cs b/Natsurainko.FluentLauncher/App.xaml.cs index 381d2fe2..7609b2ef 100644 --- a/Natsurainko.FluentLauncher/App.xaml.cs +++ b/Natsurainko.FluentLauncher/App.xaml.cs @@ -3,6 +3,7 @@ using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; using Microsoft.Windows.AppLifecycle; +using Natsurainko.FluentLauncher.Services.Launch; using Natsurainko.FluentLauncher.Services.UI; using Natsurainko.FluentLauncher.Services.UI.Messaging; using Natsurainko.FluentLauncher.Utils.Extensions; @@ -42,6 +43,7 @@ void ConfigureApplication() App.GetService().SubscribeEvents(); App.GetService().RegisterApp(this); + App.GetService().CleanRemovedJumpListItem(); // Global exception handler UnhandledException += (_, e) => @@ -58,6 +60,9 @@ protected override async void OnLaunched(LaunchActivatedEventArgs args) mainInstance.Activated += (object? sender, AppActivationArguments e) => { DispatcherQueue.TryEnqueue(() => MainWindow?.Activate()); + + if (e.Data is Windows.ApplicationModel.Activation.LaunchActivatedEventArgs redirectedArgs) + App.GetService().LaunchFromActivatedEventArgs(redirectedArgs.Arguments.Split(' ')); }; if (!mainInstance.IsCurrent) @@ -70,14 +75,6 @@ protected override async void OnLaunched(LaunchActivatedEventArgs args) return; } - string[] cmdargs = Environment.GetCommandLineArgs(); - - if (cmdargs.Length > 1 && cmdargs[1].Equals("/quick-launch")) - { - //App.GetService().LaunchFromJumpListAsync(cmdargs[2]); - return; - } - try { IWindowService mainWindowService = App.GetService().ActivateWindow("MainWindow"); diff --git a/Natsurainko.FluentLauncher/FLSerializerContext.cs b/Natsurainko.FluentLauncher/FLSerializerContext.cs index 3d4d6d71..39f6308b 100644 --- a/Natsurainko.FluentLauncher/FLSerializerContext.cs +++ b/Natsurainko.FluentLauncher/FLSerializerContext.cs @@ -2,12 +2,7 @@ using Natsurainko.FluentLauncher.Models.UI; using Nrk.FluentCore.Authentication; using Nrk.FluentCore.GameManagement.Installer; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Text.Json.Serialization; -using System.Threading.Tasks; namespace Natsurainko.FluentLauncher; @@ -38,15 +33,11 @@ namespace Natsurainko.FluentLauncher; [JsonSerializable(typeof(string[]))] [JsonSerializable(typeof(Windows.UI.Color))] [JsonSerializable(typeof(WinUIEx.WindowState))] -internal partial class FLSerializerContext : JsonSerializerContext -{ -} +internal partial class FLSerializerContext : JsonSerializerContext { } [JsonSerializable(typeof(InstanceConfig))] [JsonSourceGenerationOptions( IncludeFields = false, WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] -internal partial class InstanceConfigSerializerContext : JsonSerializerContext -{ -} \ No newline at end of file +internal partial class InstanceConfigSerializerContext : JsonSerializerContext { } \ No newline at end of file diff --git a/Natsurainko.FluentLauncher/Models/Launch/InstanceConfig.cs b/Natsurainko.FluentLauncher/Models/Launch/InstanceConfig.cs index 00c17cf5..87bade14 100644 --- a/Natsurainko.FluentLauncher/Models/Launch/InstanceConfig.cs +++ b/Natsurainko.FluentLauncher/Models/Launch/InstanceConfig.cs @@ -96,7 +96,7 @@ public IEnumerable VmParameters public DateTime? LastLaunchTime { get => lastLaunchTime; - set => App.DispatcherQueue.TryEnqueue(() => SetProperty(ref lastLaunchTime, value)); + set => SetProperty(ref lastLaunchTime, value); } protected override void OnPropertyChanged(PropertyChangedEventArgs e) diff --git a/Natsurainko.FluentLauncher/Natsurainko.FluentLauncher.csproj b/Natsurainko.FluentLauncher/Natsurainko.FluentLauncher.csproj index 29813c6a..ea18c6ac 100644 --- a/Natsurainko.FluentLauncher/Natsurainko.FluentLauncher.csproj +++ b/Natsurainko.FluentLauncher/Natsurainko.FluentLauncher.csproj @@ -76,6 +76,7 @@ + diff --git a/Natsurainko.FluentLauncher/Program.cs b/Natsurainko.FluentLauncher/Program.cs index 0c5e376a..e454f69e 100644 --- a/Natsurainko.FluentLauncher/Program.cs +++ b/Natsurainko.FluentLauncher/Program.cs @@ -13,6 +13,7 @@ using Natsurainko.FluentLauncher.Services.UI.Messaging; using Nrk.FluentCore.Resources; using System; +using System.CommandLine; using ViewModels = Natsurainko.FluentLauncher.ViewModels; using Views = Natsurainko.FluentLauncher.Views; @@ -106,6 +107,7 @@ services.AddSingleton(); services.AddSingleton(); //services.AddSingleton(); +services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -122,9 +124,36 @@ var app = builder.Build(); AppHost = app.Host; -await app.RunAsync(); +await BuildRootCommand(app).InvokeAsync(args); +//await app.RunAsync(); public partial class Program { public static IHost AppHost { get; private set; } = null!; + + public static Option MinecraftFolderOption { get; } = new (name: "--minecraftFolder") { IsRequired = true }; + + public static Option InstanceIdOption { get; } = new(name: "--instanceId") { IsRequired = true }; + + public static RootCommand BuildRootCommand(WinUIApplication application) + { + var rootCommand = new RootCommand(); + rootCommand.SetHandler(async () => await application.RunAsync()); + rootCommand.Add(BuildSubCommand()); + + return rootCommand; + } + + public static Command BuildSubCommand() + { + var quickLaunchCommand = new Command("quickLaunch"); + quickLaunchCommand.AddOption(MinecraftFolderOption); + quickLaunchCommand.AddOption(InstanceIdOption); + + quickLaunchCommand.SetHandler(async (folder, instanceId) => + await AppHost.Services.GetService()!.LaunchFromArguments(folder, instanceId), + MinecraftFolderOption, InstanceIdOption); + + return quickLaunchCommand; + } } diff --git a/Natsurainko.FluentLauncher/Services/Launch/LaunchService.cs b/Natsurainko.FluentLauncher/Services/Launch/LaunchService.cs index c26d703e..b9ace8b1 100644 --- a/Natsurainko.FluentLauncher/Services/Launch/LaunchService.cs +++ b/Natsurainko.FluentLauncher/Services/Launch/LaunchService.cs @@ -1,7 +1,9 @@ -using Natsurainko.FluentLauncher.Models.Launch; +using CommunityToolkit.Mvvm.Messaging; +using Natsurainko.FluentLauncher.Models.Launch; using Natsurainko.FluentLauncher.Services.Accounts; using Natsurainko.FluentLauncher.Services.Network; using Natsurainko.FluentLauncher.Services.Settings; +using Natsurainko.FluentLauncher.Services.UI.Messaging; using Natsurainko.FluentLauncher.Utils; using Natsurainko.FluentLauncher.Utils.Extensions; using Natsurainko.FluentLauncher.ViewModels.Common; @@ -64,7 +66,7 @@ public LaunchService( public void LaunchFromUI(MinecraftInstance instance) { - var viewModel = new LaunchTaskViewModel(instance); + var viewModel = new LaunchTaskViewModel(instance, this); viewModel.PropertyChanged += (_, e) => { if (e.PropertyName == "TaskState") @@ -74,6 +76,7 @@ public void LaunchFromUI(MinecraftInstance instance) App.DispatcherQueue.TryEnqueue(() => LaunchTasks.Insert(0, viewModel)); viewModel.Start(); + WeakReferenceMessenger.Default.Send(new GlobalNavigationMessage("Tasks/Launch")); } public async Task LaunchAsync( @@ -90,7 +93,7 @@ public async Task LaunchAsync( { InstanceConfig config = instance.GetConfig(); config.LastLaunchTime = DateTime.Now; - //App.DispatcherQueue.TryEnqueue(() => config.LastLaunchTime = DateTime.Now); + await App.GetService().AddLatestMinecraftInstance(instance); var preCheckData = await PreCheckLaunchNeeds(instance, config, cancellationToken, progress); diff --git a/Natsurainko.FluentLauncher/Services/Launch/QuickLaunchService.cs b/Natsurainko.FluentLauncher/Services/Launch/QuickLaunchService.cs new file mode 100644 index 00000000..8d79d962 --- /dev/null +++ b/Natsurainko.FluentLauncher/Services/Launch/QuickLaunchService.cs @@ -0,0 +1,222 @@ +using Microsoft.Windows.AppLifecycle; +using Microsoft.Windows.AppNotifications; +using Microsoft.Windows.AppNotifications.Builder; +using Natsurainko.FluentLauncher.Services.Settings; +using Natsurainko.FluentLauncher.Services.UI; +using Natsurainko.FluentLauncher.Utils.Extensions; +using Natsurainko.FluentLauncher.ViewModels.Common; +using Nrk.FluentCore.GameManagement; +using Nrk.FluentCore.GameManagement.Instances; +using Nrk.FluentCore.Utils; +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using Windows.UI.StartScreen; + +namespace Natsurainko.FluentLauncher.Services.Launch; + +internal class QuickLaunchService +{ + private readonly LaunchService _launchService; + private readonly SettingsService _settingsService; + private readonly NotificationService _notificationService; + + public const string PinnedUri = "ms-resource:///Resources/JumpList__Pinned"; + public const string LatestUri = "ms-resource:///Resources/JumpList__Latest"; + + public QuickLaunchService(LaunchService launchService, SettingsService settingsService, NotificationService notificationService) + { + _launchService = launchService; + _settingsService = settingsService; + _notificationService = notificationService; + } + + public void LaunchFromActivatedEventArgs(string[] args) + { + var parseResult = Program.BuildSubCommand().Parse(args); + + string? minecraftFolder = parseResult.GetValueForOption(Program.MinecraftFolderOption); + string? instanceId = parseResult.GetValueForOption(Program.InstanceIdOption); + + if (minecraftFolder == null || instanceId == null) + return; + + try + { + MinecraftInstanceParser minecraftInstanceParser = new(minecraftFolder); + MinecraftInstance instance = minecraftInstanceParser.ParseAllInstances() + .FirstOrDefault(x => (x?.InstanceId.Equals(instanceId)).GetValueOrDefault(false), null) + ?? throw new Exception("The target Minecraft instance could not be found"); + + _launchService.LaunchFromUI(instance); + } + catch (Exception ex) + { + _notificationService.NotifyWithoutContent(ex.Message); + } + } + + public async Task LaunchFromArguments(string minecraftFolder, string instanceId) + { + try + { + var appInstance = AppInstance.GetCurrent(); + var appActivationArguments = appInstance.GetActivatedEventArgs(); + var mainInstance = AppInstance.FindOrRegisterForKey("Main"); + + if (!mainInstance.IsCurrent) + { + await mainInstance.RedirectActivationToAsync(appActivationArguments); + return; + } + + MinecraftInstanceParser minecraftInstanceParser = new(minecraftFolder); + MinecraftInstance instance = minecraftInstanceParser.ParseAllInstances() + .FirstOrDefault(x => (x?.InstanceId.Equals(instanceId)).GetValueOrDefault(false), null) + ?? throw new Exception("The target Minecraft instance could not be found"); + + QuickLaunchProgressViewModel progressViewModel = new(instance); + AppNotificationManager.Default.Show(progressViewModel.AppNotification); + + using var process = await _launchService.LaunchAsync(instance, progress: progressViewModel); + await process.Process.WaitForExitAsync(); + await AppNotificationManager.Default.RemoveAllAsync(); + + if (process.Process.ExitCode != 0) + { + AppNotificationManager.Default.Show(new AppNotificationBuilder() + .AddText($"Minecraft: {instance.GetDisplayName} Crashed") + .AddText("Quick Launch cannot provide further error information") + .BuildNotification()); + } + } + catch (Exception ex) + { + var title = "Quick Launch Failed"; + var content = $"An exception occurred during the quick start process\r\n{ex}"; + + MessageBox.Show(content, title, MessageBoxButton.OK, MessageBoxImage.Error, MessageBoxResult.OK); + await AppNotificationManager.Default.RemoveAllAsync(); + } + } + + public async Task AddLatestMinecraftInstance(MinecraftInstance instance) + { + JumpList jumpList = await JumpList.LoadCurrentAsync(); + var item = JumpListItem.CreateWithArguments(GenerateCommandLineArguments(instance), instance.GetDisplayName()); + item.GroupName = LatestUri; + item.Logo = GetItemIcon(instance); + + if (IsExisted(jumpList, instance, out var existedItem)) + jumpList.Items.Remove(existedItem); + + GetStartIndexOfGroups(jumpList, out var pinStartIndex, out var latestStartIndex); + + if (latestStartIndex != -1) + jumpList.Items.Insert(latestStartIndex, item); + else jumpList.Items.Add(item); + + GetStartIndexOfGroups(jumpList, out pinStartIndex, out latestStartIndex); + + if (jumpList.Items.Count - latestStartIndex > _settingsService.MaxQuickLaunchLatestItem) + { + jumpList.Items.Skip(_settingsService.MaxQuickLaunchLatestItem) + .ToList() + .ForEach(item => jumpList.Items.Remove(item)); + } + + await jumpList.SaveAsync(); + } + + public async Task AddPinMinecraftInstance(MinecraftInstance instance) + { + JumpList jumpList = await JumpList.LoadCurrentAsync(); + var item = JumpListItem.CreateWithArguments(GenerateCommandLineArguments(instance), instance.GetDisplayName()); + item.GroupName = PinnedUri; + item.Logo = GetItemIcon(instance); + + if (IsExisted(jumpList, instance, out var existedItem, PinnedUri)) + jumpList.Items.Remove(existedItem); + + GetStartIndexOfGroups(jumpList, out var pinStartIndex, out _); + jumpList.Items.Insert(pinStartIndex == -1 ? 0 : pinStartIndex, item); + + await jumpList.SaveAsync(); + } + + public bool IsExisted(JumpList jumpList, MinecraftInstance instance, out JumpListItem? jumpListItem, string groupName = LatestUri) + { + string args = GenerateCommandLineArguments(instance); + jumpListItem = null; + + foreach (var item in jumpList.Items) + { + if (item.Arguments == args && item.GroupName == groupName) + { + jumpListItem = item; + return true; + } + } + + return false; + } + + public async void CleanRemovedJumpListItem() + { + try + { + JumpList jumpList = await JumpList.LoadCurrentAsync(); + + jumpList.Items.Where(x => x.RemovedByUser) + .ToList() + .ForEach(x => jumpList.Items.Remove(x)); + + await jumpList.SaveAsync(); + } + catch (Exception) + { + // Write into logs + } + } + + private static string GenerateCommandLineArguments(MinecraftInstance instance) + { + List argumentsList = + [ + "quickLaunch", + "--minecraftFolder", + instance.MinecraftFolderPath.ToPathParameter(), + "--instanceId", + instance.InstanceId.ToPathParameter() + ]; + + return string.Join(" ", argumentsList); + } + + private static Uri GetItemIcon(MinecraftInstance instance) => + new(string.Format("ms-appx:///Assets/Icons/{0}.png", !instance.IsVanilla ? "furnace_front" : instance.Version.Type switch + { + MinecraftVersionType.Release => "grass_block_side", + MinecraftVersionType.Snapshot => "crafting_table_front", + MinecraftVersionType.OldBeta => "dirt_path_side", + MinecraftVersionType.OldAlpha => "dirt_path_side", + _ => "grass_block_side" + }), UriKind.RelativeOrAbsolute); + + private static void GetStartIndexOfGroups(JumpList jumpList, out int pinStartIndex, out int latestStartIndex) + { + pinStartIndex = -1; + latestStartIndex = -1; + + for (int i = 0; i < jumpList.Items.Count; i++) + { + if (jumpList.Items[i].GroupName == PinnedUri && pinStartIndex == -1) + pinStartIndex = i; + else if (jumpList.Items[i].GroupName == LatestUri && latestStartIndex == -1) + latestStartIndex = i; + } + } +} \ No newline at end of file diff --git a/Natsurainko.FluentLauncher/Services/Settings/SettingsService.cs b/Natsurainko.FluentLauncher/Services/Settings/SettingsService.cs index 366e77fd..0f9d4f64 100644 --- a/Natsurainko.FluentLauncher/Services/Settings/SettingsService.cs +++ b/Natsurainko.FluentLauncher/Services/Settings/SettingsService.cs @@ -5,7 +5,6 @@ using System; using System.Collections.ObjectModel; using System.IO; -using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; using Windows.Storage; @@ -15,7 +14,6 @@ namespace Natsurainko.FluentLauncher.Services.Settings; public partial class SettingsService : SettingsContainer { public ObservableCollection MinecraftFolders { get; private set; } = []; - public ObservableCollection Javas { get; private set; } = []; [SettingItem(Default = "", Converter = typeof(JsonStringConverter))] public partial string ActiveMinecraftFolder { get; set; } @@ -23,6 +21,10 @@ public partial class SettingsService : SettingsContainer [SettingItem] //[SettingItem(typeof(GameInfo), "ActiveGameInfo", Converter = typeof(JsonStringConverter))] public partial string? ActiveInstanceId { get; set; } + #region Launch Java Settings + + public ObservableCollection Javas { get; private set; } = []; + [SettingItem(Default = "", Converter = typeof(JsonStringConverter))] public partial string ActiveJava { get; set; } @@ -35,6 +37,10 @@ public partial class SettingsService : SettingsContainer [SettingItem(Default = true, Converter = typeof(JsonStringConverter))] public partial bool EnableAutoJava { get; set; } + #endregion + + #region Global Launch Settings + [SettingItem(Default = false, Converter = typeof(JsonStringConverter))] public partial bool EnableFullScreen { get; set; } @@ -53,14 +59,29 @@ public partial class SettingsService : SettingsContainer [SettingItem(Default = "", Converter = typeof(JsonStringConverter))] public partial string GameWindowTitle { get; set; } + #endregion + + #region Quick Launch Settings + + [SettingItem(Default = 6, Converter = typeof(JsonStringConverter))] + public partial int MaxQuickLaunchLatestItem { get; set; } + + #endregion + + [SettingItem] + public partial Guid? ActiveAccountUuid { get; set; } + + #region Account Other Settings + [SettingItem(Default = false, Converter = typeof(JsonStringConverter))] public partial bool EnableDemoUser { get; set; } [SettingItem(Default = true, Converter = typeof(JsonStringConverter))] public partial bool AutoRefresh { get; set; } - [SettingItem] - public partial Guid? ActiveAccountUuid { get; set; } + #endregion + + #region Download Settings [SettingItem(Default = "Mojang", Converter = typeof(JsonStringConverter))] public partial string CurrentDownloadSource { get; set; } @@ -71,15 +92,23 @@ public partial class SettingsService : SettingsContainer [SettingItem(Default = 128, Converter = typeof(JsonStringConverter))] public partial int MaxDownloadThreads { get; set; } + #endregion + [SettingItem(Default = "en-US, English", Converter = typeof(JsonStringConverter))] // TODO: remove default value; set to system language if null public partial string CurrentLanguage { get; set; } - [SettingItem(Default = false, Converter = typeof(JsonStringConverter))] - public partial bool NavigationViewIsPaneOpen { get; set; } + #region Appearance Theme Settings [SettingItem(Default = 0, Converter = typeof(JsonStringConverter))] public partial int DisplayTheme { get; set; } + [SettingItem(Default = true, Converter = typeof(JsonStringConverter))] + public partial bool UseSystemAccentColor { get; set; } + + #endregion + + #region Appearance Background Settings + [SettingItem(Default = 1, Converter = typeof(JsonStringConverter))] public partial int BackgroundMode { get; set; } @@ -98,8 +127,9 @@ public partial class SettingsService : SettingsContainer [SettingItem(Converter = typeof(JsonStringConverter))] public partial Windows.UI.Color? CustomThemeColor { get; set; } - [SettingItem(Default = true, Converter = typeof(JsonStringConverter))] - public partial bool UseSystemAccentColor { get; set; } + #endregion + + #region Appearance Mask Settings [SettingItem(Default = false, Converter = typeof(JsonStringConverter))] public partial bool UseBackgroundMask { get; set; } @@ -107,6 +137,10 @@ public partial class SettingsService : SettingsContainer [SettingItem(Default = false, Converter = typeof(JsonStringConverter))] public partial bool UseHomeControlsMask { get; set; } + #endregion + + #region Application Window + [SettingItem(Default = 500, Converter = typeof(JsonStringConverter))] public partial double AppWindowHeight { get; set; } @@ -116,16 +150,24 @@ public partial class SettingsService : SettingsContainer [SettingItem(Default = WinUIEx.WindowState.Normal, Converter = typeof(JsonStringConverter))] public partial WinUIEx.WindowState AppWindowState { get; set; } + #endregion + + #region User Interface + [SettingItem(Default = false, Converter = typeof(JsonStringConverter))] public partial bool FinishGuide { get; set; } - [SettingItem(Default = 0, Converter = typeof(JsonStringConverter))] public partial int CoresSortByIndex { get; set; } [SettingItem(Default = 0, Converter = typeof(JsonStringConverter))] public partial int CoresFilterIndex { get; set; } + [SettingItem(Default = false, Converter = typeof(JsonStringConverter))] + public partial bool NavigationViewIsPaneOpen { get; set; } + + #endregion + [SettingItem(Default = 0u)] public partial uint SettingsVersion { get; set; } diff --git a/Natsurainko.FluentLauncher/Services/SystemServices/JumpListService.cs b/Natsurainko.FluentLauncher/Services/SystemServices/JumpListService.cs deleted file mode 100644 index e8093c3c..00000000 --- a/Natsurainko.FluentLauncher/Services/SystemServices/JumpListService.cs +++ /dev/null @@ -1,260 +0,0 @@ -using FluentLauncher.Infra.UI.Windows; -using Microsoft.Windows.AppNotifications; -using Microsoft.Windows.AppNotifications.Builder; -using Natsurainko.FluentLauncher.Services.Launch; -using Natsurainko.FluentLauncher.Utils; -using Natsurainko.FluentLauncher.Utils.Extensions; -using Natsurainko.FluentLauncher.ViewModels.Common; -using Natsurainko.FluentLauncher.ViewModels.Tasks; -using Nrk.FluentCore.GameManagement; -using Nrk.FluentCore.GameManagement.Instances; -using Nrk.FluentCore.Launch; -using Nrk.FluentCore.Management; -using Nrk.FluentCore.Utils; -using System; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Threading.Tasks; -using Windows.UI.StartScreen; - -namespace Natsurainko.FluentLauncher.Services.SystemServices; - -//internal class JumpListService -//{ -// private readonly LaunchService _launchService; - -// public JumpListService(LaunchService launchService) -// { -// _launchService = launchService; -// } - -// #region Jumplist argument and MinecraftInstance conversion - -// private static string InstanceToJumplistArg(MinecraftInstance instance) -// { -// // Do not change property names (compatibility with legacy GameInfo) -// var argJson = new JsonObject(); -// argJson["AbsoluteId"] = instance.InstanceId; -// argJson["MinecraftFolderPath"] = instance.MinecraftFolderPath; -// return "/quick-launch " + argJson.ToJsonString().ConvertToBase64(); -// } - -// private static (string mcFolderPath, string instanceId) ParseJumplistArg(string argument) -// { -// string argJson = argument["/quick-launch ".Length..]; -// argJson = argJson.ConvertFromBase64(); -// string? mcFolderPath = null, instanceId = null; -// try -// { -// var argJsonNode = JsonNode.Parse(argJson) -// ?? throw new JsonException(); -// mcFolderPath = argJsonNode["MinecraftFolderPath"]?.GetValue(); -// instanceId = argJsonNode["AbsoluteId"]?.GetValue(); -// if (mcFolderPath is null || instanceId is null) -// throw new FormatException(); -// } -// catch (Exception e) when (e is JsonException || e is FormatException) -// { -// throw new InvalidDataException($"Invalid jumplist argument: {argJson}"); -// } -// return (mcFolderPath, instanceId); -// } - -// private static MinecraftInstance JumplistArgToInstance(string argument) -// { -// var (mcFolderPath, instanceId) = ParseJumplistArg(argument); -// var instanceDir = new DirectoryInfo(Path.Combine(mcFolderPath, "versions", instanceId)); -// return MinecraftInstance.Parse(instanceDir); -// } - -// #endregion - -// private static async Task AddItem(MinecraftInstance minecraftInstance) -// { -// var list = await JumpList.LoadCurrentAsync(); -// var itemArguments = InstanceToJumplistArg(minecraftInstance); - -// var jumpListItem = list.Items.Where(item => -// { -// var (mcFolderPath, instanceId) = ParseJumplistArg(item.Arguments); -// return minecraftInstance.MinecraftFolderPath == mcFolderPath && -// minecraftInstance.InstanceId == instanceId; -// }).FirstOrDefault(); - -// if (jumpListItem != null) -// { -// list.Items.Remove(jumpListItem); -// list.Items.Insert(0, jumpListItem); -// } -// else -// { -// jumpListItem = JumpListItem.CreateWithArguments(itemArguments, minecraftInstance.GetConfig().NickName); - -// jumpListItem.GroupName = "Latest"; -// jumpListItem.Logo = new Uri(string.Format("ms-appx:///Assets/Icons/{0}.png", !minecraftInstance.IsVanilla ? "furnace_front" : minecraftInstance.Version.Type switch -// { -// MinecraftVersionType.Release => "grass_block_side", -// MinecraftVersionType.Snapshot => "crafting_table_front", -// MinecraftVersionType.OldBeta => "dirt_path_side", -// MinecraftVersionType.OldAlpha => "dirt_path_side", -// _ => "grass_block_side" -// }), UriKind.RelativeOrAbsolute); - -// list.Items.Add(jumpListItem); -// } - -// await list.SaveAsync(); -// } - -// public async Task LaunchFromJumpListAsync(string arguments) -// { -// #region Init Launch & Display Elements - -// var minecraftInstance = JumplistArgToInstance(arguments); - -// string name = minecraftInstance.GetConfig().NickName ?? "Minecraft"; -// string icon = string.Format("ms-appx:///Assets/Icons/{0}.png", !minecraftInstance.IsVanilla ? "furnace_front" : minecraftInstance.Version.Type switch -// { -// MinecraftVersionType.Release => "grass_block_side", -// MinecraftVersionType.Snapshot => "crafting_table_front", -// MinecraftVersionType.OldBeta => "dirt_path_side", -// MinecraftVersionType.OldAlpha => "dirt_path_side", -// _ => "grass_block_side" -// }); - -// #endregion - -// #region Init Notification - -// var guid = Guid.NewGuid(); - -// var appNotification = new AppNotificationBuilder() -// .AddArgument("guid", guid.ToString()) -// //.SetAppLogoOverride(new Uri(icon), AppNotificationImageCrop.Default) -// .AddText($"Launching Game: {name}") -// .AddText("This may take some time, please wait") -// .AddProgressBar(new AppNotificationProgressBar() -// .BindTitle() -// .BindValue() -// .BindValueStringOverride() -// .BindStatus()) -// //.AddButton(new AppNotificationButton("Open Launcher") -// // .AddArgument("action", "OpenApp")) -// .BuildNotification(); - -// appNotification.Tag = guid.ToString(); -// appNotification.Group = guid.ToString(); -// /* -// void NotificationInvoked(AppNotificationManager sender, AppNotificationActivatedEventArgs args) -// { -// if (args.Arguments["guid"] != guid.ToString()) -// return; - -// if (args.Arguments["action"] == "OpenApp") -// { -// try { App.GetService().ActivateWindow("MainWindow"); } -// catch (Exception e) { App.ProcessException(e); } -// } - -// AppNotificationManager.Default.NotificationInvoked -= NotificationInvoked; -// } - -// AppNotificationManager.Default.NotificationInvoked += NotificationInvoked;*/ -// AppNotificationManager.Default.Show(appNotification); - -// #endregion - -// #region Create Launch Session - -// var viewModel = new LaunchSessionViewModel(minecraftInstance); -// _launchService.LaunchSessions.Insert(0, viewModel); -// await _launchService.LaunchAsync(minecraftInstance, viewModel, viewModel.LaunchCancellationToken); - -// #endregion - -// // TODO: handle progress update -// //minecraftSession.StateChanged += MinecraftSession_StateChanged; - -// #region Progress Update - -// void MinecraftSession_StateChanged(object? sender, MinecraftSessionStateChagnedEventArgs e) -// { -// switch (e.NewState) -// { -// case MinecraftSessionState.GameExited: -// Task.Delay(1500).ContinueWith(task => System.Diagnostics.Process.GetCurrentProcess().Kill()); -// break; -// case MinecraftSessionState.Faulted: -// case MinecraftSessionState.GameCrashed: -// try -// { -// App.DispatcherQueue.TryEnqueue(() => -// { -// App.GetService().ActivateWindow("MainWindow"); -// App.MainWindow.NavigateToLaunchTasksPage(); -// }); -// } -// catch (Exception ex) { App.ProcessException(ex); } -// break; -// } -// } - - //void MinecraftSession_StateChanged(object? sender, MinecraftSessionStateChagnedEventArgs e) - //{ - // switch (e.NewState) - // { - // case MinecraftSessionState.GameExited: - // Task.Delay(1500).ContinueWith(task => System.Diagnostics.Process.GetCurrentProcess().Kill()); - // break; - // case MinecraftSessionState.Faulted: - // case MinecraftSessionState.GameCrashed: - // try - // { - // App.DispatcherQueue.TryEnqueue(() => - // { - // App.GetService().ActivateWindow("MainWindow"); - // App.MainWindow.NavigateToLaunchTasksPage(); - // }); - // } - // catch (Exception ex) { App.ProcessException(ex); } - // break; - // } - //} - -// //uint sequence = 1; -// //sessionViewModel.PropertyChanged += SessionViewModel_PropertyChanged; - -// //void SessionViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e) -// //{ -// // //if (e.PropertyName != "Progress" && e.PropertyName != "SessionState") -// // // return; - -// // var data = new AppNotificationProgressData(sequence); -// // data.Title = minecraftInstance.GetConfig().NickName; -// // data.Value = sessionViewModel.Progress; -// // data.ValueStringOverride = sessionViewModel.ProgressText; -// // data.Status = ResourceUtils.GetValue("Converters", $"_LaunchState_{sessionViewModel.SessionState}"); - -// // appNotification.Progress = data; -// // var result = AppNotificationManager.Default.UpdateAsync(data, guid.ToString(), guid.ToString()) -// // .GetAwaiter().GetResult(); - -// // sequence++; -// //} - -// #endregion -// } - -// public static async Task UpdateJumpListAsync(MinecraftInstance MinecraftInstance) -// { -// await AddItem(MinecraftInstance); - -// var list = await JumpList.LoadCurrentAsync(); - -// if (list.Items.Count > 10) -// list.Items.Remove(list.Items.Last()); -// } -//} diff --git a/Natsurainko.FluentLauncher/Services/UI/Messaging/Messages.cs b/Natsurainko.FluentLauncher/Services/UI/Messaging/Messages.cs index 8775ab98..7df7ca32 100644 --- a/Natsurainko.FluentLauncher/Services/UI/Messaging/Messages.cs +++ b/Natsurainko.FluentLauncher/Services/UI/Messaging/Messages.cs @@ -21,4 +21,9 @@ public SettingsStringValueChangedMessage(string value, string propertyName) : ba class AccountSkinCacheUpdatedMessage : ValueChangedMessage { public AccountSkinCacheUpdatedMessage(Account value) : base(value) { } +} + +class GlobalNavigationMessage : ValueChangedMessage +{ + public GlobalNavigationMessage(string pageKey) : base(pageKey) { } } \ No newline at end of file diff --git a/Natsurainko.FluentLauncher/ViewModels/Common/TaskViewModel.cs b/Natsurainko.FluentLauncher/ViewModels/Common/TaskViewModel.cs index 08aebabb..13723e13 100644 --- a/Natsurainko.FluentLauncher/ViewModels/Common/TaskViewModel.cs +++ b/Natsurainko.FluentLauncher/ViewModels/Common/TaskViewModel.cs @@ -1,9 +1,12 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.WinUI.UI.Controls; using FluentLauncher.Infra.UI.Navigation; using Microsoft.UI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media; +using Microsoft.Windows.AppNotifications; +using Microsoft.Windows.AppNotifications.Builder; using Natsurainko.FluentLauncher.Models.UI; using Natsurainko.FluentLauncher.Services.Launch; using Natsurainko.FluentLauncher.Services.Network; @@ -456,11 +459,7 @@ void NotifyException() } [RelayCommand(CanExecute = nameof(CanLaunch))] - void Launch(INavigationService navigationService) - { - navigationService.NavigateTo("Tasks/Launch"); - App.GetService().LaunchFromUI(_minecraftInstance); - } + void Launch(INavigationService navigationService) => App.GetService().LaunchFromUI(_minecraftInstance); } #endregion @@ -478,13 +477,84 @@ public LaunchProgressViewModel() { TaskName = ResourceUtils.GetValue("Tasks", "LaunchPage", $"_TaskName_{name}") }); } - public void Report(LaunchProgress value) + public virtual void Report(LaunchProgress value) { var vm = Stages[value.Stage]; App.DispatcherQueue.TryEnqueue(() => vm.UpdateProgress(value.StageProgress)); } } +class QuickLaunchProgressViewModel : LaunchProgressViewModel +{ + private uint sequence = 1; + public const string GroupName = "Natsurainko.FluentLauncher"; + + public readonly MinecraftInstance _minecraftInstance; + public readonly Guid Guid = Guid.NewGuid(); + + public AppNotification AppNotification { get; } + + public string InstanceDisplayName { get; } + + public QuickLaunchProgressViewModel(MinecraftInstance instance) : base() + { + _minecraftInstance = instance; + InstanceDisplayName = instance.GetDisplayName(); + + AppNotification = new AppNotificationBuilder() + .AddArgument("guid", Guid.ToString()) + //.SetAppLogoOverride(new Uri(icon), AppNotificationImageCrop.Default) + .AddText($"Launching Game: {InstanceDisplayName}") + .AddText("This may take some time, please wait") + .AddProgressBar(new AppNotificationProgressBar() + .BindTitle() + .BindValue() + .BindValueStringOverride() + .BindStatus()) + //.AddButton(new AppNotificationButton("Open Launcher") + // .AddArgument("action", "OpenApp")) + .BuildNotification(); + + AppNotification.Tag = Guid.ToString(); + AppNotification.Group = GroupName; + } + + public override void Report(LaunchProgress value) + { + var vm = Stages[value.Stage]; + vm.UpdateProgress(value.StageProgress); + + var data = new AppNotificationProgressData(sequence) + { + Title = InstanceDisplayName, + Value = vm.FinishedTasks / (double)vm.TotalTasks, + ValueStringOverride = $"{vm.FinishedTasks} / {vm.TotalTasks}", + Status = vm.TaskName + }; + + AppNotification.Progress = data; + AppNotificationManager.Default.UpdateAsync(data, Guid.ToString(), GroupName) + .GetAwaiter().GetResult(); + + sequence++; + + if (value.Stage == LaunchStage.LaunchProcess && vm.FinishedTasks == vm.TotalTasks) + _ = OnFinished(); + } + + private async Task OnFinished() + { + await AppNotificationManager.Default.RemoveByTagAndGroupAsync(Guid.ToString(), GroupName); + var appNotification = new AppNotificationBuilder() + //.SetAppLogoOverride(new Uri(icon), AppNotificationImageCrop.Default) + .AddText($"Minecraft: {InstanceDisplayName} Launched successfully") + .AddText("Waiting for the game window to appear") + .BuildNotification(); + + AppNotificationManager.Default.Show(appNotification); + } +} + partial class LaunchStageViewModel : ObservableObject { [ObservableProperty] @@ -590,6 +660,7 @@ public enum LaunchStageProgressType internal partial class LaunchTaskViewModel : TaskViewModel { private readonly MinecraftInstance _instance; + private readonly LaunchService _launchService; private readonly LaunchProgressViewModel launchProgressViewModel = new(); private bool _isMcProcessKilled = false; @@ -599,9 +670,10 @@ internal partial class LaunchTaskViewModel : TaskViewModel public ObservableCollection Logger { get; } = []; - public LaunchTaskViewModel(MinecraftInstance instance) + public LaunchTaskViewModel(MinecraftInstance instance, LaunchService launchService) { _instance = instance; + _launchService = launchService; StageViewModels = launchProgressViewModel.Stages.Values; TaskTitle = _instance.GetDisplayName(); @@ -645,7 +717,7 @@ protected override async void Run() try { - McProcess = await App.GetService().LaunchAsync( + McProcess = await _launchService.LaunchAsync( _instance, Process_OutputDataReceived, Process_ErrorDataReceived, diff --git a/Natsurainko.FluentLauncher/ViewModels/Cores/Manage/ConfigViewModel.cs b/Natsurainko.FluentLauncher/ViewModels/Cores/Manage/ConfigViewModel.cs index e60f5b66..ed311dc6 100644 --- a/Natsurainko.FluentLauncher/ViewModels/Cores/Manage/ConfigViewModel.cs +++ b/Natsurainko.FluentLauncher/ViewModels/Cores/Manage/ConfigViewModel.cs @@ -27,7 +27,8 @@ internal partial class ConfigViewModel : ObservableObject, INavigationAware public MinecraftInstance MinecraftInstance { get; private set; } - public InstanceConfig InstanceConfig { get; private set; } + [ObservableProperty] + private InstanceConfig instanceConfig; [ObservableProperty] private Account targetedAccount; @@ -83,14 +84,14 @@ public async Task AddArgument() DataContext = new AddVmArgumentDialogViewModel(VmArguments.Add) }.ShowAsync(); - InstanceConfig.VmParameters = VmArguments.ToArray(); + InstanceConfig.VmParameters = [.. VmArguments]; } [RelayCommand] public void RemoveArgument(string arg) { VmArguments.Remove(arg); - InstanceConfig.VmParameters = VmArguments.ToArray(); + InstanceConfig.VmParameters = [.. VmArguments]; } protected override void OnPropertyChanged(PropertyChangedEventArgs e) diff --git a/Natsurainko.FluentLauncher/ViewModels/Cores/Manage/DefaultViewModel.cs b/Natsurainko.FluentLauncher/ViewModels/Cores/Manage/DefaultViewModel.cs index 2d23f853..0736a085 100644 --- a/Natsurainko.FluentLauncher/ViewModels/Cores/Manage/DefaultViewModel.cs +++ b/Natsurainko.FluentLauncher/ViewModels/Cores/Manage/DefaultViewModel.cs @@ -13,6 +13,7 @@ using System.ComponentModel; using System.Threading.Tasks; using Windows.System; +using Windows.UI.StartScreen; #nullable disable namespace Natsurainko.FluentLauncher.ViewModels.Cores.Manage; @@ -22,22 +23,34 @@ internal partial class DefaultViewModel : ObservableObject, INavigationAware private readonly INavigationService _navigationService; private readonly GameService _gameService; private readonly NotificationService _notificationService; + private readonly QuickLaunchService _quickLaunchService; + + private JumpList jumpList; public MinecraftInstance MinecraftInstance { get; private set; } - public InstanceConfig InstanceConfig { get; private set; } + [ObservableProperty] + private InstanceConfig instanceConfig; - public DefaultViewModel(GameService gameService, INavigationService navigationService, NotificationService notificationService) + public DefaultViewModel( + GameService gameService, + INavigationService navigationService, + NotificationService notificationService, + QuickLaunchService quickLaunchService) { _gameService = gameService; _navigationService = navigationService; _notificationService = notificationService; + _quickLaunchService = quickLaunchService; } [ObservableProperty] [NotifyPropertyChangedFor(nameof(FormatSize))] private GameStorageInfo gameStorageInfo; + [ObservableProperty] + private bool pinned; + public string FormatSize { get @@ -59,17 +72,20 @@ public string FormatSize } } - void INavigationAware.OnNavigatedTo(object parameter) + async void INavigationAware.OnNavigatedTo(object parameter) { MinecraftInstance = parameter as MinecraftInstance; InstanceConfig = MinecraftInstance.GetConfig(); - Task.Run(() => + _ = Task.Run(() => { var gameStorageInfo = MinecraftInstance.GetStatistics(); App.DispatcherQueue.TryEnqueue(() => GameStorageInfo = gameStorageInfo); }); + jumpList = await JumpList.LoadCurrentAsync(); + Pinned = _quickLaunchService.IsExisted(jumpList, MinecraftInstance, out var item, QuickLaunchService.PinnedUri); + InstanceConfig.PropertyChanged += InstanceConfig_PropertyChanged; } @@ -90,4 +106,20 @@ private void InstanceConfig_PropertyChanged(object sender, PropertyChangedEventA { DataContext = new DeleteInstanceDialogViewModel(MinecraftInstance, _navigationService, _notificationService, _gameService) }.ShowAsync(); + + protected override async void OnPropertyChanged(PropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + + if (e.PropertyName == nameof(Pinned) && jumpList != null) + { + if (Pinned) + await _quickLaunchService.AddPinMinecraftInstance(MinecraftInstance); + else if (!Pinned && _quickLaunchService.IsExisted(jumpList, MinecraftInstance, out var jumpListItem, QuickLaunchService.PinnedUri)) + { + jumpList.Items.Remove(jumpListItem); + await jumpList.SaveAsync(); + } + } + } } diff --git a/Natsurainko.FluentLauncher/ViewModels/Home/HomeViewModel.cs b/Natsurainko.FluentLauncher/ViewModels/Home/HomeViewModel.cs index 951a70eb..bf25b4da 100644 --- a/Natsurainko.FluentLauncher/ViewModels/Home/HomeViewModel.cs +++ b/Natsurainko.FluentLauncher/ViewModels/Home/HomeViewModel.cs @@ -71,11 +71,7 @@ protected override void OnPropertyChanged(PropertyChangedEventArgs e) private bool CanExecuteLaunch() => ActiveMinecraftInstance is not null; [RelayCommand(CanExecute = nameof(CanExecuteLaunch))] - private void Launch() - { - _navigationService.NavigateTo("Tasks/Launch"); - _launchService.LaunchFromUI(ActiveMinecraftInstance); - } + private void Launch() => _launchService.LaunchFromUI(ActiveMinecraftInstance); [RelayCommand] public void GoToAccount() => _navigationService.NavigateTo("Settings/Navigation", "Settings/Account"); @@ -101,11 +97,8 @@ private void Launch() if (item.InstanceId.Contains(searchText)) { yield return SuggestionHelper.FromMinecraftInstance(item, - ResourceUtils.GetValue("SearchSuggest", "_D4"), () => - { - _navigationService.NavigateTo("Tasks/Launch"); - _launchService.LaunchFromUI(item); - }); + ResourceUtils.GetValue("SearchSuggest", "_D4"), + () => _launchService.LaunchFromUI(item)); } } } diff --git a/Natsurainko.FluentLauncher/ViewModels/Settings/LaunchViewModel.cs b/Natsurainko.FluentLauncher/ViewModels/Settings/LaunchViewModel.cs index af46b613..93ca473b 100644 --- a/Natsurainko.FluentLauncher/ViewModels/Settings/LaunchViewModel.cs +++ b/Natsurainko.FluentLauncher/ViewModels/Settings/LaunchViewModel.cs @@ -82,6 +82,10 @@ internal partial class LaunchViewModel : SettingsViewModelBase, ISettingsViewMod [BindToSetting(Path = nameof(SettingsService.EnableIndependencyCore))] private bool enableIndependencyCore; + [ObservableProperty] + [BindToSetting(Path = nameof(SettingsService.MaxQuickLaunchLatestItem))] + private int maxQuickLaunchLatestItem; + #endregion public bool IsMinecraftFoldersEmpty => MinecraftFolders.Count == 0; diff --git a/Natsurainko.FluentLauncher/ViewModels/ShellViewModel.cs b/Natsurainko.FluentLauncher/ViewModels/ShellViewModel.cs index 5e00759c..05f156c8 100644 --- a/Natsurainko.FluentLauncher/ViewModels/ShellViewModel.cs +++ b/Natsurainko.FluentLauncher/ViewModels/ShellViewModel.cs @@ -1,8 +1,11 @@ using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; using FluentLauncher.Infra.UI.Navigation; using Natsurainko.FluentLauncher.Services.Launch; using Natsurainko.FluentLauncher.Services.Network; +using Natsurainko.FluentLauncher.Services.UI.Messaging; using Natsurainko.FluentLauncher.ViewModels.Common; +using Natsurainko.FluentLauncher.ViewModels.OOBE; using System.Collections.ObjectModel; using System.Linq; @@ -51,6 +54,12 @@ public ShellViewModel( RunningDownloadTasks = _downloadService.DownloadTasks .Where(x => x.TaskState == TaskState.Running || x.TaskState == TaskState.Prepared) .Count()); + + WeakReferenceMessenger.Default.Register(this!, (r, m) => + { + ShellViewModel vm = (r as ShellViewModel)!; + App.DispatcherQueue.TryEnqueue(() => vm.NavigationService.NavigateTo(m.Value)); + }); } void INavigationAware.OnNavigatedTo(object? parameter) diff --git a/Natsurainko.FluentLauncher/Views/Cores/Manage/ConfigPage.xaml.cs b/Natsurainko.FluentLauncher/Views/Cores/Manage/ConfigPage.xaml.cs index 9379e346..e88f26c2 100644 --- a/Natsurainko.FluentLauncher/Views/Cores/Manage/ConfigPage.xaml.cs +++ b/Natsurainko.FluentLauncher/Views/Cores/Manage/ConfigPage.xaml.cs @@ -17,6 +17,7 @@ private void Page_Unloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) { var vm = (this.DataContext as ConfigViewModel)!; vm.inited = false; + vm.InstanceConfig = null!; ComboBox.ItemsSource = null; // Unload Binding to AccountService.Accounts } diff --git a/Natsurainko.FluentLauncher/Views/Cores/Manage/DefaultPage.xaml b/Natsurainko.FluentLauncher/Views/Cores/Manage/DefaultPage.xaml index 4587db84..3c44b4d4 100644 --- a/Natsurainko.FluentLauncher/Views/Cores/Manage/DefaultPage.xaml +++ b/Natsurainko.FluentLauncher/Views/Cores/Manage/DefaultPage.xaml @@ -7,6 +7,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:xh="using:Natsurainko.FluentLauncher.XamlHelpers" + Unloaded="Page_Unloaded" mc:Ignorable="d"> @@ -120,6 +121,13 @@ HeaderIcon="{xh:FontIcon Glyph=}" IsClickEnabled="True" /> + + + + + + + + + + + + + + + + +