From dd380ed8999d45d4ae16e9d9fdedd017b012cf5c Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Fri, 10 Jan 2025 22:18:08 -0300 Subject: [PATCH 01/44] Initial background/barebones work --- Knossos.NET/Classes/FsoBuild.cs | 20 ++- Knossos.NET/Classes/KnUtils.cs | 49 +++++- Knossos.NET/Classes/Knossos.cs | 17 +- Knossos.NET/Models/CustomLauncher.cs | 155 ++++++++++++++++++ Knossos.NET/Models/GlobalSettings.cs | 6 + Knossos.NET/Models/Nebula.cs | 18 +- .../ViewModels/Windows/MainWindowViewModel.cs | 42 ++++- Knossos.NET/Views/Windows/MainWindow.axaml | 2 + 8 files changed, 284 insertions(+), 25 deletions(-) create mode 100644 Knossos.NET/Models/CustomLauncher.cs diff --git a/Knossos.NET/Classes/FsoBuild.cs b/Knossos.NET/Classes/FsoBuild.cs index 9de8e9c3..b9b98380 100644 --- a/Knossos.NET/Classes/FsoBuild.cs +++ b/Knossos.NET/Classes/FsoBuild.cs @@ -277,9 +277,14 @@ public async Task RunFSO(FsoExecType executableType, string cmdline, fso.StartInfo.UseShellExecute = false; if (workingFolder != null) fso.StartInfo.WorkingDirectory = workingFolder; - if(Knossos.inPortableMode && Knossos.globalSettings.portableFsoPreferences) + if (Knossos.inPortableMode && Knossos.globalSettings.portableFsoPreferences || + CustomLauncher.IsCustomMode && CustomLauncher.UseCustomFSODataFolder) { - var prefPath = Path.Combine(KnUtils.KnetFolderPath!, "kn_portable", "HardLightProductions", "FreeSpaceOpen") + Path.DirectorySeparatorChar; + var prefPath = KnUtils.GetFSODataFolderPath(); + if (!prefPath.EndsWith(Path.DirectorySeparatorChar)) + { + prefPath += Path.DirectorySeparatorChar; + } Log.Add(Log.LogSeverity.Information, "FsoBuild.RunFSO()", "Used preferences path: " + prefPath); fso.StartInfo.EnvironmentVariables.Add("FSO_PREFERENCES_PATH", prefPath); } @@ -351,10 +356,17 @@ public async Task RunFSO(FsoExecType executableType, string cmdline, cmd.StartInfo.RedirectStandardInput = true; cmd.StartInfo.StandardOutputEncoding = new UTF8Encoding(false); cmd.StartInfo.WorkingDirectory = folderPath; - if (Knossos.inPortableMode && Knossos.globalSettings.portableFsoPreferences) + if (Knossos.inPortableMode && Knossos.globalSettings.portableFsoPreferences || + CustomLauncher.IsCustomMode && CustomLauncher.UseCustomFSODataFolder) { - cmd.StartInfo.EnvironmentVariables.Add("FSO_PREFERENCES_PATH", Path.Combine(KnUtils.KnetFolderPath!, "kn_portable", "HardLightProductions", "FreeSpaceOpen") + Path.DirectorySeparatorChar); + var prefPath = KnUtils.GetFSODataFolderPath(); + if (!prefPath.EndsWith(Path.DirectorySeparatorChar)) + { + prefPath += Path.DirectorySeparatorChar; + } + cmd.StartInfo.EnvironmentVariables.Add("FSO_PREFERENCES_PATH", prefPath); } + cmd.Start(); string result = cmd.StandardOutput.ReadToEnd(); output = result; diff --git a/Knossos.NET/Classes/KnUtils.cs b/Knossos.NET/Classes/KnUtils.cs index 369c181f..203e94de 100644 --- a/Knossos.NET/Classes/KnUtils.cs +++ b/Knossos.NET/Classes/KnUtils.cs @@ -120,14 +120,20 @@ public static string? KnetFolderPath /// fullpath as a string public static string GetKnossosDataFolderPath() { + var path = string.Empty; if (!Knossos.inPortableMode) { - return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "KnossosNET"); + path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "KnossosNET"); } else { - return Path.Combine(KnetFolderPath!, "kn_portable", "KnossosNET"); //If inPortableMode = true, KnetFolderPath is not null + path = Path.Combine(KnetFolderPath!, "kn_portable", "KnossosNET"); //If inPortableMode = true, KnetFolderPath is not null } + if(CustomLauncher.IsCustomMode) + { + path = Path.Combine(path, CustomLauncher.ModID!); + } + return path; } /// @@ -139,32 +145,61 @@ public static string GetKnossosDataFolderPath() /// fullpath as string public static string GetFSODataFolderPath() { + var fsoID = "FreeSpaceOpen"; + if(CustomLauncher.IsCustomMode && CustomLauncher.UseCustomFSODataFolder) + { + fsoID = CustomLauncher.ModID!; + } if (Knossos.inPortableMode && Knossos.globalSettings.portableFsoPreferences) { - return Path.Combine(KnetFolderPath!, "kn_portable", "HardLightProductions", "FreeSpaceOpen"); //If inPortableMode = true, KnetFolderPath is not null + return Path.Combine(KnetFolderPath!, "kn_portable", "HardLightProductions", fsoID); //If inPortableMode = true, KnetFolderPath is not null } else { if (!string.IsNullOrEmpty(fsoPrefPath)) { - return fsoPrefPath; + if (CustomLauncher.IsCustomMode && CustomLauncher.UseCustomFSODataFolder) + { + return ReplaceLast(fsoPrefPath, "FreeSpaceOpen", fsoID); + } + else + { + return fsoPrefPath; + } } else { if (KnUtils.isMacOS) { - return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "HardLightProductions", "FreeSpaceOpen"); + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "HardLightProductions", fsoID); } if (IsLinux) { - return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "HardLightProductions", "FreeSpaceOpen"); + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "HardLightProductions", fsoID); } - return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "HardLightProductions", "FreeSpaceOpen"); + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "HardLightProductions", fsoID); } } } + /// + /// Replace last ocurrence of a word in a string + /// + /// + /// + /// + /// + public static string ReplaceLast(string source, string find, string replace) + { + int index = source.LastIndexOf(find); + + if (index == -1) + return source; + + return source.Remove(index, find.Length).Insert(index, replace); + } + /// /// Saves fsoPrefPath so it can be used by GetFSODataFolderPath() /// diff --git a/Knossos.NET/Classes/Knossos.cs b/Knossos.NET/Classes/Knossos.cs index 9cb1f14d..0237af36 100644 --- a/Knossos.NET/Classes/Knossos.cs +++ b/Knossos.NET/Classes/Knossos.cs @@ -31,6 +31,7 @@ public static class Knossos private static bool forceUpdateDownload = false; //Only intended to test the update system! public static bool inPortableMode { get; private set; } = false; public static bool isKnDataFolderReadOnly { get; private set; } = false; + public static bool inSingleTCMode { get; private set; } = false; /// /// Static constructor @@ -54,6 +55,9 @@ static Knossos() //At this stage we can only log to console Log.WriteToConsole("Knossos() - " + ex.Message); } + //Important!!! The first time it needs to be ran after checking if we are in portable mode or not + //Due to the Knossos data folder path changing + inSingleTCMode = CustomLauncher.IsCustomMode; } /// @@ -99,7 +103,14 @@ public static async void StartUp(bool isQuickLaunch, bool forceUpdate) Log.Add(Log.LogSeverity.Information, "Knossos.StartUp()", "Running in PORTABLE MODE."); try { - Directory.CreateDirectory(Path.Combine(KnUtils.KnetFolderPath!, "kn_portable", "HardLightProductions", "FreeSpaceOpen")); + if (!inSingleTCMode) + { + Directory.CreateDirectory(Path.Combine(KnUtils.KnetFolderPath!, "kn_portable", "HardLightProductions", "FreeSpaceOpen")); + } + else + { + Directory.CreateDirectory(Path.Combine(KnUtils.KnetFolderPath!, "kn_portable", "HardLightProductions", CustomLauncher.ModID!)); + } Directory.CreateDirectory(Path.Combine(KnUtils.KnetFolderPath!, "kn_portable", "Library")); } catch (Exception ex) @@ -135,14 +146,14 @@ public static async void StartUp(bool isQuickLaunch, bool forceUpdate) } //Load base path from knossos legacy - if (globalSettings.basePath == null) + if (globalSettings.basePath == null && !inSingleTCMode) { globalSettings.basePath = KnUtils.GetBasePathFromKnossosLegacy(); } LoadBasePath(isQuickLaunch); - if (globalSettings.basePath == null && !isQuickLaunch) + if (globalSettings.basePath == null && !isQuickLaunch && !inSingleTCMode) OpenQuickSetup(); }catch(Exception ex) diff --git a/Knossos.NET/Models/CustomLauncher.cs b/Knossos.NET/Models/CustomLauncher.cs new file mode 100644 index 00000000..35186c0e --- /dev/null +++ b/Knossos.NET/Models/CustomLauncher.cs @@ -0,0 +1,155 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using static Knossos.NET.ViewModels.MainWindowViewModel; + +namespace Knossos.NET.Models +{ + /// + /// Class to handle the configuration options and optional save file of the SingleTC mode + /// + public static class CustomLauncher + { + private static bool _customFileLoaded = false; + + /// + /// If left empty Knet will try to pick up the "custom_launcher.json" file. + /// Change it to a mod id to hardcode SingleTC ON using the default settings set here. + /// + public static string? ModID { get; private set; } = null; + + /// + /// If enabled, the FSO data folder will changed to use the ModID instead "FreeSpaceOpen" + /// This gives this TC its own settings and pilot saving location. + /// + public static bool UseCustomFSODataFolder { get; private set; } = true; + + /// + /// This allows Knet to search for launcher updates (or not) at the start. + /// Disabling it will completely disable launcher updates. + /// If you are forking this and want to provide your own repo to check for updates + /// change "GitHubUpdateRepoURL" in Knossos.cs + /// + public static bool AllowLauncherUpdates { get; private set; } = true; + + /// + /// Custom title for the launcher window. It is recommended to add the mod name to it + /// Launcher version is auto-added at the end + /// + public static string WindowTitle { get; private set; } = "Knet Launcher"; + + /// + /// Starting width size of the launcher window + /// null for auto + /// + public static int? WindowWidth { get; private set; } = 960; + + /// + /// Starting height size of the launcher window + /// null for auto + /// + public static int? WindowHeight { get; private set; } = 540; + + /// + /// Show the regular FSO engine view to the menu + /// + public static bool MenuDisplayEngineEntry { get; private set; } = true; + + /// + /// Show the regular Knet debug view to the menu + /// + public static bool MenuDisplayDebugEntry { get; private set; } = true; + + /// + /// Call this AFTER checking if we are in portable mode or not. + /// The first time it runs it will try to load the "custom_launcher.json" if ModID is null + /// + public static bool IsCustomMode + { + get + { + if (ModID == null && !_customFileLoaded) + { + ReadCustomFile(); + } + return ModID != null; + } + } + + /// + /// Try read "custom_launcher.json" + /// Possible paths: + /// Portable mode ON: + /// "./kn_portable/KnossosNET/custom_launcher.json" + /// Portable mode OFF + /// "./custom_launcher.json" + /// (same path as the launcher executable) + /// Normal data folder is not used in this case to avoid conflict with multiple custom launchers + /// + private static void ReadCustomFile() + { + try + { + _customFileLoaded = true; + var filePath = Knossos.inPortableMode ? Path.Combine(KnUtils.GetKnossosDataFolderPath(), "custom_launcher.json") : + Path.Combine(KnUtils.KnetFolderPath!, "custom_launcher.json"); + + if (File.Exists(filePath)) + { + Log.Add(Log.LogSeverity.Information, "CustomLauncher.ReadCustomFile()", "Loading custom launcher data..."); + using FileStream jsonFile = File.OpenRead(filePath); + var customData = JsonSerializer.Deserialize(jsonFile)!; + + if(customData.ModID != null) + ModID = customData.ModID; + + if(customData.UseCustomFSODataFolder.HasValue) + UseCustomFSODataFolder = customData.UseCustomFSODataFolder.Value; + + if (customData.AllowLauncherUpdates.HasValue) + AllowLauncherUpdates = customData.AllowLauncherUpdates.Value; + + if (customData.WindowTitle != null) + WindowTitle = customData.WindowTitle; + + if (customData.WindowWidth != null) + WindowWidth = customData.WindowWidth; + + if (customData.WindowHeight != null) + WindowHeight = customData.WindowHeight; + + if (customData.MenuDisplayEngineEntry.HasValue) + MenuDisplayEngineEntry = customData.MenuDisplayEngineEntry.Value; + + if (customData.MenuDisplayDebugEntry.HasValue) + MenuDisplayDebugEntry = customData.MenuDisplayDebugEntry.Value; + + jsonFile.Close(); + } + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "CustomLauncher.ReadCustomFile()", ex); + } + } + + struct CustomFileData + { + public string? ModID { get; set; } + public bool? UseCustomFSODataFolder { get; set; } + public bool? AllowLauncherUpdates { get; set; } + public string? WindowTitle { get; set; } + public int? WindowWidth { get; set; } + public int? WindowHeight { get; set; } + public bool? MenuDisplayEngineEntry { get; set; } + public bool? MenuDisplayDebugEntry { get; set; } + } + } +} diff --git a/Knossos.NET/Models/GlobalSettings.cs b/Knossos.NET/Models/GlobalSettings.cs index b0e18938..2077d93f 100644 --- a/Knossos.NET/Models/GlobalSettings.cs +++ b/Knossos.NET/Models/GlobalSettings.cs @@ -675,6 +675,12 @@ public void Load() Log.Add(Log.LogSeverity.Information, "GlobalSettings.Load()", "Global settings have been loaded"); pendingChangesOnAppClose = false; + + if(CustomLauncher.IsCustomMode) + { + checkUpdate = CustomLauncher.AllowLauncherUpdates; + autoUpdate = false; + } } } diff --git a/Knossos.NET/Models/Nebula.cs b/Knossos.NET/Models/Nebula.cs index c46f44b0..1a8dfd53 100644 --- a/Knossos.NET/Models/Nebula.cs +++ b/Knossos.NET/Models/Nebula.cs @@ -154,7 +154,7 @@ public static async Task Trinity() else { //No update is needed - Dispatcher.UIThread.Invoke(() => TaskViewModel.Instance!.AddMessageTask("Nebula: repo_minimal.json is up to date!"), DispatcherPriority.Background); + Dispatcher.UIThread.Invoke(() => TaskViewModel.Instance?.AddMessageTask("Nebula: repo_minimal.json is up to date!"), DispatcherPriority.Background); Log.Add(Log.LogSeverity.Information, "Nebula.Trinity()", "repo_minimal.json is up to date!"); displayUpdates = false; repoLoaded = true; @@ -171,7 +171,7 @@ public static async Task Trinity() { try { - Dispatcher.UIThread.Invoke(() => TaskViewModel.Instance!.AddDisplayUpdatesTask(updates), DispatcherPriority.Background); + Dispatcher.UIThread.Invoke(() => TaskViewModel.Instance?.AddDisplayUpdatesTask(updates), DispatcherPriority.Background); } catch { } } @@ -227,7 +227,7 @@ private static async Task LoadPrivateMods(CancellationTokenSource? cancellationT { isInstalled.modData.inNebula = true; if (isInstalled.modData.devMode) - DeveloperModsViewModel.Instance!.UpdateVersionManager(isInstalled.modData.id); + DeveloperModsViewModel.Instance?.UpdateVersionManager(isInstalled.modData.id); } } } @@ -248,11 +248,11 @@ private static async Task LoadPrivateMods(CancellationTokenSource? cancellationT Dispatcher.UIThread.Invoke(() => MainWindowViewModel.Instance?.MarkAsUpdateAvailable(mod.id), DispatcherPriority.Background); } if(isInstalled.First().devMode) - DeveloperModsViewModel.Instance!.UpdateVersionManager(isInstalled.First().id); + DeveloperModsViewModel.Instance?.UpdateVersionManager(isInstalled.First().id); } else { - Dispatcher.UIThread.Invoke(() => MainWindowViewModel.Instance!.AddNebulaMod(mod), DispatcherPriority.Background); + Dispatcher.UIThread.Invoke(() => MainWindowViewModel.Instance?.AddNebulaMod(mod), DispatcherPriority.Background); } } } @@ -429,7 +429,7 @@ private static bool IsModUpdate(Mod mod) } }; - await Dispatcher.UIThread.InvokeAsync(() => MainWindowViewModel.Instance!.BulkLoadNebulaMods(modsTcs, true), DispatcherPriority.Background); + await Dispatcher.UIThread.InvokeAsync(() => MainWindowViewModel.Instance?.BulkLoadNebulaMods(modsTcs, true), DispatcherPriority.Background); //Engine Builds var builds = allModsInRepo.Where(m => m.type == ModType.engine).ToList(); @@ -463,10 +463,10 @@ private static bool IsModUpdate(Mod mod) // If the latest of either of these is not installed, signal the main window var installed = Knossos.GetInstalledBuild("FSO", newestStableVersion); - MainWindowViewModel.Instance!.AddMostRecent((installed == null) ? newestStableVersion! : "", false); + MainWindowViewModel.Instance?.AddMostRecent((installed == null) ? newestStableVersion! : "", false); installed = Knossos.GetInstalledBuild("FSO", newestNightlyVersion); - MainWindowViewModel.Instance!.AddMostRecent((installed == null) ? newestNightlyVersion! : "", true); - MainWindowViewModel.Instance!.UpdateBuildInstallButtons(); + MainWindowViewModel.Instance?.AddMostRecent((installed == null) ? newestNightlyVersion! : "", true); + MainWindowViewModel.Instance?.UpdateBuildInstallButtons(); await Dispatcher.UIThread.InvokeAsync(() => FsoBuildsViewModel.Instance?.BulkLoadNebulaBuilds(builds), DispatcherPriority.Background); diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index 33b4a843..1b2b7890 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -26,6 +26,10 @@ public partial class MainWindowViewModel : ViewModelBase [ObservableProperty] internal string appTitle = "Knossos.NET v" + Knossos.AppVersion; [ObservableProperty] + internal int? windowWidth = null; // null = auto on windows? must verify dosent crash on linux/mac + [ObservableProperty] + internal int? windowHeight = null; // null = auto on windows? must verify dosent crash on linux/mac + [ObservableProperty] internal ModListViewModel installedModsView = new ModListViewModel(); [ObservableProperty] internal NebulaModListViewModel nebulaModsView = new NebulaModListViewModel(); @@ -103,11 +107,45 @@ public MainWindowViewModel() forceUpdate = true; } } - FillMenuItemsNormalMode(1); + if (!CustomLauncher.IsCustomMode) + { + FillMenuItemsNormalMode(1); + } + else + { + //Apply customization for Single TC Mode + AppTitle = CustomLauncher.WindowTitle + " v" + Knossos.AppVersion; + WindowHeight = CustomLauncher.WindowHeight; + WindowWidth = CustomLauncher.WindowWidth; + FillMenuItemsCustomMode(1); + } Knossos.StartUp(isQuickLaunch, forceUpdate); } - public void FillMenuItemsNormalMode(int defaultSelectedIndex) + private void FillMenuItemsCustomMode(int defaultSelectedIndex) + { + Dispatcher.UIThread.Invoke(new Action(() => { + MenuItems = new ObservableCollection{ + new MainViewMenuItem(TaskView, null, "Tasks", "Overview of current running tasks") + }; + + if (CustomLauncher.MenuDisplayEngineEntry) + MenuItems.Add(new MainViewMenuItem(FsoBuildsView, "avares://Knossos.NET/Assets/general/menu_engine.png", "Engine", "Download new Freespace Open engine builds")); + + if (CustomLauncher.MenuDisplayDebugEntry) + { + MenuItems.Add(new MainViewMenuItem(DebugView, "avares://Knossos.NET/Assets/general/menu_debug.png", "Debug", "Debug info")); + } + + + if (MenuItems != null && MenuItems.Count() - 1 > defaultSelectedIndex) + { + SelectedMenuItem = MenuItems[defaultSelectedIndex]; + } + })); + } + + private void FillMenuItemsNormalMode(int defaultSelectedIndex) { Dispatcher.UIThread.Invoke(new Action(() => { MenuItems = new ObservableCollection{ diff --git a/Knossos.NET/Views/Windows/MainWindow.axaml b/Knossos.NET/Views/Windows/MainWindow.axaml index e087ff58..bcf83f98 100644 --- a/Knossos.NET/Views/Windows/MainWindow.axaml +++ b/Knossos.NET/Views/Windows/MainWindow.axaml @@ -4,6 +4,8 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:v="using:Knossos.NET.Views" + Width="{Binding WindowWidth}" + Height="{Binding WindowHeight}" x:DataType="vm:MainWindowViewModel" mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="900" x:Class="Knossos.NET.Views.MainWindow" From b97825fc44d97787f1183bce022f78b872bd2c42 Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Sat, 11 Jan 2025 07:07:36 -0300 Subject: [PATCH 02/44] Add optional nebula login menu button to custom launcher --- Knossos.NET/Assets/general/menu_nebula.png | Bin 0 -> 781 bytes Knossos.NET/Models/CustomLauncher.cs | 16 +++++++++++++-- Knossos.NET/Models/Nebula.cs | 2 +- .../ViewModels/Windows/MainWindowViewModel.cs | 19 +++++++++++++++++- 4 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 Knossos.NET/Assets/general/menu_nebula.png diff --git a/Knossos.NET/Assets/general/menu_nebula.png b/Knossos.NET/Assets/general/menu_nebula.png new file mode 100644 index 0000000000000000000000000000000000000000..72e34c742ca7e5cb351dc23a294a1ee910a13724 GIT binary patch literal 781 zcmV+o1M>WdP)mCDZ={43K^f*|ER~VV)3ad5laaPpwIDVAn&sIPP zBw@SLW0h6Ay!U64bSK^Y)`&Z&4^LjnN&cOKF1oPRWf{zVhOOk=&@IER-dA8jJ`G^& zU`Z-E+yz@HnSJn=$cE-C)TN{TdaLI`GzJ?DiEv}Z#)aT$wXoDI_5_wH1xwGC>>I(; z>cE?XU+kzL<|z?t)nG?69KHbnDSU8;7VC}|j%n9gdtuO&BgeuICm38ayLR9a27fhS zb>K4$o&!K(B5<**=NLQ&0KwEl061mC$Ynq?)ndc@8EFjmG{dV%vY&N&qMNHUGvV5; za0`bO7j6>H%nukpRSK!Q$oDare?99}9!6wpC$mv$dNFXDO~NfhH@6hP7TTQM(o63* z5mNN=@;OD@4ZqRxTP%KP>B}>DteA4&1~xC^!=uzZ_#-TJ%A=$4c1AaqdPk5X~{|Vi~NrrygC<09rgbeJstzD$+h-YT%u&@ zD;|8h3v{)eo*p9QEX-cI4q?*vunUm(Y#rLr$l0~>jo*Hq*;Z^S=&-ch6lks?LdH9E za(l#un@A5OdKx}Vk2bXK1e}D!CRHOkY#Fy;I3e7ylimT9|Io)t7HM1wQ*+F`;#Q0v zFl9ezS9vTuVY+`Hbr!0uD_9P2Z8r<<)8byMY-_poZu=EKk z&`}@#Z-Cnh$(zo%KWjAK1_H=yeK6W7vT-x3xp154rn1Q}?^ - /// Show the regular FSO engine view to the menu + /// Add the regular FSO engine view to the menu /// public static bool MenuDisplayEngineEntry { get; private set; } = true; /// - /// Show the regular Knet debug view to the menu + /// Add the regular Knet debug view to the menu /// public static bool MenuDisplayDebugEntry { get; private set; } = true; + /// + /// Add the regular Knet Nebula Login view to the menu + /// This is intended to allow nebula user creation and login + /// To download private versions of the TC, normally you do not want this + /// buts its there as an option + /// + public static bool MenuDisplayNebulaLoginEntry { get; private set; } = false; + /// /// Call this AFTER checking if we are in portable mode or not. /// The first time it runs it will try to load the "custom_launcher.json" if ModID is null @@ -131,6 +139,9 @@ private static void ReadCustomFile() if (customData.MenuDisplayDebugEntry.HasValue) MenuDisplayDebugEntry = customData.MenuDisplayDebugEntry.Value; + if (customData.MenuDisplayNebulaLoginEntry.HasValue) + MenuDisplayNebulaLoginEntry = customData.MenuDisplayNebulaLoginEntry.Value; + jsonFile.Close(); } } @@ -150,6 +161,7 @@ struct CustomFileData public int? WindowHeight { get; set; } public bool? MenuDisplayEngineEntry { get; set; } public bool? MenuDisplayDebugEntry { get; set; } + public bool? MenuDisplayNebulaLoginEntry { get; set; } } } } diff --git a/Knossos.NET/Models/Nebula.cs b/Knossos.NET/Models/Nebula.cs index 1a8dfd53..9ba3614b 100644 --- a/Knossos.NET/Models/Nebula.cs +++ b/Knossos.NET/Models/Nebula.cs @@ -180,7 +180,7 @@ public static async Task Trinity() { throw new TaskCanceledException(); } - if (userIsLoggedIn) + if (userIsLoggedIn && !Knossos.inSingleTCMode || userIsLoggedIn && Knossos.inSingleTCMode && CustomLauncher.MenuDisplayNebulaLoginEntry) { await LoadPrivateMods(cancellationToken).ConfigureAwait(false); } diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index 1b2b7890..74042604 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -22,6 +22,11 @@ public partial class MainWindowViewModel : ViewModelBase { public static MainWindowViewModel? Instance { get; set; } + /* Single TC mode specific stuff */ + [ObservableProperty] + internal NebulaLoginViewModel? nebulaLoginVM; + /**/ + /* UI Bindings, use the uppercase version, otherwise changes will not register */ [ObservableProperty] internal string appTitle = "Knossos.NET v" + Knossos.AppVersion; @@ -130,14 +135,22 @@ private void FillMenuItemsCustomMode(int defaultSelectedIndex) }; if (CustomLauncher.MenuDisplayEngineEntry) + { MenuItems.Add(new MainViewMenuItem(FsoBuildsView, "avares://Knossos.NET/Assets/general/menu_engine.png", "Engine", "Download new Freespace Open engine builds")); + } + + if(CustomLauncher.MenuDisplayNebulaLoginEntry) + { + if(NebulaLoginVM == null) + NebulaLoginVM = new NebulaLoginViewModel(); + MenuItems.Add(new MainViewMenuItem(NebulaLoginVM, "avares://Knossos.NET/Assets/general/menu_nebula.png", "Nebula", "Log in with your nebula account")); + } if (CustomLauncher.MenuDisplayDebugEntry) { MenuItems.Add(new MainViewMenuItem(DebugView, "avares://Knossos.NET/Assets/general/menu_debug.png", "Debug", "Debug info")); } - if (MenuItems != null && MenuItems.Count() - 1 > defaultSelectedIndex) { SelectedMenuItem = MenuItems[defaultSelectedIndex]; @@ -213,6 +226,10 @@ partial void OnSelectedMenuItemChanged(MainViewMenuItem? value) { PxoViewModel.Instance!.InitialLoad(); } + if (CurrentViewModel != null && CurrentViewModel == NebulaLoginVM) //Nebula Login (Single TC mode) + { + NebulaLoginVM.UpdateUI(); + } if (CurrentViewModel == GlobalSettingsView) //Settings { Knossos.globalSettings.Load(); From 1204d392940db4cc88e8c62749353311fcd29e87 Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Sat, 11 Jan 2025 16:27:44 -0300 Subject: [PATCH 03/44] More configuration options --- Knossos.NET/Classes/Knossos.cs | 4 ++ Knossos.NET/Models/CustomLauncher.cs | 40 +++++++++++++++++++ Knossos.NET/Models/GlobalSettings.cs | 22 +++++++--- Knossos.NET/Models/Nebula.cs | 13 ++++++ .../ViewModels/Windows/MainWindowViewModel.cs | 5 +++ 5 files changed, 78 insertions(+), 6 deletions(-) diff --git a/Knossos.NET/Classes/Knossos.cs b/Knossos.NET/Classes/Knossos.cs index 0237af36..37f28667 100644 --- a/Knossos.NET/Classes/Knossos.cs +++ b/Knossos.NET/Classes/Knossos.cs @@ -1216,6 +1216,10 @@ public static async void PlayMod(Mod mod, FsoExecType fsoExecType, bool standalo cmdline = KnUtils.CmdLineBuilder(cmdline, globalCmd); } cmdline = KnUtils.CmdLineBuilder(cmdline, modCmd?.ToArray()); + if (inSingleTCMode && CustomLauncher.CustomCmdlineArray != null && CustomLauncher.CustomCmdlineArray.Any()) + { + cmdline = KnUtils.CmdLineBuilder(cmdline, CustomLauncher.CustomCmdlineArray); + } } else { diff --git a/Knossos.NET/Models/CustomLauncher.cs b/Knossos.NET/Models/CustomLauncher.cs index 980c16b5..ec3ca7ef 100644 --- a/Knossos.NET/Models/CustomLauncher.cs +++ b/Knossos.NET/Models/CustomLauncher.cs @@ -75,6 +75,31 @@ public static class CustomLauncher /// public static bool MenuDisplayNebulaLoginEntry { get; private set; } = false; + /// + /// Display the regular Knossos settings menu item + /// If you do this you may want to add "-no_ingame_options" to the custom cmdline + /// + public static bool MenuDisplayGlobalSettingsEntry { get; private set; } = false; + + /// + /// Yet another cmdline option, pass it as a string array. + /// It has the lowest priority, same options can be overriden by mod cmdline. + /// + public static string[]? CustomCmdlineArray { get; private set; } + + /// + /// Disabling this disconnects the launcher from Nebula completely. Meaning. + /// It can not install, update or modify installations. And it cant do api calls and will not get repo_minimal.json + /// It is only good to provide static game files not meant to be changed or be updated by a 3rd party app or service + /// + public static bool UseNebulaServices { get; private set; } = true; + + /// + /// Whatever to write or not the Knossos.log file to the datafolder, disabling it will prevent this file from be written. + /// But the output to the debug console will still be there if you use it. + /// + public static bool WriteLogFile { get; private set; } = true; + /// /// Call this AFTER checking if we are in portable mode or not. /// The first time it runs it will try to load the "custom_launcher.json" if ModID is null @@ -142,6 +167,17 @@ private static void ReadCustomFile() if (customData.MenuDisplayNebulaLoginEntry.HasValue) MenuDisplayNebulaLoginEntry = customData.MenuDisplayNebulaLoginEntry.Value; + if (customData.MenuDisplayGlobalSettingsEntry.HasValue) + MenuDisplayGlobalSettingsEntry = customData.MenuDisplayGlobalSettingsEntry.Value; + + CustomCmdlineArray = customData.CustomCmdlineArray; + + if (customData.UseNebulaServices.HasValue) + UseNebulaServices = customData.UseNebulaServices.Value; + + if (customData.WriteLogFile.HasValue) + WriteLogFile = customData.WriteLogFile.Value; + jsonFile.Close(); } } @@ -162,6 +198,10 @@ struct CustomFileData public bool? MenuDisplayEngineEntry { get; set; } public bool? MenuDisplayDebugEntry { get; set; } public bool? MenuDisplayNebulaLoginEntry { get; set; } + public bool? MenuDisplayGlobalSettingsEntry { get; set; } + public string[]? CustomCmdlineArray { get; set; } + public bool? UseNebulaServices { get; set; } + public bool? WriteLogFile { get; set; } } } } diff --git a/Knossos.NET/Models/GlobalSettings.cs b/Knossos.NET/Models/GlobalSettings.cs index 2077d93f..7f9b5ee0 100644 --- a/Knossos.NET/Models/GlobalSettings.cs +++ b/Knossos.NET/Models/GlobalSettings.cs @@ -674,13 +674,9 @@ public void Load() ReadFS2IniValues(); Log.Add(Log.LogSeverity.Information, "GlobalSettings.Load()", "Global settings have been loaded"); - pendingChangesOnAppClose = false; + SetCustomModeValues(); - if(CustomLauncher.IsCustomMode) - { - checkUpdate = CustomLauncher.AllowLauncherUpdates; - autoUpdate = false; - } + pendingChangesOnAppClose = false; } } @@ -699,6 +695,19 @@ public void Load() } } + /// + /// Values controlled by CustomLauncher.cs in single tc mode + /// + private void SetCustomModeValues() + { + if (CustomLauncher.IsCustomMode) + { + checkUpdate = CustomLauncher.AllowLauncherUpdates; + enableLogFile = CustomLauncher.WriteLogFile; + autoUpdate = false; + } + } + /// /// Save setting data to the fs2_open.ini /// Stops the ini-watcher if it was enabled and re-enables it to avoid triggering a read @@ -893,6 +902,7 @@ public void WriteFS2IniValues(string? customFullPath = null) /// public void Save(bool writeIni = true) { + SetCustomModeValues(); if (writeIni) { WriteFS2IniValues(); diff --git a/Knossos.NET/Models/Nebula.cs b/Knossos.NET/Models/Nebula.cs index 9ba3614b..9f7ca3cd 100644 --- a/Knossos.NET/Models/Nebula.cs +++ b/Knossos.NET/Models/Nebula.cs @@ -89,6 +89,13 @@ private struct NebulaCache /// public static async Task Trinity() { + //Custom mode with no nebula services + if(Knossos.inSingleTCMode && !CustomLauncher.UseNebulaServices) + { + Log.Add(Log.LogSeverity.Warning, "Nebula.Trinity()", "Nebula services has been disabled."); + repoLoaded = true; + return; + } try { nebulaModDataCache.Clear(); @@ -696,6 +703,12 @@ private enum ApiMethod /// private static async Task ApiCall(string resourceUrl, HttpContent? data, bool needsLogIn = false, int timeoutSeconds = 45, ApiMethod method = ApiMethod.POST) { + //Custom mode with no nebula services + if (Knossos.inSingleTCMode && !CustomLauncher.UseNebulaServices) + { + Log.Add(Log.LogSeverity.Warning, "Nebula.Trinity()", "Nebula services has been disabled."); + return null; + } try { var client = KnUtils.GetHttpClient(); diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index 74042604..f64d5557 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -146,6 +146,11 @@ private void FillMenuItemsCustomMode(int defaultSelectedIndex) MenuItems.Add(new MainViewMenuItem(NebulaLoginVM, "avares://Knossos.NET/Assets/general/menu_nebula.png", "Nebula", "Log in with your nebula account")); } + if (CustomLauncher.MenuDisplayGlobalSettingsEntry) + { + MenuItems.Add(new MainViewMenuItem(GlobalSettingsView, "avares://Knossos.NET/Assets/general/menu_settings.png", "Config", "Change launcher and FSO engine settings")); + } + if (CustomLauncher.MenuDisplayDebugEntry) { MenuItems.Add(new MainViewMenuItem(DebugView, "avares://Knossos.NET/Assets/general/menu_debug.png", "Debug", "Debug info")); From ea06f6abe6b92f0d84d99a6f28520ca24d5f230c Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Sun, 12 Jan 2025 11:30:22 -0300 Subject: [PATCH 04/44] Custom mode home screen part 1 --- .../Assets/general/custom_home_background.jpg | Bin 0 -> 35649 bytes Knossos.NET/Assets/general/menu_home.png | Bin 0 -> 1087 bytes Knossos.NET/Classes/KnUtils.cs | 44 ++++++ .../Converters/BitmapAssetValueConverter.cs | 52 ++++--- Knossos.NET/Models/CustomLauncher.cs | 18 +++ Knossos.NET/ViewModels/CustomHomeViewModel.cs | 138 ++++++++++++++++++ .../ViewModels/Templates/Tasks/InstallMod.cs | 4 +- .../ViewModels/Windows/MainWindowViewModel.cs | 130 +++++++++++------ Knossos.NET/Views/CustomHomeView.axaml | 30 ++++ Knossos.NET/Views/CustomHomeView.axaml.cs | 13 ++ Knossos.NET/Views/Windows/MainWindow.axaml.cs | 2 +- 11 files changed, 367 insertions(+), 64 deletions(-) create mode 100644 Knossos.NET/Assets/general/custom_home_background.jpg create mode 100644 Knossos.NET/Assets/general/menu_home.png create mode 100644 Knossos.NET/ViewModels/CustomHomeViewModel.cs create mode 100644 Knossos.NET/Views/CustomHomeView.axaml create mode 100644 Knossos.NET/Views/CustomHomeView.axaml.cs diff --git a/Knossos.NET/Assets/general/custom_home_background.jpg b/Knossos.NET/Assets/general/custom_home_background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..49e7348779687d73cbb4ff7585080b14877b3205 GIT binary patch literal 35649 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 z000000000000000u>aZs2mt~C0Y3m7&;P^#6cGUc000000000000000006N6+5iXv M0|5a)036T%*&7*?_W%F@ literal 0 HcmV?d00001 diff --git a/Knossos.NET/Assets/general/menu_home.png b/Knossos.NET/Assets/general/menu_home.png new file mode 100644 index 0000000000000000000000000000000000000000..5f7c7fd5a3e5c3cbac9d4bf2afd1876fadc504cb GIT binary patch literal 1087 zcmeAS@N?(olHy`uVBq!ia0vp^Pl5O)3p0?^RDBxGw>EaktaqI0I-~7uC0&Ux0E<3qV+Fag!_Xp-lU2e8& zu7WaeoljpC)QXwS+?3os;naCIg;@vZzFYV9?sX2s^BYfo+j#wb&zsWiUIHwRK$Ow9 zuWI6)TFub*FXGSCJ=a++&hL2{vC8=KiHxPY6015E_Lx~(>qX`$ ze{NxkU-~`ewU!(6VaKB#UXv#$bbm3O`M~fI`8Cu>%VwFPQhV)`IoYsiR)HwXOH2IX#ksXZuW+_`^l#Fblfrn8dlt5 zF#PllsN~b*qsKFz@7Mbm%@&f+|Nd)@?=qg1uR}lI^J8Q>-f>^%2WP#_l{e28l=e(& zD|Y6un<4SD+IpME_BRLl?yWxCq?+n=ihy^`16IQ#1EVAb?!E+voEVnqS31R+OKKIwN zyO~qZ-#>WHH_amb{)BV;DyOL&w`^aM7&N6gJ*Ow|oOg0MUr`!gdQ;)Ip6=+5%>`D+ z1(p|1TElRzv9OrCV4dwb)dM>}TP$aI{^)z@?q2~gw zp>8TRYc}tG_2o}}Z=Er#h$^Tld${@_yQr@=G67?ZuAE zpIfiHyW!qu=2Bks>r>~yTL0&%eE;&D8rscyJ&BFmc*+Z}zo^~!)ZwRU!KO`bW)}r% z@kd@3KM~>~qb2I!AtMtV{2<{`Dc`L#mo}Vzs#0<-Z5mgyYUZZB6DI%q{=djiDyQMG z%38LmT2EYp&#&9r@ZWwpcm2Wwu5^~DV;NhD_-}2U8Tsg~@ugy^x6`-X{I^ctZ*}td sb=f?>Dt4{l`?Um|=>%97Cf6}!pY48sGdZmenAI6PUHx3vIVCg!0L48Z`~Uy| literal 0 HcmV?d00001 diff --git a/Knossos.NET/Classes/KnUtils.cs b/Knossos.NET/Classes/KnUtils.cs index 203e94de..f4f7da70 100644 --- a/Knossos.NET/Classes/KnUtils.cs +++ b/Knossos.NET/Classes/KnUtils.cs @@ -669,6 +669,50 @@ public static string GetImageCachePath() return null; } + /// + /// Downloads a image from a URL, stores it in cache and passes the local image path + /// If the image is already in cache, no download is done + /// + /// + /// + /// string path or null + public static async Task GetImagePath(string imageURL, int attempt = 1) + { + try + { + return await Task.Run(async () => + { + var imageName = Path.GetFileName(imageURL); + var imageInCachePath = Path.Combine(GetImageCachePath(), imageName); + + if (!File.Exists(imageInCachePath) || File.Exists(imageInCachePath) && new FileInfo(imageInCachePath).Length > 0) + { + //Download to cache and copy + Directory.CreateDirectory(Path.Combine(GetKnossosDataFolderPath(), "image_cache")); + using (var imageStream = await GetHttpClient().GetStreamAsync(imageURL)) + { + var fileStream = new FileStream(imageInCachePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read); + await imageStream.CopyToAsync(fileStream); + imageStream.Close(); + fileStream.Close(); + fileStream.Dispose(); + } + } + return imageInCachePath; + }); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "KnUtils.GetImagePath()", ex); + if (attempt <= 2) + { + await Task.Delay(1000); + return await GetImagePath(imageURL, attempt + 1); + } + } + return null; + } + /// /// Check FreeSpace available on the disk/partion of path /// diff --git a/Knossos.NET/Converters/BitmapAssetValueConverter.cs b/Knossos.NET/Converters/BitmapAssetValueConverter.cs index 216cabe5..d45230d5 100644 --- a/Knossos.NET/Converters/BitmapAssetValueConverter.cs +++ b/Knossos.NET/Converters/BitmapAssetValueConverter.cs @@ -4,6 +4,9 @@ using Avalonia.Platform; using System.Reflection; using Avalonia.Media.Imaging; +using System.IO; +using Avalonia.Threading; +using System.Threading.Tasks; namespace Knossos.NET.Converters { @@ -13,28 +16,41 @@ public class BitmapAssetValueConverter : IValueConverter public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - if (value == null) return null; - - if (value is not string rawUri || !targetType.IsAssignableFrom(typeof(Bitmap))) + try { - throw new NotSupportedException(); + if (value == null) return null; + + if (value is not string rawUri || !targetType.IsAssignableFrom(typeof(Bitmap))) + { + throw new NotSupportedException(); + } + + Uri uri; + + if (rawUri.StartsWith("avares://")) + { + uri = new Uri(rawUri); + var asset = AssetLoader.Open(uri); + return new Bitmap(asset); + } + else if(rawUri.ToLower().StartsWith("http")) + { + return null; + } + else if (File.Exists(Path.Combine(KnUtils.GetKnossosDataFolderPath(), rawUri))) + { + return new Bitmap(Path.Combine(KnUtils.GetKnossosDataFolderPath(), rawUri)); + } + else if (File.Exists(rawUri)) + { + return new Bitmap(rawUri); + } } - - Uri uri; - - if (rawUri.StartsWith("avares://")) + catch (Exception ex) { - uri = new Uri(rawUri); + Log.Add(Log.LogSeverity.Error, "BitmapAssetValueConverter.Convert()", ex); } - else - { - var assemblyName = Assembly.GetEntryAssembly()?.GetName().Name; - uri = new Uri($"avares://{assemblyName}/{rawUri.TrimStart('/')}"); - } - - var asset = AssetLoader.Open(uri); - - return new Bitmap(asset); + return null; } public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) diff --git a/Knossos.NET/Models/CustomLauncher.cs b/Knossos.NET/Models/CustomLauncher.cs index ec3ca7ef..17b43997 100644 --- a/Knossos.NET/Models/CustomLauncher.cs +++ b/Knossos.NET/Models/CustomLauncher.cs @@ -100,6 +100,20 @@ public static class CustomLauncher /// public static bool WriteLogFile { get; private set; } = true; + /// + /// Path to the background image for the home view + /// Supports local image in the Knet data folder, a local full path, harcoded image or remote https:// URL + /// Supports APNGs, GIF, PNG and JPG + /// Examples: + /// Harcoded Image: + /// "avares://Knossos.NET/Assets/fs2_res/kn_screen_0.jpg" + /// Data Folder image (same path to were the repo_minimal.json is downloaded for the current running mode): + /// "GgqNPDqW0AAMR80.png" + /// Remote Image (will be cached locally): + /// "https://video-meta.humix.com/poster/h2YKfXkqITvJ/pKvBUijWRO2_IChwVu.jpg" + /// + public static string? HomeBackgroundImage { get; private set; } = "avares://Knossos.NET/Assets/general/custom_home_background.jpg"; + /// /// Call this AFTER checking if we are in portable mode or not. /// The first time it runs it will try to load the "custom_launcher.json" if ModID is null @@ -178,6 +192,9 @@ private static void ReadCustomFile() if (customData.WriteLogFile.HasValue) WriteLogFile = customData.WriteLogFile.Value; + if (customData.HomeBackgroundImage != null) + HomeBackgroundImage = customData.HomeBackgroundImage; + jsonFile.Close(); } } @@ -202,6 +219,7 @@ struct CustomFileData public string[]? CustomCmdlineArray { get; set; } public bool? UseNebulaServices { get; set; } public bool? WriteLogFile { get; set; } + public string? HomeBackgroundImage { get; set; } } } } diff --git a/Knossos.NET/ViewModels/CustomHomeViewModel.cs b/Knossos.NET/ViewModels/CustomHomeViewModel.cs new file mode 100644 index 00000000..4eacaa33 --- /dev/null +++ b/Knossos.NET/ViewModels/CustomHomeViewModel.cs @@ -0,0 +1,138 @@ +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; +using Knossos.NET.Classes; +using Knossos.NET.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace Knossos.NET.ViewModels +{ + public partial class CustomHomeViewModel : ViewModelBase + { + private List modVersions = new List(); + private int activeVersionIndex = 0; + + private List nebulaModVersions = new List(); + + [ObservableProperty] + internal string? modVersion; + + [ObservableProperty] + internal string? backgroundImage = CustomLauncher.HomeBackgroundImage; + + [ObservableProperty] + internal int animate = 0; + + public CustomHomeViewModel() + { + } + + public void RemoveInstalledModVersion(Mod mod) + { + if (CustomLauncher.ModID == mod.id) + { + } + } + + public void RemoveMod(string id) + { + if (CustomLauncher.ModID == id) + { + } + } + + public void CancelModInstall(string id) + { + if (CustomLauncher.ModID == id) + { + } + } + + /// + /// Add a installed mod version of this TC. + /// It will check if the ID matches the one in CustomLauncher.ModID + /// + /// + public void AddModVersion(Mod modJson) + { + if (modJson.id == CustomLauncher.ModID) + { + Log.Add(Log.LogSeverity.Information, "CustomHomeViewModel.AddModVersion()", "Adding additional version for mod id: " + CustomLauncher.ModID + " -> " + modJson.folderName); + string currentVersion = modVersions[activeVersionIndex].version; + modVersions.Add(modJson); + modVersions.Sort((o1, o2) => -SemanticVersion.Compare(o1.version, o2.version)); + if (SemanticVersion.Compare(modJson.version, currentVersion) > 0) + { + Log.Add(Log.LogSeverity.Information, "CustomHomeViewModel.AddModVersion()", "Changing active version for " + modJson.title + " from " + modVersions[activeVersionIndex].version + " to " + modJson.version); + activeVersionIndex = modVersions.FindIndex((m) => m.version.Equals(modJson.version)); + ModVersion = modJson.version + " (+" + (modVersions.Count - 1) + ")"; + } + } + } + + /// + /// Add a Nebula mod version of this TC. + /// It will check if the ID matches the one in CustomLauncher.ModID + /// + /// + public void AddNebulaModVersion(Mod modJson) + { + if (modJson.id == CustomLauncher.ModID) + { + Log.Add(Log.LogSeverity.Information, "CustomHomeViewModel.AddNebulaModVersion()", "Adding additional nebula version for mod id: " + CustomLauncher.ModID + " -> " + modJson.version); + nebulaModVersions.Add(modJson); + nebulaModVersions.Sort((o1, o2) => -SemanticVersion.Compare(o1.version, o2.version)); + } + } + + /// + /// Tell the TC home screen an update is avalible or not + /// It will check if the mod id actually matches to the custom CustomLauncher.ModID + /// + /// + /// + public void UpdateIsAvailable(string id, bool value) + { + if (id == CustomLauncher.ModID) + { + + } + } + + /// + /// Run code when the user clicks the menu item to open this view + /// + public void ViewOpened() + { + Animate = 1; + + //download remote image if we have to + if (BackgroundImage != null && BackgroundImage.ToLower().StartsWith("http")) + { + _ = Task.Factory.StartNew(async () => + { + var temp = BackgroundImage; + BackgroundImage = ""; + var imageFile = await KnUtils.GetImagePath(temp).ConfigureAwait(false); + Dispatcher.UIThread.Invoke(() => + { + if (imageFile != null) + BackgroundImage = imageFile; + }); + }); + } + } + + /// + /// Run code when the user exit this view + /// + public void ViewClosed() + { + Animate = 0; + } + } +} diff --git a/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs b/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs index 9f568a29..f7ffc1a9 100644 --- a/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs +++ b/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs @@ -45,7 +45,7 @@ public async Task InstallMod(Mod mod, CancellationTokenSource cancelSource bool compressMod = false; //Set Mod card as "installing" - MainWindowViewModel.Instance?.NebulaModsView.SetInstalling(mod.id, cancellationTokenSource); + MainWindowViewModel.Instance?.NebulaModsView?.SetInstalling(mod.id, cancellationTokenSource); //Wait in Queue while (TaskViewModel.Instance!.taskQueue.Count > 0 && TaskViewModel.Instance!.taskQueue.Peek() != this) @@ -600,7 +600,7 @@ await Dispatcher.UIThread.InvokeAsync(() => //Remove Mod card, unmark update available, re-run dependencies checks if (installed == null) { - MainWindowViewModel.Instance?.NebulaModsView.RemoveMod(mod.id); + MainWindowViewModel.Instance?.NebulaModsView?.RemoveMod(mod.id); Knossos.AddMod(mod); await Dispatcher.UIThread.InvokeAsync(() => MainWindowViewModel.Instance?.AddInstalledMod(mod), DispatcherPriority.Background); //We cant determine if the version we are installing is the newer one at this point, but this will determine if it is newer than anything was was installed previously, what is good enoght diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index f64d5557..183b7b30 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -25,33 +25,35 @@ public partial class MainWindowViewModel : ViewModelBase /* Single TC mode specific stuff */ [ObservableProperty] internal NebulaLoginViewModel? nebulaLoginVM; + [ObservableProperty] + internal CustomHomeViewModel? customHomeVM; /**/ /* UI Bindings, use the uppercase version, otherwise changes will not register */ [ObservableProperty] internal string appTitle = "Knossos.NET v" + Knossos.AppVersion; [ObservableProperty] - internal int? windowWidth = null; // null = auto on windows? must verify dosent crash on linux/mac + internal int? windowWidth = null; [ObservableProperty] - internal int? windowHeight = null; // null = auto on windows? must verify dosent crash on linux/mac + internal int? windowHeight = null; [ObservableProperty] - internal ModListViewModel installedModsView = new ModListViewModel(); + internal ModListViewModel? installedModsView; [ObservableProperty] - internal NebulaModListViewModel nebulaModsView = new NebulaModListViewModel(); + internal NebulaModListViewModel? nebulaModsView; [ObservableProperty] - internal FsoBuildsViewModel fsoBuildsView = new FsoBuildsViewModel(); + internal FsoBuildsViewModel? fsoBuildsView; [ObservableProperty] - internal DeveloperModsViewModel developerModView = new DeveloperModsViewModel(); + internal DeveloperModsViewModel? developerModView; [ObservableProperty] - internal PxoViewModel pxoView = new PxoViewModel(); + internal PxoViewModel? pxoView; [ObservableProperty] - internal GlobalSettingsViewModel globalSettingsView = new GlobalSettingsViewModel(); + internal GlobalSettingsViewModel? globalSettingsView; [ObservableProperty] internal TaskViewModel taskView = new TaskViewModel(); [ObservableProperty] - internal CommunityViewModel communityView = new CommunityViewModel(); + internal CommunityViewModel? communityView; [ObservableProperty] - internal DebugViewModel debugView = new DebugViewModel(); + internal DebugViewModel? debugView; [ObservableProperty] internal TaskInfoButtonViewModel? taskInfoButton; [ObservableProperty] @@ -114,6 +116,14 @@ public MainWindowViewModel() } if (!CustomLauncher.IsCustomMode) { + InstalledModsView = new ModListViewModel(); + NebulaModsView = new NebulaModListViewModel(); + FsoBuildsView = new FsoBuildsViewModel(); + DeveloperModView = new DeveloperModsViewModel(); + GlobalSettingsView = new GlobalSettingsViewModel(); + PxoView = new PxoViewModel(); + CommunityView = new CommunityViewModel(); + DebugView = new DebugViewModel(); FillMenuItemsNormalMode(1); } else @@ -122,6 +132,15 @@ public MainWindowViewModel() AppTitle = CustomLauncher.WindowTitle + " v" + Knossos.AppVersion; WindowHeight = CustomLauncher.WindowHeight; WindowWidth = CustomLauncher.WindowWidth; + CustomHomeVM = new CustomHomeViewModel(); + if (CustomLauncher.MenuDisplayEngineEntry) + FsoBuildsView = new FsoBuildsViewModel(); + if(CustomLauncher.MenuDisplayGlobalSettingsEntry) + GlobalSettingsView = new GlobalSettingsViewModel(); + if(CustomLauncher.MenuDisplayDebugEntry) + DebugView = new DebugViewModel(); + if (CustomLauncher.MenuDisplayNebulaLoginEntry) + NebulaLoginVM = new NebulaLoginViewModel(); FillMenuItemsCustomMode(1); } Knossos.StartUp(isQuickLaunch, forceUpdate); @@ -134,24 +153,24 @@ private void FillMenuItemsCustomMode(int defaultSelectedIndex) new MainViewMenuItem(TaskView, null, "Tasks", "Overview of current running tasks") }; - if (CustomLauncher.MenuDisplayEngineEntry) + MenuItems.Add(new MainViewMenuItem(CustomHomeVM!, "avares://Knossos.NET/Assets/general/menu_home.png", "Home", "Home")); + + if (CustomLauncher.MenuDisplayEngineEntry && FsoBuildsView != null) { MenuItems.Add(new MainViewMenuItem(FsoBuildsView, "avares://Knossos.NET/Assets/general/menu_engine.png", "Engine", "Download new Freespace Open engine builds")); } - if(CustomLauncher.MenuDisplayNebulaLoginEntry) + if(CustomLauncher.MenuDisplayNebulaLoginEntry && NebulaLoginVM != null) { - if(NebulaLoginVM == null) - NebulaLoginVM = new NebulaLoginViewModel(); MenuItems.Add(new MainViewMenuItem(NebulaLoginVM, "avares://Knossos.NET/Assets/general/menu_nebula.png", "Nebula", "Log in with your nebula account")); } - if (CustomLauncher.MenuDisplayGlobalSettingsEntry) + if (CustomLauncher.MenuDisplayGlobalSettingsEntry && GlobalSettingsView != null) { MenuItems.Add(new MainViewMenuItem(GlobalSettingsView, "avares://Knossos.NET/Assets/general/menu_settings.png", "Config", "Change launcher and FSO engine settings")); } - if (CustomLauncher.MenuDisplayDebugEntry) + if (CustomLauncher.MenuDisplayDebugEntry && DebugView != null) { MenuItems.Add(new MainViewMenuItem(DebugView, "avares://Knossos.NET/Assets/general/menu_debug.png", "Debug", "Debug info")); } @@ -168,14 +187,14 @@ private void FillMenuItemsNormalMode(int defaultSelectedIndex) Dispatcher.UIThread.Invoke(new Action(() => { MenuItems = new ObservableCollection{ new MainViewMenuItem(TaskView, null, "Tasks", "Overview of current running tasks"), - new MainViewMenuItem(InstalledModsView, "avares://Knossos.NET/Assets/general/menu_play.png", "Play", "View and run installed Freepsace Open games and modifications"), - new MainViewMenuItem(NebulaModsView, "avares://Knossos.NET/Assets/general/menu_explore.png", "Explore", "Search and install Freespace Open games and modifications"), - new MainViewMenuItem(FsoBuildsView, "avares://Knossos.NET/Assets/general/menu_engine.png", "Engine", "Download new Freespace Open engine builds"), - new MainViewMenuItem(DeveloperModView, "avares://Knossos.NET/Assets/general/menu_develop.png", "Develop", "Develop new games and modifications for the Freespace Open Engine"), - new MainViewMenuItem(CommunityView, "avares://Knossos.NET/Assets/general/menu_community.png", "Community", "FAQs and Community Resources"), - new MainViewMenuItem(PxoView, "avares://Knossos.NET/Assets/general/menu_multiplayer.png", "Multiplayer", "View multiplayer games using PXO servers"), - new MainViewMenuItem(GlobalSettingsView, "avares://Knossos.NET/Assets/general/menu_settings.png", "Settings", "Change global Freespace Open and Knossos.NET settings"), - new MainViewMenuItem(DebugView, "avares://Knossos.NET/Assets/general/menu_debug.png", "Debug", "Debug info") + new MainViewMenuItem(InstalledModsView!, "avares://Knossos.NET/Assets/general/menu_play.png", "Play", "View and run installed Freepsace Open games and modifications"), + new MainViewMenuItem(NebulaModsView!, "avares://Knossos.NET/Assets/general/menu_explore.png", "Explore", "Search and install Freespace Open games and modifications"), + new MainViewMenuItem(FsoBuildsView!, "avares://Knossos.NET/Assets/general/menu_engine.png", "Engine", "Download new Freespace Open engine builds"), + new MainViewMenuItem(DeveloperModView!, "avares://Knossos.NET/Assets/general/menu_develop.png", "Develop", "Develop new games and modifications for the Freespace Open Engine"), + new MainViewMenuItem(CommunityView!, "avares://Knossos.NET/Assets/general/menu_community.png", "Community", "FAQs and Community Resources"), + new MainViewMenuItem(PxoView!, "avares://Knossos.NET/Assets/general/menu_multiplayer.png", "Multiplayer", "View multiplayer games using PXO servers"), + new MainViewMenuItem(GlobalSettingsView!, "avares://Knossos.NET/Assets/general/menu_settings.png", "Settings", "Change global Freespace Open and Knossos.NET settings"), + new MainViewMenuItem(DebugView!, "avares://Knossos.NET/Assets/general/menu_debug.png", "Debug", "Debug info") }; if (MenuItems != null && MenuItems.Count() - 1 > defaultSelectedIndex) { @@ -193,18 +212,22 @@ partial void OnSelectedMenuItemChanged(MainViewMenuItem? value) if (value != null) { // Things to do on tab exit - if (CurrentViewModel == InstalledModsView) //Exiting the Play tab. + if (InstalledModsView != null && CurrentViewModel == InstalledModsView) //Exiting the Play tab. { sharedSearch = InstalledModsView.Search; } - if (CurrentViewModel == NebulaModsView) //Exiting the Nebula tab. + if (NebulaModsView != null && CurrentViewModel == NebulaModsView) //Exiting the Nebula tab. { sharedSearch = NebulaModsView.Search; } - if(CurrentViewModel == GlobalSettingsView) //Exiting the settings view + if(GlobalSettingsView != null && CurrentViewModel == GlobalSettingsView) //Exiting the settings view { GlobalSettingsView.CommitPendingChanges(); } + if (CurrentViewModel != null && CurrentViewModel == CustomHomeVM) //CustomHomeView + { + CustomHomeVM.ViewClosed(); + } CurrentViewModel = value.vm; @@ -235,12 +258,16 @@ partial void OnSelectedMenuItemChanged(MainViewMenuItem? value) { NebulaLoginVM.UpdateUI(); } + if (CurrentViewModel != null && CurrentViewModel == CustomHomeVM) //CustomHomeView + { + CustomHomeVM.ViewOpened(); + } if (CurrentViewModel == GlobalSettingsView) //Settings { Knossos.globalSettings.Load(); - GlobalSettingsView.LoadData(); + GlobalSettingsView?.LoadData(); //Knossos.globalSettings.EnableIniWatch(); - GlobalSettingsView.UpdateImgCacheSize(); + GlobalSettingsView?.UpdateImgCacheSize(); } else { @@ -256,7 +283,7 @@ partial void OnSelectedMenuItemChanged(MainViewMenuItem? value) /// public void AddDevMod(Mod devmod) { - DeveloperModView.AddMod(devmod); + DeveloperModView?.AddMod(devmod); } /// @@ -264,7 +291,7 @@ public void AddDevMod(Mod devmod) /// public void RunModStatusChecks() { - InstalledModsView.RunModStatusChecks(); + InstalledModsView?.RunModStatusChecks(); } /// @@ -285,7 +312,8 @@ public void ClearViews() /// public void MarkAsUpdateAvailable(string id, bool value = true) { - InstalledModsView.UpdateIsAvailable(id, value); + InstalledModsView?.UpdateIsAvailable(id, value); + CustomHomeVM?.UpdateIsAvailable(id, value); } /// @@ -294,7 +322,8 @@ public void MarkAsUpdateAvailable(string id, bool value = true) /// public void AddInstalledMod(Mod modJson) { - InstalledModsView.AddMod(modJson); + InstalledModsView?.AddMod(modJson); + CustomHomeVM?.AddModVersion(modJson); } /// @@ -317,7 +346,8 @@ public void AddMostRecent(string buildId, bool nightly) /// public void AddNebulaMod(Mod modJson) { - NebulaModsView.AddMod(modJson); + NebulaModsView?.AddMod(modJson); + CustomHomeVM?.AddNebulaModVersion(modJson); } /// @@ -328,8 +358,16 @@ public void AddNebulaMod(Mod modJson) public void BulkLoadNebulaMods(List mods, bool clear) { if(clear) - NebulaModsView.ClearView(); - NebulaModsView.AddMods(mods); + NebulaModsView?.ClearView(); + NebulaModsView?.AddMods(mods); + if (CustomLauncher.IsCustomMode) + { + foreach (var item in mods) + { + if(item.id == CustomLauncher.ModID) + CustomHomeVM?.AddNebulaModVersion(item); + } + } } /// @@ -338,7 +376,8 @@ public void BulkLoadNebulaMods(List mods, bool clear) /// public void CancelModInstall(string id) { - NebulaModsView.CancelModInstall(id); + NebulaModsView?.CancelModInstall(id); + CustomHomeVM?.CancelModInstall(id); } /// @@ -347,7 +386,8 @@ public void CancelModInstall(string id) /// public void RemoveInstalledMod(string id) { - InstalledModsView.RemoveMod(id); + InstalledModsView?.RemoveMod(id); + CustomHomeVM?.RemoveMod(id); } /// @@ -356,7 +396,8 @@ public void RemoveInstalledMod(string id) /// public void RemoveInstalledModVersion(Mod mod) { - InstalledModsView.RemoveModVersion(mod); + InstalledModsView?.RemoveModVersion(mod); + CustomHomeVM?.RemoveInstalledModVersion(mod); } /// @@ -364,7 +405,7 @@ public void RemoveInstalledModVersion(Mod mod) /// public void GlobalSettingsLoadData() { - GlobalSettingsView.LoadData(); + GlobalSettingsView?.LoadData(); } internal void ApplySettings() @@ -373,7 +414,8 @@ internal void ApplySettings() IsMenuOpen = Knossos.globalSettings.mainMenuOpen; sharedSortType = Knossos.globalSettings.sortType; InstalledModsView?.ChangeSort(sharedSortType); - NebulaModsView.sortType = sharedSortType; + if(NebulaModsView != null) + NebulaModsView.sortType = sharedSortType; }); } @@ -388,7 +430,7 @@ public void UpdateBuildInstallButtons(){ /// public void WriteToUIConsole(string message) { - DebugView.WriteToUIConsole(message); + DebugView?.WriteToUIConsole(message); } /// @@ -398,7 +440,9 @@ internal void OpenScreenshotsFolder() { try { - KnUtils.OpenFolder(KnUtils.GetFSODataFolderPath() + Path.DirectorySeparatorChar + "screenshots"); + var path = Path.Combine(KnUtils.GetFSODataFolderPath(), "screenshots"); + Directory.CreateDirectory(path); + KnUtils.OpenFolder(path); } catch (Exception ex) { diff --git a/Knossos.NET/Views/CustomHomeView.axaml b/Knossos.NET/Views/CustomHomeView.axaml new file mode 100644 index 00000000..829a3cee --- /dev/null +++ b/Knossos.NET/Views/CustomHomeView.axaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Knossos.NET/Views/CustomHomeView.axaml.cs b/Knossos.NET/Views/CustomHomeView.axaml.cs new file mode 100644 index 00000000..31e306a2 --- /dev/null +++ b/Knossos.NET/Views/CustomHomeView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Knossos.NET.Views; + +public partial class CustomHomeView : UserControl +{ + public CustomHomeView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Knossos.NET/Views/Windows/MainWindow.axaml.cs b/Knossos.NET/Views/Windows/MainWindow.axaml.cs index cd6da281..e574d7d3 100644 --- a/Knossos.NET/Views/Windows/MainWindow.axaml.cs +++ b/Knossos.NET/Views/Windows/MainWindow.axaml.cs @@ -24,7 +24,7 @@ protected override async void OnClosing(WindowClosingEventArgs e) await Dispatcher.UIThread.InvokeAsync(() => { Knossos.Tts(string.Empty); - MainWindowViewModel.Instance?.GlobalSettingsView.CommitPendingChanges(); + MainWindowViewModel.Instance?.GlobalSettingsView?.CommitPendingChanges(); Knossos.globalSettings.SaveSettingsOnAppClose(); canClose = true; }); From cb576cb1668e043764b153526b4e1c7616f661a1 Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Sun, 12 Jan 2025 15:44:55 -0300 Subject: [PATCH 05/44] Casually rework the entire cache system --- Knossos.NET/Classes/KnUtils.cs | 183 ++++++++++++------ Knossos.NET/Classes/Knossos.cs | 14 ++ Knossos.NET/Models/Nebula.cs | 29 +-- Knossos.NET/ViewModels/CustomHomeViewModel.cs | 2 +- .../ViewModels/GlobalSettingsViewModel.cs | 4 +- .../ViewModels/Templates/ModCardViewModel.cs | 2 +- .../Templates/NebulaModCardViewModel.cs | 7 +- .../Templates/Tasks/InstallBuild.cs | 6 +- .../ViewModels/Templates/Tasks/InstallMod.cs | 6 +- .../ViewModels/Windows/ModDetailsViewModel.cs | 4 +- Knossos.NET/Views/CustomHomeView.axaml | 4 +- Knossos.NET/Views/GlobalSettingsView.axaml | 2 +- 12 files changed, 165 insertions(+), 98 deletions(-) diff --git a/Knossos.NET/Classes/KnUtils.cs b/Knossos.NET/Classes/KnUtils.cs index f4f7da70..181213e3 100644 --- a/Knossos.NET/Classes/KnUtils.cs +++ b/Knossos.NET/Classes/KnUtils.cs @@ -614,105 +614,180 @@ public static async Task GetSizeOfFolderInBytes(string folderPath, bool re /// Gets the fullpath to image storage cache /// /// - public static string GetImageCachePath() + public static string GetCachePath() { try { - return Path.Combine(GetKnossosDataFolderPath(), "image_cache"); + return Path.Combine(GetKnossosDataFolderPath(), "cache"); } catch { return string.Empty; } } /// - /// Downloads a image from a URL, stores it in cache and serves the filestream - /// If the image is already in cache, no download is done + /// Downloads a file from a URL, stores it in cache and serves the filestream + /// If the file is already in cache, a check is done to make sure it has no changed. + /// If it has changed it is re-downloaded /// - /// - /// Cached image filestream or null if failed - public static async Task GetImageStream(string imageURL, int attempt = 1) + /// + /// Cached filestream or null if failed + public static async Task GetRemoteResourceStream(string resourceURL) { try { - return await Task.Run(async () => + var localFile = await GetRemoteResource(resourceURL); + if (localFile != null) { - var imageName = Path.GetFileName(imageURL); - var imageInCachePath = Path.Combine(GetImageCachePath(), imageName); - - if (File.Exists(imageInCachePath) && new FileInfo(imageInCachePath).Length > 0) - { - return new FileStream(imageInCachePath, FileMode.Open, FileAccess.Read, FileShare.Read); - } - else - { - //Download to cache and copy - Directory.CreateDirectory(Path.Combine(GetKnossosDataFolderPath(), "image_cache")); - using (var imageStream = await GetHttpClient().GetStreamAsync(imageURL)) - { - var fileStream = new FileStream(imageInCachePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read); - await imageStream.CopyToAsync(fileStream); - imageStream.Close(); - fileStream.Seek(0, SeekOrigin.Begin); - return fileStream; - } - } - }); + var fileStream = new FileStream(localFile, FileMode.Open, FileAccess.Read, FileShare.Read); + if(fileStream.Length == 0) + return null; + return fileStream; + } } catch (Exception ex) { - Log.Add(Log.LogSeverity.Error, "KnUtils.GetImageStream()", ex); - if (attempt <= 2) - { - await Task.Delay(1000); - return await GetImageStream(imageURL, attempt + 1); - } + Log.Add(Log.LogSeverity.Error, "KnUtils.GetRemoteResourceStream()", ex); } return null; } /// - /// Downloads a image from a URL, stores it in cache and passes the local image path - /// If the image is already in cache, no download is done + /// Downloads a file from a URL, stores it in cache and returns the local path + /// If the file is already in cache, a check is done to make sure it has no changed. + /// If it has changed it is updated /// /// /// /// string path or null - public static async Task GetImagePath(string imageURL, int attempt = 1) + public static async Task GetRemoteResource(string resourceURL, int attempt = 1) { + string fileInCachePath = string.Empty; + bool cacheFileIsValid = false; try { - return await Task.Run(async () => + Directory.CreateDirectory(GetCachePath()); //make sure the cache dir exists + var fileName = Path.GetFileName(resourceURL); + fileInCachePath = Path.Combine(GetCachePath(), fileName); + var fileInCacheEtagPath = fileInCachePath + ".etag"; + string? remoteEtag = null; + bool cacheFileExists = File.Exists(fileInCachePath); + Uri uri = new Uri(resourceURL); + bool isNebulaFile = Nebula.nebulaMirrors.Contains(uri.Host.ToLower()); + cacheFileIsValid = cacheFileExists && new FileInfo(fileInCachePath).Length > 0 ? true : false; + + //file exists in cache? check it + if (cacheFileIsValid && cacheFileExists) { - var imageName = Path.GetFileName(imageURL); - var imageInCachePath = Path.Combine(GetImageCachePath(), imageName); + bool cacheFileEtagExists = File.Exists(fileInCacheEtagPath); + if (isNebulaFile) + { + //This is a nebula file, nebula files are stored by their checksum so they never update + return fileInCachePath; + } + else if (cacheFileEtagExists) + { + //etag info exist, check it + var cachedEtag = await File.ReadAllTextAsync(fileInCacheEtagPath); + remoteEtag = await GetUrlFileEtag(resourceURL); + if (cachedEtag != null && cachedEtag == remoteEtag) + { + //cache is up to date + return fileInCachePath; + } + else + { + Log.Add(Log.LogSeverity.Information, "KnUtils.GetRemoteResource()", "File: "+ fileName + " from cache is outdated, re-download. Cache etag: " + cachedEtag + " Remove etag: " + remoteEtag); + } + } + //not etag info or it has changed, re-download + } - if (!File.Exists(imageInCachePath) || File.Exists(imageInCachePath) && new FileInfo(imageInCachePath).Length > 0) + Log.Add(Log.LogSeverity.Information, "KnUtils.GetRemoteResource()", "Downloading: " + resourceURL + " to local cache."); + //download to cache + using (var imageStream = await GetHttpClient().GetStreamAsync(resourceURL)) + { + using (var fileStream = new FileStream(fileInCachePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) { - //Download to cache and copy - Directory.CreateDirectory(Path.Combine(GetKnossosDataFolderPath(), "image_cache")); - using (var imageStream = await GetHttpClient().GetStreamAsync(imageURL)) + await imageStream.CopyToAsync(fileStream); + } + } + //save etag + if (!isNebulaFile) + { + try + { + if (remoteEtag == null) + { + remoteEtag = await GetUrlFileEtag(resourceURL); + } + if (remoteEtag != null) + { + File.WriteAllText(fileInCacheEtagPath, remoteEtag, Encoding.UTF8); + } + else { - var fileStream = new FileStream(imageInCachePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read); - await imageStream.CopyToAsync(fileStream); - imageStream.Close(); - fileStream.Close(); - fileStream.Dispose(); + Log.Add(Log.LogSeverity.Error, "KnUtils.GetRemoteResource()", "Could not save etag information for file " + resourceURL + " remoteEtag value was null."); } } - return imageInCachePath; - }); + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "KnUtils.GetRemoteResource()", ex); + } + } + return fileInCachePath; } catch (Exception ex) { Log.Add(Log.LogSeverity.Error, "KnUtils.GetImagePath()", ex); - if (attempt <= 2) + if (attempt < 3) { await Task.Delay(1000); - return await GetImagePath(imageURL, attempt + 1); + return await GetRemoteResource(resourceURL, attempt + 1); + } + else + { + //If the download somehow fails, but we have a valid local version of this file, pass it, no matter if it is outdated + if (cacheFileIsValid) + { + return fileInCachePath; + } } } return null; } + /// + /// Reads etag data from a url file + /// + /// etag string or null + public static async Task GetUrlFileEtag(string url) + { + try + { + string? newEtag = null; + Log.Add(Log.LogSeverity.Information, "KnUtils.GetUrlFileEtag()", "Getting " + url + " etag."); + + var result = await KnUtils.GetHttpClient().GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + newEtag = result.Headers?.ETag?.ToString().Replace("\"", ""); + try + { + //workaround because it was not always working on some urls + if (newEtag == null && result.Headers != null) + { + var etagHeader = result.Headers.FirstOrDefault(x => x.Key != null && x.Key.ToLower() == "etag"); + newEtag = etagHeader.Value.FirstOrDefault(); + } + } + catch { } + Log.Add(Log.LogSeverity.Information, "KnUtils.GetUrlFileEtag()", Path.GetFileName(url) + " etag: " + newEtag); + return newEtag; + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "KnUtils.GetUrlFileEtag()", ex); + return null; + } + } + /// /// Check FreeSpace available on the disk/partion of path /// diff --git a/Knossos.NET/Classes/Knossos.cs b/Knossos.NET/Classes/Knossos.cs index 37f28667..12969722 100644 --- a/Knossos.NET/Classes/Knossos.cs +++ b/Knossos.NET/Classes/Knossos.cs @@ -96,6 +96,20 @@ public static async void StartUp(bool isQuickLaunch, bool forceUpdate) } } + //Rename the old cache folder + try + { + var oldCachePath = Path.Combine(KnUtils.GetKnossosDataFolderPath(), "image_cache"); + if (!Directory.Exists(KnUtils.GetCachePath()) && Directory.Exists(oldCachePath)) + { + Directory.Move(oldCachePath, KnUtils.GetCachePath()); + } + } + catch(Exception ex) + { + Log.Add(Log.LogSeverity.Error, "Knossos.Startup()", ex); + } + Log.Add(Log.LogSeverity.Information, "Knossos.StartUp()", "=== KnossosNET v" + AppVersion + " Start ==="); if (inPortableMode) diff --git a/Knossos.NET/Models/Nebula.cs b/Knossos.NET/Models/Nebula.cs index 9f7ca3cd..b443dc43 100644 --- a/Knossos.NET/Models/Nebula.cs +++ b/Knossos.NET/Models/Nebula.cs @@ -61,8 +61,9 @@ private struct NebulaCache //https://cf.fsnebula.org/storage/repo.json //https://dl.fsnebula.org/storage/repo.json - //https://aigaion.feralhosting.com/discovery/nebula/repo.json //https://fsnebula.org/storage/repo.json" + + public static string[] nebulaMirrors = { "cf.fsnebula.org", "dl.fsnebula.org", "fsnebula.org", "talos.feralhosting.com", "fsnebula.global.ssl.fastly.net" }; //lowercase, last one is the image host private static readonly string repoUrl = @"https://fsnebula.org/storage/repo_minimal.json"; private static readonly string apiURL = @"https://api.fsnebula.org/api/1/"; private static readonly string nebulaURL = @"https://fsnebula.org/"; @@ -125,7 +126,7 @@ public static async Task Trinity() try { bool displayUpdates = settings.NewerModsVersions.Any() ? true : false; - var webEtag = await GetRepoEtag().ConfigureAwait(false); + var webEtag = await KnUtils.GetUrlFileEtag(repoUrl).ConfigureAwait(false); if (!File.Exists(KnUtils.GetKnossosDataFolderPath() + Path.DirectorySeparatorChar + "repo_minimal.json") || settings.etag != webEtag) { //Download the repo_minimal.json @@ -597,30 +598,6 @@ private static async Task WaitForFileAccess(string filename) } } - /// - /// Reads the current repo ETAG on nebula - /// - /// etag string or null - private static async Task GetRepoEtag() - { - try - { - string? newEtag = null; - Log.Add(Log.LogSeverity.Information, "Nebula.GetRepoEtag()", "Getting repo_minimal.json etag."); - - var result = await KnUtils.GetHttpClient().GetAsync(repoUrl, HttpCompletionOption.ResponseHeadersRead); - newEtag = result.Headers?.ETag?.ToString().Replace("\"", ""); - - Log.Add(Log.LogSeverity.Information, "Nebula.GetRepoEtag()", "repo_minimal.json etag: " + newEtag); - return newEtag; - } - catch (Exception ex) - { - Log.Add(Log.LogSeverity.Error, "Nebula.GetRepoEtag()", ex); - return null; - } - } - /// /// Save nebula.json file /// diff --git a/Knossos.NET/ViewModels/CustomHomeViewModel.cs b/Knossos.NET/ViewModels/CustomHomeViewModel.cs index 4eacaa33..f213c29a 100644 --- a/Knossos.NET/ViewModels/CustomHomeViewModel.cs +++ b/Knossos.NET/ViewModels/CustomHomeViewModel.cs @@ -117,7 +117,7 @@ public void ViewOpened() { var temp = BackgroundImage; BackgroundImage = ""; - var imageFile = await KnUtils.GetImagePath(temp).ConfigureAwait(false); + var imageFile = await KnUtils.GetRemoteResource(temp).ConfigureAwait(false); Dispatcher.UIThread.Invoke(() => { if (imageFile != null) diff --git a/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs b/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs index 7c364720..cf8ca540 100644 --- a/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs +++ b/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs @@ -1489,7 +1489,7 @@ internal async void ClearImageCache() await Task.Run(() => { try { - var path = KnUtils.GetImageCachePath(); + var path = KnUtils.GetCachePath(); Directory.Delete(path, true); UpdateImgCacheSize(); @@ -1509,7 +1509,7 @@ public void UpdateImgCacheSize() Task.Run(async () => { try { - var path = KnUtils.GetImageCachePath(); + var path = KnUtils.GetCachePath(); if (Directory.Exists(path)) { var sizeInBytes = await KnUtils.GetSizeOfFolderInBytes(path).ConfigureAwait(false); diff --git a/Knossos.NET/ViewModels/Templates/ModCardViewModel.cs b/Knossos.NET/ViewModels/Templates/ModCardViewModel.cs index cfecc8ad..06e56752 100644 --- a/Knossos.NET/ViewModels/Templates/ModCardViewModel.cs +++ b/Knossos.NET/ViewModels/Templates/ModCardViewModel.cs @@ -346,7 +346,7 @@ private void LoadImage() { Task.Run(async () => { - using (var fs = await KnUtils.GetImageStream(tile)) + using (var fs = await KnUtils.GetRemoteResourceStream(tile)) { if(fs != null) Image = new Bitmap(fs); diff --git a/Knossos.NET/ViewModels/Templates/NebulaModCardViewModel.cs b/Knossos.NET/ViewModels/Templates/NebulaModCardViewModel.cs index dbed7442..3dee01d9 100644 --- a/Knossos.NET/ViewModels/Templates/NebulaModCardViewModel.cs +++ b/Knossos.NET/ViewModels/Templates/NebulaModCardViewModel.cs @@ -145,10 +145,11 @@ private void LoadImage(string modFullPath, string? tileString) { Task.Run(async () => { - using (var fs = await KnUtils.GetImageStream(tileString).ConfigureAwait(false)) + using (var fs = await KnUtils.GetRemoteResourceStream(tileString).ConfigureAwait(false)) { - Dispatcher.UIThread.Invoke(() => { - if(fs != null) + Dispatcher.UIThread.Invoke(() => + { + if (fs != null) TileImage = new Bitmap(fs); }); } diff --git a/Knossos.NET/ViewModels/Templates/Tasks/InstallBuild.cs b/Knossos.NET/ViewModels/Templates/Tasks/InstallBuild.cs index 2ca14611..0462e205 100644 --- a/Knossos.NET/ViewModels/Templates/Tasks/InstallBuild.cs +++ b/Knossos.NET/ViewModels/Templates/Tasks/InstallBuild.cs @@ -313,7 +313,7 @@ public partial class TaskItemViewModel : ViewModelBase { Directory.CreateDirectory(modPath + Path.DirectorySeparatorChar + "kn_images"); var uri = new Uri(modJson.tile); - using (var fs = await KnUtils.GetImageStream(modJson.tile)) + using (var fs = await KnUtils.GetRemoteResourceStream(modJson.tile)) { var tileTask = new TaskItemViewModel(); await Dispatcher.UIThread.InvokeAsync(() => TaskList.Insert(0, tileTask)); @@ -343,7 +343,7 @@ public partial class TaskItemViewModel : ViewModelBase { Directory.CreateDirectory(modPath + Path.DirectorySeparatorChar + "kn_images"); var uri = new Uri(modJson.banner); - using (var fs = await KnUtils.GetImageStream(modJson.banner)) + using (var fs = await KnUtils.GetRemoteResourceStream(modJson.banner)) { var bannerTask = new TaskItemViewModel(); await Dispatcher.UIThread.InvokeAsync(() => TaskList.Insert(0, bannerTask)); @@ -378,7 +378,7 @@ public partial class TaskItemViewModel : ViewModelBase throw new TaskCanceledException(); } var uri = new Uri(sc); - using (var fs = await KnUtils.GetImageStream(sc)) + using (var fs = await KnUtils.GetRemoteResourceStream(sc)) { var scTask = new TaskItemViewModel(); await Dispatcher.UIThread.InvokeAsync(() => TaskList.Insert(0, scTask)); diff --git a/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs b/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs index f7ffc1a9..afef9abc 100644 --- a/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs +++ b/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs @@ -445,7 +445,7 @@ public async Task InstallMod(Mod mod, CancellationTokenSource cancelSource { Directory.CreateDirectory(modPath + Path.DirectorySeparatorChar + "kn_images"); var uri = new Uri(mod.tile); - using (var fs = await KnUtils.GetImageStream(mod.tile)) + using (var fs = await KnUtils.GetRemoteResourceStream(mod.tile)) { var tileTask = new TaskItemViewModel(); await Dispatcher.UIThread.InvokeAsync(() => TaskList.Insert(0, tileTask)); @@ -474,7 +474,7 @@ public async Task InstallMod(Mod mod, CancellationTokenSource cancelSource Directory.CreateDirectory(modPath + Path.DirectorySeparatorChar + "kn_images"); Directory.CreateDirectory(modPath + Path.DirectorySeparatorChar + "kn_images"); var uri = new Uri(mod.banner); - using (var fs = await KnUtils.GetImageStream(mod.banner)) + using (var fs = await KnUtils.GetRemoteResourceStream(mod.banner)) { var bannerTask = new TaskItemViewModel(); await Dispatcher.UIThread.InvokeAsync(() => TaskList.Insert(0, bannerTask)); @@ -509,7 +509,7 @@ public async Task InstallMod(Mod mod, CancellationTokenSource cancelSource throw new TaskCanceledException(); } var uri = new Uri(sc); - using (var fs = await KnUtils.GetImageStream(sc)) + using (var fs = await KnUtils.GetRemoteResourceStream(sc)) { var scTask = new TaskItemViewModel(); await Dispatcher.UIThread.InvokeAsync(() => TaskList.Insert(0, scTask)); diff --git a/Knossos.NET/ViewModels/Windows/ModDetailsViewModel.cs b/Knossos.NET/ViewModels/Windows/ModDetailsViewModel.cs index 9b247639..45b2582b 100644 --- a/Knossos.NET/ViewModels/Windows/ModDetailsViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/ModDetailsViewModel.cs @@ -280,7 +280,7 @@ private void LoadBanner(int selectedIndex) Banner = new Bitmap(AssetLoader.Open(new Uri("avares://Knossos.NET/Assets/general/loading.png"))); Task.Run(async () => { - using (var fs = await KnUtils.GetImageStream(url).ConfigureAwait(false)) + using (var fs = await KnUtils.GetRemoteResourceStream(url).ConfigureAwait(false)) { Dispatcher.UIThread.Invoke(() => { if (fs != null) @@ -345,7 +345,7 @@ private void LoadScreenshots(int selectedIndex) { Task.Run(async () => { - using (var fs = await KnUtils.GetImageStream(scn)) + using (var fs = await KnUtils.GetRemoteResourceStream(scn)) { if (fs != null) { diff --git a/Knossos.NET/Views/CustomHomeView.axaml b/Knossos.NET/Views/CustomHomeView.axaml index 829a3cee..eb57448b 100644 --- a/Knossos.NET/Views/CustomHomeView.axaml +++ b/Knossos.NET/Views/CustomHomeView.axaml @@ -15,14 +15,14 @@ - + diff --git a/Knossos.NET/Views/GlobalSettingsView.axaml b/Knossos.NET/Views/GlobalSettingsView.axaml index 8baf56e4..2e556dca 100644 --- a/Knossos.NET/Views/GlobalSettingsView.axaml +++ b/Knossos.NET/Views/GlobalSettingsView.axaml @@ -78,7 +78,7 @@ - From 2ec27a471f41252286f4820d295d2ea0947c4722 Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Sun, 12 Jan 2025 21:58:02 -0300 Subject: [PATCH 06/44] Custom mode home screen part 2 --- Knossos.NET/ViewModels/CustomHomeViewModel.cs | 122 ++++++++++++++++-- Knossos.NET/ViewModels/TaskViewModel.cs | 3 +- .../ViewModels/Templates/Tasks/InstallMod.cs | 2 +- .../ViewModels/Windows/MainWindowViewModel.cs | 11 ++ .../ViewModels/Windows/ModInstallViewModel.cs | 2 +- Knossos.NET/Views/CustomHomeView.axaml | 31 +++++ 6 files changed, 155 insertions(+), 16 deletions(-) diff --git a/Knossos.NET/ViewModels/CustomHomeViewModel.cs b/Knossos.NET/ViewModels/CustomHomeViewModel.cs index f213c29a..1945a893 100644 --- a/Knossos.NET/ViewModels/CustomHomeViewModel.cs +++ b/Knossos.NET/ViewModels/CustomHomeViewModel.cs @@ -2,24 +2,38 @@ using CommunityToolkit.Mvvm.ComponentModel; using Knossos.NET.Classes; using Knossos.NET.Models; +using Knossos.NET.Views; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; -using System.Text; +using System.Threading; using System.Threading.Tasks; -using System.Xml.Linq; namespace Knossos.NET.ViewModels { public partial class CustomHomeViewModel : ViewModelBase { private List modVersions = new List(); - private int activeVersionIndex = 0; - private List nebulaModVersions = new List(); + private CancellationTokenSource? cancellationTokenSource = null; + + [ObservableProperty] + internal int activeVersionIndex = 0; + + internal ObservableCollection VersionItems { get; set; } = new ObservableCollection(); + + [ObservableProperty] + internal bool installed = false; + + [ObservableProperty] + internal bool installing = false; + + [ObservableProperty] + internal bool update = false; [ObservableProperty] - internal string? modVersion; + internal bool nebulaVersionsAvailable = false; [ObservableProperty] internal string? backgroundImage = CustomLauncher.HomeBackgroundImage; @@ -31,10 +45,65 @@ public CustomHomeViewModel() { } + internal void HardcodedButtonCommand(object cmd) + { + if (ActiveVersionIndex == -1) + { + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.HardcodedButtonCommand()", "Crash prevented: ActiveVersionIndex was " + ActiveVersionIndex + " and modVersions.Count() was " + modVersions.Count()); + ActiveVersionIndex = 0; + } + switch ((string)cmd) + { + case "play": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.Release); break; + case "playvr": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.Release, false, 0, true); break; + case "fred2": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.Fred2); break; + case "fred2debug": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.Fred2Debug); break; + case "debug": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.Debug); break; + case "qtfred": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.QtFred); break; + case "qtfreddebug": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.QtFredDebug); break; + case "install": Install(); break; + case "cancel": Cancel(); break; + case "update": break; + case "modify": break; + case "delete": break; + case "details": break; + case "settings": break; + case "logfile": break; + } + } + + private void Cancel() + { + Installing = false; + try + { + cancellationTokenSource?.Cancel(); + } + catch { } + cancellationTokenSource = null; + TaskViewModel.Instance?.CancelAllInstallTaskWithID(CustomLauncher.ModID!, null); + } + + private async void Install() + { + if (nebulaModVersions.Any()) + { + var dialog = new ModInstallView(); + dialog.DataContext = new ModInstallViewModel(nebulaModVersions.First(), dialog); + await dialog.ShowDialog(MainWindow.instance!); + } + else + { + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.Install()", "Tried to install but no nebula versions were loaded."); + } + } + public void RemoveInstalledModVersion(Mod mod) { if (CustomLauncher.ModID == mod.id) { + Installed = modVersions.Any(); + Installing = false; } } @@ -42,6 +111,8 @@ public void RemoveMod(string id) { if (CustomLauncher.ModID == id) { + Installed = false; + Installing = false; } } @@ -49,9 +120,16 @@ public void CancelModInstall(string id) { if (CustomLauncher.ModID == id) { + Cancel(); } } + public void SetInstalling(string id, CancellationTokenSource cancelToken) + { + cancellationTokenSource = cancelToken; + Installing = true; + } + /// /// Add a installed mod version of this TC. /// It will check if the ID matches the one in CustomLauncher.ModID @@ -62,15 +140,32 @@ public void AddModVersion(Mod modJson) if (modJson.id == CustomLauncher.ModID) { Log.Add(Log.LogSeverity.Information, "CustomHomeViewModel.AddModVersion()", "Adding additional version for mod id: " + CustomLauncher.ModID + " -> " + modJson.folderName); - string currentVersion = modVersions[activeVersionIndex].version; - modVersions.Add(modJson); - modVersions.Sort((o1, o2) => -SemanticVersion.Compare(o1.version, o2.version)); - if (SemanticVersion.Compare(modJson.version, currentVersion) > 0) + if (modVersions.Any()) { - Log.Add(Log.LogSeverity.Information, "CustomHomeViewModel.AddModVersion()", "Changing active version for " + modJson.title + " from " + modVersions[activeVersionIndex].version + " to " + modJson.version); - activeVersionIndex = modVersions.FindIndex((m) => m.version.Equals(modJson.version)); - ModVersion = modJson.version + " (+" + (modVersions.Count - 1) + ")"; + string currentVersion = modVersions[ActiveVersionIndex].version; + modVersions.Add(modJson); + modVersions.Sort((o1, o2) => -SemanticVersion.Compare(o1.version, o2.version)); + if (SemanticVersion.Compare(modJson.version, currentVersion) > 0) + { + Log.Add(Log.LogSeverity.Information, "CustomHomeViewModel.AddModVersion()", "Changing active version for " + modJson.title + " from " + modVersions[ActiveVersionIndex].version + " to " + modJson.version); + VersionItems.Clear(); + modVersions.ForEach(x => VersionItems.Add(x.version)); + ActiveVersionIndex = -1; + ActiveVersionIndex = modVersions.FindIndex((m) => m.version.Equals(modJson.version)); + } + else + { + VersionItems.Add(modJson.version); + } + } + else + { + ActiveVersionIndex = -1; + modVersions.Add(modJson); + VersionItems.Add(modJson.version); + ActiveVersionIndex = 0; } + Installed = modVersions.Any(); } } @@ -86,6 +181,7 @@ public void AddNebulaModVersion(Mod modJson) Log.Add(Log.LogSeverity.Information, "CustomHomeViewModel.AddNebulaModVersion()", "Adding additional nebula version for mod id: " + CustomLauncher.ModID + " -> " + modJson.version); nebulaModVersions.Add(modJson); nebulaModVersions.Sort((o1, o2) => -SemanticVersion.Compare(o1.version, o2.version)); + NebulaVersionsAvailable = true; } } @@ -99,7 +195,7 @@ public void UpdateIsAvailable(string id, bool value) { if (id == CustomLauncher.ModID) { - + Update = value; } } diff --git a/Knossos.NET/ViewModels/TaskViewModel.cs b/Knossos.NET/ViewModels/TaskViewModel.cs index d42174a0..0f851027 100644 --- a/Knossos.NET/ViewModels/TaskViewModel.cs +++ b/Knossos.NET/ViewModels/TaskViewModel.cs @@ -49,6 +49,7 @@ public void CancelAllRunningTasks() /// /// Cancel a ModInstall or Verify mod by mod ID and version + /// Null version will cancel all tasks with the same id /// /// /// @@ -58,7 +59,7 @@ public void CancelAllInstallTaskWithID(string id, string? version) { foreach (var task in TaskList) { - if (!task.IsCompleted && task.installID == id && task.installVersion == version) + if (!task.IsCompleted && task.installID == id && (version == null || task.installVersion == version)) { task.CancelTaskCommand(); } diff --git a/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs b/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs index afef9abc..449d2edb 100644 --- a/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs +++ b/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs @@ -45,7 +45,7 @@ public async Task InstallMod(Mod mod, CancellationTokenSource cancelSource bool compressMod = false; //Set Mod card as "installing" - MainWindowViewModel.Instance?.NebulaModsView?.SetInstalling(mod.id, cancellationTokenSource); + MainWindowViewModel.Instance?.SetInstalling(mod.id, cancellationTokenSource); //Wait in Queue while (TaskViewModel.Instance!.taskQueue.Count > 0 && TaskViewModel.Instance!.taskQueue.Peek() != this) diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index 183b7b30..482d2dba 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -8,6 +8,8 @@ using System.Linq; using System.Collections.ObjectModel; using Avalonia.Threading; +using System.Threading; +using Knossos.NET.Classes; namespace Knossos.NET.ViewModels { @@ -455,5 +457,14 @@ internal void TriggerMenuCommand() IsMenuOpen = !IsMenuOpen; Knossos.globalSettings.mainMenuOpen = IsMenuOpen; } + + /// + /// Sets a mod id as "installing" so the proper info can be displayed on the UI + /// + public void SetInstalling(string id, CancellationTokenSource cancelToken) + { + NebulaModsView?.SetInstalling(id, cancelToken); + CustomHomeVM?.SetInstalling(id, cancelToken); + } } } diff --git a/Knossos.NET/ViewModels/Windows/ModInstallViewModel.cs b/Knossos.NET/ViewModels/Windows/ModInstallViewModel.cs index 4c9a5a49..3aa32af4 100644 --- a/Knossos.NET/ViewModels/Windows/ModInstallViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/ModInstallViewModel.cs @@ -109,7 +109,7 @@ public ModInstallViewModel(Mod modJson, Window dialog, string? preSelectedVersio /// private async void InitialLoad(string id, string? preSelectedVersion = null) { - if (Nebula.userIsLoggedIn) + if (Nebula.userIsLoggedIn && !Knossos.inSingleTCMode || Nebula.userIsLoggedIn && Knossos.inSingleTCMode && CustomLauncher.MenuDisplayNebulaLoginEntry) { var ids = await Nebula.GetEditableModIDs(); if (ids != null && ids.Any() && ids.FirstOrDefault(x => x == id) != null) diff --git a/Knossos.NET/Views/CustomHomeView.axaml b/Knossos.NET/Views/CustomHomeView.axaml index eb57448b..073952e9 100644 --- a/Knossos.NET/Views/CustomHomeView.axaml +++ b/Knossos.NET/Views/CustomHomeView.axaml @@ -24,6 +24,37 @@ anim:ImageBehavior.SpeedRatio="{Binding Animate}" Source="{Binding BackgroundImage, Converter={StaticResource imageConverter}}" anim:ImageBehavior.AnimatedSource="{Binding BackgroundImage}" /> + + + + + + + + + + + + + + + + + + + From 515f9eea8c04dfc4781f1d4ae6e4af60afdb0a22 Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Mon, 13 Jan 2025 20:46:40 -0300 Subject: [PATCH 07/44] Minor corrections and initial status of the menu as a config option --- Knossos.NET/Models/CustomLauncher.cs | 10 ++++++++++ Knossos.NET/Models/Nebula.cs | 2 +- Knossos.NET/ViewModels/CustomHomeViewModel.cs | 2 +- Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs | 1 + 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Knossos.NET/Models/CustomLauncher.cs b/Knossos.NET/Models/CustomLauncher.cs index 17b43997..901e5cc8 100644 --- a/Knossos.NET/Models/CustomLauncher.cs +++ b/Knossos.NET/Models/CustomLauncher.cs @@ -57,6 +57,12 @@ public static class CustomLauncher /// public static int? WindowHeight { get; private set; } = 540; + /// + /// The first time the user opens the launcher, the main menu should be expanded or collapsed? + /// After that it will use the saved state + /// + public static bool MenuOpenFirstTime { get; private set; } = false; + /// /// Add the regular FSO engine view to the menu /// @@ -172,6 +178,9 @@ private static void ReadCustomFile() if (customData.WindowHeight != null) WindowHeight = customData.WindowHeight; + if (customData.MenuOpenFirstTime.HasValue) + MenuOpenFirstTime = customData.MenuOpenFirstTime.Value; + if (customData.MenuDisplayEngineEntry.HasValue) MenuDisplayEngineEntry = customData.MenuDisplayEngineEntry.Value; @@ -212,6 +221,7 @@ struct CustomFileData public string? WindowTitle { get; set; } public int? WindowWidth { get; set; } public int? WindowHeight { get; set; } + public bool? MenuOpenFirstTime { get; set; } public bool? MenuDisplayEngineEntry { get; set; } public bool? MenuDisplayDebugEntry { get; set; } public bool? MenuDisplayNebulaLoginEntry { get; set; } diff --git a/Knossos.NET/Models/Nebula.cs b/Knossos.NET/Models/Nebula.cs index b443dc43..f914226b 100644 --- a/Knossos.NET/Models/Nebula.cs +++ b/Knossos.NET/Models/Nebula.cs @@ -125,7 +125,7 @@ public static async Task Trinity() } try { - bool displayUpdates = settings.NewerModsVersions.Any() ? true : false; + bool displayUpdates = settings.NewerModsVersions.Any() && !CustomLauncher.IsCustomMode ? true : false; var webEtag = await KnUtils.GetUrlFileEtag(repoUrl).ConfigureAwait(false); if (!File.Exists(KnUtils.GetKnossosDataFolderPath() + Path.DirectorySeparatorChar + "repo_minimal.json") || settings.etag != webEtag) { diff --git a/Knossos.NET/ViewModels/CustomHomeViewModel.cs b/Knossos.NET/ViewModels/CustomHomeViewModel.cs index 1945a893..9a5ecbea 100644 --- a/Knossos.NET/ViewModels/CustomHomeViewModel.cs +++ b/Knossos.NET/ViewModels/CustomHomeViewModel.cs @@ -86,7 +86,7 @@ private void Cancel() private async void Install() { - if (nebulaModVersions.Any()) + if (nebulaModVersions.Any() && CustomLauncher.UseNebulaServices) { var dialog = new ModInstallView(); dialog.DataContext = new ModInstallViewModel(nebulaModVersions.First(), dialog); diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index 482d2dba..aa64acb8 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -131,6 +131,7 @@ public MainWindowViewModel() else { //Apply customization for Single TC Mode + Knossos.globalSettings.mainMenuOpen = CustomLauncher.MenuOpenFirstTime; AppTitle = CustomLauncher.WindowTitle + " v" + Knossos.AppVersion; WindowHeight = CustomLauncher.WindowHeight; WindowWidth = CustomLauncher.WindowWidth; From 534d42ed76ebf4825f9ede3d3e1e80003fb166f4 Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Tue, 14 Jan 2025 23:57:56 -0300 Subject: [PATCH 08/44] Custom mode home screen part 3 --- Knossos.NET/AppStyles.axaml | 1 + Knossos.NET/Models/GlobalSettings.cs | 4 + Knossos.NET/Models/ModSettings.cs | 2 +- Knossos.NET/ViewModels/CustomHomeViewModel.cs | 248 ++++++++++++++++-- .../ViewModels/Windows/ModDetailsViewModel.cs | 2 + Knossos.NET/Views/CustomHomeView.axaml | 72 ++++- .../Views/Windows/ModDetailsView.axaml | 4 +- 7 files changed, 290 insertions(+), 43 deletions(-) diff --git a/Knossos.NET/AppStyles.axaml b/Knossos.NET/AppStyles.axaml index 758a8213..d7f18343 100644 --- a/Knossos.NET/AppStyles.axaml +++ b/Knossos.NET/AppStyles.axaml @@ -73,6 +73,7 @@ M2 4.5C2 4.22386 2.22386 4 2.5 4H17.5C17.7761 4 18 4.22386 18 4.5C18 4.77614 17.7761 5 17.5 5H2.5C2.22386 5 2 4.77614 2 4.5Z M2 9.5C2 9.22386 2.22386 9 2.5 9H17.5C17.7761 9 18 9.22386 18 9.5C18 9.77614 17.7761 10 17.5 10H2.5C2.22386 10 2 9.77614 2 9.5Z M2.5 14C2.22386 14 2 14.2239 2 14.5C2 14.7761 2.22386 15 2.5 15H17.5C17.7761 15 18 14.7761 18 14.5C18 14.2239 17.7761 14 17.5 14H2.5Z + M31.8839 8.36612C32.372 8.85427 32.372 9.64573 31.8839 10.1339L18.0178 24L31.8839 37.8661C32.372 38.3543 32.372 39.1457 31.8839 39.6339C31.3957 40.122 30.6043 40.122 30.1161 39.6339L15.3661 24.8839C14.878 24.3957 14.878 23.6043 15.3661 23.1161L30.1161 8.36612C30.6043 7.87796 31.3957 7.87796 31.8839 8.36612Z diff --git a/Knossos.NET/Models/GlobalSettings.cs b/Knossos.NET/Models/GlobalSettings.cs index 7f9b5ee0..092fe8d8 100644 --- a/Knossos.NET/Models/GlobalSettings.cs +++ b/Knossos.NET/Models/GlobalSettings.cs @@ -705,6 +705,10 @@ private void SetCustomModeValues() checkUpdate = CustomLauncher.AllowLauncherUpdates; enableLogFile = CustomLauncher.WriteLogFile; autoUpdate = false; + if (!CustomLauncher.MenuDisplayGlobalSettingsEntry) + { + warnNewSettingsSystem = false; + } } } diff --git a/Knossos.NET/Models/ModSettings.cs b/Knossos.NET/Models/ModSettings.cs index 8c4d9d47..d189526a 100644 --- a/Knossos.NET/Models/ModSettings.cs +++ b/Knossos.NET/Models/ModSettings.cs @@ -71,7 +71,7 @@ public bool IsDefaultConfig() /// /// Load mod_settings.json data - /// Any new variabled must be added here or it will not be loaded + /// Any new variables must be added here or it will not be loaded /// /// public void Load(string modFolderPath) diff --git a/Knossos.NET/ViewModels/CustomHomeViewModel.cs b/Knossos.NET/ViewModels/CustomHomeViewModel.cs index 9a5ecbea..56873bf4 100644 --- a/Knossos.NET/ViewModels/CustomHomeViewModel.cs +++ b/Knossos.NET/ViewModels/CustomHomeViewModel.cs @@ -6,6 +6,8 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -15,6 +17,22 @@ namespace Knossos.NET.ViewModels public partial class CustomHomeViewModel : ViewModelBase { private List modVersions = new List(); + private Mod? GetActiveInstalledModVersion + { + get + { + if(ActiveVersionIndex >= 0 && ActiveVersionIndex < modVersions.Count()) + { + return modVersions[ActiveVersionIndex]; + } + else + { + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.GetActiveInstalledModVersion()", "ActiveVersionIndex was " + ActiveVersionIndex + " and modVersions.Count() was " + modVersions.Count()); + return null; + } + } + } + private List nebulaModVersions = new List(); private CancellationTokenSource? cancellationTokenSource = null; @@ -30,7 +48,7 @@ public partial class CustomHomeViewModel : ViewModelBase internal bool installing = false; [ObservableProperty] - internal bool update = false; + internal bool isUpdateReady = false; [ObservableProperty] internal bool nebulaVersionsAvailable = false; @@ -41,37 +59,42 @@ public partial class CustomHomeViewModel : ViewModelBase [ObservableProperty] internal int animate = 0; + [ObservableProperty] + internal bool nebulaServices = CustomLauncher.UseNebulaServices; + public CustomHomeViewModel() { } + /// + /// Handler for the hardcoded UI buttons + /// + /// internal void HardcodedButtonCommand(object cmd) { - if (ActiveVersionIndex == -1) - { - Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.HardcodedButtonCommand()", "Crash prevented: ActiveVersionIndex was " + ActiveVersionIndex + " and modVersions.Count() was " + modVersions.Count()); - ActiveVersionIndex = 0; - } switch ((string)cmd) { - case "play": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.Release); break; - case "playvr": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.Release, false, 0, true); break; - case "fred2": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.Fred2); break; - case "fred2debug": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.Fred2Debug); break; - case "debug": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.Debug); break; - case "qtfred": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.QtFred); break; - case "qtfreddebug": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.QtFredDebug); break; + case "play": if(GetActiveInstalledModVersion != null) Knossos.PlayMod(GetActiveInstalledModVersion, FsoExecType.Release); break; + case "playvr": if (GetActiveInstalledModVersion != null) Knossos.PlayMod(GetActiveInstalledModVersion, FsoExecType.Release, false, 0, true); break; + case "fred2": if (GetActiveInstalledModVersion != null) Knossos.PlayMod(GetActiveInstalledModVersion, FsoExecType.Fred2); break; + case "fred2debug": if (GetActiveInstalledModVersion != null) Knossos.PlayMod(GetActiveInstalledModVersion, FsoExecType.Fred2Debug); break; + case "debug": if (GetActiveInstalledModVersion != null) Knossos.PlayMod(GetActiveInstalledModVersion, FsoExecType.Debug); break; + case "qtfred": if (GetActiveInstalledModVersion != null) Knossos.PlayMod(GetActiveInstalledModVersion, FsoExecType.QtFred); break; + case "qtfreddebug": if (GetActiveInstalledModVersion != null) Knossos.PlayMod(GetActiveInstalledModVersion, FsoExecType.QtFredDebug); break; case "install": Install(); break; case "cancel": Cancel(); break; - case "update": break; - case "modify": break; - case "delete": break; - case "details": break; - case "settings": break; - case "logfile": break; + case "update": Update(); break; + case "modify": Modify(); break; + case "delete": if (GetActiveInstalledModVersion != null) RemoveInstalledModVersion(GetActiveInstalledModVersion); break; + case "details": Details(); break; + case "settings": Settings(); break; + case "logfile": OpenFS2Log(); break; } } + /// + /// Calls to cancel running install tasks + /// private void Cancel() { Installing = false; @@ -84,6 +107,9 @@ private void Cancel() TaskViewModel.Instance?.CancelAllInstallTaskWithID(CustomLauncher.ModID!, null); } + /// + /// Opens mod install window for this mod id + /// private async void Install() { if (nebulaModVersions.Any() && CustomLauncher.UseNebulaServices) @@ -98,24 +124,182 @@ private async void Install() } } - public void RemoveInstalledModVersion(Mod mod) + /// + /// Opens mod install window for this mod id in modify active version mode + /// + private async void Modify() { - if (CustomLauncher.ModID == mod.id) + if (GetActiveInstalledModVersion != null && CustomLauncher.UseNebulaServices) { - Installed = modVersions.Any(); - Installing = false; + var dialog = new ModInstallView(); + dialog.DataContext = new ModInstallViewModel(GetActiveInstalledModVersion, dialog, GetActiveInstalledModVersion.version); + await dialog.ShowDialog(MainWindow.instance!); + } + else + { + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.Modify()", "GetActiveInstalledModVersion was null. ActiveVersionIndex: " + ActiveVersionIndex + " modVerions.count()" + modVersions.Count()); + } + } + + /// + /// Opens mod install window for this mod id + /// + private async void Update() + { + if (GetActiveInstalledModVersion != null && CustomLauncher.UseNebulaServices) + { + var dialog = new ModInstallView(); + dialog.DataContext = new ModInstallViewModel(GetActiveInstalledModVersion, dialog); + await dialog.ShowDialog(MainWindow.instance!); + } + else + { + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.Update()", "GetActiveInstalledModVersion was null. ActiveVersionIndex: " + ActiveVersionIndex + " modVerions.count()" + modVersions.Count()); + } + } + + /// + /// Opens this mod details dialog + /// + private async void Details() + { + if (MainWindow.instance != null) + { + var dialog = new ModDetailsView(); + var mod = GetActiveInstalledModVersion != null ? GetActiveInstalledModVersion : nebulaModVersions.FirstOrDefault(); + if (mod != null) + { + dialog.DataContext = new ModDetailsViewModel(mod, dialog); + await dialog.ShowDialog(MainWindow.instance); + } + else + { + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.Details()", "Mod was null, not installed or nebulas versions of this modid were found."); + } + } + } + + /// + /// Opens this mod settings dialog + /// + internal async void Settings() + { + if (MainWindow.instance != null) + { + if (GetActiveInstalledModVersion != null) + { + var dialog = new ModSettingsView(); + dialog.DataContext = new ModSettingsViewModel(GetActiveInstalledModVersion); + await dialog.ShowDialog(MainWindow.instance); + } + else + { + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.Settings()", "Mod was null, not installed versions of this modid were found."); + } } } + /// + /// Opens the fs_open.log file, if it exists. + /// + private void OpenFS2Log() + { + if (File.Exists(Path.Combine(KnUtils.GetFSODataFolderPath(), "data", "fs2_open.log"))) + { + try + { + var cmd = new Process(); + cmd.StartInfo.FileName = Path.Combine(KnUtils.GetFSODataFolderPath(), "data", "fs2_open.log"); + cmd.StartInfo.UseShellExecute = true; + cmd.Start(); + cmd.Dispose(); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.OpenFS2Log", ex); + } + } + else + { + if (MainWindow.instance != null) + MessageBox.Show(MainWindow.instance, "Log File " + Path.Combine(KnUtils.GetFSODataFolderPath(), "data", "fs2_open.log") + " not found.", "File not found", MessageBox.MessageBoxButtons.OK); + } + } + + /// + /// Removes ONE installed mod version from the disk + /// + /// + public async void RemoveInstalledModVersion(Mod mod) + { + try + { + if (CustomLauncher.ModID == mod.id) + { + if (TaskViewModel.Instance!.IsSafeState()) + { + if (GetActiveInstalledModVersion != null) + { + if (modVersions.Count > 1) + { + var resp = await MessageBox.Show(MainWindow.instance!, "You are about to delete version " + GetActiveInstalledModVersion.version + ", this will remove this version only. Do you want to continue?", "Delete version", MessageBox.MessageBoxButtons.YesNo); + if (resp == MessageBox.MessageBoxResult.Yes) + { + var delete = modVersions[ActiveVersionIndex]; + var verDel = VersionItems[ActiveVersionIndex]; + modVersions.Remove(delete); + Knossos.RemoveMod(delete); + VersionItems.Remove(verDel); + ActiveVersionIndex = modVersions.Count() - 1; + } + } + else + { + var resp = await MessageBox.Show(MainWindow.instance!, "You are about to delete the last installed version. Do you want to continue?", "Delete last version", MessageBox.MessageBoxButtons.YesNo); + if (resp == MessageBox.MessageBoxResult.Yes) + { + //Last version + modVersions[0].installed = false; + MainWindowViewModel.Instance?.AddNebulaMod(modVersions[0]); + Knossos.RemoveMod(modVersions[0].id); + Installed = false; + Installing = false; + modVersions.Clear(); + VersionItems.Clear(); + ActiveVersionIndex = 0; + } + } + } + } + else + { + await MessageBox.Show(MainWindow.instance!, "You can not delete a mod while other install tasks are running, wait until they finish and try again.", "Tasks are running", MessageBox.MessageBoxButtons.OK); + } + } + }catch(Exception ex) + { + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.RemoveInstalledModVersion()", ex); + } + } + + /// + /// This deletes all versions of this mod + /// Not implemented or needed + /// + /// public void RemoveMod(string id) { if (CustomLauncher.ModID == id) { - Installed = false; - Installing = false; + //Installed = false; + //Installing = false; } } + /// + /// Remove starts cancellation of a install taks with this mod id + /// + /// public void CancelModInstall(string id) { if (CustomLauncher.ModID == id) @@ -124,10 +308,18 @@ public void CancelModInstall(string id) } } + /// + /// Sets the install mode, so the cancel tasks button can be displayed + /// + /// + /// public void SetInstalling(string id, CancellationTokenSource cancelToken) { - cancellationTokenSource = cancelToken; - Installing = true; + if (CustomLauncher.ModID == id) + { + cancellationTokenSource = cancelToken; + Installing = true; + } } /// @@ -195,7 +387,7 @@ public void UpdateIsAvailable(string id, bool value) { if (id == CustomLauncher.ModID) { - Update = value; + IsUpdateReady = value; } } diff --git a/Knossos.NET/ViewModels/Windows/ModDetailsViewModel.cs b/Knossos.NET/ViewModels/Windows/ModDetailsViewModel.cs index 45b2582b..1df476d6 100644 --- a/Knossos.NET/ViewModels/Windows/ModDetailsViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/ModDetailsViewModel.cs @@ -86,6 +86,8 @@ public partial class ModDetailsViewModel : ViewModelBase [ObservableProperty] internal bool isLocalMod = false; [ObservableProperty] + internal bool nebulaServices = !CustomLauncher.IsCustomMode || (CustomLauncher.IsCustomMode && CustomLauncher.UseNebulaServices) ? true : false; + [ObservableProperty] internal ObservableCollection screenshots = new ObservableCollection(); internal ObservableCollection VersionItems { get; set; } = new ObservableCollection(); diff --git a/Knossos.NET/Views/CustomHomeView.axaml b/Knossos.NET/Views/CustomHomeView.axaml index 073952e9..149ef3c6 100644 --- a/Knossos.NET/Views/CustomHomeView.axaml +++ b/Knossos.NET/Views/CustomHomeView.axaml @@ -24,26 +24,52 @@ anim:ImageBehavior.SpeedRatio="{Binding Animate}" Source="{Binding BackgroundImage, Converter={StaticResource imageConverter}}" anim:ImageBehavior.AnimatedSource="{Binding BackgroundImage}" /> - - + + - - - + + + + + + + + + + + + + + + + + + IsVisible="{Binding !Installing}" IsEnabled="{Binding NebulaServices}"> + + + + + + + + diff --git a/Knossos.NET/Views/Windows/ModDetailsView.axaml b/Knossos.NET/Views/Windows/ModDetailsView.axaml index d90b6108..8d3065be 100644 --- a/Knossos.NET/Views/Windows/ModDetailsView.axaml +++ b/Knossos.NET/Views/Windows/ModDetailsView.axaml @@ -30,13 +30,13 @@ - + - + internal Queue taskQueue { get; set; } = new Queue(); + [ObservableProperty] + internal bool buttonsVisible = true; + public TaskViewModel() { Instance = this; @@ -57,7 +61,7 @@ public void CancelAllInstallTaskWithID(string id, string? version) { Dispatcher.UIThread.Invoke(() => { - foreach (var task in TaskList) + foreach (var task in TaskList.ToList()) { if (!task.IsCompleted && task.installID == id && (version == null || task.installVersion == version)) { @@ -67,13 +71,22 @@ public void CancelAllInstallTaskWithID(string id, string? version) }); } + /// + /// SHow or hide buttons on taskview + /// + /// + public void ShowButtons(bool state) + { + ButtonsVisible = state; + } + /// /// Checks if all tasks in queue are mark as cancelled or completed /// /// true if all completed or cancelled, false if there is running tasks public bool IsSafeState() { - foreach (var task in TaskList) + foreach (var task in TaskList.ToList()) { if (!task.IsCancelled && !task.IsCompleted) { @@ -254,7 +267,20 @@ await Dispatcher.UIThread.InvokeAsync(async () => TaskList.Add(newTask); taskQueue.Enqueue(newTask); }); - return await newTask.InstallBuild(build, sender,sender.cancellationTokenSource,modJson, modifyPkgs, cleanupOldVersions).ConfigureAwait(false); + var res = await newTask.InstallBuild(build, sender,sender.cancellationTokenSource,modJson, modifyPkgs, cleanupOldVersions).ConfigureAwait(false); + if (res != null && Knossos.inSingleTCMode) + { + try + { + TaskList.Remove(newTask); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "TaskViewModel.InstallBuild()", ex); + } + Dispatcher.UIThread.Invoke(() => TaskViewModel.Instance?.AddMessageTask("Completed: " + newTask.Name), DispatcherPriority.Background); + } + return res; } /// @@ -290,7 +316,19 @@ await Dispatcher.UIThread.InvokeAsync(async () => TaskList.Add(newTask); taskQueue.Enqueue(newTask); }); - await newTask.InstallMod(mod, cancelSource, reinstallPkgs, manualCompress, cleanupOldVersions, cleanInstall, allowHardlinks).ConfigureAwait(false); + var res = await newTask.InstallMod(mod, cancelSource, reinstallPkgs, manualCompress, cleanupOldVersions, cleanInstall, allowHardlinks).ConfigureAwait(false); + if(res && Knossos.inSingleTCMode) + { + try + { + TaskList.Remove(newTask); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "TaskViewModel.InstallMod()", ex); + } + Dispatcher.UIThread.Invoke(() => TaskViewModel.Instance?.AddMessageTask("Completed: " + newTask.Name), DispatcherPriority.Background); + } } } } @@ -317,7 +355,19 @@ public async Task CompressMod(Mod mod) TaskList.Add(newTask); taskQueue.Enqueue(newTask); }); - await newTask.CompressMod(mod, cancelSource).ConfigureAwait(false); + var res = await newTask.CompressMod(mod, cancelSource).ConfigureAwait(false); + if (res && Knossos.inSingleTCMode) + { + try + { + TaskList.Remove(newTask); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "TaskViewModel.CompressMod()", ex); + } + Dispatcher.UIThread.Invoke(() => TaskViewModel.Instance?.AddMessageTask("Completed: " + newTask.Name), DispatcherPriority.Background); + } } } } @@ -343,7 +393,19 @@ public async Task DecompressMod(Mod mod) TaskList.Add(newTask); taskQueue.Enqueue(newTask); }); - await newTask.DecompressMod(mod, cancelSource).ConfigureAwait(false); + var res = await newTask.DecompressMod(mod, cancelSource).ConfigureAwait(false); + if (res && Knossos.inSingleTCMode) + { + try + { + TaskList.Remove(newTask); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "TaskViewModel.DecompressMod()", ex); + } + Dispatcher.UIThread.Invoke(() => TaskViewModel.Instance?.AddMessageTask("Completed: " + newTask.Name), DispatcherPriority.Background); + } } } } diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index aa64acb8..9e8f30e6 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -229,6 +229,8 @@ partial void OnSelectedMenuItemChanged(MainViewMenuItem? value) } if (CurrentViewModel != null && CurrentViewModel == CustomHomeVM) //CustomHomeView { + CustomHomeVM.TaskVM = null; + TaskView?.ShowButtons(true); CustomHomeVM.ViewClosed(); } @@ -263,6 +265,8 @@ partial void OnSelectedMenuItemChanged(MainViewMenuItem? value) } if (CurrentViewModel != null && CurrentViewModel == CustomHomeVM) //CustomHomeView { + CustomHomeVM.TaskVM = TaskView; + TaskView?.ShowButtons(false); CustomHomeVM.ViewOpened(); } if (CurrentViewModel == GlobalSettingsView) //Settings @@ -313,10 +317,10 @@ public void ClearViews() /// /// /// - public void MarkAsUpdateAvailable(string id, bool value = true) + public void MarkAsUpdateAvailable(string id, bool value = true, string? newVersion = null) { InstalledModsView?.UpdateIsAvailable(id, value); - CustomHomeVM?.UpdateIsAvailable(id, value); + CustomHomeVM?.UpdateIsAvailable(id, value, newVersion); } /// diff --git a/Knossos.NET/ViewModels/Windows/ModInstallViewModel.cs b/Knossos.NET/ViewModels/Windows/ModInstallViewModel.cs index 3aa32af4..1cdc16f3 100644 --- a/Knossos.NET/ViewModels/Windows/ModInstallViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/ModInstallViewModel.cs @@ -162,7 +162,8 @@ private async void UpdateSelectedVersion() { await SelectedMod.LoadFulLNebulaData(); } - if(SelectedMod != null && !SelectedMod.GetMissingDependenciesList().Any()) + //load all dependencies mods in singletc mode so they can be modified too at any time + if(SelectedMod != null && !SelectedMod.GetMissingDependenciesList().Any() && !Knossos.inSingleTCMode) { allMods = ModVersions.ToList(); } @@ -327,167 +328,171 @@ private async Task ProcessMod(Mod mod, List allMods, List? processed = { if (processed == null) processed = new List(); - var dependencies = mod.GetMissingDependenciesList(false, true); + //load all dependencies mods in singletc mode so they can be modified too at any time + var dependencies = Knossos.inSingleTCMode ? mod.GetModDependencyList(false, true) : mod.GetMissingDependenciesList(false, true); //Display this mod on install list AddModToList(mod); //Add this mod here to avoid possible looping processed.Add(mod); - foreach (var dep in dependencies) + if (dependencies != null) { - var modDep = await dep.SelectModNebula(allMods); - if (modDep != null) + foreach (var dep in dependencies) { - //Is this dependecy mod already is installed? - var modInstalled = Knossos.GetInstalledMod(modDep.id, modDep.version); - - //Check Cache - var modInCache = modCache.FirstOrDefault(x => x.id == modDep.id && x.version == modDep.version); - if (modInCache != null) + var modDep = await dep.SelectModNebula(allMods); + if (modDep != null) { - modDep = modInCache; - } - else - { - //Load Nebula data first to check the packages and add to cache - await modDep.LoadFulLNebulaData(); - modCache.Add(modDep); - } + //Is this dependecy mod already is installed? + var modInstalled = Knossos.GetInstalledMod(modDep.id, modDep.version); - //If this is an engine build then check if contains valid executables - if (modDep.type == ModType.engine) - { - //Set a max amount of attempts to get an alternative version in case we need an alternative version - //This is because if user request "FSO" builds with an an incompatible cpu arch this is going to try - //with every FSO build in nebula that sastifies the dependency, incluiding nightlies. - var attempt = 0; - var maxAttempts = 10; - while (modDep != null && ++attempt < maxAttempts && modDep.packages.Any(x => FsoBuild.IsEnviromentStringValidInstall(x.environment)) == false) + //Check Cache + var modInCache = modCache.FirstOrDefault(x => x.id == modDep.id && x.version == modDep.version); + if (modInCache != null) { - //This build is not valid for this pc, delete from allmods list and resend to process - var remove = allMods.FirstOrDefault(x => x.id == modDep.id && x.version == modDep.version); - if (remove != null) + modDep = modInCache; + } + else + { + //Load Nebula data first to check the packages and add to cache + await modDep.LoadFulLNebulaData(); + modCache.Add(modDep); + } + + //If this is an engine build then check if contains valid executables + if (modDep.type == ModType.engine) + { + //Set a max amount of attempts to get an alternative version in case we need an alternative version + //This is because if user request "FSO" builds with an an incompatible cpu arch this is going to try + //with every FSO build in nebula that sastifies the dependency, incluiding nightlies. + var attempt = 0; + var maxAttempts = 10; + while (modDep != null && ++attempt < maxAttempts && modDep.packages.Any(x => FsoBuild.IsEnviromentStringValidInstall(x.environment)) == false) { - allMods.Remove(remove); - var alternativeVersion = await dep.SelectModNebula(allMods); - if (alternativeVersion != null) + //This build is not valid for this pc, delete from allmods list and resend to process + var remove = allMods.FirstOrDefault(x => x.id == modDep.id && x.version == modDep.version); + if (remove != null) { - //Check Cache - modInCache = modCache.FirstOrDefault(x => x.id == alternativeVersion.id && x.version == alternativeVersion.version); - if (modInCache != null) - { - alternativeVersion = modInCache; - } - else + allMods.Remove(remove); + var alternativeVersion = await dep.SelectModNebula(allMods); + if (alternativeVersion != null) { - //Load Nebula data first to check the packages and add to cache - await alternativeVersion.LoadFulLNebulaData(); - modCache.Add(alternativeVersion); + //Check Cache + modInCache = modCache.FirstOrDefault(x => x.id == alternativeVersion.id && x.version == alternativeVersion.version); + if (modInCache != null) + { + alternativeVersion = modInCache; + } + else + { + //Load Nebula data first to check the packages and add to cache + await alternativeVersion.LoadFulLNebulaData(); + modCache.Add(alternativeVersion); + } } + modDep = alternativeVersion; + } + else + { + //if for some reason we cant find modDep on allMods (it should never happen) we have to break or we are going to loop + break; } - modDep = alternativeVersion; - } - else - { - //if for some reason we cant find modDep on allMods (it should never happen) we have to break or we are going to loop - break; } + //if we cant find a alternative version in nebula, we have to skip the rest. + if (modDep == null || attempt == maxAttempts) + continue; } - //if we cant find a alternative version in nebula, we have to skip the rest. - if (modDep == null || attempt == maxAttempts) - continue; - } - //Make sure to mark all needed pkgs this mod need as required - modDep.isEnabled = true; - modDep.isSelected = true; + //Make sure to mark all needed pkgs this mod need as required + modDep.isEnabled = true; + modDep.isSelected = true; - foreach (var pkg in modDep.packages) - { - if (dep != null && dep.packages != null) + foreach (var pkg in modDep.packages) { - //Auto select needed packages and inform via tooltip, updating the foreground - var depPkg = dep.packages.FirstOrDefault(dp => dp == pkg.name); - if (depPkg != null && pkg.status != "required") + if (dep != null && dep.packages != null) { - pkg.isEnabled = true; - pkg.isSelected = true; - pkg.isRequired = true; - - var originalPkg = mod.FindPackageWithDependency(dep.originalDependency); - if(originalPkg != null) + //Auto select needed packages and inform via tooltip, updating the foreground + var depPkg = dep.packages.FirstOrDefault(dp => dp == pkg.name); + if (depPkg != null && pkg.status != "required") { - if (!pkg.tooltip.Contains(mod + "\nPKG: " + originalPkg.name)) + pkg.isEnabled = true; + pkg.isSelected = true; + pkg.isRequired = true; + + var originalPkg = mod.FindPackageWithDependency(dep.originalDependency); + if (originalPkg != null) + { + if (!pkg.tooltip.Contains(mod + "\nPKG: " + originalPkg.name)) + { + pkg.tooltip += "\n\nRequired by MOD: " + mod + "\nPKG: " + originalPkg.name; + } + } + else { - pkg.tooltip += "\n\nRequired by MOD: " + mod + "\nPKG: " + originalPkg.name; + if (!pkg.tooltip.Contains(mod.ToString())) + { + pkg.tooltip += "\n\nRequired by MOD: " + mod; + } } } else { - if (!pkg.tooltip.Contains(mod.ToString())) + switch (pkg.status) { - pkg.tooltip += "\n\nRequired by MOD: " + mod; + case "required": + pkg.isEnabled = false; + pkg.isSelected = true; + break; + case "recommended": + pkg.isSelected = true; + break; + case "optional": + //No need to do anything here + break; } } } - else - { - switch(pkg.status) - { - case "required": - pkg.isEnabled = false; - pkg.isSelected = true; - break; - case "recommended": - pkg.isSelected = true; - break; - case "optional": - //No need to do anything here - break; - } - } - } - //If mod is already installed, non-installed pkgs are all unselected - //and all installed ones are selected - if (modInstalled != null && modInstalled.packages != null) - { - var packageIsInstalled = modInstalled.packages.FirstOrDefault(m => m.name == pkg.name); - if (packageIsInstalled != null) + //If mod is already installed, non-installed pkgs are all unselected + //and all installed ones are selected + if (modInstalled != null && modInstalled.packages != null) { - //Pkg is installed - if (pkg.status == "required") + var packageIsInstalled = modInstalled.packages.FirstOrDefault(m => m.name == pkg.name); + if (packageIsInstalled != null) { - pkg.isEnabled = false; + //Pkg is installed + if (pkg.status == "required") + { + pkg.isEnabled = false; + } + else + { + pkg.isEnabled = true; + } + pkg.isSelected = true; } else { - pkg.isEnabled = true; - } - pkg.isSelected = true; - } - else - { - //Pkg is not installed - //ONLY if the currently selected mod is also installed - //For new mod or new mod version installs only if the package is not needed - if (IsInstalled || !IsInstalled && !pkg.isRequired) - { - pkg.isEnabled = true; - pkg.isSelected = false; + //Pkg is not installed + //ONLY if the currently selected mod is also installed + //For new mod or new mod version installs only if the package is not needed + if (IsInstalled || !IsInstalled && !pkg.isRequired) + { + pkg.isEnabled = true; + pkg.isSelected = false; + } } } } - } - //If process this depmod own dependencies if we havent done already - //Otherwise re-add it to the list to enabled any potential new pkg needed - if (processed.IndexOf(modDep) == -1) - { - await ProcessMod(modDep, allMods, processed); - } - else - { - AddModToList(modDep); + //If process this depmod own dependencies if we havent done already + //Otherwise re-add it to the list to enabled any potential new pkg needed + if (processed.IndexOf(modDep) == -1) + { + await ProcessMod(modDep, allMods, processed); + } + else + { + AddModToList(modDep); + } } } } @@ -564,6 +569,8 @@ internal void VerifyCommand() /// internal void InstallMod() { + if(Knossos.inSingleTCMode) + TaskViewModel.Instance?.CleanCommand(); foreach (var mod in ModInstallList) { var cleanOldVersions = false; diff --git a/Knossos.NET/Views/CustomHomeView.axaml b/Knossos.NET/Views/CustomHomeView.axaml index 149ef3c6..65d435b3 100644 --- a/Knossos.NET/Views/CustomHomeView.axaml +++ b/Knossos.NET/Views/CustomHomeView.axaml @@ -77,6 +77,9 @@ + diff --git a/Knossos.NET/Views/TaskView.axaml b/Knossos.NET/Views/TaskView.axaml index c33d3e0d..5c4eb4bc 100644 --- a/Knossos.NET/Views/TaskView.axaml +++ b/Knossos.NET/Views/TaskView.axaml @@ -16,7 +16,7 @@ - From 2b1f49921eb48e4eba3be2d08a5fc8ff29c69cae Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Fri, 17 Jan 2025 19:44:16 -0300 Subject: [PATCH 12/44] Properly report the correct knet data folder for portable AND custom mode --- Knossos.NET/Classes/KnUtils.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Knossos.NET/Classes/KnUtils.cs b/Knossos.NET/Classes/KnUtils.cs index 181213e3..ca9b760f 100644 --- a/Knossos.NET/Classes/KnUtils.cs +++ b/Knossos.NET/Classes/KnUtils.cs @@ -120,20 +120,22 @@ public static string? KnetFolderPath /// fullpath as a string public static string GetKnossosDataFolderPath() { - var path = string.Empty; if (!Knossos.inPortableMode) { - path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "KnossosNET"); + if (CustomLauncher.IsCustomMode) + { + //In custom mode store config files inside modid a subfolder + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "KnossosNET", CustomLauncher.ModID!); + } + else + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "KnossosNET"); + } } else { - path = Path.Combine(KnetFolderPath!, "kn_portable", "KnossosNET"); //If inPortableMode = true, KnetFolderPath is not null - } - if(CustomLauncher.IsCustomMode) - { - path = Path.Combine(path, CustomLauncher.ModID!); + return Path.Combine(KnetFolderPath!, "kn_portable", "KnossosNET"); //If inPortableMode = true, KnetFolderPath is not null } - return path; } /// From 6b2aa7c6724cdd3a172fb0b1a73436514a908f00 Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Fri, 17 Jan 2025 22:22:11 -0300 Subject: [PATCH 13/44] Custom home screen part 5 --- .../Converters/TextFileToStringConverter.cs | 64 +++++++ Knossos.NET/Models/CustomLauncher.cs | 14 ++ Knossos.NET/ViewModels/CustomHomeViewModel.cs | 21 +++ Knossos.NET/Views/CustomHomeView.axaml | 175 ++++++++++-------- Knossos.NET/Views/Windows/MainWindow.axaml | 2 + 5 files changed, 195 insertions(+), 81 deletions(-) create mode 100644 Knossos.NET/Converters/TextFileToStringConverter.cs diff --git a/Knossos.NET/Converters/TextFileToStringConverter.cs b/Knossos.NET/Converters/TextFileToStringConverter.cs new file mode 100644 index 00000000..909a7050 --- /dev/null +++ b/Knossos.NET/Converters/TextFileToStringConverter.cs @@ -0,0 +1,64 @@ +using Avalonia.Data.Converters; +using System; +using System.Globalization; +using Avalonia.Platform; +using System.IO; +using System.Text; + +namespace Knossos.NET.Converters +{ + public class TextFileToStringConverter : IValueConverter + { + public static TextFileToStringConverter Instance { get; } = new(); + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + try + { + if (value == null) return null; + + if (value is not string rawUri || !targetType.IsAssignableFrom(typeof(String))) + { + throw new NotSupportedException(); + } + + Uri uri; + + if (rawUri.StartsWith("avares://")) + { + uri = new Uri(rawUri); + var asset = AssetLoader.Open(uri); + if (asset != null) + { + using (var reader = new StreamReader(asset, Encoding.UTF8)) + { + return reader.ReadToEnd(); + } + } + } + else if(rawUri.ToLower().StartsWith("http")) + { + return null; + } + else if (File.Exists(Path.Combine(KnUtils.GetKnossosDataFolderPath(), rawUri))) + { + return File.ReadAllText(Path.Combine(KnUtils.GetKnossosDataFolderPath(), rawUri)); + } + else if (File.Exists(rawUri)) + { + return File.ReadAllText(rawUri); + } + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "BitmapAssetValueConverter.Convert()", ex); + } + return null; + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/Knossos.NET/Models/CustomLauncher.cs b/Knossos.NET/Models/CustomLauncher.cs index fd46475e..685e1754 100644 --- a/Knossos.NET/Models/CustomLauncher.cs +++ b/Knossos.NET/Models/CustomLauncher.cs @@ -47,12 +47,14 @@ public static class CustomLauncher /// /// Starting width size of the launcher window + /// This is also the min width /// null for auto /// public static int? WindowWidth { get; private set; } = 1024; /// /// Starting height size of the launcher window + /// This is also the min height /// null for auto /// public static int? WindowHeight { get; private set; } = 540; @@ -108,6 +110,7 @@ public static class CustomLauncher /// /// Path to the background image for the home view + /// It is recommended this image to be about 200px less in width than the starting WindowWidth /// Supports local image in the Knet data folder, a local full path, harcoded image or remote https:// URL /// Supports APNGs, GIF, PNG and JPG /// Examples: @@ -120,6 +123,13 @@ public static class CustomLauncher /// public static string? HomeBackgroundImage { get; private set; } = "avares://Knossos.NET/Assets/general/custom_home_background.jpg"; + /// + /// Set a path to the welcome HTML message on home screen + /// Uses the same path rules as HomeBackgroundImage + /// null to disable or put a path to a empty file if you want to display it at some point + /// + public static string? HomeWelcomeHtml { get; private set; } = null; + /// /// Call this AFTER checking if we are in portable mode or not. /// The first time it runs it will try to load the "custom_launcher.json" if ModID is null @@ -204,6 +214,9 @@ private static void ReadCustomFile() if (customData.HomeBackgroundImage != null) HomeBackgroundImage = customData.HomeBackgroundImage; + if (customData.HomeWelcomeHtml != null) + HomeWelcomeHtml = customData.HomeWelcomeHtml; + jsonFile.Close(); } } @@ -230,6 +243,7 @@ struct CustomFileData public bool? UseNebulaServices { get; set; } public bool? WriteLogFile { get; set; } public string? HomeBackgroundImage { get; set; } + public string? HomeWelcomeHtml { get; set; } } } } diff --git a/Knossos.NET/ViewModels/CustomHomeViewModel.cs b/Knossos.NET/ViewModels/CustomHomeViewModel.cs index 48ed7a57..326ae240 100644 --- a/Knossos.NET/ViewModels/CustomHomeViewModel.cs +++ b/Knossos.NET/ViewModels/CustomHomeViewModel.cs @@ -62,6 +62,12 @@ private Mod? GetActiveInstalledModVersion [ObservableProperty] internal bool nebulaServices = CustomLauncher.UseNebulaServices; + [ObservableProperty] + internal bool welcomeVisible = false; + + [ObservableProperty] + internal string? welcomeHtml = CustomLauncher.HomeWelcomeHtml; + /// /// Handled in mainview, displays a small task viewer in the home screen /// @@ -423,6 +429,21 @@ public void ViewOpened() }); }); } + //download remote WelcomeHTML + if (WelcomeHtml != null && WelcomeHtml.ToLower().StartsWith("http")) + { + _ = Task.Factory.StartNew(async () => + { + var temp = WelcomeHtml; + WelcomeHtml = ""; + var htmlFile = await KnUtils.GetRemoteResource(temp).ConfigureAwait(false); + Dispatcher.UIThread.Invoke(() => + { + if (htmlFile != null) + WelcomeHtml = htmlFile; + }); + }); + } } /// diff --git a/Knossos.NET/Views/CustomHomeView.axaml b/Knossos.NET/Views/CustomHomeView.axaml index 65d435b3..a640c92e 100644 --- a/Knossos.NET/Views/CustomHomeView.axaml +++ b/Knossos.NET/Views/CustomHomeView.axaml @@ -8,7 +8,9 @@ xmlns:vm="using:Knossos.NET.ViewModels" xmlns:anim="https://github.com/whistyun/AnimatedImage.Avalonia" x:DataType="vm:CustomHomeViewModel" + Name="CustomTCView" Background="{StaticResource BackgroundColorPrimary}" + xmlns:HtmlRenderer="clr-namespace:TheArtOfDev.HtmlRenderer.Avalonia;assembly=Avalonia.HtmlRenderer" xmlns:cvt="clr-namespace:Knossos.NET.Converters;assembly=Knossos.NET"> @@ -16,97 +18,108 @@ + - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + - + \ No newline at end of file diff --git a/Knossos.NET/Views/Windows/MainWindow.axaml b/Knossos.NET/Views/Windows/MainWindow.axaml index bcf83f98..0cae4c62 100644 --- a/Knossos.NET/Views/Windows/MainWindow.axaml +++ b/Knossos.NET/Views/Windows/MainWindow.axaml @@ -6,6 +6,8 @@ xmlns:v="using:Knossos.NET.Views" Width="{Binding WindowWidth}" Height="{Binding WindowHeight}" + MinWidth="{Binding WindowWidth}" + MinHeight="{Binding WindowHeight}" x:DataType="vm:MainWindowViewModel" mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="900" x:Class="Knossos.NET.Views.MainWindow" From dfb4e8a0f569f672c04f8bf0157f980ed71b2dec Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Sat, 18 Jan 2025 19:47:21 -0300 Subject: [PATCH 14/44] Custom home screen part 6 --- Knossos.NET/Assets/general/discordicon.png | Bin 2859 -> 2590 bytes .../Converters/BitmapAssetValueConverter.cs | 10 +- .../Converters/TextFileToStringConverter.cs | 11 +- Knossos.NET/Models/CustomLauncher.cs | 40 ++++-- Knossos.NET/ViewModels/CustomHomeViewModel.cs | 119 +++++++++++++----- .../ViewModels/Windows/MainWindowViewModel.cs | 1 + Knossos.NET/Views/CustomHomeView.axaml | 38 +++++- Knossos.NET/Views/CustomHomeView.axaml.cs | 66 +++++++++- 8 files changed, 227 insertions(+), 58 deletions(-) diff --git a/Knossos.NET/Assets/general/discordicon.png b/Knossos.NET/Assets/general/discordicon.png index 85c58e06edeeb98000e2d255bd0069cec563e307..fb3659cdca0cae12583e17f2921ade55cd55e829 100644 GIT binary patch literal 2590 zcmV+(3gPvMP)GJaO<>lovGBRpvYUAVM9UUF^_V!<2UpYBBLqkJXS64|%N!;Ar;Nala*482- zBJ1nxDk>_xyu6ZA#!cf-TOhK7b48ymN`w;LN9 zf`Wo5C@7ein2U>x%*@Qdz`(1ktD&Kx`1ttH(9oToozv6Pe0+TW|Nl?OinRa$05Nn@ zPE!Ex>GI_-Y2pa=T{k~fN88}l9P1~$kWbmMaNftJcEW`aw-kUQmx{~3s-XAJoYH#} z(cIVo00{<3L_t(|+U(nHbD}U10N~w_fV>F;ii)(pRJ~fOxBvgo-Ss**=e4L1VoSj1 zGn1LjCQEh+aCdiicX!tT5UG^FgO7TPXq^ZlT=82HtrqZ!7f>l2a}U12 z$_!#EEq|Y8Pe_f&A3!0uvAOzGEI^$hX<-7&m|6sx=;(kVkp>(2 zU9bW*UPu?X>B=klAMSz*kED@hmvYy;%z@fgTFsa!>_J^~(hF{(CZSo7iEs_|>_|HY zsHGo}cDUPx!?nVrwx5xPfYSCSSjay)N_yDt~%)TTnKhVqK{y{$VLx z^GYp~cWqr0VXtHBnkY!Sm9I;zY|)bDK!uz=ZQ+!&e%ssE6&u>Cm|AEI7x=9zDgNT( zw*3&>Ks0QFRIp|CZ$DA()JD%NMn+KJ{0hcTNKTK z@(aO9fK=63Kp|m~tO1;nNRJt;;f%!?shU9qXG8&#oNNsMN|z2x;H*VfF~$*`6o-K*$oMB6kG-C=q-PVtj$m@<52k zNx~@7G+=W8ZNn^rAfF^;4xEcg27(}0ZH`i(rHVD2o0^Tx=u4JFWThc{Kr*-oVo={h z;&DwYh(4JdzC5n)>zm6(wmdk)j8^7!xW4WfHj<3I3#UAv^zSMf5u-1$2!!-V%07Wp zT_h=M5ON>^3lOr_)c-6YIUCS-6?S5@{7WwkxV#A!xsmRBo&e}_TzHf zt?vD0k=Wg?oj6Ufk5x$M!H_zF-w+WqXoEiKU4!DN) zNU73wxnyOzWLReHl4ei9)bF$mky_)cc{3O|dke`NcV92zpZSXG=phs3GNBjH2&W@f zkO?U(BWO6B+RG!jFxwT%w-J)k%}{q<9QQocelJBigKGjV5t`=?981>y(TI^AmFuof z@mt`w85^iERVGjpZilnOVP8JcsX<%O_@T3bL8zPel-LK))ESYW!hVpSS zFXIlJ3gqTvHiS=w6_-46=)gtndXTwR%BT&@Q+%0T#Dz|cB*Usg_)5)~Wc#KFE-icY zE@!EQ1$`;+Tm83bV8f+oz)1?LK|m;bHv{tnT;1$cX;T*1Va+C~Qg&@Y^NK$$2SKcIA3{7&<``c4zP@U;YA~u;thy z85J0@CmHTr7ux9MT<`VOTmM=v<}tKYBP!OY=(Al`H~7lC)*uaTnx7 zRh!WG1B@B4R^xO65+s(i%m!mZi$qy@0BW2O7rs(#P>Cdjj~r0u4K1P2Yz_u>P;fo% zLBnup?tmio-wC#wz{)%)<7QVO>NUJPb3D*0*!%PeD+0q3MxS6m1JJKGL8D`rLZ?DD z>46#H0FkarJFNwvLVR%ye{^j;8;>nDx7Jj2F847b&BD*h)mmT)s;{5K~#9rzFIdXGqydvG&z z_U8=Q25IW4H}4VLQ1YJFHatB&Jv}`=J$+Zd0B;d&2lM16cK`qY07*qoM6N<$f+v#T AhyVZp literal 2859 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 KnUtils.GetRemoteResource(rawUri)).Result; + if (localPath != null) + { + return new Bitmap(localPath); + } } else if (File.Exists(Path.Combine(KnUtils.GetKnossosDataFolderPath(), rawUri))) { diff --git a/Knossos.NET/Converters/TextFileToStringConverter.cs b/Knossos.NET/Converters/TextFileToStringConverter.cs index 909a7050..b6ad7f99 100644 --- a/Knossos.NET/Converters/TextFileToStringConverter.cs +++ b/Knossos.NET/Converters/TextFileToStringConverter.cs @@ -4,6 +4,7 @@ using Avalonia.Platform; using System.IO; using System.Text; +using System.Threading.Tasks; namespace Knossos.NET.Converters { @@ -11,7 +12,7 @@ public class TextFileToStringConverter : IValueConverter { public static TextFileToStringConverter Instance { get; } = new(); - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo? culture) { try { @@ -36,9 +37,13 @@ public class TextFileToStringConverter : IValueConverter } } } - else if(rawUri.ToLower().StartsWith("http")) + else if (rawUri.ToLower().StartsWith("http")) { - return null; + var localPath = Task.Run(() => KnUtils.GetRemoteResource(rawUri)).Result; + if (localPath != null) + { + return File.ReadAllText(localPath); + } } else if (File.Exists(Path.Combine(KnUtils.GetKnossosDataFolderPath(), rawUri))) { diff --git a/Knossos.NET/Models/CustomLauncher.cs b/Knossos.NET/Models/CustomLauncher.cs index 685e1754..e0fb2498 100644 --- a/Knossos.NET/Models/CustomLauncher.cs +++ b/Knossos.NET/Models/CustomLauncher.cs @@ -1,17 +1,19 @@ -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; +using System; using System.IO; -using System.Linq; -using System.Text; using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; -using static Knossos.NET.ViewModels.MainWindowViewModel; namespace Knossos.NET.Models { + /// + /// Data struct for the custom mode dynamic link button + /// + public struct LinkButton + { + public string ToolTip { get; set; } + public string IconPath { get; set; } + public string LinkURL { get; set; } + } + /// /// Class to handle the configuration options and optional save file of the SingleTC mode /// @@ -130,6 +132,19 @@ public static class CustomLauncher /// public static string? HomeWelcomeHtml { get; private set; } = null; + /// + /// Thickness string to use as margin for the WelcomeHTML display + /// left, up, right, down + /// + public static string? HomeWelcomeMargin { get; private set; } = "50,50,50,0"; + + /// + /// Optional Link buttons that are displayed in the home screen that + /// if clicked opens a external web link in user browser + /// Icon path follows the same rules as HomeBackgroundImage, so URL, embedded and local images are supported. + /// + public static LinkButton[]? HomeLinkButtons { get; private set; } + /// /// Call this AFTER checking if we are in portable mode or not. /// The first time it runs it will try to load the "custom_launcher.json" if ModID is null @@ -217,6 +232,11 @@ private static void ReadCustomFile() if (customData.HomeWelcomeHtml != null) HomeWelcomeHtml = customData.HomeWelcomeHtml; + if (customData.HomeWelcomeMargin != null) + HomeWelcomeMargin = customData.HomeWelcomeMargin; + + HomeLinkButtons = customData.HomeLinkButtons; + jsonFile.Close(); } } @@ -244,6 +264,8 @@ struct CustomFileData public bool? WriteLogFile { get; set; } public string? HomeBackgroundImage { get; set; } public string? HomeWelcomeHtml { get; set; } + public string? HomeWelcomeMargin { get; set; } + public LinkButton[]? HomeLinkButtons { get; set; } } } } diff --git a/Knossos.NET/ViewModels/CustomHomeViewModel.cs b/Knossos.NET/ViewModels/CustomHomeViewModel.cs index 326ae240..e0638954 100644 --- a/Knossos.NET/ViewModels/CustomHomeViewModel.cs +++ b/Knossos.NET/ViewModels/CustomHomeViewModel.cs @@ -1,4 +1,6 @@ -using Avalonia.Threading; +using Avalonia; +using Avalonia.Platform.Storage; +using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using Knossos.NET.Classes; using Knossos.NET.Models; @@ -10,7 +12,6 @@ using System.IO; using System.Linq; using System.Threading; -using System.Threading.Tasks; namespace Knossos.NET.ViewModels { @@ -63,10 +64,19 @@ private Mod? GetActiveInstalledModVersion internal bool nebulaServices = CustomLauncher.UseNebulaServices; [ObservableProperty] - internal bool welcomeVisible = false; + internal string? welcomeHtml = CustomLauncher.HomeWelcomeHtml; [ObservableProperty] - internal string? welcomeHtml = CustomLauncher.HomeWelcomeHtml; + internal Thickness welcomeMargin = new Thickness(50, 50, 50, 0); + + [ObservableProperty] + internal bool showBasePathSelector = false; + + [ObservableProperty] + internal string newBasePath = string.Empty; + + [ObservableProperty] + internal bool changeBasePathButtonVisible = false; /// /// Handled in mainview, displays a small task viewer in the home screen @@ -76,6 +86,28 @@ private Mod? GetActiveInstalledModVersion public CustomHomeViewModel() { + if (CustomLauncher.HomeWelcomeMargin != null) + { + try + { + WelcomeMargin = Thickness.Parse(CustomLauncher.HomeWelcomeMargin); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.Constructor()", ex); + } + } + } + + /// + /// Check if we are in normal mode, but we dont have a saved base path + /// + public void CheckBasePath() + { + if (!Knossos.inPortableMode && Knossos.GetKnossosLibraryPath() == null) + { + ShowBasePathSelector = true; + } } /// @@ -413,45 +445,64 @@ public void UpdateIsAvailable(string id, bool value, string? newVersion) public void ViewOpened() { Animate = 1; + } + + /// + /// Run code when the user exit this view + /// + public void ViewClosed() + { + Animate = 0; + } - //download remote image if we have to - if (BackgroundImage != null && BackgroundImage.ToLower().StartsWith("http")) + /// + /// Changes the knossos library path, reloads settings and nebula repo + /// + internal async void BrowseFolderCommand() + { + if (MainWindow.instance != null) { - _ = Task.Factory.StartNew(async () => + ChangeBasePathButtonVisible = false; + NewBasePath = string.Empty; + FolderPickerOpenOptions options = new FolderPickerOpenOptions(); + options.AllowMultiple = false; + + var result = await MainWindow.instance.StorageProvider.OpenFolderPickerAsync(options); + + try { - var temp = BackgroundImage; - BackgroundImage = ""; - var imageFile = await KnUtils.GetRemoteResource(temp).ConfigureAwait(false); - Dispatcher.UIThread.Invoke(() => + if (result != null && result.Count > 0) { - if (imageFile != null) - BackgroundImage = imageFile; - }); - }); - } - //download remote WelcomeHTML - if (WelcomeHtml != null && WelcomeHtml.ToLower().StartsWith("http")) - { - _ = Task.Factory.StartNew(async () => + + // Test if we can write to the new library directory + using (StreamWriter writer = new StreamWriter(result[0].Path.LocalPath.ToString() + Path.DirectorySeparatorChar + "test.txt")) + { + writer.WriteLine("test"); + } + File.Delete(Path.Combine(result[0].Path.LocalPath.ToString() + Path.DirectorySeparatorChar + "test.txt")); + NewBasePath = result[0].Path.LocalPath.ToString(); + ChangeBasePathButtonVisible = true; + } + } + catch (Exception ex) { - var temp = WelcomeHtml; - WelcomeHtml = ""; - var htmlFile = await KnUtils.GetRemoteResource(temp).ConfigureAwait(false); - Dispatcher.UIThread.Invoke(() => - { - if (htmlFile != null) - WelcomeHtml = htmlFile; - }); - }); + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.BrowseFolderCommand() - test read/write was not successful: ", ex); + await Dispatcher.UIThread.Invoke(async () => { + await MessageBox.Show(null, "We were not able to write to this folder. Please select another library folder.", "Cannot Select Folder", MessageBox.MessageBoxButtons.OK); + }).ConfigureAwait(false); + } } } - /// - /// Run code when the user exit this view - /// - public void ViewClosed() + internal void ChangeBasePath() { - Animate = 0; + if (NewBasePath != string.Empty) + { + Knossos.globalSettings.basePath = NewBasePath; + Knossos.globalSettings.Save(); + Knossos.ResetBasePath(); + ShowBasePathSelector = false; + } } } } diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index 9e8f30e6..3f630734 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -147,6 +147,7 @@ public MainWindowViewModel() FillMenuItemsCustomMode(1); } Knossos.StartUp(isQuickLaunch, forceUpdate); + CustomHomeVM?.CheckBasePath(); } private void FillMenuItemsCustomMode(int defaultSelectedIndex) diff --git a/Knossos.NET/Views/CustomHomeView.axaml b/Knossos.NET/Views/CustomHomeView.axaml index a640c92e..e5ce6048 100644 --- a/Knossos.NET/Views/CustomHomeView.axaml +++ b/Knossos.NET/Views/CustomHomeView.axaml @@ -28,16 +28,42 @@ Source="{Binding BackgroundImage, Converter={StaticResource imageConverter}}" anim:ImageBehavior.AnimatedSource="{Binding BackgroundImage}" /> - - - - - + + + + + + + + + + + + + + + + + + + + + - + diff --git a/Knossos.NET/Views/CustomHomeView.axaml.cs b/Knossos.NET/Views/CustomHomeView.axaml.cs index 31e306a2..9a08eaed 100644 --- a/Knossos.NET/Views/CustomHomeView.axaml.cs +++ b/Knossos.NET/Views/CustomHomeView.axaml.cs @@ -1,13 +1,75 @@ -using Avalonia; using Avalonia.Controls; -using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; +using Knossos.NET.Converters; +using Knossos.NET.Models; +using System; +using System.Collections.Generic; +using System.Linq; namespace Knossos.NET.Views; public partial class CustomHomeView : UserControl { + internal List buttonUrls = new List(); + public CustomHomeView() { InitializeComponent(); + + //Generate Link Buttons + try + { + if (CustomLauncher.HomeLinkButtons != null && CustomLauncher.HomeLinkButtons.Any()) + { + var buttonPanel = this.FindControl("LinkButtons")!; + if (buttonPanel != null) + { + int index = 0; + foreach (var b in CustomLauncher.HomeLinkButtons) + { + var linkButton = new Button { Tag = index, Name = b.ToolTip }; + if (b.IconPath != null) + { + var converter = new BitmapAssetValueConverter(); + var bitmap = converter.Convert(b.IconPath, typeof(Bitmap), null, null); + if(bitmap != null) + { + linkButton.Content = new Image { Source = (Bitmap)bitmap, Width = 30, Height = 30 }; + } + } + + linkButton.Click += (_, __) => + { + //This code runs when the button is clicked + try + { + if(linkButton.Tag != null) + { + var url = buttonUrls[(int)linkButton.Tag]; + KnUtils.OpenBrowserURL(url); + } + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "CustomHomeView.Constructor(LinkButtonClick)", ex); + } + }; + + buttonPanel.Children.Add(linkButton); + index++; + buttonUrls.Add(b.LinkURL); + ToolTip.SetTip(linkButton, b.ToolTip); + } + } + else + { + Log.Add(Log.LogSeverity.Error, "CustomHomeView.Constructor()", "Unable to find LinkButtons panel."); + } + } + } + catch(Exception ex) + { + Log.Add(Log.LogSeverity.Error, "CustomHomeView.Constructor()", ex); + } } } \ No newline at end of file From 68998077517e320b92895b34f13f0daf7fffd386 Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Sat, 18 Jan 2025 20:24:47 -0300 Subject: [PATCH 15/44] Enable install only if we have nebula data --- Knossos.NET/Views/CustomHomeView.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Knossos.NET/Views/CustomHomeView.axaml b/Knossos.NET/Views/CustomHomeView.axaml index e5ce6048..33972ee3 100644 --- a/Knossos.NET/Views/CustomHomeView.axaml +++ b/Knossos.NET/Views/CustomHomeView.axaml @@ -99,7 +99,7 @@ - + public static bool MenuDisplayGlobalSettingsEntry { get; private set; } = false; + /// + /// Add custom buttons to the menu + /// + public static CustomMenuButton[]? CustomMenuButtons { get; private set; } + /// /// Yet another cmdline option, pass it as a string array. /// It has the lowest priority, same options can be overriden by mod cmdline. @@ -218,6 +232,8 @@ private static void ReadCustomFile() if (customData.MenuDisplayGlobalSettingsEntry.HasValue) MenuDisplayGlobalSettingsEntry = customData.MenuDisplayGlobalSettingsEntry.Value; + CustomMenuButtons = customData.CustomMenuButtons; + CustomCmdlineArray = customData.CustomCmdlineArray; if (customData.UseNebulaServices.HasValue) @@ -266,6 +282,7 @@ struct CustomFileData public string? HomeWelcomeHtml { get; set; } public string? HomeWelcomeMargin { get; set; } public LinkButton[]? HomeLinkButtons { get; set; } + public CustomMenuButton[]? CustomMenuButtons { get; set; } } } } diff --git a/Knossos.NET/ViewModels/Templates/HtmlContentViewModel.cs b/Knossos.NET/ViewModels/Templates/HtmlContentViewModel.cs new file mode 100644 index 00000000..78d1aca8 --- /dev/null +++ b/Knossos.NET/ViewModels/Templates/HtmlContentViewModel.cs @@ -0,0 +1,30 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Knossos.NET.ViewModels +{ + public partial class HtmlContentViewModel : ViewModelBase + { + [ObservableProperty] + internal string? htmlData = null; + + private string? savedHtmlData = null; + + public HtmlContentViewModel() + { + } + + public HtmlContentViewModel(string htmlData) + { + savedHtmlData = htmlData; + } + + /// + /// Loads the HTML content to the view + /// + public void Navigate() + { + if(savedHtmlData != null) + HtmlData = savedHtmlData; + } + } +} diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index 3f630734..21ada628 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -159,6 +159,28 @@ private void FillMenuItemsCustomMode(int defaultSelectedIndex) MenuItems.Add(new MainViewMenuItem(CustomHomeVM!, "avares://Knossos.NET/Assets/general/menu_home.png", "Home", "Home")); + if(CustomLauncher.CustomMenuButtons != null && CustomLauncher.CustomMenuButtons.Any()) + { + foreach(var button in CustomLauncher.CustomMenuButtons) + { + try + { + switch (button.Type.ToLower()) + { + case "htmlcontent" : + MenuItems.Add(new MainViewMenuItem(new HtmlContentViewModel(button.LinkURL), button.IconPath, button.Name, button.ToolTip)); + break; + default: + throw new NotImplementedException("button type: "+ button.Type + " is not supported."); + } + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "MainWindowViewModel.FillMenuItemsCustomMode()", ex); + } + } + } + if (CustomLauncher.MenuDisplayEngineEntry && FsoBuildsView != null) { MenuItems.Add(new MainViewMenuItem(FsoBuildsView, "avares://Knossos.NET/Assets/general/menu_engine.png", "Engine", "Download new Freespace Open engine builds")); @@ -281,6 +303,15 @@ partial void OnSelectedMenuItemChanged(MainViewMenuItem? value) { //Knossos.globalSettings.DisableIniWatch(); } + + //Custom Views + if (CurrentViewModel != null && CustomLauncher.IsCustomMode) + { + if (CurrentViewModel.GetType() == typeof(HtmlContentViewModel)) + { + ((HtmlContentViewModel)CurrentViewModel).Navigate(); + } + } } } diff --git a/Knossos.NET/Views/Templates/HtmlContentView.axaml b/Knossos.NET/Views/Templates/HtmlContentView.axaml new file mode 100644 index 00000000..e08545a1 --- /dev/null +++ b/Knossos.NET/Views/Templates/HtmlContentView.axaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + diff --git a/Knossos.NET/Views/Templates/HtmlContentView.axaml.cs b/Knossos.NET/Views/Templates/HtmlContentView.axaml.cs new file mode 100644 index 00000000..0c5724ce --- /dev/null +++ b/Knossos.NET/Views/Templates/HtmlContentView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Knossos.NET.Views; + +public partial class HtmlContentView : UserControl +{ + public HtmlContentView() + { + InitializeComponent(); + } +} \ No newline at end of file From 427441f1d52dfc4e087c619dea8cfdca8850e327 Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Sun, 19 Jan 2025 11:44:34 -0300 Subject: [PATCH 17/44] Add optional stock community menu item --- Knossos.NET/Models/CustomLauncher.cs | 9 +++++++++ Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/Knossos.NET/Models/CustomLauncher.cs b/Knossos.NET/Models/CustomLauncher.cs index 9f512141..c307f706 100644 --- a/Knossos.NET/Models/CustomLauncher.cs +++ b/Knossos.NET/Models/CustomLauncher.cs @@ -94,6 +94,11 @@ public static class CustomLauncher /// public static bool MenuDisplayNebulaLoginEntry { get; private set; } = false; + /// + /// Display the regular Knossos community menu item + /// + public static bool MenuDisplayCommunityEntry { get; private set; } = false; + /// /// Display the regular Knossos settings menu item /// If you do this you may want to add "-no_ingame_options" to the custom cmdline @@ -232,6 +237,9 @@ private static void ReadCustomFile() if (customData.MenuDisplayGlobalSettingsEntry.HasValue) MenuDisplayGlobalSettingsEntry = customData.MenuDisplayGlobalSettingsEntry.Value; + if (customData.MenuDisplayCommunityEntry.HasValue) + MenuDisplayCommunityEntry = customData.MenuDisplayCommunityEntry.Value; + CustomMenuButtons = customData.CustomMenuButtons; CustomCmdlineArray = customData.CustomCmdlineArray; @@ -275,6 +283,7 @@ struct CustomFileData public bool? MenuDisplayDebugEntry { get; set; } public bool? MenuDisplayNebulaLoginEntry { get; set; } public bool? MenuDisplayGlobalSettingsEntry { get; set; } + public bool? MenuDisplayCommunityEntry { get; set; } public string[]? CustomCmdlineArray { get; set; } public bool? UseNebulaServices { get; set; } public bool? WriteLogFile { get; set; } diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index 21ada628..cbf51906 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -142,6 +142,8 @@ public MainWindowViewModel() GlobalSettingsView = new GlobalSettingsViewModel(); if(CustomLauncher.MenuDisplayDebugEntry) DebugView = new DebugViewModel(); + if (CustomLauncher.MenuDisplayCommunityEntry) + CommunityView = new CommunityViewModel(); if (CustomLauncher.MenuDisplayNebulaLoginEntry) NebulaLoginVM = new NebulaLoginViewModel(); FillMenuItemsCustomMode(1); @@ -191,6 +193,11 @@ private void FillMenuItemsCustomMode(int defaultSelectedIndex) MenuItems.Add(new MainViewMenuItem(NebulaLoginVM, "avares://Knossos.NET/Assets/general/menu_nebula.png", "Nebula", "Log in with your nebula account")); } + if (CustomLauncher.MenuDisplayCommunityEntry && CommunityView != null) + { + MenuItems.Add(new MainViewMenuItem(CommunityView!, "avares://Knossos.NET/Assets/general/menu_community.png", "Community", "FAQs and Community Resources")); + } + if (CustomLauncher.MenuDisplayGlobalSettingsEntry && GlobalSettingsView != null) { MenuItems.Add(new MainViewMenuItem(GlobalSettingsView, "avares://Knossos.NET/Assets/general/menu_settings.png", "Config", "Change launcher and FSO engine settings")); From 215b13225549d820176dceb7bdce34462fe71adc Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Sun, 19 Jan 2025 12:15:45 -0300 Subject: [PATCH 18/44] crash fix and wrong folder creation --- Knossos.NET/Classes/Knossos.cs | 9 +-------- Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs | 4 ++-- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/Knossos.NET/Classes/Knossos.cs b/Knossos.NET/Classes/Knossos.cs index 12969722..b2dd0a69 100644 --- a/Knossos.NET/Classes/Knossos.cs +++ b/Knossos.NET/Classes/Knossos.cs @@ -117,14 +117,7 @@ public static async void StartUp(bool isQuickLaunch, bool forceUpdate) Log.Add(Log.LogSeverity.Information, "Knossos.StartUp()", "Running in PORTABLE MODE."); try { - if (!inSingleTCMode) - { - Directory.CreateDirectory(Path.Combine(KnUtils.KnetFolderPath!, "kn_portable", "HardLightProductions", "FreeSpaceOpen")); - } - else - { - Directory.CreateDirectory(Path.Combine(KnUtils.KnetFolderPath!, "kn_portable", "HardLightProductions", CustomLauncher.ModID!)); - } + Directory.CreateDirectory(KnUtils.GetFSODataFolderPath()); Directory.CreateDirectory(Path.Combine(KnUtils.KnetFolderPath!, "kn_portable", "Library")); } catch (Exception ex) diff --git a/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs b/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs index 449d2edb..bc07d567 100644 --- a/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs +++ b/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs @@ -669,9 +669,9 @@ await Dispatcher.UIThread.InvokeAsync(() => //Remove the mod version from Knossos and physical files await Task.Run(() => Knossos.RemoveMod(version)); //Remove mod version from UI mod versions list - await Dispatcher.UIThread.InvokeAsync(() => MainWindowViewModel.Instance!.RemoveInstalledModVersion(version)); + await Dispatcher.UIThread.InvokeAsync(() => MainWindowViewModel.Instance?.RemoveInstalledModVersion(version)); //If the dev editor is open and loaded this mod id, reset it - await Dispatcher.UIThread.InvokeAsync(() => DeveloperModsViewModel.Instance!.ResetModEditor(mod.id)); + await Dispatcher.UIThread.InvokeAsync(() => DeveloperModsViewModel.Instance?.ResetModEditor(mod.id)); } } else From c7e091ba1571d9307d5bd5e847ff340ab2e36ffc Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Sun, 19 Jan 2025 20:30:55 -0300 Subject: [PATCH 19/44] Hide globalsettings warning if mod is using -no_ingame_options --- Knossos.NET/ViewModels/CustomHomeViewModel.cs | 16 +++++++++++++++ .../ViewModels/GlobalSettingsViewModel.cs | 20 +++++++++++++++++++ .../ViewModels/Windows/MainWindowViewModel.cs | 4 ++++ Knossos.NET/Views/GlobalSettingsView.axaml | 6 +++--- 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/Knossos.NET/ViewModels/CustomHomeViewModel.cs b/Knossos.NET/ViewModels/CustomHomeViewModel.cs index e0638954..5ffaa9df 100644 --- a/Knossos.NET/ViewModels/CustomHomeViewModel.cs +++ b/Knossos.NET/ViewModels/CustomHomeViewModel.cs @@ -110,6 +110,22 @@ public void CheckBasePath() } } + /// + /// Checks if the current active mod version is passing a cmdline argument to fso + /// + /// + /// + public bool ActiveVersionHasCmdline(string cmdlineToCheck) + { + if(GetActiveInstalledModVersion != null) + { + var res = GetActiveInstalledModVersion.GetModCmdLine()?.ToLower().Contains(cmdlineToCheck.ToLower()); + if (res.HasValue) + return res.Value; + } + return false; + } + /// /// Handler for the hardcoded UI buttons /// diff --git a/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs b/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs index cf8ca540..8534c50c 100644 --- a/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs +++ b/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs @@ -60,6 +60,8 @@ public partial class GlobalSettingsViewModel : ViewModelBase internal bool isAVX = false; [ObservableProperty] internal bool isAVX2 = false; + [ObservableProperty] + internal bool displaySettingsWarning = false; /* Knossos Settings */ [ObservableProperty] @@ -526,6 +528,24 @@ public GlobalSettingsViewModel() isPortableMode = Knossos.inPortableMode; } + public void CheckDisplaySettingsWarning() + { + if(Knossos.inSingleTCMode) + { + DisplaySettingsWarning = true; + if(CustomLauncher.CustomCmdlineArray != null && CustomLauncher.CustomCmdlineArray.FirstOrDefault(x => x.ToLower() == "no_ingame_options") != null ) + { + DisplaySettingsWarning = false; + return; + } + var res = MainWindowViewModel.Instance?.CustomHomeVM?.ActiveVersionHasCmdline("no_ingame_options"); + if (res.HasValue) + { + DisplaySettingsWarning = !res.Value; + } + } + } + /// /// Check if the settings where changed from the ones in the GlobalSettings instance compared to the ones here /// If they did change update data stored in GlobalSettings and save file diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index cbf51906..bd781cba 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -301,6 +301,10 @@ partial void OnSelectedMenuItemChanged(MainViewMenuItem? value) } if (CurrentViewModel == GlobalSettingsView) //Settings { + if(Knossos.inSingleTCMode) + { + GlobalSettingsView?.CheckDisplaySettingsWarning(); + } Knossos.globalSettings.Load(); GlobalSettingsView?.LoadData(); //Knossos.globalSettings.EnableIniWatch(); diff --git a/Knossos.NET/Views/GlobalSettingsView.axaml b/Knossos.NET/Views/GlobalSettingsView.axaml index 2e556dca..72a4333c 100644 --- a/Knossos.NET/Views/GlobalSettingsView.axaml +++ b/Knossos.NET/Views/GlobalSettingsView.axaml @@ -136,7 +136,7 @@ - Note, video settings for most mods using FSO version 24.2.0 or higher will need to be changed in-game by default. To change these new settings, play a modern mod and then go to the in-game "Options" menu. + Note, video settings for most mods using FSO version 24.2.0 or higher will need to be changed in-game by default. To change these new settings, play a modern mod and then go to the in-game "Options" menu. @@ -237,7 +237,7 @@ - Note, audio settings for most mods using FSO version 24.2.0 or higher will need to be changed in-game by default. To change these new settings, play a modern mod and then go to the in-game "Options" menu. + Note, audio settings for most mods using FSO version 24.2.0 or higher will need to be changed in-game by default. To change these new settings, play a modern mod and then go to the in-game "Options" menu. @@ -309,7 +309,7 @@ - Note, input settings for most mods using FSO version 24.2.0 or higher will need to be changed in-game by default. To change these new settings, play a modern mod and then go to the in-game "Options" menu. + Note, input settings for most mods using FSO version 24.2.0 or higher will need to be changed in-game by default. To change these new settings, play a modern mod and then go to the in-game "Options" menu. Date: Mon, 20 Jan 2025 21:03:00 -0300 Subject: [PATCH 20/44] Advanced mod upload not selecting last version when unselecting upload Also remove the experimental feature warning --- .../ViewModels/Windows/DevModAdvancedUploadViewModel.cs | 5 ++++- Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Knossos.NET/ViewModels/Windows/DevModAdvancedUploadViewModel.cs b/Knossos.NET/ViewModels/Windows/DevModAdvancedUploadViewModel.cs index d73db93c..4cd8999a 100644 --- a/Knossos.NET/ViewModels/Windows/DevModAdvancedUploadViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/DevModAdvancedUploadViewModel.cs @@ -107,7 +107,10 @@ internal bool Upload OtherVersions[0].IsEnabled = false; // Disable "auto" CustomHash = ""; //Select what should be the latest version of a mod id - OtherVersionsSelectedIndex = OtherVersions.Count() - 1; + if(OtherVersions.Count() >= 2) + OtherVersionsSelectedIndex = 1; + else + OtherVersionsSelectedIndex = 0; } else { diff --git a/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml b/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml index b3cf77b4..8ccc80ac 100644 --- a/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml +++ b/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml @@ -20,7 +20,6 @@ - From d11fc7b80a6f0a50ec75c16f3dc1796aec2a48c3 Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Mon, 20 Jan 2025 21:16:43 -0300 Subject: [PATCH 21/44] Change default renderer to software --- Knossos.NET/Program.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Knossos.NET/Program.cs b/Knossos.NET/Program.cs index e321a365..76d7f82f 100644 --- a/Knossos.NET/Program.cs +++ b/Knossos.NET/Program.cs @@ -12,15 +12,15 @@ internal class Program [STAThread] public static void Main(string[] args) { - bool softwareRendering = false; + bool softwareRendering = true; bool isQuickLaunch = false; //Check app args foreach (var arg in args) { - if (arg.ToLower() == "-software") + if (arg.ToLower() == "-hardware") { - softwareRendering = true; + softwareRendering = false; } if (arg.ToLower() == "-playmod") { @@ -38,9 +38,9 @@ public static void Main(string[] args) //Check enviroment variables var renderMode = KnUtils.GetEnvironmentVariable("KNET_RENDER_MODE"); - if (renderMode != null && renderMode.ToLower() == "software") + if (renderMode != null && renderMode.ToLower() == "hardware") { - softwareRendering = true; + softwareRendering = false; } //Start App From 02a33cce4be2a2dd87d07182eb5de5e3d8ac23fb Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Mon, 20 Jan 2025 20:00:18 -0500 Subject: [PATCH 22/44] Add Sectioning to Options Menu (#267) --- Knossos.NET/Classes/Knossos.cs | 6 +- .../ViewModels/GlobalSettingsViewModel.cs | 2 +- Knossos.NET/ViewModels/TaskViewModel.cs | 4 +- .../Templates/Tasks/CreateModVersion.cs | 2 +- .../ViewModels/Templates/Tasks/InstallTool.cs | 2 +- .../Windows/DevModCreateNewViewModel.cs | 2 +- .../Windows/Fs2InstallerViewModel.cs | 4 +- Knossos.NET/Views/GlobalSettingsView.axaml | 305 ++++++++++-------- .../Views/Templates/DevModPkgMgrView.axaml | 2 +- .../Views/Windows/AddSapiVoicesView.axaml | 6 +- .../Windows/CleanupKnossosLibraryView.axaml | 2 +- .../Windows/DevModAdvancedUploadView.axaml | 6 +- .../Views/Windows/DevModCreateNewView.axaml | 2 +- .../Views/Windows/Fs2InstallerView.axaml | 2 +- .../Views/Windows/ModInstallView.axaml | 4 +- .../Views/Windows/QuickSetupView.axaml | 22 +- README.md | 4 +- 17 files changed, 203 insertions(+), 174 deletions(-) diff --git a/Knossos.NET/Classes/Knossos.cs b/Knossos.NET/Classes/Knossos.cs index 9cb1f14d..c989812e 100644 --- a/Knossos.NET/Classes/Knossos.cs +++ b/Knossos.NET/Classes/Knossos.cs @@ -601,9 +601,9 @@ await Dispatcher.UIThread.Invoke(async () => } else { - Log.Add(Log.LogSeverity.Error, "Knossos.CheckKnetUpdates()", "Get latest version from github resulted in null or tag_name being null."); + Log.Add(Log.LogSeverity.Error, "Knossos.CheckKnetUpdates()", "Get latest version from GitHub resulted in null or tag_name being null."); if (forceUpdateDownload) - Console.WriteLine("Update Error! Get latest version from github resulted in null or tag_name being null."); + Console.WriteLine("Update Error! Get latest version from GitHub resulted in null or tag_name being null."); } } catch (Exception ex) @@ -881,7 +881,7 @@ public static async void PlayMod(Mod mod, FsoExecType fsoExecType, bool standalo var queryConflict = dependencyList.GroupBy(x => x.id).Where(g => g.Count() > 1).ToList(); if (queryConflict.Count() > 0) { - var outputString = "There is a dependency conflict for this mod: " + mod + " Knet will try to adjust but the mod may present issues or not work at all. \nThis may be be resolved manually using custom dependencies for this mod.\n"; + var outputString = "There is a dependency conflict for this mod: " + mod + " KnossosNET will try to adjust but the mod may present issues or not work at all. \nThis may be be resolved manually using custom dependencies for this mod.\n"; foreach (var conflictGroup in queryConflict) { foreach (var conflictDep in conflictGroup) diff --git a/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs b/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs index 7c364720..fbcde121 100644 --- a/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs +++ b/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs @@ -1125,7 +1125,7 @@ internal async void BrowseFolderCommand() { Log.Add(Log.LogSeverity.Error, "GlobalSettings.BrowseFolderCommand() - test read/write was not successful: ", ex); await Dispatcher.UIThread.Invoke(async () => { - await MessageBox.Show(null, "Knossos was not able to write to this folder. Please select another library folder.", "Cannot Select Folder", MessageBox.MessageBoxButtons.OK); + await MessageBox.Show(null, "KnossosNET was not able to write to this folder. Please select another library folder.", "Cannot Select Folder", MessageBox.MessageBoxButtons.OK); }).ConfigureAwait(false); } } diff --git a/Knossos.NET/ViewModels/TaskViewModel.cs b/Knossos.NET/ViewModels/TaskViewModel.cs index d42174a0..c8c73880 100644 --- a/Knossos.NET/ViewModels/TaskViewModel.cs +++ b/Knossos.NET/ViewModels/TaskViewModel.cs @@ -243,7 +243,7 @@ public int NumberOfTasks() { await Dispatcher.UIThread.InvokeAsync(async () => { - await MessageBox.Show(MainWindow.instance!, "Knossos library path is not set! Before installing mods go to settings and select a library folder.", "Error", MessageBox.MessageBoxButtons.OK); + await MessageBox.Show(MainWindow.instance!, "KnossosNET library path is not set! Before installing mods go to settings and select a library folder.", "Error", MessageBox.MessageBoxButtons.OK); }); return null; } @@ -269,7 +269,7 @@ public async void InstallMod(Mod mod, List? reinstallPkgs = null, bo { await Dispatcher.UIThread.InvokeAsync(async () => { - await MessageBox.Show(MainWindow.instance!, "Knossos library path is not set! Before installing mods go to settings and select a library folder.", "Error", MessageBox.MessageBoxButtons.OK); + await MessageBox.Show(MainWindow.instance!, "KnossosNET library path is not set! Before installing mods go to settings and select a library folder.", "Error", MessageBox.MessageBoxButtons.OK); }); return; } diff --git a/Knossos.NET/ViewModels/Templates/Tasks/CreateModVersion.cs b/Knossos.NET/ViewModels/Templates/Tasks/CreateModVersion.cs index 121e2fab..765f32d7 100644 --- a/Knossos.NET/ViewModels/Templates/Tasks/CreateModVersion.cs +++ b/Knossos.NET/ViewModels/Templates/Tasks/CreateModVersion.cs @@ -56,7 +56,7 @@ public async Task CreateModVersion(Mod oldMod, string newVersion, Cancella using (StreamWriter writer = new StreamWriter(newDir + Path.DirectorySeparatorChar + "knossos_net_download.token")) { - writer.WriteLine("Warning: This token indicates an incomplete folder copy. If this token is present on the next Knet startup this folder WILL BE DELETED."); + writer.WriteLine("Warning: This token indicates an incomplete folder copy. If this token is present on the next KnossosNET startup this folder WILL BE DELETED."); } await KnUtils.CopyDirectoryAsync(currentDir.FullName, newDir, true, cancellationTokenSource, copyCallback); diff --git a/Knossos.NET/ViewModels/Templates/Tasks/InstallTool.cs b/Knossos.NET/ViewModels/Templates/Tasks/InstallTool.cs index c08c7508..a564e595 100644 --- a/Knossos.NET/ViewModels/Templates/Tasks/InstallTool.cs +++ b/Knossos.NET/ViewModels/Templates/Tasks/InstallTool.cs @@ -55,7 +55,7 @@ public async Task InstallTool(Tool tool, Tool? updateFrom, Action fi var libPath = Knossos.GetKnossosLibraryPath(); if (string.IsNullOrEmpty(libPath)) - throw new TaskCanceledException("Knossos library path is empty!"); + throw new TaskCanceledException("KnossosNET library path is empty!"); var toolPath = Path.Combine(libPath, "tools", tool.name); if (updateFrom != null) diff --git a/Knossos.NET/ViewModels/Windows/DevModCreateNewViewModel.cs b/Knossos.NET/ViewModels/Windows/DevModCreateNewViewModel.cs index 335ac20f..a388c0fa 100644 --- a/Knossos.NET/ViewModels/Windows/DevModCreateNewViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/DevModCreateNewViewModel.cs @@ -103,7 +103,7 @@ private async Task Verify() //Is library set? if(Knossos.GetKnossosLibraryPath() == null ) { - await MessageBox.Show(MainWindow.instance, "Knossos library path is not set. Go to the settings tab and set the Knossos libreary location.", "Validation error", MessageBox.MessageBoxButtons.OK); + await MessageBox.Show(MainWindow.instance, "KnossosNET library path is not set. Go to the settings tab and set the KnossosNET library location.", "Validation error", MessageBox.MessageBoxButtons.OK); return false; } //Version diff --git a/Knossos.NET/ViewModels/Windows/Fs2InstallerViewModel.cs b/Knossos.NET/ViewModels/Windows/Fs2InstallerViewModel.cs index e8d2c2a6..0f3b00ac 100644 --- a/Knossos.NET/ViewModels/Windows/Fs2InstallerViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/Fs2InstallerViewModel.cs @@ -70,7 +70,7 @@ internal async void InstallFS2Command() { if(Knossos.GetKnossosLibraryPath() == null) { - await MessageBox.Show(MainWindow.instance!, "The Knossos library path is not set, first set the library path in the settings tab before installing FS2 Retail.", "Library path is null", MessageBox.MessageBoxButtons.OK); + await MessageBox.Show(MainWindow.instance!, "The KnossosNET library path is not set, first set the library path in the settings tab before installing FS2 Retail.", "Library path is null", MessageBox.MessageBoxButtons.OK); return; } @@ -310,7 +310,7 @@ await Task.Run(() => { } } catch { } - InstallText = "Install Complete!, Knossos is reloading the library..."; + InstallText = "Install Complete!, KnossosNET is reloading the library..."; Knossos.ResetBasePath(); if(gogExe != null) { diff --git a/Knossos.NET/Views/GlobalSettingsView.axaml b/Knossos.NET/Views/GlobalSettingsView.axaml index 8baf56e4..35fda68f 100644 --- a/Knossos.NET/Views/GlobalSettingsView.axaml +++ b/Knossos.NET/Views/GlobalSettingsView.axaml @@ -21,126 +21,140 @@ - - - - - - Library Folder - - - - - - Stats - - - - - - + + + + + + + + Library Folder + + + + + + Stats + + + + + + + + + + + + - - - - - - - - Mod Compression - - Disabled - Manual - Always - Mod Support - - - - - - - - - - - - - - - Stable - RC - Nightly - Delete older versions - - - - - - - - - - - Concurrent Subtasks - - 1 - 2 - 3 - 4 - - - - - Bandwidth Limit - - Unlimited - 0.5 MB/s - 1 MB/s - 2 MB/s - 3 MB/s - 4 MB/s - 5 MB/s - 6 MB/s - 7 MB/s - 8 MB/s - 9 MB/s - 10 MB/s - - - - - Mirrors - - dl.fsnebula.org - cf.fsnebula.org - talos.feralhosting.com + + Mod Compression + + Disabled + Manual + Always + Mod Support + + + + + + + + + + + + + + + Stable + RC + Nightly + Delete older versions + + + + + + + + + + + Concurrent Subtasks + + 1 + 2 + 3 + 4 + + + + + Bandwidth Limit + + Unlimited + 0.5 MB/s + 1 MB/s + 2 MB/s + 3 MB/s + 4 MB/s + 5 MB/s + 6 MB/s + 7 MB/s + 8 MB/s + 9 MB/s + 10 MB/s + + + + + Mirrors + + dl.fsnebula.org + cf.fsnebula.org + talos.feralhosting.com + + + + + + + + + + + Information + Warning + Error + - - - - - - - - - - Information - Warning - Error - - - + + - - Note, video settings for most mods using FSO version 24.2.0 or higher will need to be changed in-game by default. To change these new settings, play a modern mod and then go to the in-game "Options" menu. - + + + + + + Note, video settings for most mods using FSO version 24.2.0 or higher will need to be changed in-game by default. + To change these new settings, play a modern mod and then go to the in-game "Options" menu. + + + - + Display Resolution 32 Bit @@ -148,9 +162,8 @@ - - + Texture Filtering @@ -216,7 +229,6 @@ - @@ -233,13 +245,20 @@ - + - - Note, audio settings for most mods using FSO version 24.2.0 or higher will need to be changed in-game by default. To change these new settings, play a modern mod and then go to the in-game "Options" menu. - + + + + + + Note, audio settings for most mods using FSO version 24.2.0 or higher will need to be changed in-game by default. + To change these new settings, play a modern mod and then go to the in-game "Options" menu. + Playback Devices @@ -263,23 +282,23 @@ - - + + - - + + - - + + - - + + @@ -305,13 +324,19 @@ - + - - Note, input settings for most mods using FSO version 24.2.0 or higher will need to be changed in-game by default. To change these new settings, play a modern mod and then go to the in-game "Options" menu. - + + + + + + Note, input settings for most mods using FSO version 24.2.0 or higher will need to be changed in-game by default. To change these new settings, play a modern mod and then go to the in-game "Options" menu. + @@ -352,11 +377,15 @@ - + - - + + + + @@ -403,7 +432,7 @@ - + @@ -426,7 +455,7 @@ - + diff --git a/Knossos.NET/Views/Templates/DevModPkgMgrView.axaml b/Knossos.NET/Views/Templates/DevModPkgMgrView.axaml index f99c075b..dd5b805d 100644 --- a/Knossos.NET/Views/Templates/DevModPkgMgrView.axaml +++ b/Knossos.NET/Views/Templates/DevModPkgMgrView.axaml @@ -62,7 +62,7 @@ Recommended Optional - Make VP + Make VP diff --git a/Knossos.NET/Views/Windows/ModInstallView.axaml b/Knossos.NET/Views/Windows/ModInstallView.axaml index 7349a37a..3afa519a 100644 --- a/Knossos.NET/Views/Windows/ModInstallView.axaml +++ b/Knossos.NET/Views/Windows/ModInstallView.axaml @@ -73,8 +73,8 @@ Remove old versions of this mod - Force clean install - Force clean install + Allow hardlinks diff --git a/Knossos.NET/Views/Windows/QuickSetupView.axaml b/Knossos.NET/Views/Windows/QuickSetupView.axaml index d540c57b..c07665f3 100644 --- a/Knossos.NET/Views/Windows/QuickSetupView.axaml +++ b/Knossos.NET/Views/Windows/QuickSetupView.axaml @@ -27,9 +27,9 @@ Welcome To Knossos.NET - This quick setup guide will help you through the basics of configuring the Knossos launcher for the first time. If you already know how to configure the launcher you can close this window. + This quick setup guide will help you through the basics of configuring the KnossosNET launcher for the first time. If you already know how to configure the launcher you can close this window. Note, you can reopen this guide at any time, just go to the "Settings" tab and click the "Quick Setup Guide" button. - If at any point you ever need assistance with Knossos plese ask on the Knossos Discord channel linked below. + If you ever need assistance with KnossosNET please ask on the Knossos Discord channel linked below. @@ -37,15 +37,15 @@ Setting up the library folder - First, you must set a library folder. Go to the "Settings" tab and under the "Knossos" section click on the "Browse" button and choose or create a folder for Knossos to use. It is highly recommended to set this as an empty folder in a location with a large amount of available storage. Once you have set the folder be sure to click the "Save" button in the upper right corner of the "Settings" tab. - The Knossos library folder is where all game and mod data will be saved to. Make sure you always have space available on this drive before installing a new mod or update. + First, you must set a library folder. Go to the "Settings" tab and under the "KnossosNET" section click on the "Browse" button and choose or create a folder for Knossos to use. It is highly recommended to set this as an empty folder in a location with a large amount of available storage. Once you have set the folder be sure to click the "Save" button in the upper right corner of the "Settings" tab. + The KnossosNET library folder is where all game and mod data will be saved to. Make sure you always have space available on this drive before installing a new mod or update. Current Library Folder Go to the "Settings" tab and set the library folder to continue. - Knet is running in portable mode - This means Knet settings and FSO pilots, settings and data are saved inside the 'kn_portable' folder, you can not set a library folder in this mode. + KnossosNET is running in portable mode + This means KnossosNET settings and FSO pilots, settings and data are saved inside the 'kn_portable' folder, you can not set a library folder in this mode. If you ever need to stop using the portable mode move, rename or delete the 'kn_portable' folder Current Library Folder @@ -57,8 +57,8 @@ Download an FSO engine build Wait for repo_minimal.json to finish downloading before you continue... - Now you need a game engine build, which are known as "FSO Builds". These builds are what mods run on, and Knossos also uses it to get and display system data. This is why you are not going to be able to set most of the settings in the "Settings" tab before downloading one. - Click the button below to download the lastest stable build, which is what most casual players use. You can also go to the "Engine" tab, and pick a specific build. The most recent build should be always the top one on the list. + Now you need a game engine build, which are known as "FSO Builds". These builds are what mods run on, and KnossosNET also uses it to get and display system data. This is why you are not going to be able to set most of the settings in the "Settings" tab before downloading one. + Click the button below to download the latest stable build, which is what most casual players use. You can also go to the "Engine" tab, and pick a specific build. The most recent build should be always the top one on the list. Go to the "Engine" tab, select "Stable Builds" and download the newest stable, it should be always the top one on the list. + + + + + + + + + + + + + From 38001d246b74ea55d5e7457f0909ec1ea92acd09 Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Mon, 13 Jan 2025 20:46:40 -0300 Subject: [PATCH 29/44] Minor corrections and initial status of the menu as a config option --- Knossos.NET/Models/CustomLauncher.cs | 10 ++++++++++ Knossos.NET/Models/Nebula.cs | 2 +- Knossos.NET/ViewModels/CustomHomeViewModel.cs | 2 +- Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs | 1 + 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Knossos.NET/Models/CustomLauncher.cs b/Knossos.NET/Models/CustomLauncher.cs index 17b43997..901e5cc8 100644 --- a/Knossos.NET/Models/CustomLauncher.cs +++ b/Knossos.NET/Models/CustomLauncher.cs @@ -57,6 +57,12 @@ public static class CustomLauncher /// public static int? WindowHeight { get; private set; } = 540; + /// + /// The first time the user opens the launcher, the main menu should be expanded or collapsed? + /// After that it will use the saved state + /// + public static bool MenuOpenFirstTime { get; private set; } = false; + /// /// Add the regular FSO engine view to the menu /// @@ -172,6 +178,9 @@ private static void ReadCustomFile() if (customData.WindowHeight != null) WindowHeight = customData.WindowHeight; + if (customData.MenuOpenFirstTime.HasValue) + MenuOpenFirstTime = customData.MenuOpenFirstTime.Value; + if (customData.MenuDisplayEngineEntry.HasValue) MenuDisplayEngineEntry = customData.MenuDisplayEngineEntry.Value; @@ -212,6 +221,7 @@ struct CustomFileData public string? WindowTitle { get; set; } public int? WindowWidth { get; set; } public int? WindowHeight { get; set; } + public bool? MenuOpenFirstTime { get; set; } public bool? MenuDisplayEngineEntry { get; set; } public bool? MenuDisplayDebugEntry { get; set; } public bool? MenuDisplayNebulaLoginEntry { get; set; } diff --git a/Knossos.NET/Models/Nebula.cs b/Knossos.NET/Models/Nebula.cs index b443dc43..f914226b 100644 --- a/Knossos.NET/Models/Nebula.cs +++ b/Knossos.NET/Models/Nebula.cs @@ -125,7 +125,7 @@ public static async Task Trinity() } try { - bool displayUpdates = settings.NewerModsVersions.Any() ? true : false; + bool displayUpdates = settings.NewerModsVersions.Any() && !CustomLauncher.IsCustomMode ? true : false; var webEtag = await KnUtils.GetUrlFileEtag(repoUrl).ConfigureAwait(false); if (!File.Exists(KnUtils.GetKnossosDataFolderPath() + Path.DirectorySeparatorChar + "repo_minimal.json") || settings.etag != webEtag) { diff --git a/Knossos.NET/ViewModels/CustomHomeViewModel.cs b/Knossos.NET/ViewModels/CustomHomeViewModel.cs index 1945a893..9a5ecbea 100644 --- a/Knossos.NET/ViewModels/CustomHomeViewModel.cs +++ b/Knossos.NET/ViewModels/CustomHomeViewModel.cs @@ -86,7 +86,7 @@ private void Cancel() private async void Install() { - if (nebulaModVersions.Any()) + if (nebulaModVersions.Any() && CustomLauncher.UseNebulaServices) { var dialog = new ModInstallView(); dialog.DataContext = new ModInstallViewModel(nebulaModVersions.First(), dialog); diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index 482d2dba..aa64acb8 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -131,6 +131,7 @@ public MainWindowViewModel() else { //Apply customization for Single TC Mode + Knossos.globalSettings.mainMenuOpen = CustomLauncher.MenuOpenFirstTime; AppTitle = CustomLauncher.WindowTitle + " v" + Knossos.AppVersion; WindowHeight = CustomLauncher.WindowHeight; WindowWidth = CustomLauncher.WindowWidth; From 34391672252b7497256ce292d4b68fa3d2cbc68a Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Tue, 14 Jan 2025 23:57:56 -0300 Subject: [PATCH 30/44] Custom mode home screen part 3 --- Knossos.NET/AppStyles.axaml | 1 + Knossos.NET/Models/GlobalSettings.cs | 4 + Knossos.NET/Models/ModSettings.cs | 2 +- Knossos.NET/ViewModels/CustomHomeViewModel.cs | 248 ++++++++++++++++-- .../ViewModels/Windows/ModDetailsViewModel.cs | 2 + Knossos.NET/Views/CustomHomeView.axaml | 72 ++++- .../Views/Windows/ModDetailsView.axaml | 4 +- 7 files changed, 290 insertions(+), 43 deletions(-) diff --git a/Knossos.NET/AppStyles.axaml b/Knossos.NET/AppStyles.axaml index 758a8213..d7f18343 100644 --- a/Knossos.NET/AppStyles.axaml +++ b/Knossos.NET/AppStyles.axaml @@ -73,6 +73,7 @@ M2 4.5C2 4.22386 2.22386 4 2.5 4H17.5C17.7761 4 18 4.22386 18 4.5C18 4.77614 17.7761 5 17.5 5H2.5C2.22386 5 2 4.77614 2 4.5Z M2 9.5C2 9.22386 2.22386 9 2.5 9H17.5C17.7761 9 18 9.22386 18 9.5C18 9.77614 17.7761 10 17.5 10H2.5C2.22386 10 2 9.77614 2 9.5Z M2.5 14C2.22386 14 2 14.2239 2 14.5C2 14.7761 2.22386 15 2.5 15H17.5C17.7761 15 18 14.7761 18 14.5C18 14.2239 17.7761 14 17.5 14H2.5Z + M31.8839 8.36612C32.372 8.85427 32.372 9.64573 31.8839 10.1339L18.0178 24L31.8839 37.8661C32.372 38.3543 32.372 39.1457 31.8839 39.6339C31.3957 40.122 30.6043 40.122 30.1161 39.6339L15.3661 24.8839C14.878 24.3957 14.878 23.6043 15.3661 23.1161L30.1161 8.36612C30.6043 7.87796 31.3957 7.87796 31.8839 8.36612Z diff --git a/Knossos.NET/Models/GlobalSettings.cs b/Knossos.NET/Models/GlobalSettings.cs index 7f9b5ee0..092fe8d8 100644 --- a/Knossos.NET/Models/GlobalSettings.cs +++ b/Knossos.NET/Models/GlobalSettings.cs @@ -705,6 +705,10 @@ private void SetCustomModeValues() checkUpdate = CustomLauncher.AllowLauncherUpdates; enableLogFile = CustomLauncher.WriteLogFile; autoUpdate = false; + if (!CustomLauncher.MenuDisplayGlobalSettingsEntry) + { + warnNewSettingsSystem = false; + } } } diff --git a/Knossos.NET/Models/ModSettings.cs b/Knossos.NET/Models/ModSettings.cs index 3104c79c..f73e8f4b 100644 --- a/Knossos.NET/Models/ModSettings.cs +++ b/Knossos.NET/Models/ModSettings.cs @@ -81,7 +81,7 @@ public void SetInitialFilePath(string modFullPath) /// /// Load mod_settings.json data - /// Any new variabled must be added here or it will not be loaded + /// Any new variables must be added here or it will not be loaded /// /// public void Load(string modFolderPath) diff --git a/Knossos.NET/ViewModels/CustomHomeViewModel.cs b/Knossos.NET/ViewModels/CustomHomeViewModel.cs index 9a5ecbea..56873bf4 100644 --- a/Knossos.NET/ViewModels/CustomHomeViewModel.cs +++ b/Knossos.NET/ViewModels/CustomHomeViewModel.cs @@ -6,6 +6,8 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -15,6 +17,22 @@ namespace Knossos.NET.ViewModels public partial class CustomHomeViewModel : ViewModelBase { private List modVersions = new List(); + private Mod? GetActiveInstalledModVersion + { + get + { + if(ActiveVersionIndex >= 0 && ActiveVersionIndex < modVersions.Count()) + { + return modVersions[ActiveVersionIndex]; + } + else + { + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.GetActiveInstalledModVersion()", "ActiveVersionIndex was " + ActiveVersionIndex + " and modVersions.Count() was " + modVersions.Count()); + return null; + } + } + } + private List nebulaModVersions = new List(); private CancellationTokenSource? cancellationTokenSource = null; @@ -30,7 +48,7 @@ public partial class CustomHomeViewModel : ViewModelBase internal bool installing = false; [ObservableProperty] - internal bool update = false; + internal bool isUpdateReady = false; [ObservableProperty] internal bool nebulaVersionsAvailable = false; @@ -41,37 +59,42 @@ public partial class CustomHomeViewModel : ViewModelBase [ObservableProperty] internal int animate = 0; + [ObservableProperty] + internal bool nebulaServices = CustomLauncher.UseNebulaServices; + public CustomHomeViewModel() { } + /// + /// Handler for the hardcoded UI buttons + /// + /// internal void HardcodedButtonCommand(object cmd) { - if (ActiveVersionIndex == -1) - { - Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.HardcodedButtonCommand()", "Crash prevented: ActiveVersionIndex was " + ActiveVersionIndex + " and modVersions.Count() was " + modVersions.Count()); - ActiveVersionIndex = 0; - } switch ((string)cmd) { - case "play": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.Release); break; - case "playvr": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.Release, false, 0, true); break; - case "fred2": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.Fred2); break; - case "fred2debug": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.Fred2Debug); break; - case "debug": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.Debug); break; - case "qtfred": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.QtFred); break; - case "qtfreddebug": Knossos.PlayMod(modVersions[ActiveVersionIndex], FsoExecType.QtFredDebug); break; + case "play": if(GetActiveInstalledModVersion != null) Knossos.PlayMod(GetActiveInstalledModVersion, FsoExecType.Release); break; + case "playvr": if (GetActiveInstalledModVersion != null) Knossos.PlayMod(GetActiveInstalledModVersion, FsoExecType.Release, false, 0, true); break; + case "fred2": if (GetActiveInstalledModVersion != null) Knossos.PlayMod(GetActiveInstalledModVersion, FsoExecType.Fred2); break; + case "fred2debug": if (GetActiveInstalledModVersion != null) Knossos.PlayMod(GetActiveInstalledModVersion, FsoExecType.Fred2Debug); break; + case "debug": if (GetActiveInstalledModVersion != null) Knossos.PlayMod(GetActiveInstalledModVersion, FsoExecType.Debug); break; + case "qtfred": if (GetActiveInstalledModVersion != null) Knossos.PlayMod(GetActiveInstalledModVersion, FsoExecType.QtFred); break; + case "qtfreddebug": if (GetActiveInstalledModVersion != null) Knossos.PlayMod(GetActiveInstalledModVersion, FsoExecType.QtFredDebug); break; case "install": Install(); break; case "cancel": Cancel(); break; - case "update": break; - case "modify": break; - case "delete": break; - case "details": break; - case "settings": break; - case "logfile": break; + case "update": Update(); break; + case "modify": Modify(); break; + case "delete": if (GetActiveInstalledModVersion != null) RemoveInstalledModVersion(GetActiveInstalledModVersion); break; + case "details": Details(); break; + case "settings": Settings(); break; + case "logfile": OpenFS2Log(); break; } } + /// + /// Calls to cancel running install tasks + /// private void Cancel() { Installing = false; @@ -84,6 +107,9 @@ private void Cancel() TaskViewModel.Instance?.CancelAllInstallTaskWithID(CustomLauncher.ModID!, null); } + /// + /// Opens mod install window for this mod id + /// private async void Install() { if (nebulaModVersions.Any() && CustomLauncher.UseNebulaServices) @@ -98,24 +124,182 @@ private async void Install() } } - public void RemoveInstalledModVersion(Mod mod) + /// + /// Opens mod install window for this mod id in modify active version mode + /// + private async void Modify() { - if (CustomLauncher.ModID == mod.id) + if (GetActiveInstalledModVersion != null && CustomLauncher.UseNebulaServices) { - Installed = modVersions.Any(); - Installing = false; + var dialog = new ModInstallView(); + dialog.DataContext = new ModInstallViewModel(GetActiveInstalledModVersion, dialog, GetActiveInstalledModVersion.version); + await dialog.ShowDialog(MainWindow.instance!); + } + else + { + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.Modify()", "GetActiveInstalledModVersion was null. ActiveVersionIndex: " + ActiveVersionIndex + " modVerions.count()" + modVersions.Count()); + } + } + + /// + /// Opens mod install window for this mod id + /// + private async void Update() + { + if (GetActiveInstalledModVersion != null && CustomLauncher.UseNebulaServices) + { + var dialog = new ModInstallView(); + dialog.DataContext = new ModInstallViewModel(GetActiveInstalledModVersion, dialog); + await dialog.ShowDialog(MainWindow.instance!); + } + else + { + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.Update()", "GetActiveInstalledModVersion was null. ActiveVersionIndex: " + ActiveVersionIndex + " modVerions.count()" + modVersions.Count()); + } + } + + /// + /// Opens this mod details dialog + /// + private async void Details() + { + if (MainWindow.instance != null) + { + var dialog = new ModDetailsView(); + var mod = GetActiveInstalledModVersion != null ? GetActiveInstalledModVersion : nebulaModVersions.FirstOrDefault(); + if (mod != null) + { + dialog.DataContext = new ModDetailsViewModel(mod, dialog); + await dialog.ShowDialog(MainWindow.instance); + } + else + { + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.Details()", "Mod was null, not installed or nebulas versions of this modid were found."); + } + } + } + + /// + /// Opens this mod settings dialog + /// + internal async void Settings() + { + if (MainWindow.instance != null) + { + if (GetActiveInstalledModVersion != null) + { + var dialog = new ModSettingsView(); + dialog.DataContext = new ModSettingsViewModel(GetActiveInstalledModVersion); + await dialog.ShowDialog(MainWindow.instance); + } + else + { + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.Settings()", "Mod was null, not installed versions of this modid were found."); + } } } + /// + /// Opens the fs_open.log file, if it exists. + /// + private void OpenFS2Log() + { + if (File.Exists(Path.Combine(KnUtils.GetFSODataFolderPath(), "data", "fs2_open.log"))) + { + try + { + var cmd = new Process(); + cmd.StartInfo.FileName = Path.Combine(KnUtils.GetFSODataFolderPath(), "data", "fs2_open.log"); + cmd.StartInfo.UseShellExecute = true; + cmd.Start(); + cmd.Dispose(); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.OpenFS2Log", ex); + } + } + else + { + if (MainWindow.instance != null) + MessageBox.Show(MainWindow.instance, "Log File " + Path.Combine(KnUtils.GetFSODataFolderPath(), "data", "fs2_open.log") + " not found.", "File not found", MessageBox.MessageBoxButtons.OK); + } + } + + /// + /// Removes ONE installed mod version from the disk + /// + /// + public async void RemoveInstalledModVersion(Mod mod) + { + try + { + if (CustomLauncher.ModID == mod.id) + { + if (TaskViewModel.Instance!.IsSafeState()) + { + if (GetActiveInstalledModVersion != null) + { + if (modVersions.Count > 1) + { + var resp = await MessageBox.Show(MainWindow.instance!, "You are about to delete version " + GetActiveInstalledModVersion.version + ", this will remove this version only. Do you want to continue?", "Delete version", MessageBox.MessageBoxButtons.YesNo); + if (resp == MessageBox.MessageBoxResult.Yes) + { + var delete = modVersions[ActiveVersionIndex]; + var verDel = VersionItems[ActiveVersionIndex]; + modVersions.Remove(delete); + Knossos.RemoveMod(delete); + VersionItems.Remove(verDel); + ActiveVersionIndex = modVersions.Count() - 1; + } + } + else + { + var resp = await MessageBox.Show(MainWindow.instance!, "You are about to delete the last installed version. Do you want to continue?", "Delete last version", MessageBox.MessageBoxButtons.YesNo); + if (resp == MessageBox.MessageBoxResult.Yes) + { + //Last version + modVersions[0].installed = false; + MainWindowViewModel.Instance?.AddNebulaMod(modVersions[0]); + Knossos.RemoveMod(modVersions[0].id); + Installed = false; + Installing = false; + modVersions.Clear(); + VersionItems.Clear(); + ActiveVersionIndex = 0; + } + } + } + } + else + { + await MessageBox.Show(MainWindow.instance!, "You can not delete a mod while other install tasks are running, wait until they finish and try again.", "Tasks are running", MessageBox.MessageBoxButtons.OK); + } + } + }catch(Exception ex) + { + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.RemoveInstalledModVersion()", ex); + } + } + + /// + /// This deletes all versions of this mod + /// Not implemented or needed + /// + /// public void RemoveMod(string id) { if (CustomLauncher.ModID == id) { - Installed = false; - Installing = false; + //Installed = false; + //Installing = false; } } + /// + /// Remove starts cancellation of a install taks with this mod id + /// + /// public void CancelModInstall(string id) { if (CustomLauncher.ModID == id) @@ -124,10 +308,18 @@ public void CancelModInstall(string id) } } + /// + /// Sets the install mode, so the cancel tasks button can be displayed + /// + /// + /// public void SetInstalling(string id, CancellationTokenSource cancelToken) { - cancellationTokenSource = cancelToken; - Installing = true; + if (CustomLauncher.ModID == id) + { + cancellationTokenSource = cancelToken; + Installing = true; + } } /// @@ -195,7 +387,7 @@ public void UpdateIsAvailable(string id, bool value) { if (id == CustomLauncher.ModID) { - Update = value; + IsUpdateReady = value; } } diff --git a/Knossos.NET/ViewModels/Windows/ModDetailsViewModel.cs b/Knossos.NET/ViewModels/Windows/ModDetailsViewModel.cs index 45b2582b..1df476d6 100644 --- a/Knossos.NET/ViewModels/Windows/ModDetailsViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/ModDetailsViewModel.cs @@ -86,6 +86,8 @@ public partial class ModDetailsViewModel : ViewModelBase [ObservableProperty] internal bool isLocalMod = false; [ObservableProperty] + internal bool nebulaServices = !CustomLauncher.IsCustomMode || (CustomLauncher.IsCustomMode && CustomLauncher.UseNebulaServices) ? true : false; + [ObservableProperty] internal ObservableCollection screenshots = new ObservableCollection(); internal ObservableCollection VersionItems { get; set; } = new ObservableCollection(); diff --git a/Knossos.NET/Views/CustomHomeView.axaml b/Knossos.NET/Views/CustomHomeView.axaml index 073952e9..149ef3c6 100644 --- a/Knossos.NET/Views/CustomHomeView.axaml +++ b/Knossos.NET/Views/CustomHomeView.axaml @@ -24,26 +24,52 @@ anim:ImageBehavior.SpeedRatio="{Binding Animate}" Source="{Binding BackgroundImage, Converter={StaticResource imageConverter}}" anim:ImageBehavior.AnimatedSource="{Binding BackgroundImage}" /> - - + + - - - + + + + + + + + + + + + + + + + + + IsVisible="{Binding !Installing}" IsEnabled="{Binding NebulaServices}"> + + + + + + + + diff --git a/Knossos.NET/Views/Windows/ModDetailsView.axaml b/Knossos.NET/Views/Windows/ModDetailsView.axaml index d90b6108..8d3065be 100644 --- a/Knossos.NET/Views/Windows/ModDetailsView.axaml +++ b/Knossos.NET/Views/Windows/ModDetailsView.axaml @@ -30,13 +30,13 @@ - + - + - public static int? WindowWidth { get; private set; } = 960; + public static int? WindowWidth { get; private set; } = 1024; /// /// Starting height size of the launcher window diff --git a/Knossos.NET/Models/Nebula.cs b/Knossos.NET/Models/Nebula.cs index f914226b..a57dd525 100644 --- a/Knossos.NET/Models/Nebula.cs +++ b/Knossos.NET/Models/Nebula.cs @@ -253,7 +253,7 @@ private static async Task LoadPrivateMods(CancellationTokenSource? cancellationT var newer = isInstalled.MaxBy(x => new SemanticVersion(x.version)); if (newer != null && ( new SemanticVersion(newer.version) < new SemanticVersion(mod.version) || newer.version == mod.version && newer.lastUpdate != mod.lastUpdate)) { - Dispatcher.UIThread.Invoke(() => MainWindowViewModel.Instance?.MarkAsUpdateAvailable(mod.id), DispatcherPriority.Background); + Dispatcher.UIThread.Invoke(() => MainWindowViewModel.Instance?.MarkAsUpdateAvailable(mod.id, true, mod.version), DispatcherPriority.Background); } if(isInstalled.First().devMode) DeveloperModsViewModel.Instance?.UpdateVersionManager(isInstalled.First().id); @@ -426,7 +426,7 @@ private static bool IsModUpdate(Mod mod) var newer = isInstalled.MaxBy(x => new SemanticVersion(x.version)); if (newer != null && ( new SemanticVersion(newer.version) < new SemanticVersion(m.version) || newer.version == m.version && newer.lastUpdate != m.lastUpdate )) { - await Dispatcher.UIThread.InvokeAsync(() => MainWindowViewModel.Instance?.MarkAsUpdateAvailable(m.id), DispatcherPriority.Background); + await Dispatcher.UIThread.InvokeAsync(() => MainWindowViewModel.Instance?.MarkAsUpdateAvailable(m.id, true, m.version), DispatcherPriority.Background); } modsTcs.Remove(m); } diff --git a/Knossos.NET/ViewModels/CustomHomeViewModel.cs b/Knossos.NET/ViewModels/CustomHomeViewModel.cs index 56873bf4..48ed7a57 100644 --- a/Knossos.NET/ViewModels/CustomHomeViewModel.cs +++ b/Knossos.NET/ViewModels/CustomHomeViewModel.cs @@ -62,6 +62,12 @@ private Mod? GetActiveInstalledModVersion [ObservableProperty] internal bool nebulaServices = CustomLauncher.UseNebulaServices; + /// + /// Handled in mainview, displays a small task viewer in the home screen + /// + [ObservableProperty] + public ViewModelBase? taskVM; + public CustomHomeViewModel() { } @@ -250,7 +256,7 @@ public async void RemoveInstalledModVersion(Mod mod) modVersions.Remove(delete); Knossos.RemoveMod(delete); VersionItems.Remove(verDel); - ActiveVersionIndex = modVersions.Count() - 1; + ActiveVersionIndex = 0; } } else @@ -383,11 +389,15 @@ public void AddNebulaModVersion(Mod modJson) /// /// /// - public void UpdateIsAvailable(string id, bool value) + public void UpdateIsAvailable(string id, bool value, string? newVersion) { if (id == CustomLauncher.ModID) { IsUpdateReady = value; + if (IsUpdateReady && newVersion != null) + { + Dispatcher.UIThread.Invoke(() => TaskViewModel.Instance?.AddMessageTask("An update is available! " + newVersion), DispatcherPriority.Background); + } } } diff --git a/Knossos.NET/ViewModels/TaskViewModel.cs b/Knossos.NET/ViewModels/TaskViewModel.cs index eb154784..b785cc6b 100644 --- a/Knossos.NET/ViewModels/TaskViewModel.cs +++ b/Knossos.NET/ViewModels/TaskViewModel.cs @@ -10,6 +10,7 @@ using System.IO; using CommunityToolkit.Mvvm.ComponentModel; using Avalonia.Controls; +using System.Linq; namespace Knossos.NET.ViewModels { @@ -28,6 +29,9 @@ public partial class TaskViewModel : ViewModelBase /// internal Queue taskQueue { get; set; } = new Queue(); + [ObservableProperty] + internal bool buttonsVisible = true; + public TaskViewModel() { Instance = this; @@ -57,7 +61,7 @@ public void CancelAllInstallTaskWithID(string id, string? version) { Dispatcher.UIThread.Invoke(() => { - foreach (var task in TaskList) + foreach (var task in TaskList.ToList()) { if (!task.IsCompleted && task.installID == id && (version == null || task.installVersion == version)) { @@ -67,13 +71,22 @@ public void CancelAllInstallTaskWithID(string id, string? version) }); } + /// + /// SHow or hide buttons on taskview + /// + /// + public void ShowButtons(bool state) + { + ButtonsVisible = state; + } + /// /// Checks if all tasks in queue are mark as cancelled or completed /// /// true if all completed or cancelled, false if there is running tasks public bool IsSafeState() { - foreach (var task in TaskList) + foreach (var task in TaskList.ToList()) { if (!task.IsCancelled && !task.IsCompleted) { @@ -254,7 +267,20 @@ await Dispatcher.UIThread.InvokeAsync(async () => TaskList.Add(newTask); taskQueue.Enqueue(newTask); }); - return await newTask.InstallBuild(build, sender,sender.cancellationTokenSource,modJson, modifyPkgs, cleanupOldVersions).ConfigureAwait(false); + var res = await newTask.InstallBuild(build, sender,sender.cancellationTokenSource,modJson, modifyPkgs, cleanupOldVersions).ConfigureAwait(false); + if (res != null && Knossos.inSingleTCMode) + { + try + { + TaskList.Remove(newTask); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "TaskViewModel.InstallBuild()", ex); + } + Dispatcher.UIThread.Invoke(() => TaskViewModel.Instance?.AddMessageTask("Completed: " + newTask.Name), DispatcherPriority.Background); + } + return res; } /// @@ -290,7 +316,19 @@ await Dispatcher.UIThread.InvokeAsync(async () => TaskList.Add(newTask); taskQueue.Enqueue(newTask); }); - await newTask.InstallMod(mod, cancelSource, reinstallPkgs, manualCompress, cleanupOldVersions, cleanInstall, allowHardlinks).ConfigureAwait(false); + var res = await newTask.InstallMod(mod, cancelSource, reinstallPkgs, manualCompress, cleanupOldVersions, cleanInstall, allowHardlinks).ConfigureAwait(false); + if(res && Knossos.inSingleTCMode) + { + try + { + TaskList.Remove(newTask); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "TaskViewModel.InstallMod()", ex); + } + Dispatcher.UIThread.Invoke(() => TaskViewModel.Instance?.AddMessageTask("Completed: " + newTask.Name), DispatcherPriority.Background); + } } } } @@ -317,7 +355,19 @@ public async Task CompressMod(Mod mod) TaskList.Add(newTask); taskQueue.Enqueue(newTask); }); - await newTask.CompressMod(mod, cancelSource).ConfigureAwait(false); + var res = await newTask.CompressMod(mod, cancelSource).ConfigureAwait(false); + if (res && Knossos.inSingleTCMode) + { + try + { + TaskList.Remove(newTask); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "TaskViewModel.CompressMod()", ex); + } + Dispatcher.UIThread.Invoke(() => TaskViewModel.Instance?.AddMessageTask("Completed: " + newTask.Name), DispatcherPriority.Background); + } } } } @@ -343,7 +393,19 @@ public async Task DecompressMod(Mod mod) TaskList.Add(newTask); taskQueue.Enqueue(newTask); }); - await newTask.DecompressMod(mod, cancelSource).ConfigureAwait(false); + var res = await newTask.DecompressMod(mod, cancelSource).ConfigureAwait(false); + if (res && Knossos.inSingleTCMode) + { + try + { + TaskList.Remove(newTask); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "TaskViewModel.DecompressMod()", ex); + } + Dispatcher.UIThread.Invoke(() => TaskViewModel.Instance?.AddMessageTask("Completed: " + newTask.Name), DispatcherPriority.Background); + } } } } diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index aa64acb8..9e8f30e6 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -229,6 +229,8 @@ partial void OnSelectedMenuItemChanged(MainViewMenuItem? value) } if (CurrentViewModel != null && CurrentViewModel == CustomHomeVM) //CustomHomeView { + CustomHomeVM.TaskVM = null; + TaskView?.ShowButtons(true); CustomHomeVM.ViewClosed(); } @@ -263,6 +265,8 @@ partial void OnSelectedMenuItemChanged(MainViewMenuItem? value) } if (CurrentViewModel != null && CurrentViewModel == CustomHomeVM) //CustomHomeView { + CustomHomeVM.TaskVM = TaskView; + TaskView?.ShowButtons(false); CustomHomeVM.ViewOpened(); } if (CurrentViewModel == GlobalSettingsView) //Settings @@ -313,10 +317,10 @@ public void ClearViews() /// /// /// - public void MarkAsUpdateAvailable(string id, bool value = true) + public void MarkAsUpdateAvailable(string id, bool value = true, string? newVersion = null) { InstalledModsView?.UpdateIsAvailable(id, value); - CustomHomeVM?.UpdateIsAvailable(id, value); + CustomHomeVM?.UpdateIsAvailable(id, value, newVersion); } /// diff --git a/Knossos.NET/ViewModels/Windows/ModInstallViewModel.cs b/Knossos.NET/ViewModels/Windows/ModInstallViewModel.cs index 3aa32af4..1cdc16f3 100644 --- a/Knossos.NET/ViewModels/Windows/ModInstallViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/ModInstallViewModel.cs @@ -162,7 +162,8 @@ private async void UpdateSelectedVersion() { await SelectedMod.LoadFulLNebulaData(); } - if(SelectedMod != null && !SelectedMod.GetMissingDependenciesList().Any()) + //load all dependencies mods in singletc mode so they can be modified too at any time + if(SelectedMod != null && !SelectedMod.GetMissingDependenciesList().Any() && !Knossos.inSingleTCMode) { allMods = ModVersions.ToList(); } @@ -327,167 +328,171 @@ private async Task ProcessMod(Mod mod, List allMods, List? processed = { if (processed == null) processed = new List(); - var dependencies = mod.GetMissingDependenciesList(false, true); + //load all dependencies mods in singletc mode so they can be modified too at any time + var dependencies = Knossos.inSingleTCMode ? mod.GetModDependencyList(false, true) : mod.GetMissingDependenciesList(false, true); //Display this mod on install list AddModToList(mod); //Add this mod here to avoid possible looping processed.Add(mod); - foreach (var dep in dependencies) + if (dependencies != null) { - var modDep = await dep.SelectModNebula(allMods); - if (modDep != null) + foreach (var dep in dependencies) { - //Is this dependecy mod already is installed? - var modInstalled = Knossos.GetInstalledMod(modDep.id, modDep.version); - - //Check Cache - var modInCache = modCache.FirstOrDefault(x => x.id == modDep.id && x.version == modDep.version); - if (modInCache != null) + var modDep = await dep.SelectModNebula(allMods); + if (modDep != null) { - modDep = modInCache; - } - else - { - //Load Nebula data first to check the packages and add to cache - await modDep.LoadFulLNebulaData(); - modCache.Add(modDep); - } + //Is this dependecy mod already is installed? + var modInstalled = Knossos.GetInstalledMod(modDep.id, modDep.version); - //If this is an engine build then check if contains valid executables - if (modDep.type == ModType.engine) - { - //Set a max amount of attempts to get an alternative version in case we need an alternative version - //This is because if user request "FSO" builds with an an incompatible cpu arch this is going to try - //with every FSO build in nebula that sastifies the dependency, incluiding nightlies. - var attempt = 0; - var maxAttempts = 10; - while (modDep != null && ++attempt < maxAttempts && modDep.packages.Any(x => FsoBuild.IsEnviromentStringValidInstall(x.environment)) == false) + //Check Cache + var modInCache = modCache.FirstOrDefault(x => x.id == modDep.id && x.version == modDep.version); + if (modInCache != null) { - //This build is not valid for this pc, delete from allmods list and resend to process - var remove = allMods.FirstOrDefault(x => x.id == modDep.id && x.version == modDep.version); - if (remove != null) + modDep = modInCache; + } + else + { + //Load Nebula data first to check the packages and add to cache + await modDep.LoadFulLNebulaData(); + modCache.Add(modDep); + } + + //If this is an engine build then check if contains valid executables + if (modDep.type == ModType.engine) + { + //Set a max amount of attempts to get an alternative version in case we need an alternative version + //This is because if user request "FSO" builds with an an incompatible cpu arch this is going to try + //with every FSO build in nebula that sastifies the dependency, incluiding nightlies. + var attempt = 0; + var maxAttempts = 10; + while (modDep != null && ++attempt < maxAttempts && modDep.packages.Any(x => FsoBuild.IsEnviromentStringValidInstall(x.environment)) == false) { - allMods.Remove(remove); - var alternativeVersion = await dep.SelectModNebula(allMods); - if (alternativeVersion != null) + //This build is not valid for this pc, delete from allmods list and resend to process + var remove = allMods.FirstOrDefault(x => x.id == modDep.id && x.version == modDep.version); + if (remove != null) { - //Check Cache - modInCache = modCache.FirstOrDefault(x => x.id == alternativeVersion.id && x.version == alternativeVersion.version); - if (modInCache != null) - { - alternativeVersion = modInCache; - } - else + allMods.Remove(remove); + var alternativeVersion = await dep.SelectModNebula(allMods); + if (alternativeVersion != null) { - //Load Nebula data first to check the packages and add to cache - await alternativeVersion.LoadFulLNebulaData(); - modCache.Add(alternativeVersion); + //Check Cache + modInCache = modCache.FirstOrDefault(x => x.id == alternativeVersion.id && x.version == alternativeVersion.version); + if (modInCache != null) + { + alternativeVersion = modInCache; + } + else + { + //Load Nebula data first to check the packages and add to cache + await alternativeVersion.LoadFulLNebulaData(); + modCache.Add(alternativeVersion); + } } + modDep = alternativeVersion; + } + else + { + //if for some reason we cant find modDep on allMods (it should never happen) we have to break or we are going to loop + break; } - modDep = alternativeVersion; - } - else - { - //if for some reason we cant find modDep on allMods (it should never happen) we have to break or we are going to loop - break; } + //if we cant find a alternative version in nebula, we have to skip the rest. + if (modDep == null || attempt == maxAttempts) + continue; } - //if we cant find a alternative version in nebula, we have to skip the rest. - if (modDep == null || attempt == maxAttempts) - continue; - } - //Make sure to mark all needed pkgs this mod need as required - modDep.isEnabled = true; - modDep.isSelected = true; + //Make sure to mark all needed pkgs this mod need as required + modDep.isEnabled = true; + modDep.isSelected = true; - foreach (var pkg in modDep.packages) - { - if (dep != null && dep.packages != null) + foreach (var pkg in modDep.packages) { - //Auto select needed packages and inform via tooltip, updating the foreground - var depPkg = dep.packages.FirstOrDefault(dp => dp == pkg.name); - if (depPkg != null && pkg.status != "required") + if (dep != null && dep.packages != null) { - pkg.isEnabled = true; - pkg.isSelected = true; - pkg.isRequired = true; - - var originalPkg = mod.FindPackageWithDependency(dep.originalDependency); - if(originalPkg != null) + //Auto select needed packages and inform via tooltip, updating the foreground + var depPkg = dep.packages.FirstOrDefault(dp => dp == pkg.name); + if (depPkg != null && pkg.status != "required") { - if (!pkg.tooltip.Contains(mod + "\nPKG: " + originalPkg.name)) + pkg.isEnabled = true; + pkg.isSelected = true; + pkg.isRequired = true; + + var originalPkg = mod.FindPackageWithDependency(dep.originalDependency); + if (originalPkg != null) + { + if (!pkg.tooltip.Contains(mod + "\nPKG: " + originalPkg.name)) + { + pkg.tooltip += "\n\nRequired by MOD: " + mod + "\nPKG: " + originalPkg.name; + } + } + else { - pkg.tooltip += "\n\nRequired by MOD: " + mod + "\nPKG: " + originalPkg.name; + if (!pkg.tooltip.Contains(mod.ToString())) + { + pkg.tooltip += "\n\nRequired by MOD: " + mod; + } } } else { - if (!pkg.tooltip.Contains(mod.ToString())) + switch (pkg.status) { - pkg.tooltip += "\n\nRequired by MOD: " + mod; + case "required": + pkg.isEnabled = false; + pkg.isSelected = true; + break; + case "recommended": + pkg.isSelected = true; + break; + case "optional": + //No need to do anything here + break; } } } - else - { - switch(pkg.status) - { - case "required": - pkg.isEnabled = false; - pkg.isSelected = true; - break; - case "recommended": - pkg.isSelected = true; - break; - case "optional": - //No need to do anything here - break; - } - } - } - //If mod is already installed, non-installed pkgs are all unselected - //and all installed ones are selected - if (modInstalled != null && modInstalled.packages != null) - { - var packageIsInstalled = modInstalled.packages.FirstOrDefault(m => m.name == pkg.name); - if (packageIsInstalled != null) + //If mod is already installed, non-installed pkgs are all unselected + //and all installed ones are selected + if (modInstalled != null && modInstalled.packages != null) { - //Pkg is installed - if (pkg.status == "required") + var packageIsInstalled = modInstalled.packages.FirstOrDefault(m => m.name == pkg.name); + if (packageIsInstalled != null) { - pkg.isEnabled = false; + //Pkg is installed + if (pkg.status == "required") + { + pkg.isEnabled = false; + } + else + { + pkg.isEnabled = true; + } + pkg.isSelected = true; } else { - pkg.isEnabled = true; - } - pkg.isSelected = true; - } - else - { - //Pkg is not installed - //ONLY if the currently selected mod is also installed - //For new mod or new mod version installs only if the package is not needed - if (IsInstalled || !IsInstalled && !pkg.isRequired) - { - pkg.isEnabled = true; - pkg.isSelected = false; + //Pkg is not installed + //ONLY if the currently selected mod is also installed + //For new mod or new mod version installs only if the package is not needed + if (IsInstalled || !IsInstalled && !pkg.isRequired) + { + pkg.isEnabled = true; + pkg.isSelected = false; + } } } } - } - //If process this depmod own dependencies if we havent done already - //Otherwise re-add it to the list to enabled any potential new pkg needed - if (processed.IndexOf(modDep) == -1) - { - await ProcessMod(modDep, allMods, processed); - } - else - { - AddModToList(modDep); + //If process this depmod own dependencies if we havent done already + //Otherwise re-add it to the list to enabled any potential new pkg needed + if (processed.IndexOf(modDep) == -1) + { + await ProcessMod(modDep, allMods, processed); + } + else + { + AddModToList(modDep); + } } } } @@ -564,6 +569,8 @@ internal void VerifyCommand() /// internal void InstallMod() { + if(Knossos.inSingleTCMode) + TaskViewModel.Instance?.CleanCommand(); foreach (var mod in ModInstallList) { var cleanOldVersions = false; diff --git a/Knossos.NET/Views/CustomHomeView.axaml b/Knossos.NET/Views/CustomHomeView.axaml index 149ef3c6..65d435b3 100644 --- a/Knossos.NET/Views/CustomHomeView.axaml +++ b/Knossos.NET/Views/CustomHomeView.axaml @@ -77,6 +77,9 @@ + diff --git a/Knossos.NET/Views/TaskView.axaml b/Knossos.NET/Views/TaskView.axaml index c33d3e0d..5c4eb4bc 100644 --- a/Knossos.NET/Views/TaskView.axaml +++ b/Knossos.NET/Views/TaskView.axaml @@ -16,7 +16,7 @@ - From 0b95abc109788e817f735c9a4101993c656c6040 Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Fri, 17 Jan 2025 19:44:16 -0300 Subject: [PATCH 32/44] Properly report the correct knet data folder for portable AND custom mode --- Knossos.NET/Classes/KnUtils.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Knossos.NET/Classes/KnUtils.cs b/Knossos.NET/Classes/KnUtils.cs index 181213e3..ca9b760f 100644 --- a/Knossos.NET/Classes/KnUtils.cs +++ b/Knossos.NET/Classes/KnUtils.cs @@ -120,20 +120,22 @@ public static string? KnetFolderPath /// fullpath as a string public static string GetKnossosDataFolderPath() { - var path = string.Empty; if (!Knossos.inPortableMode) { - path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "KnossosNET"); + if (CustomLauncher.IsCustomMode) + { + //In custom mode store config files inside modid a subfolder + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "KnossosNET", CustomLauncher.ModID!); + } + else + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "KnossosNET"); + } } else { - path = Path.Combine(KnetFolderPath!, "kn_portable", "KnossosNET"); //If inPortableMode = true, KnetFolderPath is not null - } - if(CustomLauncher.IsCustomMode) - { - path = Path.Combine(path, CustomLauncher.ModID!); + return Path.Combine(KnetFolderPath!, "kn_portable", "KnossosNET"); //If inPortableMode = true, KnetFolderPath is not null } - return path; } /// From 2723ea0523a541778e6a6b89b9fe0ea3abe81184 Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Fri, 17 Jan 2025 22:22:11 -0300 Subject: [PATCH 33/44] Custom home screen part 5 --- .../Converters/TextFileToStringConverter.cs | 64 +++++++ Knossos.NET/Models/CustomLauncher.cs | 14 ++ Knossos.NET/ViewModels/CustomHomeViewModel.cs | 21 +++ Knossos.NET/Views/CustomHomeView.axaml | 175 ++++++++++-------- Knossos.NET/Views/Windows/MainWindow.axaml | 2 + 5 files changed, 195 insertions(+), 81 deletions(-) create mode 100644 Knossos.NET/Converters/TextFileToStringConverter.cs diff --git a/Knossos.NET/Converters/TextFileToStringConverter.cs b/Knossos.NET/Converters/TextFileToStringConverter.cs new file mode 100644 index 00000000..909a7050 --- /dev/null +++ b/Knossos.NET/Converters/TextFileToStringConverter.cs @@ -0,0 +1,64 @@ +using Avalonia.Data.Converters; +using System; +using System.Globalization; +using Avalonia.Platform; +using System.IO; +using System.Text; + +namespace Knossos.NET.Converters +{ + public class TextFileToStringConverter : IValueConverter + { + public static TextFileToStringConverter Instance { get; } = new(); + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + try + { + if (value == null) return null; + + if (value is not string rawUri || !targetType.IsAssignableFrom(typeof(String))) + { + throw new NotSupportedException(); + } + + Uri uri; + + if (rawUri.StartsWith("avares://")) + { + uri = new Uri(rawUri); + var asset = AssetLoader.Open(uri); + if (asset != null) + { + using (var reader = new StreamReader(asset, Encoding.UTF8)) + { + return reader.ReadToEnd(); + } + } + } + else if(rawUri.ToLower().StartsWith("http")) + { + return null; + } + else if (File.Exists(Path.Combine(KnUtils.GetKnossosDataFolderPath(), rawUri))) + { + return File.ReadAllText(Path.Combine(KnUtils.GetKnossosDataFolderPath(), rawUri)); + } + else if (File.Exists(rawUri)) + { + return File.ReadAllText(rawUri); + } + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "BitmapAssetValueConverter.Convert()", ex); + } + return null; + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/Knossos.NET/Models/CustomLauncher.cs b/Knossos.NET/Models/CustomLauncher.cs index fd46475e..685e1754 100644 --- a/Knossos.NET/Models/CustomLauncher.cs +++ b/Knossos.NET/Models/CustomLauncher.cs @@ -47,12 +47,14 @@ public static class CustomLauncher /// /// Starting width size of the launcher window + /// This is also the min width /// null for auto /// public static int? WindowWidth { get; private set; } = 1024; /// /// Starting height size of the launcher window + /// This is also the min height /// null for auto /// public static int? WindowHeight { get; private set; } = 540; @@ -108,6 +110,7 @@ public static class CustomLauncher /// /// Path to the background image for the home view + /// It is recommended this image to be about 200px less in width than the starting WindowWidth /// Supports local image in the Knet data folder, a local full path, harcoded image or remote https:// URL /// Supports APNGs, GIF, PNG and JPG /// Examples: @@ -120,6 +123,13 @@ public static class CustomLauncher /// public static string? HomeBackgroundImage { get; private set; } = "avares://Knossos.NET/Assets/general/custom_home_background.jpg"; + /// + /// Set a path to the welcome HTML message on home screen + /// Uses the same path rules as HomeBackgroundImage + /// null to disable or put a path to a empty file if you want to display it at some point + /// + public static string? HomeWelcomeHtml { get; private set; } = null; + /// /// Call this AFTER checking if we are in portable mode or not. /// The first time it runs it will try to load the "custom_launcher.json" if ModID is null @@ -204,6 +214,9 @@ private static void ReadCustomFile() if (customData.HomeBackgroundImage != null) HomeBackgroundImage = customData.HomeBackgroundImage; + if (customData.HomeWelcomeHtml != null) + HomeWelcomeHtml = customData.HomeWelcomeHtml; + jsonFile.Close(); } } @@ -230,6 +243,7 @@ struct CustomFileData public bool? UseNebulaServices { get; set; } public bool? WriteLogFile { get; set; } public string? HomeBackgroundImage { get; set; } + public string? HomeWelcomeHtml { get; set; } } } } diff --git a/Knossos.NET/ViewModels/CustomHomeViewModel.cs b/Knossos.NET/ViewModels/CustomHomeViewModel.cs index 48ed7a57..326ae240 100644 --- a/Knossos.NET/ViewModels/CustomHomeViewModel.cs +++ b/Knossos.NET/ViewModels/CustomHomeViewModel.cs @@ -62,6 +62,12 @@ private Mod? GetActiveInstalledModVersion [ObservableProperty] internal bool nebulaServices = CustomLauncher.UseNebulaServices; + [ObservableProperty] + internal bool welcomeVisible = false; + + [ObservableProperty] + internal string? welcomeHtml = CustomLauncher.HomeWelcomeHtml; + /// /// Handled in mainview, displays a small task viewer in the home screen /// @@ -423,6 +429,21 @@ public void ViewOpened() }); }); } + //download remote WelcomeHTML + if (WelcomeHtml != null && WelcomeHtml.ToLower().StartsWith("http")) + { + _ = Task.Factory.StartNew(async () => + { + var temp = WelcomeHtml; + WelcomeHtml = ""; + var htmlFile = await KnUtils.GetRemoteResource(temp).ConfigureAwait(false); + Dispatcher.UIThread.Invoke(() => + { + if (htmlFile != null) + WelcomeHtml = htmlFile; + }); + }); + } } /// diff --git a/Knossos.NET/Views/CustomHomeView.axaml b/Knossos.NET/Views/CustomHomeView.axaml index 65d435b3..a640c92e 100644 --- a/Knossos.NET/Views/CustomHomeView.axaml +++ b/Knossos.NET/Views/CustomHomeView.axaml @@ -8,7 +8,9 @@ xmlns:vm="using:Knossos.NET.ViewModels" xmlns:anim="https://github.com/whistyun/AnimatedImage.Avalonia" x:DataType="vm:CustomHomeViewModel" + Name="CustomTCView" Background="{StaticResource BackgroundColorPrimary}" + xmlns:HtmlRenderer="clr-namespace:TheArtOfDev.HtmlRenderer.Avalonia;assembly=Avalonia.HtmlRenderer" xmlns:cvt="clr-namespace:Knossos.NET.Converters;assembly=Knossos.NET"> @@ -16,97 +18,108 @@ + - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + - + \ No newline at end of file diff --git a/Knossos.NET/Views/Windows/MainWindow.axaml b/Knossos.NET/Views/Windows/MainWindow.axaml index bcf83f98..0cae4c62 100644 --- a/Knossos.NET/Views/Windows/MainWindow.axaml +++ b/Knossos.NET/Views/Windows/MainWindow.axaml @@ -6,6 +6,8 @@ xmlns:v="using:Knossos.NET.Views" Width="{Binding WindowWidth}" Height="{Binding WindowHeight}" + MinWidth="{Binding WindowWidth}" + MinHeight="{Binding WindowHeight}" x:DataType="vm:MainWindowViewModel" mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="900" x:Class="Knossos.NET.Views.MainWindow" From 2223e0d14372306b05e8e069608b4edc1c146ef4 Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Sat, 18 Jan 2025 19:47:21 -0300 Subject: [PATCH 34/44] Custom home screen part 6 --- Knossos.NET/Assets/general/discordicon.png | Bin 2859 -> 2590 bytes .../Converters/BitmapAssetValueConverter.cs | 10 +- .../Converters/TextFileToStringConverter.cs | 11 +- Knossos.NET/Models/CustomLauncher.cs | 40 ++++-- Knossos.NET/ViewModels/CustomHomeViewModel.cs | 119 +++++++++++++----- .../ViewModels/Windows/MainWindowViewModel.cs | 1 + Knossos.NET/Views/CustomHomeView.axaml | 38 +++++- Knossos.NET/Views/CustomHomeView.axaml.cs | 66 +++++++++- 8 files changed, 227 insertions(+), 58 deletions(-) diff --git a/Knossos.NET/Assets/general/discordicon.png b/Knossos.NET/Assets/general/discordicon.png index 85c58e06edeeb98000e2d255bd0069cec563e307..fb3659cdca0cae12583e17f2921ade55cd55e829 100644 GIT binary patch literal 2590 zcmV+(3gPvMP)GJaO<>lovGBRpvYUAVM9UUF^_V!<2UpYBBLqkJXS64|%N!;Ar;Nala*482- zBJ1nxDk>_xyu6ZA#!cf-TOhK7b48ymN`w;LN9 zf`Wo5C@7ein2U>x%*@Qdz`(1ktD&Kx`1ttH(9oToozv6Pe0+TW|Nl?OinRa$05Nn@ zPE!Ex>GI_-Y2pa=T{k~fN88}l9P1~$kWbmMaNftJcEW`aw-kUQmx{~3s-XAJoYH#} z(cIVo00{<3L_t(|+U(nHbD}U10N~w_fV>F;ii)(pRJ~fOxBvgo-Ss**=e4L1VoSj1 zGn1LjCQEh+aCdiicX!tT5UG^FgO7TPXq^ZlT=82HtrqZ!7f>l2a}U12 z$_!#EEq|Y8Pe_f&A3!0uvAOzGEI^$hX<-7&m|6sx=;(kVkp>(2 zU9bW*UPu?X>B=klAMSz*kED@hmvYy;%z@fgTFsa!>_J^~(hF{(CZSo7iEs_|>_|HY zsHGo}cDUPx!?nVrwx5xPfYSCSSjay)N_yDt~%)TTnKhVqK{y{$VLx z^GYp~cWqr0VXtHBnkY!Sm9I;zY|)bDK!uz=ZQ+!&e%ssE6&u>Cm|AEI7x=9zDgNT( zw*3&>Ks0QFRIp|CZ$DA()JD%NMn+KJ{0hcTNKTK z@(aO9fK=63Kp|m~tO1;nNRJt;;f%!?shU9qXG8&#oNNsMN|z2x;H*VfF~$*`6o-K*$oMB6kG-C=q-PVtj$m@<52k zNx~@7G+=W8ZNn^rAfF^;4xEcg27(}0ZH`i(rHVD2o0^Tx=u4JFWThc{Kr*-oVo={h z;&DwYh(4JdzC5n)>zm6(wmdk)j8^7!xW4WfHj<3I3#UAv^zSMf5u-1$2!!-V%07Wp zT_h=M5ON>^3lOr_)c-6YIUCS-6?S5@{7WwkxV#A!xsmRBo&e}_TzHf zt?vD0k=Wg?oj6Ufk5x$M!H_zF-w+WqXoEiKU4!DN) zNU73wxnyOzWLReHl4ei9)bF$mky_)cc{3O|dke`NcV92zpZSXG=phs3GNBjH2&W@f zkO?U(BWO6B+RG!jFxwT%w-J)k%}{q<9QQocelJBigKGjV5t`=?981>y(TI^AmFuof z@mt`w85^iERVGjpZilnOVP8JcsX<%O_@T3bL8zPel-LK))ESYW!hVpSS zFXIlJ3gqTvHiS=w6_-46=)gtndXTwR%BT&@Q+%0T#Dz|cB*Usg_)5)~Wc#KFE-icY zE@!EQ1$`;+Tm83bV8f+oz)1?LK|m;bHv{tnT;1$cX;T*1Va+C~Qg&@Y^NK$$2SKcIA3{7&<``c4zP@U;YA~u;thy z85J0@CmHTr7ux9MT<`VOTmM=v<}tKYBP!OY=(Al`H~7lC)*uaTnx7 zRh!WG1B@B4R^xO65+s(i%m!mZi$qy@0BW2O7rs(#P>Cdjj~r0u4K1P2Yz_u>P;fo% zLBnup?tmio-wC#wz{)%)<7QVO>NUJPb3D*0*!%PeD+0q3MxS6m1JJKGL8D`rLZ?DD z>46#H0FkarJFNwvLVR%ye{^j;8;>nDx7Jj2F847b&BD*h)mmT)s;{5K~#9rzFIdXGqydvG&z z_U8=Q25IW4H}4VLQ1YJFHatB&Jv}`=J$+Zd0B;d&2lM16cK`qY07*qoM6N<$f+v#T AhyVZp literal 2859 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 KnUtils.GetRemoteResource(rawUri)).Result; + if (localPath != null) + { + return new Bitmap(localPath); + } } else if (File.Exists(Path.Combine(KnUtils.GetKnossosDataFolderPath(), rawUri))) { diff --git a/Knossos.NET/Converters/TextFileToStringConverter.cs b/Knossos.NET/Converters/TextFileToStringConverter.cs index 909a7050..b6ad7f99 100644 --- a/Knossos.NET/Converters/TextFileToStringConverter.cs +++ b/Knossos.NET/Converters/TextFileToStringConverter.cs @@ -4,6 +4,7 @@ using Avalonia.Platform; using System.IO; using System.Text; +using System.Threading.Tasks; namespace Knossos.NET.Converters { @@ -11,7 +12,7 @@ public class TextFileToStringConverter : IValueConverter { public static TextFileToStringConverter Instance { get; } = new(); - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo? culture) { try { @@ -36,9 +37,13 @@ public class TextFileToStringConverter : IValueConverter } } } - else if(rawUri.ToLower().StartsWith("http")) + else if (rawUri.ToLower().StartsWith("http")) { - return null; + var localPath = Task.Run(() => KnUtils.GetRemoteResource(rawUri)).Result; + if (localPath != null) + { + return File.ReadAllText(localPath); + } } else if (File.Exists(Path.Combine(KnUtils.GetKnossosDataFolderPath(), rawUri))) { diff --git a/Knossos.NET/Models/CustomLauncher.cs b/Knossos.NET/Models/CustomLauncher.cs index 685e1754..e0fb2498 100644 --- a/Knossos.NET/Models/CustomLauncher.cs +++ b/Knossos.NET/Models/CustomLauncher.cs @@ -1,17 +1,19 @@ -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; +using System; using System.IO; -using System.Linq; -using System.Text; using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; -using static Knossos.NET.ViewModels.MainWindowViewModel; namespace Knossos.NET.Models { + /// + /// Data struct for the custom mode dynamic link button + /// + public struct LinkButton + { + public string ToolTip { get; set; } + public string IconPath { get; set; } + public string LinkURL { get; set; } + } + /// /// Class to handle the configuration options and optional save file of the SingleTC mode /// @@ -130,6 +132,19 @@ public static class CustomLauncher /// public static string? HomeWelcomeHtml { get; private set; } = null; + /// + /// Thickness string to use as margin for the WelcomeHTML display + /// left, up, right, down + /// + public static string? HomeWelcomeMargin { get; private set; } = "50,50,50,0"; + + /// + /// Optional Link buttons that are displayed in the home screen that + /// if clicked opens a external web link in user browser + /// Icon path follows the same rules as HomeBackgroundImage, so URL, embedded and local images are supported. + /// + public static LinkButton[]? HomeLinkButtons { get; private set; } + /// /// Call this AFTER checking if we are in portable mode or not. /// The first time it runs it will try to load the "custom_launcher.json" if ModID is null @@ -217,6 +232,11 @@ private static void ReadCustomFile() if (customData.HomeWelcomeHtml != null) HomeWelcomeHtml = customData.HomeWelcomeHtml; + if (customData.HomeWelcomeMargin != null) + HomeWelcomeMargin = customData.HomeWelcomeMargin; + + HomeLinkButtons = customData.HomeLinkButtons; + jsonFile.Close(); } } @@ -244,6 +264,8 @@ struct CustomFileData public bool? WriteLogFile { get; set; } public string? HomeBackgroundImage { get; set; } public string? HomeWelcomeHtml { get; set; } + public string? HomeWelcomeMargin { get; set; } + public LinkButton[]? HomeLinkButtons { get; set; } } } } diff --git a/Knossos.NET/ViewModels/CustomHomeViewModel.cs b/Knossos.NET/ViewModels/CustomHomeViewModel.cs index 326ae240..e0638954 100644 --- a/Knossos.NET/ViewModels/CustomHomeViewModel.cs +++ b/Knossos.NET/ViewModels/CustomHomeViewModel.cs @@ -1,4 +1,6 @@ -using Avalonia.Threading; +using Avalonia; +using Avalonia.Platform.Storage; +using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using Knossos.NET.Classes; using Knossos.NET.Models; @@ -10,7 +12,6 @@ using System.IO; using System.Linq; using System.Threading; -using System.Threading.Tasks; namespace Knossos.NET.ViewModels { @@ -63,10 +64,19 @@ private Mod? GetActiveInstalledModVersion internal bool nebulaServices = CustomLauncher.UseNebulaServices; [ObservableProperty] - internal bool welcomeVisible = false; + internal string? welcomeHtml = CustomLauncher.HomeWelcomeHtml; [ObservableProperty] - internal string? welcomeHtml = CustomLauncher.HomeWelcomeHtml; + internal Thickness welcomeMargin = new Thickness(50, 50, 50, 0); + + [ObservableProperty] + internal bool showBasePathSelector = false; + + [ObservableProperty] + internal string newBasePath = string.Empty; + + [ObservableProperty] + internal bool changeBasePathButtonVisible = false; /// /// Handled in mainview, displays a small task viewer in the home screen @@ -76,6 +86,28 @@ private Mod? GetActiveInstalledModVersion public CustomHomeViewModel() { + if (CustomLauncher.HomeWelcomeMargin != null) + { + try + { + WelcomeMargin = Thickness.Parse(CustomLauncher.HomeWelcomeMargin); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.Constructor()", ex); + } + } + } + + /// + /// Check if we are in normal mode, but we dont have a saved base path + /// + public void CheckBasePath() + { + if (!Knossos.inPortableMode && Knossos.GetKnossosLibraryPath() == null) + { + ShowBasePathSelector = true; + } } /// @@ -413,45 +445,64 @@ public void UpdateIsAvailable(string id, bool value, string? newVersion) public void ViewOpened() { Animate = 1; + } + + /// + /// Run code when the user exit this view + /// + public void ViewClosed() + { + Animate = 0; + } - //download remote image if we have to - if (BackgroundImage != null && BackgroundImage.ToLower().StartsWith("http")) + /// + /// Changes the knossos library path, reloads settings and nebula repo + /// + internal async void BrowseFolderCommand() + { + if (MainWindow.instance != null) { - _ = Task.Factory.StartNew(async () => + ChangeBasePathButtonVisible = false; + NewBasePath = string.Empty; + FolderPickerOpenOptions options = new FolderPickerOpenOptions(); + options.AllowMultiple = false; + + var result = await MainWindow.instance.StorageProvider.OpenFolderPickerAsync(options); + + try { - var temp = BackgroundImage; - BackgroundImage = ""; - var imageFile = await KnUtils.GetRemoteResource(temp).ConfigureAwait(false); - Dispatcher.UIThread.Invoke(() => + if (result != null && result.Count > 0) { - if (imageFile != null) - BackgroundImage = imageFile; - }); - }); - } - //download remote WelcomeHTML - if (WelcomeHtml != null && WelcomeHtml.ToLower().StartsWith("http")) - { - _ = Task.Factory.StartNew(async () => + + // Test if we can write to the new library directory + using (StreamWriter writer = new StreamWriter(result[0].Path.LocalPath.ToString() + Path.DirectorySeparatorChar + "test.txt")) + { + writer.WriteLine("test"); + } + File.Delete(Path.Combine(result[0].Path.LocalPath.ToString() + Path.DirectorySeparatorChar + "test.txt")); + NewBasePath = result[0].Path.LocalPath.ToString(); + ChangeBasePathButtonVisible = true; + } + } + catch (Exception ex) { - var temp = WelcomeHtml; - WelcomeHtml = ""; - var htmlFile = await KnUtils.GetRemoteResource(temp).ConfigureAwait(false); - Dispatcher.UIThread.Invoke(() => - { - if (htmlFile != null) - WelcomeHtml = htmlFile; - }); - }); + Log.Add(Log.LogSeverity.Error, "CustomHomeViewModel.BrowseFolderCommand() - test read/write was not successful: ", ex); + await Dispatcher.UIThread.Invoke(async () => { + await MessageBox.Show(null, "We were not able to write to this folder. Please select another library folder.", "Cannot Select Folder", MessageBox.MessageBoxButtons.OK); + }).ConfigureAwait(false); + } } } - /// - /// Run code when the user exit this view - /// - public void ViewClosed() + internal void ChangeBasePath() { - Animate = 0; + if (NewBasePath != string.Empty) + { + Knossos.globalSettings.basePath = NewBasePath; + Knossos.globalSettings.Save(); + Knossos.ResetBasePath(); + ShowBasePathSelector = false; + } } } } diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index 9e8f30e6..3f630734 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -147,6 +147,7 @@ public MainWindowViewModel() FillMenuItemsCustomMode(1); } Knossos.StartUp(isQuickLaunch, forceUpdate); + CustomHomeVM?.CheckBasePath(); } private void FillMenuItemsCustomMode(int defaultSelectedIndex) diff --git a/Knossos.NET/Views/CustomHomeView.axaml b/Knossos.NET/Views/CustomHomeView.axaml index a640c92e..e5ce6048 100644 --- a/Knossos.NET/Views/CustomHomeView.axaml +++ b/Knossos.NET/Views/CustomHomeView.axaml @@ -28,16 +28,42 @@ Source="{Binding BackgroundImage, Converter={StaticResource imageConverter}}" anim:ImageBehavior.AnimatedSource="{Binding BackgroundImage}" /> - - - - - + + + + + + + + + + + + + + + + + + + + + - + diff --git a/Knossos.NET/Views/CustomHomeView.axaml.cs b/Knossos.NET/Views/CustomHomeView.axaml.cs index 31e306a2..9a08eaed 100644 --- a/Knossos.NET/Views/CustomHomeView.axaml.cs +++ b/Knossos.NET/Views/CustomHomeView.axaml.cs @@ -1,13 +1,75 @@ -using Avalonia; using Avalonia.Controls; -using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; +using Knossos.NET.Converters; +using Knossos.NET.Models; +using System; +using System.Collections.Generic; +using System.Linq; namespace Knossos.NET.Views; public partial class CustomHomeView : UserControl { + internal List buttonUrls = new List(); + public CustomHomeView() { InitializeComponent(); + + //Generate Link Buttons + try + { + if (CustomLauncher.HomeLinkButtons != null && CustomLauncher.HomeLinkButtons.Any()) + { + var buttonPanel = this.FindControl("LinkButtons")!; + if (buttonPanel != null) + { + int index = 0; + foreach (var b in CustomLauncher.HomeLinkButtons) + { + var linkButton = new Button { Tag = index, Name = b.ToolTip }; + if (b.IconPath != null) + { + var converter = new BitmapAssetValueConverter(); + var bitmap = converter.Convert(b.IconPath, typeof(Bitmap), null, null); + if(bitmap != null) + { + linkButton.Content = new Image { Source = (Bitmap)bitmap, Width = 30, Height = 30 }; + } + } + + linkButton.Click += (_, __) => + { + //This code runs when the button is clicked + try + { + if(linkButton.Tag != null) + { + var url = buttonUrls[(int)linkButton.Tag]; + KnUtils.OpenBrowserURL(url); + } + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "CustomHomeView.Constructor(LinkButtonClick)", ex); + } + }; + + buttonPanel.Children.Add(linkButton); + index++; + buttonUrls.Add(b.LinkURL); + ToolTip.SetTip(linkButton, b.ToolTip); + } + } + else + { + Log.Add(Log.LogSeverity.Error, "CustomHomeView.Constructor()", "Unable to find LinkButtons panel."); + } + } + } + catch(Exception ex) + { + Log.Add(Log.LogSeverity.Error, "CustomHomeView.Constructor()", ex); + } } } \ No newline at end of file From 4c9c7e7e46c6cc610a23e3687213ec3d86a49c67 Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Sat, 18 Jan 2025 20:24:47 -0300 Subject: [PATCH 35/44] Enable install only if we have nebula data --- Knossos.NET/Views/CustomHomeView.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Knossos.NET/Views/CustomHomeView.axaml b/Knossos.NET/Views/CustomHomeView.axaml index e5ce6048..33972ee3 100644 --- a/Knossos.NET/Views/CustomHomeView.axaml +++ b/Knossos.NET/Views/CustomHomeView.axaml @@ -99,7 +99,7 @@ - + public static bool MenuDisplayGlobalSettingsEntry { get; private set; } = false; + /// + /// Add custom buttons to the menu + /// + public static CustomMenuButton[]? CustomMenuButtons { get; private set; } + /// /// Yet another cmdline option, pass it as a string array. /// It has the lowest priority, same options can be overriden by mod cmdline. @@ -218,6 +232,8 @@ private static void ReadCustomFile() if (customData.MenuDisplayGlobalSettingsEntry.HasValue) MenuDisplayGlobalSettingsEntry = customData.MenuDisplayGlobalSettingsEntry.Value; + CustomMenuButtons = customData.CustomMenuButtons; + CustomCmdlineArray = customData.CustomCmdlineArray; if (customData.UseNebulaServices.HasValue) @@ -266,6 +282,7 @@ struct CustomFileData public string? HomeWelcomeHtml { get; set; } public string? HomeWelcomeMargin { get; set; } public LinkButton[]? HomeLinkButtons { get; set; } + public CustomMenuButton[]? CustomMenuButtons { get; set; } } } } diff --git a/Knossos.NET/ViewModels/Templates/HtmlContentViewModel.cs b/Knossos.NET/ViewModels/Templates/HtmlContentViewModel.cs new file mode 100644 index 00000000..78d1aca8 --- /dev/null +++ b/Knossos.NET/ViewModels/Templates/HtmlContentViewModel.cs @@ -0,0 +1,30 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Knossos.NET.ViewModels +{ + public partial class HtmlContentViewModel : ViewModelBase + { + [ObservableProperty] + internal string? htmlData = null; + + private string? savedHtmlData = null; + + public HtmlContentViewModel() + { + } + + public HtmlContentViewModel(string htmlData) + { + savedHtmlData = htmlData; + } + + /// + /// Loads the HTML content to the view + /// + public void Navigate() + { + if(savedHtmlData != null) + HtmlData = savedHtmlData; + } + } +} diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index 3f630734..21ada628 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -159,6 +159,28 @@ private void FillMenuItemsCustomMode(int defaultSelectedIndex) MenuItems.Add(new MainViewMenuItem(CustomHomeVM!, "avares://Knossos.NET/Assets/general/menu_home.png", "Home", "Home")); + if(CustomLauncher.CustomMenuButtons != null && CustomLauncher.CustomMenuButtons.Any()) + { + foreach(var button in CustomLauncher.CustomMenuButtons) + { + try + { + switch (button.Type.ToLower()) + { + case "htmlcontent" : + MenuItems.Add(new MainViewMenuItem(new HtmlContentViewModel(button.LinkURL), button.IconPath, button.Name, button.ToolTip)); + break; + default: + throw new NotImplementedException("button type: "+ button.Type + " is not supported."); + } + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "MainWindowViewModel.FillMenuItemsCustomMode()", ex); + } + } + } + if (CustomLauncher.MenuDisplayEngineEntry && FsoBuildsView != null) { MenuItems.Add(new MainViewMenuItem(FsoBuildsView, "avares://Knossos.NET/Assets/general/menu_engine.png", "Engine", "Download new Freespace Open engine builds")); @@ -281,6 +303,15 @@ partial void OnSelectedMenuItemChanged(MainViewMenuItem? value) { //Knossos.globalSettings.DisableIniWatch(); } + + //Custom Views + if (CurrentViewModel != null && CustomLauncher.IsCustomMode) + { + if (CurrentViewModel.GetType() == typeof(HtmlContentViewModel)) + { + ((HtmlContentViewModel)CurrentViewModel).Navigate(); + } + } } } diff --git a/Knossos.NET/Views/Templates/HtmlContentView.axaml b/Knossos.NET/Views/Templates/HtmlContentView.axaml new file mode 100644 index 00000000..e08545a1 --- /dev/null +++ b/Knossos.NET/Views/Templates/HtmlContentView.axaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + diff --git a/Knossos.NET/Views/Templates/HtmlContentView.axaml.cs b/Knossos.NET/Views/Templates/HtmlContentView.axaml.cs new file mode 100644 index 00000000..0c5724ce --- /dev/null +++ b/Knossos.NET/Views/Templates/HtmlContentView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Knossos.NET.Views; + +public partial class HtmlContentView : UserControl +{ + public HtmlContentView() + { + InitializeComponent(); + } +} \ No newline at end of file From 7fe46071c7b76978a40afe1bfd27ac0d81a4899a Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Sun, 19 Jan 2025 11:44:34 -0300 Subject: [PATCH 37/44] Add optional stock community menu item --- Knossos.NET/Models/CustomLauncher.cs | 9 +++++++++ Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/Knossos.NET/Models/CustomLauncher.cs b/Knossos.NET/Models/CustomLauncher.cs index 9f512141..c307f706 100644 --- a/Knossos.NET/Models/CustomLauncher.cs +++ b/Knossos.NET/Models/CustomLauncher.cs @@ -94,6 +94,11 @@ public static class CustomLauncher /// public static bool MenuDisplayNebulaLoginEntry { get; private set; } = false; + /// + /// Display the regular Knossos community menu item + /// + public static bool MenuDisplayCommunityEntry { get; private set; } = false; + /// /// Display the regular Knossos settings menu item /// If you do this you may want to add "-no_ingame_options" to the custom cmdline @@ -232,6 +237,9 @@ private static void ReadCustomFile() if (customData.MenuDisplayGlobalSettingsEntry.HasValue) MenuDisplayGlobalSettingsEntry = customData.MenuDisplayGlobalSettingsEntry.Value; + if (customData.MenuDisplayCommunityEntry.HasValue) + MenuDisplayCommunityEntry = customData.MenuDisplayCommunityEntry.Value; + CustomMenuButtons = customData.CustomMenuButtons; CustomCmdlineArray = customData.CustomCmdlineArray; @@ -275,6 +283,7 @@ struct CustomFileData public bool? MenuDisplayDebugEntry { get; set; } public bool? MenuDisplayNebulaLoginEntry { get; set; } public bool? MenuDisplayGlobalSettingsEntry { get; set; } + public bool? MenuDisplayCommunityEntry { get; set; } public string[]? CustomCmdlineArray { get; set; } public bool? UseNebulaServices { get; set; } public bool? WriteLogFile { get; set; } diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index 21ada628..cbf51906 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -142,6 +142,8 @@ public MainWindowViewModel() GlobalSettingsView = new GlobalSettingsViewModel(); if(CustomLauncher.MenuDisplayDebugEntry) DebugView = new DebugViewModel(); + if (CustomLauncher.MenuDisplayCommunityEntry) + CommunityView = new CommunityViewModel(); if (CustomLauncher.MenuDisplayNebulaLoginEntry) NebulaLoginVM = new NebulaLoginViewModel(); FillMenuItemsCustomMode(1); @@ -191,6 +193,11 @@ private void FillMenuItemsCustomMode(int defaultSelectedIndex) MenuItems.Add(new MainViewMenuItem(NebulaLoginVM, "avares://Knossos.NET/Assets/general/menu_nebula.png", "Nebula", "Log in with your nebula account")); } + if (CustomLauncher.MenuDisplayCommunityEntry && CommunityView != null) + { + MenuItems.Add(new MainViewMenuItem(CommunityView!, "avares://Knossos.NET/Assets/general/menu_community.png", "Community", "FAQs and Community Resources")); + } + if (CustomLauncher.MenuDisplayGlobalSettingsEntry && GlobalSettingsView != null) { MenuItems.Add(new MainViewMenuItem(GlobalSettingsView, "avares://Knossos.NET/Assets/general/menu_settings.png", "Config", "Change launcher and FSO engine settings")); From 7f64e23cd8cc2464b7a0e2caaeb576b9aa0def0f Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Sun, 19 Jan 2025 12:15:45 -0300 Subject: [PATCH 38/44] crash fix and wrong folder creation --- Knossos.NET/Classes/Knossos.cs | 9 +-------- Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs | 4 ++-- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/Knossos.NET/Classes/Knossos.cs b/Knossos.NET/Classes/Knossos.cs index 536b6a7a..dd7ea3aa 100644 --- a/Knossos.NET/Classes/Knossos.cs +++ b/Knossos.NET/Classes/Knossos.cs @@ -117,14 +117,7 @@ public static async void StartUp(bool isQuickLaunch, bool forceUpdate) Log.Add(Log.LogSeverity.Information, "Knossos.StartUp()", "Running in PORTABLE MODE."); try { - if (!inSingleTCMode) - { - Directory.CreateDirectory(Path.Combine(KnUtils.KnetFolderPath!, "kn_portable", "HardLightProductions", "FreeSpaceOpen")); - } - else - { - Directory.CreateDirectory(Path.Combine(KnUtils.KnetFolderPath!, "kn_portable", "HardLightProductions", CustomLauncher.ModID!)); - } + Directory.CreateDirectory(KnUtils.GetFSODataFolderPath()); Directory.CreateDirectory(Path.Combine(KnUtils.KnetFolderPath!, "kn_portable", "Library")); } catch (Exception ex) diff --git a/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs b/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs index 2bf1912d..ad56d04c 100644 --- a/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs +++ b/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs @@ -671,9 +671,9 @@ await Dispatcher.UIThread.InvokeAsync(() => //Remove the mod version from Knossos and physical files await Task.Run(() => Knossos.RemoveMod(version)); //Remove mod version from UI mod versions list - await Dispatcher.UIThread.InvokeAsync(() => MainWindowViewModel.Instance!.RemoveInstalledModVersion(version)); + await Dispatcher.UIThread.InvokeAsync(() => MainWindowViewModel.Instance?.RemoveInstalledModVersion(version)); //If the dev editor is open and loaded this mod id, reset it - await Dispatcher.UIThread.InvokeAsync(() => DeveloperModsViewModel.Instance!.ResetModEditor(mod.id)); + await Dispatcher.UIThread.InvokeAsync(() => DeveloperModsViewModel.Instance?.ResetModEditor(mod.id)); } } else From 9d024129af45f7fb24c85aef550776f8deef6d50 Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Sun, 19 Jan 2025 20:30:55 -0300 Subject: [PATCH 39/44] Hide globalsettings warning if mod is using -no_ingame_options --- Knossos.NET/ViewModels/CustomHomeViewModel.cs | 16 +++++++++++++++ .../ViewModels/GlobalSettingsViewModel.cs | 20 +++++++++++++++++++ .../ViewModels/Windows/MainWindowViewModel.cs | 4 ++++ Knossos.NET/Views/GlobalSettingsView.axaml | 8 ++++---- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/Knossos.NET/ViewModels/CustomHomeViewModel.cs b/Knossos.NET/ViewModels/CustomHomeViewModel.cs index e0638954..5ffaa9df 100644 --- a/Knossos.NET/ViewModels/CustomHomeViewModel.cs +++ b/Knossos.NET/ViewModels/CustomHomeViewModel.cs @@ -110,6 +110,22 @@ public void CheckBasePath() } } + /// + /// Checks if the current active mod version is passing a cmdline argument to fso + /// + /// + /// + public bool ActiveVersionHasCmdline(string cmdlineToCheck) + { + if(GetActiveInstalledModVersion != null) + { + var res = GetActiveInstalledModVersion.GetModCmdLine()?.ToLower().Contains(cmdlineToCheck.ToLower()); + if (res.HasValue) + return res.Value; + } + return false; + } + /// /// Handler for the hardcoded UI buttons /// diff --git a/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs b/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs index 474f1f23..0e86904d 100644 --- a/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs +++ b/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs @@ -60,6 +60,8 @@ public partial class GlobalSettingsViewModel : ViewModelBase internal bool isAVX = false; [ObservableProperty] internal bool isAVX2 = false; + [ObservableProperty] + internal bool displaySettingsWarning = false; /* Knossos Settings */ [ObservableProperty] @@ -526,6 +528,24 @@ public GlobalSettingsViewModel() isPortableMode = Knossos.inPortableMode; } + public void CheckDisplaySettingsWarning() + { + if(Knossos.inSingleTCMode) + { + DisplaySettingsWarning = true; + if(CustomLauncher.CustomCmdlineArray != null && CustomLauncher.CustomCmdlineArray.FirstOrDefault(x => x.ToLower() == "no_ingame_options") != null ) + { + DisplaySettingsWarning = false; + return; + } + var res = MainWindowViewModel.Instance?.CustomHomeVM?.ActiveVersionHasCmdline("no_ingame_options"); + if (res.HasValue) + { + DisplaySettingsWarning = !res.Value; + } + } + } + /// /// Check if the settings where changed from the ones in the GlobalSettings instance compared to the ones here /// If they did change update data stored in GlobalSettings and save file diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index cbf51906..bd781cba 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -301,6 +301,10 @@ partial void OnSelectedMenuItemChanged(MainViewMenuItem? value) } if (CurrentViewModel == GlobalSettingsView) //Settings { + if(Knossos.inSingleTCMode) + { + GlobalSettingsView?.CheckDisplaySettingsWarning(); + } Knossos.globalSettings.Load(); GlobalSettingsView?.LoadData(); //Knossos.globalSettings.EnableIniWatch(); diff --git a/Knossos.NET/Views/GlobalSettingsView.axaml b/Knossos.NET/Views/GlobalSettingsView.axaml index 35fda68f..22245690 100644 --- a/Knossos.NET/Views/GlobalSettingsView.axaml +++ b/Knossos.NET/Views/GlobalSettingsView.axaml @@ -82,7 +82,7 @@ - @@ -147,7 +147,7 @@ - + Note, video settings for most mods using FSO version 24.2.0 or higher will need to be changed in-game by default. To change these new settings, play a modern mod and then go to the in-game "Options" menu. @@ -255,7 +255,7 @@ - + Note, audio settings for most mods using FSO version 24.2.0 or higher will need to be changed in-game by default. To change these new settings, play a modern mod and then go to the in-game "Options" menu. @@ -334,7 +334,7 @@ - + Note, input settings for most mods using FSO version 24.2.0 or higher will need to be changed in-game by default. To change these new settings, play a modern mod and then go to the in-game "Options" menu. Date: Tue, 21 Jan 2025 20:56:01 -0300 Subject: [PATCH 40/44] setting warnings not showing --- Knossos.NET/ViewModels/GlobalSettingsViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs b/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs index 0e86904d..54019739 100644 --- a/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs +++ b/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs @@ -61,7 +61,7 @@ public partial class GlobalSettingsViewModel : ViewModelBase [ObservableProperty] internal bool isAVX2 = false; [ObservableProperty] - internal bool displaySettingsWarning = false; + internal bool displaySettingsWarning = true; /* Knossos Settings */ [ObservableProperty] From e2c5e2d2539518c20f88cf6ec40f1abed2f488ab Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Tue, 21 Jan 2025 21:10:46 -0300 Subject: [PATCH 41/44] rename config to options --- Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index bd781cba..c0433af0 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -200,7 +200,7 @@ private void FillMenuItemsCustomMode(int defaultSelectedIndex) if (CustomLauncher.MenuDisplayGlobalSettingsEntry && GlobalSettingsView != null) { - MenuItems.Add(new MainViewMenuItem(GlobalSettingsView, "avares://Knossos.NET/Assets/general/menu_settings.png", "Config", "Change launcher and FSO engine settings")); + MenuItems.Add(new MainViewMenuItem(GlobalSettingsView, "avares://Knossos.NET/Assets/general/menu_settings.png", "Options", "Change launcher and FSO engine settings")); } if (CustomLauncher.MenuDisplayDebugEntry && DebugView != null) From 6cecda0f4dfd92306cab8c046141e7265b63d834 Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Tue, 21 Jan 2025 22:27:30 -0300 Subject: [PATCH 42/44] Make home buttons costumizable --- Knossos.NET/Models/CustomLauncher.cs | 23 ++++++++++++++ Knossos.NET/Views/CustomHomeView.axaml | 20 ++++++------ Knossos.NET/Views/CustomHomeView.axaml.cs | 38 +++++++++++++++++++++++ 3 files changed, 71 insertions(+), 10 deletions(-) diff --git a/Knossos.NET/Models/CustomLauncher.cs b/Knossos.NET/Models/CustomLauncher.cs index c307f706..e1dbfa98 100644 --- a/Knossos.NET/Models/CustomLauncher.cs +++ b/Knossos.NET/Models/CustomLauncher.cs @@ -14,6 +14,21 @@ public struct LinkButton public string LinkURL { get; set; } } + public struct HomeCustomButtonConfig + { + public HomeCustomButtonConfig() + { + } + + public string? ButtonID { get; set; } = null; //ButtonLaunch, ButtonModify, ButtonUpdate, ButtonInstall, ButtonInfo, ButtonDetails, ButtonSettings + public string? DisplayText { get; set; } = null; + public string? ToolTip { get; set; } = null; + public int? FontSize { get; set; } = null; + public string? BackgroundHexColor { get; set; } = null; //#CD3632 hex color value + public string? ForegroundHexColor { get; set; } = null; //#CD3632 hex color value + public string? BorderHexColer { get; set; } = null; //#CD3632 hex color value + } + public struct CustomMenuButton { public string Name { get; set; } @@ -164,6 +179,11 @@ public static class CustomLauncher /// public static LinkButton[]? HomeLinkButtons { get; private set; } + /// + /// Costumisable data for home screen buttons + /// + public static HomeCustomButtonConfig[]? HomeButtonConfigs { get; private set; } + /// /// Call this AFTER checking if we are in portable mode or not. /// The first time it runs it will try to load the "custom_launcher.json" if ModID is null @@ -261,6 +281,8 @@ private static void ReadCustomFile() HomeLinkButtons = customData.HomeLinkButtons; + HomeButtonConfigs = customData.HomeButtonConfigs; + jsonFile.Close(); } } @@ -292,6 +314,7 @@ struct CustomFileData public string? HomeWelcomeMargin { get; set; } public LinkButton[]? HomeLinkButtons { get; set; } public CustomMenuButton[]? CustomMenuButtons { get; set; } + public HomeCustomButtonConfig[]? HomeButtonConfigs { get; set; } } } } diff --git a/Knossos.NET/Views/CustomHomeView.axaml b/Knossos.NET/Views/CustomHomeView.axaml index 33972ee3..440cd90d 100644 --- a/Knossos.NET/Views/CustomHomeView.axaml +++ b/Knossos.NET/Views/CustomHomeView.axaml @@ -20,7 +20,7 @@ - + - @@ -134,10 +134,10 @@ HorizontalContentAlignment="Center" Foreground="White"> - - diff --git a/Knossos.NET/Views/CustomHomeView.axaml.cs b/Knossos.NET/Views/CustomHomeView.axaml.cs index 9a08eaed..e80b3f2d 100644 --- a/Knossos.NET/Views/CustomHomeView.axaml.cs +++ b/Knossos.NET/Views/CustomHomeView.axaml.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Avalonia.Media; using Avalonia.Media.Imaging; using Knossos.NET.Converters; using Knossos.NET.Models; @@ -71,5 +72,42 @@ public CustomHomeView() { Log.Add(Log.LogSeverity.Error, "CustomHomeView.Constructor()", ex); } + + //Home Buttons Custimizations + try + { + if (CustomLauncher.HomeButtonConfigs != null && CustomLauncher.HomeButtonConfigs.Any()) + { + foreach(var config in CustomLauncher.HomeButtonConfigs) + { + if (config.ButtonID == null) + continue; + var button = this.FindControl public static int? WindowHeight { get; private set; } = 540; + /// + /// Configurable option to show the task buttom at the end of the menu buttom list + /// instead of at the beginning. + /// + public static bool MenuTaskButtonAtTheEnd { get; private set; } = false; + /// /// The first time the user opens the launcher, the main menu should be expanded or collapsed? /// After that it will use the saved state @@ -159,6 +165,13 @@ public static class CustomLauncher /// public static string? HomeBackgroundImage { get; private set; } = "avares://Knossos.NET/Assets/general/custom_home_background.jpg"; + /// + /// Change background scretch mode + /// Possible Values: + /// None, Fill, Uniform, UniformToFill + /// + public static string HomeBackgroundStretchMode { get; private set; } = "Fill"; + /// /// Set a path to the welcome HTML message on home screen /// Uses the same path rules as HomeBackgroundImage @@ -181,6 +194,7 @@ public static class CustomLauncher /// /// Costumisable data for home screen buttons + /// Allows to change home buttons display text, color and tooltips /// public static HomeCustomButtonConfig[]? HomeButtonConfigs { get; private set; } @@ -242,6 +256,9 @@ private static void ReadCustomFile() if (customData.WindowHeight != null) WindowHeight = customData.WindowHeight; + if (customData.MenuTaskButtonAtTheEnd.HasValue) + MenuTaskButtonAtTheEnd = customData.MenuTaskButtonAtTheEnd.Value; + if (customData.MenuOpenFirstTime.HasValue) MenuOpenFirstTime = customData.MenuOpenFirstTime.Value; @@ -279,6 +296,9 @@ private static void ReadCustomFile() if (customData.HomeWelcomeMargin != null) HomeWelcomeMargin = customData.HomeWelcomeMargin; + if(customData.HomeBackgroundStretchMode != null) + HomeBackgroundStretchMode = customData.HomeBackgroundStretchMode; + HomeLinkButtons = customData.HomeLinkButtons; HomeButtonConfigs = customData.HomeButtonConfigs; @@ -300,6 +320,7 @@ struct CustomFileData public string? WindowTitle { get; set; } public int? WindowWidth { get; set; } public int? WindowHeight { get; set; } + public bool? MenuTaskButtonAtTheEnd { get; set; } public bool? MenuOpenFirstTime { get; set; } public bool? MenuDisplayEngineEntry { get; set; } public bool? MenuDisplayDebugEntry { get; set; } @@ -310,8 +331,9 @@ struct CustomFileData public bool? UseNebulaServices { get; set; } public bool? WriteLogFile { get; set; } public string? HomeBackgroundImage { get; set; } - public string? HomeWelcomeHtml { get; set; } public string? HomeWelcomeMargin { get; set; } + public string? HomeWelcomeHtml { get; set; } + public string? HomeBackgroundStretchMode { get; set; } public LinkButton[]? HomeLinkButtons { get; set; } public CustomMenuButton[]? CustomMenuButtons { get; set; } public HomeCustomButtonConfig[]? HomeButtonConfigs { get; set; } diff --git a/Knossos.NET/ViewLocator.cs b/Knossos.NET/ViewLocator.cs index 15875caf..ce201689 100644 --- a/Knossos.NET/ViewLocator.cs +++ b/Knossos.NET/ViewLocator.cs @@ -20,6 +20,27 @@ public Control Build(object? data) } else { + try + { + if (data.GetType() == typeof(AxamlExternalContentViewModel)) + { + var externalView = ((AxamlExternalContentViewModel)data).GetView(); + if (externalView != null) + { + return externalView; + } + else + { + return new TextBlock { Text = "Unable to load the external Axamal data for external view : " + ((AxamlExternalContentViewModel)data).name }; + } + } + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "ViewLocator", ex); + return new TextBlock { Text = "An error has ocurred opening the external Axamal data for : " + name + " Error:\n"+ ex.ToString() , TextWrapping = Avalonia.Media.TextWrapping.Wrap }; + } + return new TextBlock { Text = "Not Found: " + name }; } } diff --git a/Knossos.NET/ViewModels/CustomHomeViewModel.cs b/Knossos.NET/ViewModels/CustomHomeViewModel.cs index 5ffaa9df..ebc8dcd6 100644 --- a/Knossos.NET/ViewModels/CustomHomeViewModel.cs +++ b/Knossos.NET/ViewModels/CustomHomeViewModel.cs @@ -78,6 +78,9 @@ private Mod? GetActiveInstalledModVersion [ObservableProperty] internal bool changeBasePathButtonVisible = false; + [ObservableProperty] + internal string stretchMode = CustomLauncher.HomeBackgroundStretchMode; + /// /// Handled in mainview, displays a small task viewer in the home screen /// diff --git a/Knossos.NET/ViewModels/Templates/AxamlExternalContentViewModel.cs b/Knossos.NET/ViewModels/Templates/AxamlExternalContentViewModel.cs new file mode 100644 index 00000000..83d38ce8 --- /dev/null +++ b/Knossos.NET/ViewModels/Templates/AxamlExternalContentViewModel.cs @@ -0,0 +1,64 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Knossos.NET.Converters; +using System; + +namespace Knossos.NET.ViewModels +{ + public partial class AxamlExternalContentViewModel : ViewModelBase + { + private string externalPath; + public string name = string.Empty; + private UserControl? view; + + public AxamlExternalContentViewModel(string externalPath, string name) + { + this.externalPath = externalPath; + this.name = name; + } + + /// + /// Return the generated view element + /// + /// Usercontrol or null + public UserControl? GetView() + { + if(view == null) + { + var conv = new TextFileToStringConverter(); + var result = conv.Convert(externalPath, typeof(string), null, null); + if (result != null) + { + view = (UserControl)AvaloniaRuntimeXamlLoader.Parse((string)result); + } + else + { + Log.Add(Log.LogSeverity.Error, "AxamlExternalContentViewModel.LoadData()", "Unable to load remote axaml file " + externalPath); + } + } + + return view; + } + + //Hooks so the CustomView to use internal functions + + /// + /// Open Link in user Web Browser + /// + /// + internal void OpenLink (object? param) + { + try + { + if (param != null) + { + KnUtils.OpenBrowserURL((string)param); + } + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "AxamlExternalContentViewModel.OpenLink()", ex); + } + } + } +} diff --git a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index c0433af0..7781a0de 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -9,7 +9,6 @@ using System.Collections.ObjectModel; using Avalonia.Threading; using System.Threading; -using Knossos.NET.Classes; namespace Knossos.NET.ViewModels { @@ -66,7 +65,10 @@ public partial class MainWindowViewModel : ViewModelBase private MainViewMenuItem? selectedMenuItem; [ObservableProperty] internal ViewModelBase? currentViewModel; - + [ObservableProperty] + internal int taskButtomRow = 0; + [ObservableProperty] + internal int buttomListRow = 1; internal string sharedSearch = string.Empty; @@ -146,7 +148,14 @@ public MainWindowViewModel() CommunityView = new CommunityViewModel(); if (CustomLauncher.MenuDisplayNebulaLoginEntry) NebulaLoginVM = new NebulaLoginViewModel(); - FillMenuItemsCustomMode(1); + FillMenuItemsCustomMode(CustomLauncher.MenuTaskButtonAtTheEnd ? 0 : 1); + if(CustomLauncher.MenuTaskButtonAtTheEnd) + { + //Fix the UI for task on the bottom + TaskButtomRow = 1; + ButtomListRow = 0; + MainWindow.instance?.FixMarginButtomTasks(); + } } Knossos.StartUp(isQuickLaunch, forceUpdate); CustomHomeVM?.CheckBasePath(); @@ -155,13 +164,22 @@ public MainWindowViewModel() private void FillMenuItemsCustomMode(int defaultSelectedIndex) { Dispatcher.UIThread.Invoke(new Action(() => { - MenuItems = new ObservableCollection{ - new MainViewMenuItem(TaskView, null, "Tasks", "Overview of current running tasks") - }; - MenuItems.Add(new MainViewMenuItem(CustomHomeVM!, "avares://Knossos.NET/Assets/general/menu_home.png", "Home", "Home")); + if (CustomLauncher.MenuTaskButtonAtTheEnd) + { + MenuItems = new ObservableCollection{ + new MainViewMenuItem(CustomHomeVM!, "avares://Knossos.NET/Assets/general/menu_home.png", "Home", "Home") + }; + } + else + { + MenuItems = new ObservableCollection{ + new MainViewMenuItem(TaskView, null, "Tasks", "Overview of current running tasks"), + new MainViewMenuItem(CustomHomeVM!, "avares://Knossos.NET/Assets/general/menu_home.png", "Home", "Home") + }; + } - if(CustomLauncher.CustomMenuButtons != null && CustomLauncher.CustomMenuButtons.Any()) + if (CustomLauncher.CustomMenuButtons != null && CustomLauncher.CustomMenuButtons.Any()) { foreach(var button in CustomLauncher.CustomMenuButtons) { @@ -172,6 +190,9 @@ private void FillMenuItemsCustomMode(int defaultSelectedIndex) case "htmlcontent" : MenuItems.Add(new MainViewMenuItem(new HtmlContentViewModel(button.LinkURL), button.IconPath, button.Name, button.ToolTip)); break; + case "axamlcontent": + MenuItems.Add(new MainViewMenuItem(new AxamlExternalContentViewModel(button.LinkURL, button.Name), button.IconPath, button.Name, button.ToolTip)); + break; default: throw new NotImplementedException("button type: "+ button.Type + " is not supported."); } @@ -208,6 +229,11 @@ private void FillMenuItemsCustomMode(int defaultSelectedIndex) MenuItems.Add(new MainViewMenuItem(DebugView, "avares://Knossos.NET/Assets/general/menu_debug.png", "Debug", "Debug info")); } + if (CustomLauncher.MenuTaskButtonAtTheEnd) + { + MenuItems.Add(new MainViewMenuItem(TaskView, null, "Tasks", "Overview of current running tasks")); + } + if (MenuItems != null && MenuItems.Count() - 1 > defaultSelectedIndex) { SelectedMenuItem = MenuItems[defaultSelectedIndex]; diff --git a/Knossos.NET/Views/CustomHomeView.axaml b/Knossos.NET/Views/CustomHomeView.axaml index 440cd90d..623640ad 100644 --- a/Knossos.NET/Views/CustomHomeView.axaml +++ b/Knossos.NET/Views/CustomHomeView.axaml @@ -23,7 +23,7 @@ - diff --git a/Knossos.NET/Views/CustomHomeView.axaml.cs b/Knossos.NET/Views/CustomHomeView.axaml.cs index e80b3f2d..d57f7da1 100644 --- a/Knossos.NET/Views/CustomHomeView.axaml.cs +++ b/Knossos.NET/Views/CustomHomeView.axaml.cs @@ -22,7 +22,7 @@ public CustomHomeView() { if (CustomLauncher.HomeLinkButtons != null && CustomLauncher.HomeLinkButtons.Any()) { - var buttonPanel = this.FindControl("LinkButtons")!; + var buttonPanel = this.FindControl("LinkButtons"); if (buttonPanel != null) { int index = 0; @@ -82,7 +82,7 @@ public CustomHomeView() { if (config.ButtonID == null) continue; - var button = this.FindControl - - + +