diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 77b5a9e..1484828 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,9 +1,9 @@ name: Build Installer -run-name: 4.1.1-ci.${{ github.run_number }} | ${{ github.event_name != 'workflow_dispatch' && (github.event.head_commit.message || format('`[PR]` {0}', github.event.pull_request.title)) || 'Manual Build' }} +run-name: 5.0.0-ci.${{ github.run_number }} | ${{ github.event_name != 'workflow_dispatch' && (github.event.head_commit.message || format('`[PR]` {0}', github.event.pull_request.title)) || 'Manual Build' }} env: - DEVVERSION: "4.1.1" + DEVVERSION: "5.0.0" on: push: diff --git a/Directory.Build.props b/Directory.Build.props index c9613f8..2dc79e9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 4.1.1 + 5.0.0 net8.0 @@ -10,6 +10,6 @@ linux-x64 enable - 11.2.1 + 11.2.3 diff --git a/MelonLoader.Installer/.editorconfig b/MelonLoader.Installer/.editorconfig index fc7a9b1..7a2086d 100644 --- a/MelonLoader.Installer/.editorconfig +++ b/MelonLoader.Installer/.editorconfig @@ -12,7 +12,7 @@ indent_style = space tab_width = 4 # New line preferences -end_of_line = crlf +end_of_line = lf insert_final_newline = false #### .NET Code Actions #### @@ -88,71 +88,71 @@ dotnet_style_allow_statement_immediately_after_block_experimental = false #### C# Coding Conventions #### # var preferences -csharp_style_var_elsewhere = true -csharp_style_var_for_built_in_types = true -csharp_style_var_when_type_is_apparent = true +csharp_style_var_elsewhere = true:warning +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning # Expression-bodied members -csharp_style_expression_bodied_accessors = true -csharp_style_expression_bodied_constructors = false -csharp_style_expression_bodied_indexers = true -csharp_style_expression_bodied_lambdas = true -csharp_style_expression_bodied_local_functions = false -csharp_style_expression_bodied_methods = false -csharp_style_expression_bodied_operators = false -csharp_style_expression_bodied_properties = true +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent # Pattern matching preferences -csharp_style_pattern_matching_over_as_with_null_check = true -csharp_style_pattern_matching_over_is_with_cast_check = true -csharp_style_prefer_extended_property_pattern = true -csharp_style_prefer_not_pattern = true -csharp_style_prefer_pattern_matching = true -csharp_style_prefer_switch_expression = true +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_switch_expression = true:suggestion # Null-checking preferences -csharp_style_conditional_delegate_call = true +csharp_style_conditional_delegate_call = true:suggestion # Modifier preferences -csharp_prefer_static_anonymous_function = true -csharp_prefer_static_local_function = true +csharp_prefer_static_anonymous_function = true:suggestion +csharp_prefer_static_local_function = true:suggestion csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async -csharp_style_prefer_readonly_struct = true -csharp_style_prefer_readonly_struct_member = true +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_readonly_struct_member = true:suggestion # Code-block preferences -csharp_prefer_braces = when_multiline -csharp_prefer_simple_using_statement = true -csharp_prefer_system_threading_lock = true -csharp_style_namespace_declarations = file_scoped -csharp_style_prefer_method_group_conversion = true -csharp_style_prefer_primary_constructors = true -csharp_style_prefer_top_level_statements = false +csharp_prefer_braces = when_multiline:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_system_threading_lock = true:suggestion +csharp_style_namespace_declarations = file_scoped:warning +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_prefer_top_level_statements = false:silent # Expression-level preferences -csharp_prefer_simple_default_expression = true -csharp_style_deconstructed_variable_declaration = true -csharp_style_implicit_object_creation_when_type_is_apparent = true -csharp_style_inlined_variable_declaration = true -csharp_style_prefer_index_operator = true -csharp_style_prefer_local_over_anonymous_function = true -csharp_style_prefer_null_check_over_type_check = true -csharp_style_prefer_range_operator = true -csharp_style_prefer_tuple_swap = true -csharp_style_prefer_utf8_string_literals = true -csharp_style_throw_expression = true -csharp_style_unused_value_assignment_preference = discard_variable -csharp_style_unused_value_expression_statement_preference = discard_variable +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent # 'using' directive preferences -csharp_using_directive_placement = outside_namespace +csharp_using_directive_placement = outside_namespace:warning # New line preferences -csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true -csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = false -csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true -csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false -csharp_style_allow_embedded_statements_on_same_line_experimental = false +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = false:silent +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:warning +csharp_style_allow_embedded_statements_on_same_line_experimental = false:warning #### C# Formatting Rules #### @@ -242,3 +242,41 @@ dotnet_naming_style.begins_with_i.required_prefix = I dotnet_naming_style.begins_with_i.required_suffix = dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case +csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion + +[*.{cs,vb}] +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = false:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = lf +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +dotnet_style_namespace_match_folder = true:warning +dotnet_style_readonly_field = true:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning +dotnet_style_allow_multiple_blank_lines_experimental = false:warning +dotnet_style_allow_statement_immediately_after_block_experimental = false:warning +dotnet_code_quality_unused_parameters = non_public:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning \ No newline at end of file diff --git a/MelonLoader.Installer/App.axaml b/MelonLoader.Installer/App.axaml index 4b98e36..3e80c63 100644 --- a/MelonLoader.Installer/App.axaml +++ b/MelonLoader.Installer/App.axaml @@ -6,9 +6,8 @@ - + - \ No newline at end of file diff --git a/MelonLoader.Installer/Assets/bg.png b/MelonLoader.Installer/Assets/bg.png index 728c9a5..08cad83 100644 Binary files a/MelonLoader.Installer/Assets/bg.png and b/MelonLoader.Installer/Assets/bg.png differ diff --git a/MelonLoader.Installer/Assets/loading-anim.gif b/MelonLoader.Installer/Assets/loading-anim.gif new file mode 100644 index 0000000..9a92310 Binary files /dev/null and b/MelonLoader.Installer/Assets/loading-anim.gif differ diff --git a/MelonLoader.Installer/Config.cs b/MelonLoader.Installer/Config.cs index e8b059a..dea2552 100644 --- a/MelonLoader.Installer/Config.cs +++ b/MelonLoader.Installer/Config.cs @@ -13,12 +13,14 @@ internal static class Config public static string LocalZipCache { get; private set; } = Path.Combine(CacheDir, "Local Build"); public static string GameListPath { get; private set; } = Path.Combine(CacheDir, "games.txt"); - public static string[] LoadGameList() + static Config() { - if (!File.Exists(GameListPath)) - return []; + Directory.CreateDirectory(CacheDir); + } - return File.ReadAllLines(GameListPath); + public static string[] LoadGameList() + { + return !File.Exists(GameListPath) ? [] : File.ReadAllLines(GameListPath); } public static void SaveGameList(IEnumerable gamePaths) diff --git a/MelonLoader.Installer/GameLaunchers/EgsLauncher.cs b/MelonLoader.Installer/GameLaunchers/EgsLauncher.cs index f9bfb9a..e84a857 100644 --- a/MelonLoader.Installer/GameLaunchers/EgsLauncher.cs +++ b/MelonLoader.Installer/GameLaunchers/EgsLauncher.cs @@ -4,8 +4,6 @@ namespace MelonLoader.Installer.GameLaunchers; -#pragma warning disable CA1416 - public class EgsLauncher : GameLauncher { private static readonly string? manifestDir; @@ -19,36 +17,42 @@ static EgsLauncher() internal EgsLauncher() : base("/Assets/egs.png") { } - public override void AddGames() + public override void GetAddGameTasks(List tasks) { if (manifestDir == null) return; - foreach (var item in Directory.EnumerateFiles(manifestDir, "*.item")) + foreach (var file in Directory.GetFiles(manifestDir, "*.item")) + { + tasks.Add(AddGameAsync(file)); + } + } + + private async Task AddGameAsync(string item) + { + JsonNode? json; + try + { + using var str = File.OpenRead(item); + json = await JsonNode.ParseAsync(str); + } + catch { - JsonNode? json; - try - { - json = JsonNode.Parse(File.ReadAllText(item)); - } - catch - { - continue; - } - - if (json == null || (bool?)json["bIsExecutable"] != true) - continue; - - var dir = (string?)json["InstallLocation"]; - if (dir == null || !Directory.Exists(dir)) - continue; - - var name = (string?)json["DisplayName"]; - if (name == null) - continue; - - GameManager.TryAddGame(dir, name, this, null, out _); + return; } + + if (json == null || (bool?)json["bIsExecutable"] != true) + return; + + var dir = (string?)json["InstallLocation"]; + if (dir == null || !Directory.Exists(dir)) + return; + + var name = (string?)json["DisplayName"]; + if (name == null) + return; + + GameManager.TryAddGame(dir, name, this, null, out _); } } #endif \ No newline at end of file diff --git a/MelonLoader.Installer/GameLaunchers/GameLauncher.cs b/MelonLoader.Installer/GameLaunchers/GameLauncher.cs index c8f5cea..2c7b81e 100644 --- a/MelonLoader.Installer/GameLaunchers/GameLauncher.cs +++ b/MelonLoader.Installer/GameLaunchers/GameLauncher.cs @@ -13,5 +13,5 @@ public abstract class GameLauncher(string iconPath) public string IconPath => iconPath; - public abstract void AddGames(); + public abstract void GetAddGameTasks(List tasks); } diff --git a/MelonLoader.Installer/GameLaunchers/GogLauncher.cs b/MelonLoader.Installer/GameLaunchers/GogLauncher.cs index 77ef37e..07c63ee 100644 --- a/MelonLoader.Installer/GameLaunchers/GogLauncher.cs +++ b/MelonLoader.Installer/GameLaunchers/GogLauncher.cs @@ -3,8 +3,6 @@ namespace MelonLoader.Installer.GameLaunchers; -#pragma warning disable CA1416 - public class GogLauncher : GameLauncher { private static readonly RegistryKey? gamesKey; @@ -16,7 +14,7 @@ static GogLauncher() gamesKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\WOW6432Node\GOG.com\Games"); } - public override void AddGames() + public override void GetAddGameTasks(List tasks) { if (gamesKey == null) return; @@ -34,7 +32,7 @@ public override void AddGames() if (name == null) continue; - GameManager.TryAddGame(path, name, this, null, out _); + tasks.Add(Task.Run(() => GameManager.TryAddGame(path, name, this, null, out _))); } } } diff --git a/MelonLoader.Installer/GameLaunchers/SteamLauncher.cs b/MelonLoader.Installer/GameLaunchers/SteamLauncher.cs index cc3b880..9d572ac 100644 --- a/MelonLoader.Installer/GameLaunchers/SteamLauncher.cs +++ b/MelonLoader.Installer/GameLaunchers/SteamLauncher.cs @@ -4,8 +4,6 @@ namespace MelonLoader.Installer.GameLaunchers; -#pragma warning disable CA1416 - public class SteamLauncher : GameLauncher { private static readonly string? steamPath; @@ -28,7 +26,7 @@ static SteamLauncher() internal SteamLauncher() : base("/Assets/steam.png") { } - public override void AddGames() + public override void GetAddGameTasks(List tasks) { if (steamPath == null) return; @@ -49,30 +47,38 @@ public override void AddGames() var acfs = Directory.EnumerateFiles(steamapps, "*.acf"); foreach (var acfPath in acfs) { - VToken acf; - try - { - acf = VdfConvert.Deserialize(File.ReadAllText(acfPath)).Value; - } - catch - { - continue; - } - - var id = ((VProperty?)acf.FirstOrDefault(x => ((VProperty)x).Key == "appid"))?.Value?.ToString(); - var name = ((VProperty?)acf.FirstOrDefault(x => ((VProperty)x).Key == "name"))?.Value?.ToString(); - var dirName = ((VProperty?)acf.FirstOrDefault(x => ((VProperty)x).Key == "installdir"))?.Value?.ToString(); - - if (id == null || name == null || dirName == null) - continue; - - var appDir = Path.Combine(steamapps, "common", dirName); - if (!Directory.Exists(appDir)) - continue; - - var iconPath = Path.Combine(steamPath, "appcache", "librarycache", id + "_icon.jpg"); - GameManager.TryAddGame(appDir, name, this, iconPath, out _); + tasks.Add(AddGameAsync(acfPath, steamapps)); } } } + + private async Task AddGameAsync(string acfPath, string steamapps) + { + if (steamPath == null) + return; + + VToken acf; + try + { + acf = VdfConvert.Deserialize(await File.ReadAllTextAsync(acfPath)).Value; + } + catch + { + return; + } + + var id = ((VProperty?)acf.FirstOrDefault(x => ((VProperty)x).Key == "appid"))?.Value?.ToString(); + var name = ((VProperty?)acf.FirstOrDefault(x => ((VProperty)x).Key == "name"))?.Value?.ToString(); + var dirName = ((VProperty?)acf.FirstOrDefault(x => ((VProperty)x).Key == "installdir"))?.Value?.ToString(); + + if (id == null || name == null || dirName == null) + return; + + var appDir = Path.Combine(steamapps, "common", dirName); + if (!Directory.Exists(appDir)) + return; + + var iconPath = Path.Combine(steamPath, "appcache", "librarycache", id + "_icon.jpg"); + GameManager.TryAddGame(appDir, name, this, iconPath, out _); + } } diff --git a/MelonLoader.Installer/GameManager.cs b/MelonLoader.Installer/GameManager.cs index 28d5065..f1af819 100644 --- a/MelonLoader.Installer/GameManager.cs +++ b/MelonLoader.Installer/GameManager.cs @@ -9,34 +9,44 @@ namespace MelonLoader.Installer; internal static class GameManager { - private static bool inited; + public static bool Initialized { get; private set; } public static ObservableCollection Games { get; } = []; - public static void Init() + public static Task InitAsync(InstallProgressEventHandler onProgress) { - if (inited) - return; + if (Initialized) + return Task.CompletedTask; + + Initialized = true; - inited = true; + onProgress?.Invoke(0, "Loading game library"); + var finishedTasks = 0; + var tasks = new List(); foreach (var launcher in GameLauncher.Launchers) { - launcher.AddGames(); + launcher.GetAddGameTasks(tasks); + } + + GetLoadSavedGamesTasks(tasks); + + var contTasks = new Task[tasks.Count]; + + for (var i = 0; i < tasks.Count; i++) + { + contTasks[i] = tasks[i].ContinueWith((t) => onProgress?.Invoke(finishedTasks++ / (double)tasks.Count, null)); } - LoadSavedGames(); + return Task.WhenAll(contTasks); } - private static void LoadSavedGames() + private static void GetLoadSavedGamesTasks(List tasks) { foreach (var gamePath in Config.LoadGameList()) { - TryAddGame(gamePath, null, null, null, out _); + tasks.Add(Task.Run(() => TryAddGame(gamePath, null, null, null, out _))); } - - // In case it was manually edited or if any games were removed - SaveManualGameList(); } public static void SaveManualGameList() @@ -60,6 +70,7 @@ private static void AddGameSorted(GameModel game) Games.Insert(i, game); return; } + if (!gameHasMl && iHasMl) continue; @@ -106,7 +117,7 @@ public static void RemoveGame(GameModel game) var dataDirs = rawDataDirs.Where(x => File.Exists(x[..^5] + ".exe")).ToArray(); if (dataDirs.Length == 0) { - dataDirs = rawDataDirs.Where(x => File.Exists(x[..^5] + ".x86_64")).ToArray(); + dataDirs = [.. rawDataDirs.Where(x => File.Exists(x[..^5] + ".x86_64"))]; if (dataDirs.Length != 0) { linux = true; @@ -117,7 +128,7 @@ public static void RemoveGame(GameModel game) return null; } } - + if (dataDirs.Length > 1) { errorMessage = "The selected directory contains multiple Unity games?"; @@ -126,10 +137,13 @@ public static void RemoveGame(GameModel game) var exe = dataDirs[0][..^5] + (linux ? ".x86_64" : ".exe"); - if (Games.Any(x => x.Path.Equals(exe, StringComparison.OrdinalIgnoreCase))) + lock (Games) { - errorMessage = "Game is already listed."; - return null; + if (Games.Any(x => x.Path.Equals(exe, StringComparison.OrdinalIgnoreCase))) + { + errorMessage = "Game is already listed."; + return null; + } } var is64 = true; @@ -172,7 +186,17 @@ public static void RemoveGame(GameModel game) var result = new GameModel(exe, customName ?? Path.GetFileNameWithoutExtension(exe), !is64, linux, launcher, icon, mlVersion, isProtected); errorMessage = null; - AddGameSorted(result); + lock (Games) + { + // We're doing it again, in case another Task managed to already add a copy of the same game at this point + if (Games.Any(x => x.Path.Equals(exe, StringComparison.OrdinalIgnoreCase))) + { + errorMessage = "Game is already listed."; + return null; + } + + AddGameSorted(result); + } return result; } diff --git a/MelonLoader.Installer/IconExtractor.cs b/MelonLoader.Installer/IconExtractor.cs index 1542f3b..a464452 100644 --- a/MelonLoader.Installer/IconExtractor.cs +++ b/MelonLoader.Installer/IconExtractor.cs @@ -3,8 +3,6 @@ namespace MelonLoader.Installer; -#pragma warning disable CA1416 - internal class IconExtractor { public static Avalonia.Media.Imaging.Bitmap? GetExeIcon(string filePath) diff --git a/MelonLoader.Installer/InstallerUtils.cs b/MelonLoader.Installer/InstallerUtils.cs index e69c267..b98877b 100644 --- a/MelonLoader.Installer/InstallerUtils.cs +++ b/MelonLoader.Installer/InstallerUtils.cs @@ -51,8 +51,8 @@ static InstallerUtils() var buffer = new byte[1024 * 16]; while (position < destination.Length - 1) { - var read = await content.ReadAsync(buffer, 0, buffer.Length); - await destination.WriteAsync(buffer, 0, read); + var read = await content.ReadAsync(buffer); + await destination.WriteAsync(buffer.AsMemory(0, read)); position += read; diff --git a/MelonLoader.Installer/MLManager.cs b/MelonLoader.Installer/MLManager.cs index e7c9500..4b243a0 100644 --- a/MelonLoader.Installer/MLManager.cs +++ b/MelonLoader.Installer/MLManager.cs @@ -1,5 +1,4 @@ using Semver; -using System.Diagnostics.CodeAnalysis; using System.Text.Json.Nodes; #if WINDOWS @@ -10,8 +9,7 @@ namespace MelonLoader.Installer; internal static class MLManager { - private static bool inited; - internal static readonly string[] proxyNames = + internal static readonly string[] proxyNames = [ "version.dll", "winmm.dll", @@ -26,6 +24,8 @@ internal static class MLManager public static List Versions { get; } = []; + public static bool RefreshedOnce { get; private set; } + static MLManager() { Program.Exiting += HandleExit; @@ -43,41 +43,31 @@ private static void HandleExit() } } - public static async Task Init() - { - if (inited) - return true; - - inited = await RefreshVersions(); - return inited; - } - - private static Task RefreshVersions() + public static async Task RefreshVersionsAsync(InstallProgressEventHandler onProgress) { Versions.Clear(); if (localBuild != null) Versions.Add(localBuild); - return GetVersionsAsync(Versions); - } + onProgress?.Invoke(0, "Fetching MelonLoader workflow builds"); - private static async Task GetVersionsAsync(List versions) - { HttpResponseMessage resp; try { - resp = await InstallerUtils.Http.GetAsync(Config.MelonLoaderBuildWorkflowApi).ConfigureAwait(false); + resp = await InstallerUtils.Http.GetAsync(Config.MelonLoaderBuildWorkflowApi); } - catch + catch (Exception ex) { - return false; + return $"Failed to fetch MelonLoader workflows: {ex.Message}"; } if (!resp.IsSuccessStatusCode) - return false; + return $"Failed to fetch MelonLoader CI versions: Status code '{resp.StatusCode}'"; - var relStr = await resp.Content.ReadAsStringAsync().ConfigureAwait(false); + onProgress?.Invoke(0.4, "Adding MelonLoader workflow builds"); + + var relStr = await resp.Content.ReadAsStringAsync(); var runsJson = JsonNode.Parse(relStr)!["workflow_runs"]!.AsArray(); // All run names must follow the following format: "{SemVersion} Remaining name" @@ -90,7 +80,7 @@ private static async Task GetVersionsAsync(List versions) if (!SemVersion.TryParse(runName[..runVerEnd], SemVersionStyles.Any, out var runVersion)) continue; - + var version = new MLVersion { Version = runVersion, @@ -102,22 +92,26 @@ private static async Task GetVersionsAsync(List versions) if (version.DownloadUrlWin == null && version.DownloadUrlWinX86 == null && version.DownloadUrlLinux == null) continue; - versions.Add(version); + Versions.Add(version); } + onProgress?.Invoke(0.5, "Fetching MelonLoader releases"); + try { - resp = await InstallerUtils.Http.GetAsync(Config.MelonLoaderReleasesApi).ConfigureAwait(false); + resp = await InstallerUtils.Http.GetAsync(Config.MelonLoaderReleasesApi); } - catch + catch (Exception ex) { - return false; + return $"Failed to fetch MelonLoader releases: {ex.Message}"; } if (!resp.IsSuccessStatusCode) - return false; + return $"Failed to fetch MelonLoader releases: Status code '{resp.StatusCode}'"; - relStr = await resp.Content.ReadAsStringAsync().ConfigureAwait(false); + onProgress?.Invoke(0.9, "Adding MelonLoader releases"); + + relStr = await resp.Content.ReadAsStringAsync(); var releasesJson = JsonNode.Parse(relStr)!.AsArray(); foreach (var release in releasesJson) @@ -143,10 +137,11 @@ private static async Task GetVersionsAsync(List versions) if (version.DownloadUrlWin == null && version.DownloadUrlWinX86 == null && version.DownloadUrlLinux == null) continue; - versions.Add(version); + Versions.Add(version); } - return true; + RefreshedOnce = true; + return null; } public static string? Uninstall(string gameDir, bool removeUserFiles) @@ -387,6 +382,7 @@ void SetProgress(double progress, string? newStatus = null) onFinished?.Invoke("Failed to download MelonLoader: " + result); return; } + bufferStr.Seek(0, SeekOrigin.Begin); currentTask++; diff --git a/MelonLoader.Installer/MLVersion.cs b/MelonLoader.Installer/MLVersion.cs index f6e9900..1f1dd66 100644 --- a/MelonLoader.Installer/MLVersion.cs +++ b/MelonLoader.Installer/MLVersion.cs @@ -16,7 +16,7 @@ public class MLVersion { x86 = false; linux = false; - + var mlDir = Path.Combine(gameDir, "MelonLoader"); if (!Directory.Exists(mlDir)) return null; @@ -36,6 +36,7 @@ public class MLVersion } } } + if (mlAssemblyPath == null) return null; @@ -45,7 +46,7 @@ public class MLVersion var fileVersionRaw = FileVersionInfo.GetVersionInfo(mlAssemblyPath).FileVersion!; var fileVersion = System.Version.Parse(fileVersionRaw); version = SemVersion.ParsedFrom(fileVersion.Major, fileVersion.Minor, fileVersion.Build, - fileVersion.Revision == 0 ? string.Empty : $"ci.{fileVersion.Revision}"); + fileVersion.Revision <= 0 ? string.Empty : $"ci.{fileVersion.Revision}"); } catch { diff --git a/MelonLoader.Installer/MelonLoader.Installer.csproj b/MelonLoader.Installer/MelonLoader.Installer.csproj index 901bdbb..d15095c 100644 --- a/MelonLoader.Installer/MelonLoader.Installer.csproj +++ b/MelonLoader.Installer/MelonLoader.Installer.csproj @@ -15,10 +15,10 @@ en true true + MelonLoader.Installer.Program Lava Gang discord.gg/2Wn3N2P - MelonLoader.Installer.Program @@ -42,13 +42,14 @@ - + + - + diff --git a/MelonLoader.Installer/Program.cs b/MelonLoader.Installer/Program.cs index 728cc61..50cb8a1 100644 --- a/MelonLoader.Installer/Program.cs +++ b/MelonLoader.Installer/Program.cs @@ -1,6 +1,14 @@ using Avalonia; using System.Diagnostics; -using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +[assembly: SupportedOSPlatform( +#if WINDOWS + "windows" +#elif LINUX + "linux" +#endif +)] namespace MelonLoader.Installer; @@ -22,9 +30,6 @@ internal static class Program [STAThread] private static void Main(string[] args) { - if (!Directory.Exists(Config.CacheDir)) - Directory.CreateDirectory(Config.CacheDir); - if (args.Length >= 3) { if (!int.TryParse(args[2], out var pid)) @@ -42,11 +47,6 @@ private static void Main(string[] args) } } -#if WINDOWS - if (Updater.CheckLegacyUpdate()) - return; -#endif - if (!CheckProcessLock()) return; @@ -60,7 +60,7 @@ private static void Main(string[] args) } Exiting?.Invoke(); - + processLock.Dispose(); File.Delete(processLockPath); } @@ -99,9 +99,9 @@ private static bool CheckProcessLock() GrabAttention(proc); return false; } - catch + catch { - return false; + return false; } } #else diff --git a/MelonLoader.Installer/Updater.cs b/MelonLoader.Installer/Updater.cs index 046257e..447b01a 100644 --- a/MelonLoader.Installer/Updater.cs +++ b/MelonLoader.Installer/Updater.cs @@ -1,36 +1,51 @@ using System.Diagnostics; -using System.Runtime.InteropServices; using System.Text.Json.Nodes; +#if LINUX +using System.Runtime.InteropServices; +#endif + namespace MelonLoader.Installer; public static partial class Updater { - public static volatile UpdateState State; - public static event InstallProgressEventHandler? Progress; - - public static async Task UpdateIfPossible() + public static async Task UpdateIfPossibleAsync(InstallProgressEventHandler onProgress) { - if (State != UpdateState.None) - return null; - // Don't auto-update on CI builds if (Program.Version.Revision > 0) - { - State = UpdateState.AlreadyChecked; - return null; - } + return false; + + onProgress?.Invoke(0, "Checking for updates"); var downloadUrl = await CheckForUpdateAsync(); if (downloadUrl == null) + return false; + + var newPath = Path.GetTempFileName() +#if WINDOWS + + ".exe" +#endif + ; + + onProgress?.Invoke(0, "Updating"); + + await using (var newStr = File.OpenWrite(newPath)) { - State = UpdateState.AlreadyChecked; - return null; + var result = await InstallerUtils.DownloadFileAsync(downloadUrl, newStr, (progress, newStatus) => onProgress?.Invoke(progress, newStatus)); + if (result != null) + { + throw new Exception("Failed to download the latest installer version: " + result); + } } - State = UpdateState.Updating; +#if LINUX + // Make the file executable on Unix + Chmod(newPath, S_IRUSR | S_IXUSR | S_IWUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); +#endif + + Process.Start(newPath, ["-handleupdate", Environment.ProcessPath!, Environment.ProcessId.ToString()]); - return Task.Run(() => UpdateAsync(downloadUrl)); + return true; } public static bool WaitAndRemoveApp(string originalPath, int prevPID) @@ -76,52 +91,6 @@ public static void HandleUpdate(string originalPath, int prevPID) Process.Start(originalPath, ["-cleanup", Environment.ProcessPath!, Environment.ProcessId.ToString()]); } - private static async Task UpdateAsync(string downloadUrl) - { - var newPath = Path.GetTempFileName() -#if WINDOWS - + ".exe" -#endif - ; - - await using (var newStr = File.OpenWrite(newPath)) - { - var result = await InstallerUtils.DownloadFileAsync(downloadUrl, newStr, (progress, newStatus) => Progress?.Invoke(progress, newStatus)); - if (result != null) - { - throw new Exception("Failed to download the latest installer version: " + result); - } - } - -#if LINUX - // Make the file executable on Unix - Chmod(newPath, S_IRUSR | S_IXUSR | S_IWUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); -#endif - - Process.Start(newPath, ["-handleupdate", Environment.ProcessPath!, Environment.ProcessId.ToString()]); - - State = UpdateState.Done; - } - -#if WINDOWS - public static bool CheckLegacyUpdate() - { - if (!Environment.ProcessPath!.EndsWith(".tmp.exe", StringComparison.OrdinalIgnoreCase)) - return false; - - var dir = Path.GetDirectoryName(Environment.ProcessPath)!; - var name = Path.GetFileNameWithoutExtension(Environment.ProcessPath); - name = name.Remove(name.Length - 4) + ".exe"; - - var final = Path.Combine(dir, name); - - var prevProc = Process.GetProcessesByName(name); - - HandleUpdate(final, prevProc.FirstOrDefault()?.Id ?? 0); - return true; - } -#endif - private static async Task CheckForUpdateAsync() { HttpResponseMessage response; @@ -161,10 +130,10 @@ public static bool CheckLegacyUpdate() ".Linux" #endif )); - + return asset?["browser_download_url"]?.ToString(); } - + #if LINUX // user permissions const int S_IRUSR = 0x100; @@ -182,12 +151,4 @@ public static bool CheckLegacyUpdate() [LibraryImport("libc", EntryPoint = "chmod", StringMarshalling = StringMarshalling.Utf8)] private static partial int Chmod(string pathname, int mode); #endif - - public enum UpdateState - { - None, - Updating, - Done, - AlreadyChecked - } } diff --git a/MelonLoader.Installer/ViewModels/DialogBoxModel.cs b/MelonLoader.Installer/ViewModels/DialogBoxModel.cs index 897c853..fcbdbb8 100644 --- a/MelonLoader.Installer/ViewModels/DialogBoxModel.cs +++ b/MelonLoader.Installer/ViewModels/DialogBoxModel.cs @@ -3,8 +3,8 @@ public class DialogBoxModel : ViewModelBase { public required string Message { get; init; } - public string ConfirmText { get; init; } = "YES"; - public string CancelText { get; init; } = "NO"; + public string ConfirmText { get; init; } = "Yes"; + public string CancelText { get; init; } = "No"; public bool IsError { get; init; } public bool IsConfirmation { get; init; } } diff --git a/MelonLoader.Installer/ViewModels/GameModel.cs b/MelonLoader.Installer/ViewModels/GameModel.cs index 1bfd0b3..670336c 100644 --- a/MelonLoader.Installer/ViewModels/GameModel.cs +++ b/MelonLoader.Installer/ViewModels/GameModel.cs @@ -38,9 +38,9 @@ public SemVersion? MLVersion public bool ValidateGame() { var exeExtIdx = path.LastIndexOf('.'); - + var pathNoExt = exeExtIdx != -1 ? path[..exeExtIdx] : path; - + if (!File.Exists(path) || !Directory.Exists(pathNoExt + "_Data")) { GameManager.RemoveGame(this); @@ -50,10 +50,10 @@ public bool ValidateGame() var newMlVersion = Installer.MLVersion.GetMelonLoaderVersion(Dir, out var ml86, out var mlLinux); if (newMlVersion != null && (ml86 != Is32Bit || mlLinux != IsLinux)) newMlVersion = null; - - if (newMlVersion == MLVersion) + + if (newMlVersion == MLVersion) return true; - + MLVersion = newMlVersion; GameManager.ResortGame(this); diff --git a/MelonLoader.Installer/ViewModels/MainViewModel.cs b/MelonLoader.Installer/ViewModels/MainViewModel.cs index f2ae8bd..bd3973f 100644 --- a/MelonLoader.Installer/ViewModels/MainViewModel.cs +++ b/MelonLoader.Installer/ViewModels/MainViewModel.cs @@ -5,9 +5,6 @@ namespace MelonLoader.Installer.ViewModels; public partial class MainViewModel : ViewModelBase { - [ObservableProperty] - private bool ready; - [ObservableProperty] private string? search; diff --git a/MelonLoader.Installer/Views/DetailsView.axaml b/MelonLoader.Installer/Views/DetailsView.axaml index 66213ee..0b2d662 100644 --- a/MelonLoader.Installer/Views/DetailsView.axaml +++ b/MelonLoader.Installer/Views/DetailsView.axaml @@ -3,81 +3,83 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="clr-namespace:MelonLoader.Installer.ViewModels" - mc:Ignorable="d" d:DesignWidth="450" d:DesignHeight="650" + mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="650" x:Class="MelonLoader.Installer.Views.DetailsView" x:DataType="vm:DetailsViewModel"> - - - - Select a version - - Or select a - zipped version - - Enable Nightly builds - Keep mods and settings - - - - - + + + + + + + - - - + + + Select a version + + + Or select a + zipped version + + + Enable Nightly builds + Keep mods and settings + + + + + + + + + + + + + How do I start MelonLoader? + - - - How do I start MelonLoader? - - Linux Launch Instructions - - In order to start MelonLoader under Wine, you'll need to export the following variable: - WINEDLLOVERRIDES="version=n,b" - On Steam, you can set the launch options to: - WINEDLLOVERRIDES="version=n,b" %command% - - - In order to start MelonLoader, you'll need to export the following variables: - - LD_PRELOAD="libversion.so" - On Steam, you can set the launch options to: - - + + In order to start MelonLoader under Wine, you'll need to export the following variable: + WINEDLLOVERRIDES="version=n,b" + On Steam, you can set the launch options to: + WINEDLLOVERRIDES="version=n,b" %command% + + + In order to start MelonLoader, you'll need to export the following variables: + + LD_PRELOAD="libversion.so" + On Steam, you can set the launch options to: + + \ No newline at end of file diff --git a/MelonLoader.Installer/Views/DetailsView.axaml.cs b/MelonLoader.Installer/Views/DetailsView.axaml.cs index e3e9320..67be99f 100644 --- a/MelonLoader.Installer/Views/DetailsView.axaml.cs +++ b/MelonLoader.Installer/Views/DetailsView.axaml.cs @@ -5,13 +5,12 @@ using Avalonia.Threading; using MelonLoader.Installer.ViewModels; using System.ComponentModel; -using System.Runtime.InteropServices; namespace MelonLoader.Installer.Views; public partial class DetailsView : UserControl { - public DetailsViewModel? Model => (DetailsViewModel?)DataContext; + public DetailsViewModel? Model => DataContext as DetailsViewModel; public DetailsView() { @@ -35,14 +34,14 @@ private void PropertyChangedHandler(object? sender, PropertyChangedEventArgs cha UpdateVersionInfo(); } } - - protected override async void OnDataContextChanged(EventArgs e) + + protected override void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); if (Model == null) return; - + #if LINUX if (Model.Game.IsLinux) { @@ -56,12 +55,6 @@ protected override async void OnDataContextChanged(EventArgs e) Model.Game.PropertyChanged += PropertyChangedHandler; UpdateVersionList(); - - if (!await MLManager.Init()) - { - Model.Offline = true; - DialogBox.ShowError("Failed to fetch MelonLoader releases. Ensure you're online."); - } } private void NightlyToggleHandler(object sender, RoutedEventArgs args) @@ -78,6 +71,8 @@ public void UpdateVersionList() if (NightlyCheck.IsChecked != true) en = en.Where(x => !x.Version.IsPrerelease || x.IsLocalPath); + Model.Offline = !en.Any(); + VersionCombobox.ItemsSource = en; VersionCombobox.SelectedIndex = 0; } @@ -86,13 +81,13 @@ private void BackClickHandler(object sender, RoutedEventArgs args) { if (Model == null) return; - + if (Model.LinuxInstructions) { Model.LinuxInstructions = false; return; } - + if (Model.Installing) return; @@ -176,9 +171,9 @@ private void OnOperationFinished(string? errorMessage, bool addedLocalBuild = fa if (addedLocalBuild) return; - bool isInstall = true; - string operationType = Model.Game.MLInstalled ? "Installed" : "Uninstalled"; - if (Model.Game.MLInstalled + var isInstall = true; + var operationType = Model.Game.MLInstalled ? "Installed" : "Uninstalled"; + if (Model.Game.MLInstalled && (Model.Game.MLVersion != null) && (currentMLVersion != null)) { @@ -192,7 +187,7 @@ private void OnOperationFinished(string? errorMessage, bool addedLocalBuild = fa }; } - DialogBox.ShowNotice("SUCCESS!", $"Successfully {operationType}{((!Model.Game.MLInstalled || isInstall) ? string.Empty : " to")}\nMelonLoader v{(Model.Game.MLInstalled ? Model.Game.MLVersion : currentMLVersion)}"); + DialogBox.ShowNotice("Success!", $"Successfully {operationType}{((!Model.Game.MLInstalled || isInstall) ? string.Empty : " to")}\nMelonLoader v{(Model.Game.MLInstalled ? Model.Game.MLVersion : currentMLVersion)}"); } private void OpenDirHandler(object sender, RoutedEventArgs args) @@ -268,7 +263,7 @@ private void ShowLinuxInstructionsHandler(object sender, TappedEventArgs args) { if (Model == null) return; - + Model.LinuxInstructions = true; } } \ No newline at end of file diff --git a/MelonLoader.Installer/Views/DialogBox.axaml b/MelonLoader.Installer/Views/DialogBox.axaml index a042d9c..9e2cf78 100644 --- a/MelonLoader.Installer/Views/DialogBox.axaml +++ b/MelonLoader.Installer/Views/DialogBox.axaml @@ -11,7 +11,7 @@ WindowStartupLocation="CenterScreen" Background="#121217" CanResize="False" - Padding="15" + Padding="40 15" Height="100" SizeToContent="WidthAndHeight" RenderOptions.BitmapInterpolationMode="HighQuality"> @@ -22,7 +22,7 @@ - + + + + + + + + + + + + + + + + + + + + No Unity games have been found on your device. Please add one manually using the button below. - - Loading... - - - - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/MelonLoader.Installer/Views/MainView.axaml.cs b/MelonLoader.Installer/Views/MainView.axaml.cs index 570326b..9712fed 100644 --- a/MelonLoader.Installer/Views/MainView.axaml.cs +++ b/MelonLoader.Installer/Views/MainView.axaml.cs @@ -8,7 +8,9 @@ namespace MelonLoader.Installer.Views; public partial class MainView : UserControl { private static bool showedNotice; - + + private static DateTime _lastTimeCheckedVersions; + public MainViewModel? Model => (MainViewModel?)DataContext; public MainView() @@ -20,20 +22,25 @@ protected override async void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); - if (Model == null) + if (Model == null || MainWindow.Instance == null) return; - // if the updater has already ran, we already did all initialization - if (Updater.State == Updater.UpdateState.None) + if (!GameManager.Initialized) { - Model.Ready = false; - if (!await DoInit()) - { - return; - } - Model.Ready = true; + await GameManager.InitAsync(MainWindow.Instance.SetLoadStatus); } + if (DateTime.UtcNow - _lastTimeCheckedVersions > TimeSpan.FromMinutes(3)) + { + _lastTimeCheckedVersions = DateTime.UtcNow; + + var error = await MLManager.RefreshVersionsAsync(MainWindow.Instance.SetLoadStatus); + if (error != null) + DialogBox.ShowError(error); + } + + MainWindow.Instance.FinishLoad(); + OnGameListUpdate(null, null); GameManager.Games.CollectionChanged += OnGameListUpdate; @@ -42,42 +49,11 @@ protected override async void OnDataContextChanged(EventArgs e) showedNotice = true; DialogBox.ShowNotice(""" You're currently using a bleeding-edge CI build. - Please note that this build will not auto-update, so it's recommended to use a stable release instead. + Please note that this build will not auto-update, so it's recommended to use a stable one instead. """); } } - private static async Task DoInit() - { - try - { - var checkUpdate = Task.Run(Updater.UpdateIfPossible); - var otherInit = Task.WhenAll(Task.Run(MLManager.Init), Task.Factory.StartNew(GameManager.Init, TaskCreationOptions.LongRunning)); - if (await checkUpdate is { } updateTask) - { - _ = MainWindow.Instance.HandleUpdate(updateTask); - return false; - } - await otherInit; - } - catch (Exception ex) - { - CrashException(ex); - } - return true; - } - - private static void CrashException(Exception ex) - { - Program.LogCrashException(ex); - - DialogBox.ShowError(""" - An error has occurred while loading the game library! - Please report this issue in the official Discord server in the #ml-support channel. - Include the crash log named 'melonloader-installer-crash.log', located next to the executable. - """, () => MainWindow.Instance.Close()); - } - protected override void OnUnloaded(RoutedEventArgs e) { base.OnUnloaded(e); diff --git a/MelonLoader.Installer/Views/MainWindow.axaml b/MelonLoader.Installer/Views/MainWindow.axaml index f40c41a..9db940e 100644 --- a/MelonLoader.Installer/Views/MainWindow.axaml +++ b/MelonLoader.Installer/Views/MainWindow.axaml @@ -3,7 +3,8 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" - Width="500" Height="700" + MinWidth="700" MinHeight="700" + Width="1200" Height="800" x:Class="MelonLoader.Installer.Views.MainWindow" Icon="/Assets/icon.ico" Title="MelonLoader Installer" diff --git a/MelonLoader.Installer/Views/MainWindow.axaml.cs b/MelonLoader.Installer/Views/MainWindow.axaml.cs index 63eddcb..fd5a097 100644 --- a/MelonLoader.Installer/Views/MainWindow.axaml.cs +++ b/MelonLoader.Installer/Views/MainWindow.axaml.cs @@ -9,28 +9,43 @@ public partial class MainWindow : Window { public static MainWindow Instance { get; private set; } = null!; + private static ViewState? _lastViewState; + + public static bool InLoadView => _currentLoadView != null; + + private static LoadView? _currentLoadView; + + private static readonly List _closeLocks = []; + public MainWindow() { Instance = this; InitializeComponent(); - ShowMainView(); + InitAsync(); } - public async Task HandleUpdate(Task updaterTask) + public async void InitAsync() { - try - { - SetViewport(new UpdaterView()); - await updaterTask; - Close(); - } - catch (Exception ex) + using (RequestCloseLock()) { - DialogBox.ShowError(ex.Message); - ShowMainView(); + if (await Updater.UpdateIfPossibleAsync(SetLoadStatus)) + { + Close(); + return; + } } + + ShowMainView(); + FinishLoad(); + } + + public static CloseLock RequestCloseLock() + { + var loc = new CloseLock(); + _closeLocks.Add(loc); + return loc; } protected override void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) @@ -39,7 +54,7 @@ protected override void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) if (!IsVisible) return; - + Topmost = true; Topmost = false; #if WINDOWS @@ -50,27 +65,107 @@ protected override void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) protected override void OnClosing(WindowClosingEventArgs e) { - if (Updater.State == Updater.UpdateState.Updating || Content is DetailsView { Model.Installing: true }) - e.Cancel = true; + if (_closeLocks.Count != 0) + return; base.OnClosing(e); } - public void SetViewport(UserControl view) + public void SetViewport(ViewModelBase? model = null) where T : UserControl, new() { - Viewport.Child = view; + if (!Dispatcher.UIThread.CheckAccess()) + { + Dispatcher.UIThread.Invoke(() => SetViewport(model)); + return; + } + + _lastViewState = new ViewState(model); + + if (!InLoadView) + { + Viewport.Child = _lastViewState.Get(); + } } public void ShowMainView() { - SetViewport(new MainView()); + SetViewport(); + } + + public void SetLoadStatus(double progress, string? newStatus) + { + if (!Dispatcher.UIThread.CheckAccess()) + { + Dispatcher.UIThread.Invoke(() => SetLoadStatus(progress, newStatus)); + return; + } + + if (_currentLoadView == null) + { + if (_lastViewState != null) + { + _lastViewState.Model = Viewport.Child?.DataContext as ViewModelBase; + } + + _currentLoadView = new LoadView(); + Viewport.Child = _currentLoadView; + } + + _currentLoadView.SetProgress(progress, newStatus); + } + + public void FinishLoad() + { + if (!Dispatcher.UIThread.CheckAccess()) + { + Dispatcher.UIThread.Invoke(FinishLoad); + return; + } + + if (!InLoadView) + return; + + _currentLoadView = null; + + if (_lastViewState == null) + { + Viewport.Child = null; + return; + } + + Viewport.Child = _lastViewState.Get(); } public void ShowDetailsView(GameModel game) { - SetViewport(new DetailsView() + SetViewport(new DetailsViewModel(game)); + } + + public class CloseLock : IDisposable + { + public void Dispose() { - DataContext = new DetailsViewModel(game) - }); + _closeLocks.Remove(this); + GC.SuppressFinalize(this); + } + } + + private abstract class ViewState(ViewModelBase? model) + { + public ViewModelBase? Model { get; set; } = model; + + public abstract UserControl Get(); + } + + private class ViewState(ViewModelBase? model) : ViewState(model) where T : UserControl, new() + { + public override UserControl Get() + { + var obj = new T(); + if (Model != null) + obj.DataContext = Model; + + return obj; + } } } diff --git a/MelonLoader.Installer/Views/UpdaterView.axaml b/MelonLoader.Installer/Views/UpdaterView.axaml deleted file mode 100644 index fbac528..0000000 --- a/MelonLoader.Installer/Views/UpdaterView.axaml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - Updating... - - - \ No newline at end of file diff --git a/MelonLoader.Installer/Views/UpdaterView.axaml.cs b/MelonLoader.Installer/Views/UpdaterView.axaml.cs deleted file mode 100644 index 691604c..0000000 --- a/MelonLoader.Installer/Views/UpdaterView.axaml.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Threading; - -namespace MelonLoader.Installer.Views; - -public partial class UpdaterView : UserControl -{ - public UpdaterView() - { - InitializeComponent(); - - Updater.Progress += (progress, _) => Dispatcher.UIThread.Post(() => OnProgress(progress)); - } - - private void OnProgress(double progress) - { - Progress.Value = progress * 100; - } -} \ No newline at end of file