From e3b9afcf642759f3001c2ebfe8f925c51d5fb29c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:37:14 +0000 Subject: [PATCH 1/6] Initial plan From b5c7d3f42ce75b47430ee2521dc856ed7d290626 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:41:17 +0000 Subject: [PATCH 2/6] Add new fields and .csproj reading logic Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- src/Common/CsprojReader.cs | 175 ++++++++++++++++++++++++++++++ src/Models/ConfigInfo.cs | 33 ++++++ src/Models/PacketConfigModel.cs | 66 +++++++++++ src/ViewModels/PacketViewModel.cs | 107 ++++++++++++++++++ src/Views/PacketView.axaml | 127 +++++++++++++++++----- 5 files changed, 481 insertions(+), 27 deletions(-) create mode 100644 src/Common/CsprojReader.cs create mode 100644 src/Models/ConfigInfo.cs diff --git a/src/Common/CsprojReader.cs b/src/Common/CsprojReader.cs new file mode 100644 index 0000000..2744aef --- /dev/null +++ b/src/Common/CsprojReader.cs @@ -0,0 +1,175 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Xml.Linq; + +namespace GeneralUpdate.Tool.Avalonia.Common; + +/// +/// Utility class for reading .csproj files +/// +public static class CsprojReader +{ + /// + /// Read MainAppName from .csproj file + /// + public static string ReadMainAppName(string releaseDirectory) + { + try + { + var csprojFile = FindCsprojFile(releaseDirectory); + if (string.IsNullOrEmpty(csprojFile)) + return string.Empty; + + var doc = XDocument.Load(csprojFile); + var outputType = GetElementValue(doc, "OutputType"); + + // Check if OutputType contains WinExe/Exe (case-insensitive) + if (string.IsNullOrEmpty(outputType) || + (!outputType.Equals("WinExe", StringComparison.OrdinalIgnoreCase) && + !outputType.Equals("Exe", StringComparison.OrdinalIgnoreCase))) + { + return string.Empty; + } + + // Extract .csproj filename without extension + var projectName = Path.GetFileNameWithoutExtension(csprojFile); + + // Search for matching .exe file recursively + var exeFile = FindExeFile(releaseDirectory, projectName); + if (!string.IsNullOrEmpty(exeFile)) + { + return Path.GetFileNameWithoutExtension(exeFile); + } + + // Fallback to AssemblyName or OutputName + var assemblyName = GetElementValue(doc, "AssemblyName"); + if (!string.IsNullOrEmpty(assemblyName)) + return assemblyName; + + var outputName = GetElementValue(doc, "OutputName"); + if (!string.IsNullOrEmpty(outputName)) + return outputName; + + return string.Empty; + } + catch (Exception ex) + { + Trace.WriteLine($"Error reading MainAppName: {ex.Message}"); + return string.Empty; + } + } + + /// + /// Read ClientVersion from .csproj file or .exe file version + /// + public static string ReadClientVersion(string releaseDirectory) + { + try + { + var csprojFile = FindCsprojFile(releaseDirectory); + if (string.IsNullOrEmpty(csprojFile)) + return string.Empty; + + var doc = XDocument.Load(csprojFile); + + // Try to read Version tag + var version = GetElementValue(doc, "Version"); + if (!string.IsNullOrEmpty(version)) + return version; + + // Fallback to .exe file version + var projectName = Path.GetFileNameWithoutExtension(csprojFile); + var exeFile = FindExeFile(releaseDirectory, projectName); + + if (!string.IsNullOrEmpty(exeFile) && File.Exists(exeFile)) + { + var versionInfo = FileVersionInfo.GetVersionInfo(exeFile); + if (!string.IsNullOrEmpty(versionInfo.FileVersion)) + return versionInfo.FileVersion; + } + + return string.Empty; + } + catch (Exception ex) + { + Trace.WriteLine($"Error reading ClientVersion: {ex.Message}"); + return string.Empty; + } + } + + /// + /// Read OutputPath from .csproj file + /// + public static string ReadOutputPath(string releaseDirectory) + { + try + { + var csprojFile = FindCsprojFile(releaseDirectory); + if (string.IsNullOrEmpty(csprojFile)) + return string.Empty; + + var doc = XDocument.Load(csprojFile); + var outputPath = GetElementValue(doc, "OutputPath"); + + return outputPath ?? string.Empty; + } + catch (Exception ex) + { + Trace.WriteLine($"Error reading OutputPath: {ex.Message}"); + return string.Empty; + } + } + + /// + /// Find .csproj file in the directory + /// + private static string FindCsprojFile(string directory) + { + if (!Directory.Exists(directory)) + return string.Empty; + + var csprojFiles = Directory.GetFiles(directory, "*.csproj", SearchOption.TopDirectoryOnly); + return csprojFiles.FirstOrDefault() ?? string.Empty; + } + + /// + /// Find .exe file with matching name recursively + /// + private static string FindExeFile(string directory, string baseName) + { + if (!Directory.Exists(directory)) + return string.Empty; + + try + { + var exeFiles = Directory.GetFiles(directory, $"{baseName}.exe", SearchOption.AllDirectories); + return exeFiles.FirstOrDefault() ?? string.Empty; + } + catch (Exception ex) + { + Trace.WriteLine($"Error searching for exe file: {ex.Message}"); + return string.Empty; + } + } + + /// + /// Get element value from XDocument + /// + private static string GetElementValue(XDocument doc, string elementName) + { + try + { + // Search in all PropertyGroup elements + var elements = doc.Descendants() + .Where(e => e.Name.LocalName == elementName); + + return elements.FirstOrDefault()?.Value?.Trim() ?? string.Empty; + } + catch + { + return string.Empty; + } + } +} diff --git a/src/Models/ConfigInfo.cs b/src/Models/ConfigInfo.cs new file mode 100644 index 0000000..fa31e08 --- /dev/null +++ b/src/Models/ConfigInfo.cs @@ -0,0 +1,33 @@ +using Newtonsoft.Json; + +namespace GeneralUpdate.Tool.Avalonia.Models; + +/// +/// Configuration information for package serialization +/// +public class ConfigInfo +{ + [JsonProperty("reportUrl")] + public string ReportUrl { get; set; } + + [JsonProperty("updateUrl")] + public string UpdateUrl { get; set; } + + [JsonProperty("appName")] + public string AppName { get; set; } + + [JsonProperty("mainAppName")] + public string MainAppName { get; set; } + + [JsonProperty("clientVersion")] + public string ClientVersion { get; set; } + + [JsonProperty("packetName")] + public string PacketName { get; set; } + + [JsonProperty("format")] + public string Format { get; set; } + + [JsonProperty("encoding")] + public string Encoding { get; set; } +} diff --git a/src/Models/PacketConfigModel.cs b/src/Models/PacketConfigModel.cs index 33ffb58..3333195 100644 --- a/src/Models/PacketConfigModel.cs +++ b/src/Models/PacketConfigModel.cs @@ -5,6 +5,7 @@ namespace GeneralUpdate.Tool.Avalonia.Models; public class PacketConfigModel : ObservableObject { private string _appDirectory, _releaseDirectory, _patchDirectory, _name, _path, _driverDirectory; + private string _reportUrl, _updateUrl, _appName, _mainAppName, _clientVersion; private PlatformModel _platform; private FormatModel _format; private EncodingModel _encoding; @@ -109,4 +110,69 @@ public string DriverDirectory OnPropertyChanged(nameof(DriverDirectory)); } } + + /// + /// 报告地址 + /// + public string ReportUrl + { + get => _reportUrl; + set + { + _reportUrl = value; + OnPropertyChanged(nameof(ReportUrl)); + } + } + + /// + /// 更新地址 + /// + public string UpdateUrl + { + get => _updateUrl; + set + { + _updateUrl = value; + OnPropertyChanged(nameof(UpdateUrl)); + } + } + + /// + /// 应用程序名称 + /// + public string AppName + { + get => _appName; + set + { + _appName = value; + OnPropertyChanged(nameof(AppName)); + } + } + + /// + /// 主应用程序名称 + /// + public string MainAppName + { + get => _mainAppName; + set + { + _mainAppName = value; + OnPropertyChanged(nameof(MainAppName)); + } + } + + /// + /// 客户端版本 + /// + public string ClientVersion + { + get => _clientVersion; + set + { + _clientVersion = value; + OnPropertyChanged(nameof(ClientVersion)); + } + } } \ No newline at end of file diff --git a/src/ViewModels/PacketViewModel.cs b/src/ViewModels/PacketViewModel.cs index 762d1bb..00a1540 100644 --- a/src/ViewModels/PacketViewModel.cs +++ b/src/ViewModels/PacketViewModel.cs @@ -10,7 +10,9 @@ using CommunityToolkit.Mvvm.Input; using GeneralUpdate.Common.Compress; using GeneralUpdate.Differential; +using GeneralUpdate.Tool.Avalonia.Common; using GeneralUpdate.Tool.Avalonia.Models; +using Newtonsoft.Json; using Nlnet.Avalonia.Controls; namespace GeneralUpdate.Tool.Avalonia.ViewModels; @@ -94,6 +96,11 @@ private void ResetAction() ConfigModel.AppDirectory = GetPlatformSpecificPath(); ConfigModel.PatchDirectory = GetPlatformSpecificPath(); ConfigModel.DriverDirectory = string.Empty; + ConfigModel.ReportUrl = string.Empty; + ConfigModel.UpdateUrl = string.Empty; + ConfigModel.AppName = string.Empty; + ConfigModel.MainAppName = string.Empty; + ConfigModel.ClientVersion = string.Empty; ConfigModel.Encoding = Encodings.First(); ConfigModel.Format = Formats.First(); } @@ -142,6 +149,13 @@ private async Task BuildPacketAction() { try { + // Validate required fields + if (!ValidateRequiredFields()) + return; + + // Read configuration from .csproj + ReadProjectConfiguration(); + await DifferentialCore.Instance.Clean(ConfigModel.AppDirectory, ConfigModel.ReleaseDirectory, ConfigModel.PatchDirectory); @@ -165,6 +179,9 @@ await DifferentialCore.Instance.Clean(ConfigModel.AppDirectory, } } + // Create and save ConfigInfo JSON file + var configInfoPath = await CreateConfigInfoFile(); + var directoryInfo = new DirectoryInfo(ConfigModel.PatchDirectory); var parentDirectory = directoryInfo.Parent!.FullName; var operationType = ConfigModel.Format.Value; @@ -255,4 +272,94 @@ private void CopyDriverFiles(string sourceDir, string targetDir) CopyDriverFiles(dir, destDir); } } + + /// + /// Validate required fields + /// + private bool ValidateRequiredFields() + { + var errors = new System.Collections.Generic.List(); + + if (string.IsNullOrWhiteSpace(ConfigModel.UpdateUrl)) + errors.Add("UpdateUrl"); + + if (string.IsNullOrWhiteSpace(ConfigModel.ReportUrl)) + errors.Add("ReportUrl"); + + if (string.IsNullOrWhiteSpace(ConfigModel.AppDirectory)) + errors.Add("AppDirectory"); + + if (string.IsNullOrWhiteSpace(ConfigModel.ReleaseDirectory)) + errors.Add("ReleaseDirectory"); + + if (string.IsNullOrWhiteSpace(ConfigModel.PatchDirectory)) + errors.Add("PatchDirectory"); + + if (errors.Any()) + { + var message = $"The following required fields must be filled:\n{string.Join(", ", errors)}"; + MessageBox.ShowAsync(message, "Validation Error", Buttons.OK).Wait(); + return false; + } + + return true; + } + + /// + /// Read project configuration from .csproj file + /// + private void ReadProjectConfiguration() + { + try + { + // Read MainAppName + ConfigModel.MainAppName = CsprojReader.ReadMainAppName(ConfigModel.ReleaseDirectory); + + // Read ClientVersion + ConfigModel.ClientVersion = CsprojReader.ReadClientVersion(ConfigModel.ReleaseDirectory); + + // Set AppName to MainAppName if MainAppName is not empty + if (!string.IsNullOrEmpty(ConfigModel.MainAppName)) + { + ConfigModel.AppName = ConfigModel.MainAppName; + } + } + catch (Exception ex) + { + Trace.WriteLine($"Error reading project configuration: {ex.Message}"); + } + } + + /// + /// Create ConfigInfo JSON file in patch directory + /// + private async Task CreateConfigInfoFile() + { + try + { + var configInfo = new ConfigInfo + { + ReportUrl = ConfigModel.ReportUrl, + UpdateUrl = ConfigModel.UpdateUrl, + AppName = ConfigModel.AppName, + MainAppName = ConfigModel.MainAppName, + ClientVersion = ConfigModel.ClientVersion, + PacketName = ConfigModel.Name, + Format = ConfigModel.Format.Value, + Encoding = ConfigModel.Encoding.DisplayName + }; + + var json = JsonConvert.SerializeObject(configInfo, Formatting.Indented); + var configFilePath = Path.Combine(ConfigModel.PatchDirectory, "config.json"); + + await File.WriteAllTextAsync(configFilePath, json, Encoding.UTF8); + + return configFilePath; + } + catch (Exception ex) + { + Trace.WriteLine($"Error creating config info file: {ex.Message}"); + throw; + } + } } \ No newline at end of file diff --git a/src/Views/PacketView.axaml b/src/Views/PacketView.axaml index 76c3c24..922579e 100644 --- a/src/Views/PacketView.axaml +++ b/src/Views/PacketView.axaml @@ -14,17 +14,42 @@ - - + + + Text="UpdateUrl*" /> + + + + + + + + - + + + Text="ReleaseDirectory*" /> - + + + Text="PatchDirectory*" /> - + + - + + + + + + + + + + + + + + - + IsReadOnly="True" + Text="{Binding ConfigModel.Name, Mode=OneWay}" /> + + - + + From 31c3a6bfa1eb6e5a736aa4c3594a01fcc0069f01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:43:06 +0000 Subject: [PATCH 3/6] Fix nullable warnings and improve exe file detection Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- src/Common/CsprojReader.cs | 16 +++++++++++++++- src/Models/ConfigInfo.cs | 16 ++++++++-------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/Common/CsprojReader.cs b/src/Common/CsprojReader.cs index 2744aef..cd047b6 100644 --- a/src/Common/CsprojReader.cs +++ b/src/Common/CsprojReader.cs @@ -144,8 +144,22 @@ private static string FindExeFile(string directory, string baseName) try { + // First try to find .exe file (Windows) var exeFiles = Directory.GetFiles(directory, $"{baseName}.exe", SearchOption.AllDirectories); - return exeFiles.FirstOrDefault() ?? string.Empty; + if (exeFiles.Any()) + return exeFiles.First(); + + // Then try to find executable without extension (Linux/Mac) + var allFiles = Directory.GetFiles(directory, baseName, SearchOption.AllDirectories); + foreach (var file in allFiles) + { + var fileInfo = new FileInfo(file); + // Check if file is executable (on Unix systems) or if it's an exact match + if (fileInfo.Name == baseName) + return file; + } + + return string.Empty; } catch (Exception ex) { diff --git a/src/Models/ConfigInfo.cs b/src/Models/ConfigInfo.cs index fa31e08..58f89ba 100644 --- a/src/Models/ConfigInfo.cs +++ b/src/Models/ConfigInfo.cs @@ -8,26 +8,26 @@ namespace GeneralUpdate.Tool.Avalonia.Models; public class ConfigInfo { [JsonProperty("reportUrl")] - public string ReportUrl { get; set; } + public string ReportUrl { get; set; } = string.Empty; [JsonProperty("updateUrl")] - public string UpdateUrl { get; set; } + public string UpdateUrl { get; set; } = string.Empty; [JsonProperty("appName")] - public string AppName { get; set; } + public string AppName { get; set; } = string.Empty; [JsonProperty("mainAppName")] - public string MainAppName { get; set; } + public string MainAppName { get; set; } = string.Empty; [JsonProperty("clientVersion")] - public string ClientVersion { get; set; } + public string ClientVersion { get; set; } = string.Empty; [JsonProperty("packetName")] - public string PacketName { get; set; } + public string PacketName { get; set; } = string.Empty; [JsonProperty("format")] - public string Format { get; set; } + public string Format { get; set; } = string.Empty; [JsonProperty("encoding")] - public string Encoding { get; set; } + public string Encoding { get; set; } = string.Empty; } From 8f14ae330d6f0661703a4fddc715af642745bb20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:44:19 +0000 Subject: [PATCH 4/6] Address code review feedback Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- src/Common/CsprojReader.cs | 13 ++++++++++++- src/ViewModels/PacketViewModel.cs | 10 +++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Common/CsprojReader.cs b/src/Common/CsprojReader.cs index cd047b6..f21c084 100644 --- a/src/Common/CsprojReader.cs +++ b/src/Common/CsprojReader.cs @@ -131,11 +131,22 @@ private static string FindCsprojFile(string directory) return string.Empty; var csprojFiles = Directory.GetFiles(directory, "*.csproj", SearchOption.TopDirectoryOnly); - return csprojFiles.FirstOrDefault() ?? string.Empty; + + if (csprojFiles.Length == 0) + return string.Empty; + + if (csprojFiles.Length > 1) + { + Trace.WriteLine($"Warning: Multiple .csproj files found in {directory}. Using the first one: {csprojFiles[0]}"); + } + + return csprojFiles[0]; } /// /// Find .exe file with matching name recursively + /// Note: Uses SearchOption.AllDirectories which may be slow for large directory trees. + /// This is acceptable as release directories are typically small. /// private static string FindExeFile(string directory, string baseName) { diff --git a/src/ViewModels/PacketViewModel.cs b/src/ViewModels/PacketViewModel.cs index 00a1540..ce010f3 100644 --- a/src/ViewModels/PacketViewModel.cs +++ b/src/ViewModels/PacketViewModel.cs @@ -150,7 +150,7 @@ private async Task BuildPacketAction() try { // Validate required fields - if (!ValidateRequiredFields()) + if (!await ValidateRequiredFields()) return; // Read configuration from .csproj @@ -276,7 +276,7 @@ private void CopyDriverFiles(string sourceDir, string targetDir) /// /// Validate required fields /// - private bool ValidateRequiredFields() + private async Task ValidateRequiredFields() { var errors = new System.Collections.Generic.List(); @@ -298,7 +298,7 @@ private bool ValidateRequiredFields() if (errors.Any()) { var message = $"The following required fields must be filled:\n{string.Join(", ", errors)}"; - MessageBox.ShowAsync(message, "Validation Error", Buttons.OK).Wait(); + await MessageBox.ShowAsync(message, "Validation Error", Buttons.OK); return false; } @@ -345,8 +345,8 @@ private async Task CreateConfigInfoFile() MainAppName = ConfigModel.MainAppName, ClientVersion = ConfigModel.ClientVersion, PacketName = ConfigModel.Name, - Format = ConfigModel.Format.Value, - Encoding = ConfigModel.Encoding.DisplayName + Format = ConfigModel.Format?.Value ?? string.Empty, + Encoding = ConfigModel.Encoding?.DisplayName ?? string.Empty }; var json = JsonConvert.SerializeObject(configInfo, Formatting.Indented); From de5b0b2512d29866dc23842d94ff51cf921e96e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:53:59 +0000 Subject: [PATCH 5/6] Use existing Configinfo class from GeneralUpdate.Common.Shared.Object Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- src/Models/ConfigInfo.cs | 33 ------------------------------- src/ViewModels/PacketViewModel.cs | 10 ++++------ 2 files changed, 4 insertions(+), 39 deletions(-) delete mode 100644 src/Models/ConfigInfo.cs diff --git a/src/Models/ConfigInfo.cs b/src/Models/ConfigInfo.cs deleted file mode 100644 index 58f89ba..0000000 --- a/src/Models/ConfigInfo.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Newtonsoft.Json; - -namespace GeneralUpdate.Tool.Avalonia.Models; - -/// -/// Configuration information for package serialization -/// -public class ConfigInfo -{ - [JsonProperty("reportUrl")] - public string ReportUrl { get; set; } = string.Empty; - - [JsonProperty("updateUrl")] - public string UpdateUrl { get; set; } = string.Empty; - - [JsonProperty("appName")] - public string AppName { get; set; } = string.Empty; - - [JsonProperty("mainAppName")] - public string MainAppName { get; set; } = string.Empty; - - [JsonProperty("clientVersion")] - public string ClientVersion { get; set; } = string.Empty; - - [JsonProperty("packetName")] - public string PacketName { get; set; } = string.Empty; - - [JsonProperty("format")] - public string Format { get; set; } = string.Empty; - - [JsonProperty("encoding")] - public string Encoding { get; set; } = string.Empty; -} diff --git a/src/ViewModels/PacketViewModel.cs b/src/ViewModels/PacketViewModel.cs index ce010f3..fc50e9e 100644 --- a/src/ViewModels/PacketViewModel.cs +++ b/src/ViewModels/PacketViewModel.cs @@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using GeneralUpdate.Common.Compress; +using GeneralUpdate.Common.Shared.Object; using GeneralUpdate.Differential; using GeneralUpdate.Tool.Avalonia.Common; using GeneralUpdate.Tool.Avalonia.Models; @@ -331,22 +332,19 @@ private void ReadProjectConfiguration() } /// - /// Create ConfigInfo JSON file in patch directory + /// Create Configinfo JSON file in patch directory /// private async Task CreateConfigInfoFile() { try { - var configInfo = new ConfigInfo + var configInfo = new Configinfo { ReportUrl = ConfigModel.ReportUrl, UpdateUrl = ConfigModel.UpdateUrl, AppName = ConfigModel.AppName, MainAppName = ConfigModel.MainAppName, - ClientVersion = ConfigModel.ClientVersion, - PacketName = ConfigModel.Name, - Format = ConfigModel.Format?.Value ?? string.Empty, - Encoding = ConfigModel.Encoding?.DisplayName ?? string.Empty + ClientVersion = ConfigModel.ClientVersion }; var json = JsonConvert.SerializeObject(configInfo, Formatting.Indented); From 13e5d8587c216a71a90d1d73e7f938b49298885a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 14:20:35 +0000 Subject: [PATCH 6/6] Add visual separators between field categories in PacketView Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- src/Views/PacketView.axaml | 393 ++++++++++++++++++++----------------- 1 file changed, 209 insertions(+), 184 deletions(-) diff --git a/src/Views/PacketView.axaml b/src/Views/PacketView.axaml index 922579e..8f700d1 100644 --- a/src/Views/PacketView.axaml +++ b/src/Views/PacketView.axaml @@ -14,196 +14,221 @@ - - - - - - - - - - - - - + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - - - - - - - - - - - - - - - + + + + - - - + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + +