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/Assets/general/custom_home_background.jpg b/Knossos.NET/Assets/general/custom_home_background.jpg new file mode 100644 index 00000000..49e73487 Binary files /dev/null and b/Knossos.NET/Assets/general/custom_home_background.jpg differ diff --git a/Knossos.NET/Assets/general/discordicon.png b/Knossos.NET/Assets/general/discordicon.png index 85c58e06..fb3659cd 100644 Binary files a/Knossos.NET/Assets/general/discordicon.png and b/Knossos.NET/Assets/general/discordicon.png differ diff --git a/Knossos.NET/Assets/general/menu_home.png b/Knossos.NET/Assets/general/menu_home.png new file mode 100644 index 00000000..5f7c7fd5 Binary files /dev/null and b/Knossos.NET/Assets/general/menu_home.png differ diff --git a/Knossos.NET/Assets/general/menu_nebula.png b/Knossos.NET/Assets/general/menu_nebula.png new file mode 100644 index 00000000..72e34c74 Binary files /dev/null and b/Knossos.NET/Assets/general/menu_nebula.png differ 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..ca9b760f 100644 --- a/Knossos.NET/Classes/KnUtils.cs +++ b/Knossos.NET/Classes/KnUtils.cs @@ -122,7 +122,15 @@ public static string GetKnossosDataFolderPath() { if (!Knossos.inPortableMode) { - return 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 { @@ -139,32 +147,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() /// @@ -579,61 +616,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 filestream or null if failed + public static async Task GetRemoteResourceStream(string resourceURL) + { + try + { + var localFile = await GetRemoteResource(resourceURL); + if (localFile != null) + { + 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.GetRemoteResourceStream()", ex); + } + return null; + } + + /// + /// 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 /// /// - /// Cached image filestream or null if failed - public static async Task GetImageStream(string imageURL, int attempt = 1) + /// + /// string path or null + 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) && 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)) { - return new FileStream(imageInCachePath, FileMode.Open, FileAccess.Read, FileShare.Read); + await imageStream.CopyToAsync(fileStream); } - else + } + //save etag + if (!isNebulaFile) + { + try { - //Download to cache and copy - Directory.CreateDirectory(Path.Combine(GetKnossosDataFolderPath(), "image_cache")); - using (var imageStream = await GetHttpClient().GetStreamAsync(imageURL)) + 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.Seek(0, SeekOrigin.Begin); - return fileStream; + Log.Add(Log.LogSeverity.Error, "KnUtils.GetRemoteResource()", "Could not save etag information for file " + resourceURL + " remoteEtag value was null."); } } - }); + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "KnUtils.GetRemoteResource()", ex); + } + } + return fileInCachePath; } catch (Exception ex) { - Log.Add(Log.LogSeverity.Error, "KnUtils.GetImageStream()", ex); - if (attempt <= 2) + Log.Add(Log.LogSeverity.Error, "KnUtils.GetImagePath()", ex); + if (attempt < 3) { await Task.Delay(1000); - return await GetImageStream(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 9cb1f14d..dd7ea3aa 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; } /// @@ -92,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) @@ -99,7 +117,7 @@ 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")); + Directory.CreateDirectory(KnUtils.GetFSODataFolderPath()); Directory.CreateDirectory(Path.Combine(KnUtils.KnetFolderPath!, "kn_portable", "Library")); } catch (Exception ex) @@ -135,14 +153,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) @@ -601,9 +619,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 +899,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) @@ -1205,6 +1223,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/Converters/BitmapAssetValueConverter.cs b/Knossos.NET/Converters/BitmapAssetValueConverter.cs index 216cabe5..45cf61b9 100644 --- a/Knossos.NET/Converters/BitmapAssetValueConverter.cs +++ b/Knossos.NET/Converters/BitmapAssetValueConverter.cs @@ -2,8 +2,9 @@ using System; using System.Globalization; using Avalonia.Platform; -using System.Reflection; using Avalonia.Media.Imaging; +using System.IO; +using System.Threading.Tasks; namespace Knossos.NET.Converters { @@ -11,30 +12,57 @@ public class BitmapAssetValueConverter : IValueConverter { public static BitmapAssetValueConverter 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) { - if (value == null) return null; - - if (value is not string rawUri || !targetType.IsAssignableFrom(typeof(Bitmap))) + try { - throw new NotSupportedException(); - } + if (value == null) return null; - Uri uri; + if(!targetType.IsAssignableFrom(typeof(Bitmap))) + throw new NotSupportedException(); - if (rawUri.StartsWith("avares://")) - { - uri = new Uri(rawUri); + if (value is not string rawUri) + { + if(parameter is string) + { + rawUri = (string)parameter; + } + else + { + 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")) + { + var localPath = Task.Run(() => KnUtils.GetRemoteResource(rawUri)).Result; + if (localPath != null) + { + return new Bitmap(localPath); + } + } + 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); + } } - else + catch (Exception ex) { - var assemblyName = Assembly.GetEntryAssembly()?.GetName().Name; - uri = new Uri($"avares://{assemblyName}/{rawUri.TrimStart('/')}"); + Log.Add(Log.LogSeverity.Error, "BitmapAssetValueConverter.Convert()", ex); } - - 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/Converters/TextFileToStringConverter.cs b/Knossos.NET/Converters/TextFileToStringConverter.cs new file mode 100644 index 00000000..b6ad7f99 --- /dev/null +++ b/Knossos.NET/Converters/TextFileToStringConverter.cs @@ -0,0 +1,69 @@ +using Avalonia.Data.Converters; +using System; +using System.Globalization; +using Avalonia.Platform; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +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")) + { + var localPath = Task.Run(() => KnUtils.GetRemoteResource(rawUri)).Result; + if (localPath != null) + { + return File.ReadAllText(localPath); + } + } + 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/Knossos.NET.csproj b/Knossos.NET/Knossos.NET.csproj index 1c3ebf66..e24bdb3d 100644 --- a/Knossos.NET/Knossos.NET.csproj +++ b/Knossos.NET/Knossos.NET.csproj @@ -78,6 +78,7 @@ + diff --git a/Knossos.NET/Models/CustomLauncher.cs b/Knossos.NET/Models/CustomLauncher.cs new file mode 100644 index 00000000..e0f7ad9e --- /dev/null +++ b/Knossos.NET/Models/CustomLauncher.cs @@ -0,0 +1,342 @@ +using System; +using System.IO; +using System.Text.Json; + +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; } + } + + 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; } + public string ToolTip { get; set; } + public string IconPath { get; set; } + public string Type { get; set; } //HtmlContent, AxamlContent + public string LinkURL { get; set; } + } + + /// + /// 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 + /// 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; + + /// + /// 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 + /// + public static bool MenuOpenFirstTime { get; private set; } = false; + + /// + /// Add the regular FSO engine view to the menu + /// + public static bool MenuDisplayEngineEntry { get; private set; } = true; + + /// + /// 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; + + /// + /// 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 + /// + 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. + /// + 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; + + /// + /// 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: + /// 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"; + + /// + /// 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 + /// 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; + + /// + /// 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; } + + /// + /// Customisable data for home screen buttons + /// Allows to change home buttons display text, color and tooltips + /// + 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 + /// + 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.MenuTaskButtonAtTheEnd.HasValue) + MenuTaskButtonAtTheEnd = customData.MenuTaskButtonAtTheEnd.Value; + + if (customData.MenuOpenFirstTime.HasValue) + MenuOpenFirstTime = customData.MenuOpenFirstTime.Value; + + if (customData.MenuDisplayEngineEntry.HasValue) + MenuDisplayEngineEntry = customData.MenuDisplayEngineEntry.Value; + + if (customData.MenuDisplayDebugEntry.HasValue) + MenuDisplayDebugEntry = customData.MenuDisplayDebugEntry.Value; + + if (customData.MenuDisplayNebulaLoginEntry.HasValue) + MenuDisplayNebulaLoginEntry = customData.MenuDisplayNebulaLoginEntry.Value; + + if (customData.MenuDisplayGlobalSettingsEntry.HasValue) + MenuDisplayGlobalSettingsEntry = customData.MenuDisplayGlobalSettingsEntry.Value; + + if (customData.MenuDisplayCommunityEntry.HasValue) + MenuDisplayCommunityEntry = customData.MenuDisplayCommunityEntry.Value; + + CustomMenuButtons = customData.CustomMenuButtons; + + CustomCmdlineArray = customData.CustomCmdlineArray; + + if (customData.UseNebulaServices.HasValue) + UseNebulaServices = customData.UseNebulaServices.Value; + + if (customData.WriteLogFile.HasValue) + WriteLogFile = customData.WriteLogFile.Value; + + if (customData.HomeBackgroundImage != null) + HomeBackgroundImage = customData.HomeBackgroundImage; + + if (customData.HomeWelcomeHtml != null) + HomeWelcomeHtml = customData.HomeWelcomeHtml; + + if (customData.HomeWelcomeMargin != null) + HomeWelcomeMargin = customData.HomeWelcomeMargin; + + if(customData.HomeBackgroundStretchMode != null) + HomeBackgroundStretchMode = customData.HomeBackgroundStretchMode; + + HomeLinkButtons = customData.HomeLinkButtons; + + HomeButtonConfigs = customData.HomeButtonConfigs; + + 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? MenuTaskButtonAtTheEnd { get; set; } + public bool? MenuOpenFirstTime { get; set; } + public bool? MenuDisplayEngineEntry { get; set; } + 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; } + public string? HomeBackgroundImage { 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/Models/GlobalSettings.cs b/Knossos.NET/Models/GlobalSettings.cs index b0e18938..092fe8d8 100644 --- a/Knossos.NET/Models/GlobalSettings.cs +++ b/Knossos.NET/Models/GlobalSettings.cs @@ -674,6 +674,8 @@ public void Load() ReadFS2IniValues(); Log.Add(Log.LogSeverity.Information, "GlobalSettings.Load()", "Global settings have been loaded"); + SetCustomModeValues(); + pendingChangesOnAppClose = false; } @@ -693,6 +695,23 @@ 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; + if (!CustomLauncher.MenuDisplayGlobalSettingsEntry) + { + warnNewSettingsSystem = 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 @@ -887,6 +906,7 @@ public void WriteFS2IniValues(string? customFullPath = null) /// public void Save(bool writeIni = true) { + SetCustomModeValues(); if (writeIni) { WriteFS2IniValues(); diff --git a/Knossos.NET/Models/ModSettings.cs b/Knossos.NET/Models/ModSettings.cs index 8c4d9d47..f73e8f4b 100644 --- a/Knossos.NET/Models/ModSettings.cs +++ b/Knossos.NET/Models/ModSettings.cs @@ -69,9 +69,19 @@ public bool IsDefaultConfig() return true; } + /// + /// Sets the filepath to the mod folder for the mod settings system, if it is not already set. + /// + /// + public void SetInitialFilePath(string modFullPath) + { + if (filePath == null) + filePath = modFullPath + Path.DirectorySeparatorChar + "mod_settings.json"; + } + /// /// 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/Models/Nebula.cs b/Knossos.NET/Models/Nebula.cs index c46f44b0..a57dd525 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/"; @@ -89,6 +90,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(); @@ -117,8 +125,8 @@ public static async Task Trinity() } try { - bool displayUpdates = settings.NewerModsVersions.Any() ? true : false; - var webEtag = await GetRepoEtag().ConfigureAwait(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) { //Download the repo_minimal.json @@ -154,7 +162,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 +179,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 { } } @@ -180,7 +188,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); } @@ -227,7 +235,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); } } } @@ -245,14 +253,14 @@ 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); + 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); } } } @@ -418,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); } @@ -429,7 +437,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 +471,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); @@ -590,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 /// @@ -696,6 +680,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/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 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 new file mode 100644 index 00000000..ebc8dcd6 --- /dev/null +++ b/Knossos.NET/ViewModels/CustomHomeViewModel.cs @@ -0,0 +1,527 @@ +using Avalonia; +using Avalonia.Platform.Storage; +using Avalonia.Threading; +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.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; + +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; + + [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 isUpdateReady = false; + + [ObservableProperty] + internal bool nebulaVersionsAvailable = false; + + [ObservableProperty] + internal string? backgroundImage = CustomLauncher.HomeBackgroundImage; + + [ObservableProperty] + internal int animate = 0; + + [ObservableProperty] + internal bool nebulaServices = CustomLauncher.UseNebulaServices; + + [ObservableProperty] + internal string? welcomeHtml = CustomLauncher.HomeWelcomeHtml; + + [ObservableProperty] + 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; + + [ObservableProperty] + internal string stretchMode = CustomLauncher.HomeBackgroundStretchMode; + + /// + /// Handled in mainview, displays a small task viewer in the home screen + /// + [ObservableProperty] + public ViewModelBase? taskVM; + + 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; + } + } + + /// + /// 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 + /// + /// + internal void HardcodedButtonCommand(object cmd) + { + switch ((string)cmd) + { + 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": 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; + try + { + cancellationTokenSource?.Cancel(); + } + catch { } + cancellationTokenSource = null; + TaskViewModel.Instance?.CancelAllInstallTaskWithID(CustomLauncher.ModID!, null); + } + + /// + /// Opens mod install window for this mod id + /// + private async void Install() + { + if (nebulaModVersions.Any() && CustomLauncher.UseNebulaServices) + { + 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."); + } + } + + /// + /// Opens mod install window for this mod id in modify active version mode + /// + private async void Modify() + { + if (GetActiveInstalledModVersion != null && CustomLauncher.UseNebulaServices) + { + 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 = 0; + } + } + 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; + } + } + + /// + /// Remove starts cancellation of a install taks with this mod id + /// + /// + public void CancelModInstall(string id) + { + if (CustomLauncher.ModID == id) + { + Cancel(); + } + } + + /// + /// Sets the install mode, so the cancel tasks button can be displayed + /// + /// + /// + public void SetInstalling(string id, CancellationTokenSource cancelToken) + { + if (CustomLauncher.ModID == id) + { + cancellationTokenSource = cancelToken; + Installing = true; + } + } + + /// + /// 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); + if (modVersions.Any()) + { + 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(); + } + } + + /// + /// 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)); + NebulaVersionsAvailable = true; + } + } + + /// + /// 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, 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); + } + } + } + + /// + /// Run code when the user clicks the menu item to open this view + /// + public void ViewOpened() + { + Animate = 1; + } + + /// + /// Run code when the user exit this view + /// + public void ViewClosed() + { + Animate = 0; + } + + /// + /// Changes the knossos library path, reloads settings and nebula repo + /// + internal async void BrowseFolderCommand() + { + if (MainWindow.instance != null) + { + ChangeBasePathButtonVisible = false; + NewBasePath = string.Empty; + FolderPickerOpenOptions options = new FolderPickerOpenOptions(); + options.AllowMultiple = false; + + var result = await MainWindow.instance.StorageProvider.OpenFolderPickerAsync(options); + + try + { + if (result != null && result.Count > 0) + { + + // 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) + { + 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); + } + } + } + + internal void ChangeBasePath() + { + if (NewBasePath != string.Empty) + { + Knossos.globalSettings.basePath = NewBasePath; + Knossos.globalSettings.Save(); + Knossos.ResetBasePath(); + ShowBasePathSelector = false; + } + } + } +} diff --git a/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs b/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs index 7c364720..54019739 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 = true; /* 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 @@ -1125,7 +1145,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); } } @@ -1489,7 +1509,7 @@ internal async void ClearImageCache() await Task.Run(() => { try { - var path = KnUtils.GetImageCachePath(); + var path = KnUtils.GetCachePath(); Directory.Delete(path, true); UpdateImgCacheSize(); @@ -1509,7 +1529,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/TaskViewModel.cs b/Knossos.NET/ViewModels/TaskViewModel.cs index d42174a0..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; @@ -49,6 +53,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 /// /// /// @@ -56,9 +61,9 @@ 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 && task.installVersion == version) + if (!task.IsCompleted && task.installID == id && (version == null || task.installVersion == version)) { task.CancelTaskCommand(); } @@ -66,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) { @@ -243,7 +257,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; } @@ -253,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; } /// @@ -269,7 +296,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; } @@ -289,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); + } } } } @@ -316,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); + } } } } @@ -342,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/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/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/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/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/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 9f568a29..ad56d04c 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) @@ -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)); @@ -555,6 +555,8 @@ public async Task InstallMod(Mod mod, CancellationTokenSource cancelSource mod.ClearUnusedData(); } + mod.modSettings.SetInitialFilePath(mod.fullPath); + //We have to compress? if (compressMod) { @@ -600,7 +602,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 @@ -669,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 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/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/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/ViewModels/Windows/MainWindowViewModel.cs b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs index 33b4a843..7781a0de 100644 --- a/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/MainWindowViewModel.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Collections.ObjectModel; using Avalonia.Threading; +using System.Threading; namespace Knossos.NET.ViewModels { @@ -22,27 +23,38 @@ public partial class MainWindowViewModel : ViewModelBase { public static MainWindowViewModel? Instance { get; set; } + /* 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 ModListViewModel installedModsView = new ModListViewModel(); + internal int? windowWidth = null; + [ObservableProperty] + internal int? windowHeight = null; + [ObservableProperty] + 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] @@ -53,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; @@ -103,23 +118,142 @@ public MainWindowViewModel() forceUpdate = true; } } - FillMenuItemsNormalMode(1); + 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 + { + //Apply customization for Single TC Mode + Knossos.globalSettings.mainMenuOpen = CustomLauncher.MenuOpenFirstTime; + 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.MenuDisplayCommunityEntry) + CommunityView = new CommunityViewModel(); + if (CustomLauncher.MenuDisplayNebulaLoginEntry) + NebulaLoginVM = new NebulaLoginViewModel(); + 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(); + } + + private void FillMenuItemsCustomMode(int defaultSelectedIndex) + { + Dispatcher.UIThread.Invoke(new Action(() => { + + 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()) + { + 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; + 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."); + } + } + 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")); + } + + if(CustomLauncher.MenuDisplayNebulaLoginEntry && NebulaLoginVM != null) + { + 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", "Options", "Change launcher and FSO engine settings")); + } + + if (CustomLauncher.MenuDisplayDebugEntry && DebugView != null) + { + 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]; + } + })); } - public void FillMenuItemsNormalMode(int defaultSelectedIndex) + 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) { @@ -137,18 +271,24 @@ 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.TaskVM = null; + TaskView?.ShowButtons(true); + CustomHomeVM.ViewClosed(); + } CurrentViewModel = value.vm; @@ -175,17 +315,40 @@ partial void OnSelectedMenuItemChanged(MainViewMenuItem? value) { PxoViewModel.Instance!.InitialLoad(); } + if (CurrentViewModel != null && CurrentViewModel == NebulaLoginVM) //Nebula Login (Single TC mode) + { + NebulaLoginVM.UpdateUI(); + } + if (CurrentViewModel != null && CurrentViewModel == CustomHomeVM) //CustomHomeView + { + CustomHomeVM.TaskVM = TaskView; + TaskView?.ShowButtons(false); + CustomHomeVM.ViewOpened(); + } if (CurrentViewModel == GlobalSettingsView) //Settings { + if(Knossos.inSingleTCMode) + { + GlobalSettingsView?.CheckDisplaySettingsWarning(); + } Knossos.globalSettings.Load(); - GlobalSettingsView.LoadData(); + GlobalSettingsView?.LoadData(); //Knossos.globalSettings.EnableIniWatch(); - GlobalSettingsView.UpdateImgCacheSize(); + GlobalSettingsView?.UpdateImgCacheSize(); } else { //Knossos.globalSettings.DisableIniWatch(); } + + //Custom Views + if (CurrentViewModel != null && CustomLauncher.IsCustomMode) + { + if (CurrentViewModel.GetType() == typeof(HtmlContentViewModel)) + { + ((HtmlContentViewModel)CurrentViewModel).Navigate(); + } + } } } @@ -196,7 +359,7 @@ partial void OnSelectedMenuItemChanged(MainViewMenuItem? value) /// public void AddDevMod(Mod devmod) { - DeveloperModView.AddMod(devmod); + DeveloperModView?.AddMod(devmod); } /// @@ -204,7 +367,7 @@ public void AddDevMod(Mod devmod) /// public void RunModStatusChecks() { - InstalledModsView.RunModStatusChecks(); + InstalledModsView?.RunModStatusChecks(); } /// @@ -223,9 +386,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); + InstalledModsView?.UpdateIsAvailable(id, value); + CustomHomeVM?.UpdateIsAvailable(id, value, newVersion); } /// @@ -234,7 +398,8 @@ public void MarkAsUpdateAvailable(string id, bool value = true) /// public void AddInstalledMod(Mod modJson) { - InstalledModsView.AddMod(modJson); + InstalledModsView?.AddMod(modJson); + CustomHomeVM?.AddModVersion(modJson); } /// @@ -257,7 +422,8 @@ public void AddMostRecent(string buildId, bool nightly) /// public void AddNebulaMod(Mod modJson) { - NebulaModsView.AddMod(modJson); + NebulaModsView?.AddMod(modJson); + CustomHomeVM?.AddNebulaModVersion(modJson); } /// @@ -268,8 +434,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); + } + } } /// @@ -278,7 +452,8 @@ public void BulkLoadNebulaMods(List mods, bool clear) /// public void CancelModInstall(string id) { - NebulaModsView.CancelModInstall(id); + NebulaModsView?.CancelModInstall(id); + CustomHomeVM?.CancelModInstall(id); } /// @@ -287,7 +462,8 @@ public void CancelModInstall(string id) /// public void RemoveInstalledMod(string id) { - InstalledModsView.RemoveMod(id); + InstalledModsView?.RemoveMod(id); + CustomHomeVM?.RemoveMod(id); } /// @@ -296,7 +472,8 @@ public void RemoveInstalledMod(string id) /// public void RemoveInstalledModVersion(Mod mod) { - InstalledModsView.RemoveModVersion(mod); + InstalledModsView?.RemoveModVersion(mod); + CustomHomeVM?.RemoveInstalledModVersion(mod); } /// @@ -304,7 +481,7 @@ public void RemoveInstalledModVersion(Mod mod) /// public void GlobalSettingsLoadData() { - GlobalSettingsView.LoadData(); + GlobalSettingsView?.LoadData(); } internal void ApplySettings() @@ -313,7 +490,8 @@ internal void ApplySettings() IsMenuOpen = Knossos.globalSettings.mainMenuOpen; sharedSortType = Knossos.globalSettings.sortType; InstalledModsView?.ChangeSort(sharedSortType); - NebulaModsView.sortType = sharedSortType; + if(NebulaModsView != null) + NebulaModsView.sortType = sharedSortType; }); } @@ -328,7 +506,7 @@ public void UpdateBuildInstallButtons(){ /// public void WriteToUIConsole(string message) { - DebugView.WriteToUIConsole(message); + DebugView?.WriteToUIConsole(message); } /// @@ -338,7 +516,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) { @@ -351,5 +531,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/ModDetailsViewModel.cs b/Knossos.NET/ViewModels/Windows/ModDetailsViewModel.cs index 9b247639..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(); @@ -280,7 +282,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 +347,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/ViewModels/Windows/ModInstallViewModel.cs b/Knossos.NET/ViewModels/Windows/ModInstallViewModel.cs index 4c9a5a49..1cdc16f3 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) @@ -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 new file mode 100644 index 00000000..623640ad --- /dev/null +++ b/Knossos.NET/Views/CustomHomeView.axaml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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..d57f7da1 --- /dev/null +++ b/Knossos.NET/Views/CustomHomeView.axaml.cs @@ -0,0 +1,113 @@ +using Avalonia.Controls; +using Avalonia.Media; +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); + } + + //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 - - - - 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/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 @@ - 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/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 diff --git a/Knossos.NET/Views/Windows/AddSapiVoicesView.axaml b/Knossos.NET/Views/Windows/AddSapiVoicesView.axaml index 3eda8df3..36262a85 100644 --- a/Knossos.NET/Views/Windows/AddSapiVoicesView.axaml +++ b/Knossos.NET/Views/Windows/AddSapiVoicesView.axaml @@ -20,9 +20,9 @@ Freespace 2 Open uses Windows SAPI as TTS engine. SAPI is now legacy and Windows 10/11 uses a new TTS system. Most of the voices for the new system can still be used by SAPI and FSO. In order to use it you will need to copy the registry entries, KnossosNET can do this for you. - - Once the voices have finished installing click on the following button to copy the OneCore registry keys to the Sapi one - The first time you do this a backup of the original sapi keys will be saved to the knossos data folder in cae you ever need it. + + Once the voices have finished installing click on the following button to copy the OneCore registry keys to the SAPI one + The first time you do this a backup of the original SAPI keys will be saved to the KnossosNET data folder in case you ever need it. Your user must have admin rights in order to work. Thats it! Press "Reload Data" button on the settings tab to update the list of installed voices diff --git a/Knossos.NET/Views/Windows/CleanupKnossosLibraryView.axaml b/Knossos.NET/Views/Windows/CleanupKnossosLibraryView.axaml index 751c7278..afc666d7 100644 --- a/Knossos.NET/Views/Windows/CleanupKnossosLibraryView.axaml +++ b/Knossos.NET/Views/Windows/CleanupKnossosLibraryView.axaml @@ -9,7 +9,7 @@ x:DataType="vm:CleanupKnossosLibraryViewModel" Icon="/Assets/knossos-icon.ico" WindowStartupLocation="CenterScreen" - Title="Knossos Library Cleanup" + Title="KnossosNET Library Cleanup" SizeToContent="WidthAndHeight" CanResize="False"> diff --git a/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml b/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml index b3cf77b4..59f5361b 100644 --- a/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml +++ b/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml @@ -20,17 +20,16 @@ - - + - + @@ -44,7 +43,7 @@ - + diff --git a/Knossos.NET/Views/Windows/DevModCreateNewView.axaml b/Knossos.NET/Views/Windows/DevModCreateNewView.axaml index dbf8e567..0e411773 100644 --- a/Knossos.NET/Views/Windows/DevModCreateNewView.axaml +++ b/Knossos.NET/Views/Windows/DevModCreateNewView.axaml @@ -43,7 +43,7 @@ - + diff --git a/Knossos.NET/Views/Windows/Fs2InstallerView.axaml b/Knossos.NET/Views/Windows/Fs2InstallerView.axaml index 4b18d533..f6547165 100644 --- a/Knossos.NET/Views/Windows/Fs2InstallerView.axaml +++ b/Knossos.NET/Views/Windows/Fs2InstallerView.axaml @@ -26,7 +26,7 @@ - + diff --git a/Knossos.NET/Views/Windows/MainWindow.axaml b/Knossos.NET/Views/Windows/MainWindow.axaml index e087ff58..499e588f 100644 --- a/Knossos.NET/Views/Windows/MainWindow.axaml +++ b/Knossos.NET/Views/Windows/MainWindow.axaml @@ -4,6 +4,10 @@ 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}" + 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" @@ -36,8 +40,8 @@ - - + +