diff --git a/imgs/GeneralUpdate_h.png b/imgs/GeneralUpdate_h.png deleted file mode 100644 index c96045d..0000000 Binary files a/imgs/GeneralUpdate_h.png and /dev/null differ diff --git a/src/App.axaml b/src/App.axaml index 1eeae2d..836b843 100644 --- a/src/App.axaml +++ b/src/App.axaml @@ -1,19 +1,16 @@ - - - - - - - - + + + + + - - + + - \ No newline at end of file + diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 8f10b61..3352d7a 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -1,25 +1,18 @@ -using Avalonia; +using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; -using GeneralUpdate.Tool.Avalonia.ViewModels; -using GeneralUpdate.Tool.Avalonia.Views; +using GeneralUpdate.Tools.ViewModels; +using GeneralUpdate.Tools.Views; -namespace GeneralUpdate.Tool.Avalonia; +namespace GeneralUpdate.Tools; public partial class App : Application { - public override void Initialize() - { - AvaloniaXamlLoader.Load(this); - } - + public override void Initialize() { AvaloniaXamlLoader.Load(this); } public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - desktop.MainWindow = new MainWindow(); - } - + desktop.MainWindow = new MainWindow { DataContext = new MainWindowViewModel() }; base.OnFrameworkInitializationCompleted(); } -} \ No newline at end of file +} diff --git a/src/Assets/avalonia-logo.ico b/src/Assets/avalonia-logo.ico index da8d49f..f7da8bb 100644 Binary files a/src/Assets/avalonia-logo.ico and b/src/Assets/avalonia-logo.ico differ diff --git a/src/Common/CsprojReader.cs b/src/Common/CsprojReader.cs deleted file mode 100644 index f21c084..0000000 --- a/src/Common/CsprojReader.cs +++ /dev/null @@ -1,200 +0,0 @@ -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); - - 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) - { - if (!Directory.Exists(directory)) - return string.Empty; - - try - { - // First try to find .exe file (Windows) - var exeFiles = Directory.GetFiles(directory, $"{baseName}.exe", SearchOption.AllDirectories); - 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) - { - 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/Common/ZipUtility.cs b/src/Common/ZipUtility.cs deleted file mode 100644 index 1f08392..0000000 --- a/src/Common/ZipUtility.cs +++ /dev/null @@ -1,244 +0,0 @@ -using System; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Threading.Tasks; - -namespace GeneralUpdate.Tool.Avalonia.Common; - -/// -/// Utility class for zip file compression operations -/// -public static class ZipUtility -{ - /// - /// Characters that are invalid in file names across all platforms - /// Includes platform-specific invalid chars and common problematic characters - /// - private static readonly char[] InvalidFileNameChars = - Path.GetInvalidFileNameChars() - .Concat(new[] { '<', '>', ':', '"', '/', '\\', '|', '?', '*' }) - .Distinct() - .ToArray(); - - /// - /// Sanitizes a string to be used as a filename by replacing invalid characters - /// - /// The filename to sanitize - /// The replacement character for invalid characters (default: '_') - /// Sanitized filename - public static string SanitizeFileName(string fileName, char replacement = '_') - { - if (string.IsNullOrWhiteSpace(fileName)) - return fileName; - - var sanitized = fileName; - foreach (var invalidChar in InvalidFileNameChars) - { - sanitized = sanitized.Replace(invalidChar, replacement); - } - - return sanitized; - } - /// - /// Compresses a directory into a zip file - /// - /// Source directory to compress - /// Destination zip file path - /// Compression level (default: Optimal) - /// Whether to include the base directory in the archive - /// Thrown when sourceDirectory or destinationZipFile is null or empty - /// Thrown when sourceDirectory does not exist - public static void CompressDirectory( - string sourceDirectory, - string destinationZipFile, - CompressionLevel compressionLevel = CompressionLevel.Optimal, - bool includeBaseDirectory = false) - { - if (string.IsNullOrWhiteSpace(sourceDirectory)) - throw new ArgumentNullException(nameof(sourceDirectory)); - - if (string.IsNullOrWhiteSpace(destinationZipFile)) - throw new ArgumentNullException(nameof(destinationZipFile)); - - if (!Directory.Exists(sourceDirectory)) - throw new DirectoryNotFoundException($"Source directory not found: {sourceDirectory}"); - - // Ensure the destination directory exists - var destinationDir = Path.GetDirectoryName(destinationZipFile); - if (!string.IsNullOrEmpty(destinationDir) && !Directory.Exists(destinationDir)) - { - Directory.CreateDirectory(destinationDir); - } - - // Delete existing zip file if it exists - if (File.Exists(destinationZipFile)) - { - File.Delete(destinationZipFile); - } - - // Create the zip archive - ZipFile.CreateFromDirectory(sourceDirectory, destinationZipFile, compressionLevel, includeBaseDirectory); - } - - /// - /// Compresses a directory into a zip file asynchronously - /// - /// Source directory to compress - /// Destination zip file path - /// Compression level (default: Optimal) - /// Whether to include the base directory in the archive - /// Task representing the asynchronous operation - /// Thrown when sourceDirectory or destinationZipFile is null or empty - /// Thrown when sourceDirectory does not exist - public static Task CompressDirectoryAsync( - string sourceDirectory, - string destinationZipFile, - CompressionLevel compressionLevel = CompressionLevel.Optimal, - bool includeBaseDirectory = false) - { - return Task.Run(() => CompressDirectory(sourceDirectory, destinationZipFile, compressionLevel, includeBaseDirectory)); - } - - /// - /// Extracts a zip file to a directory - /// - /// Source zip file to extract - /// Destination directory for extraction - /// Whether to overwrite existing files - /// Thrown when sourceZipFile or destinationDirectory is null or empty - /// Thrown when sourceZipFile does not exist - /// Thrown when a zip entry attempts to extract outside the destination directory (zip slip attack) - public static void ExtractZipFile( - string sourceZipFile, - string destinationDirectory, - bool overwriteFiles = true) - { - if (string.IsNullOrWhiteSpace(sourceZipFile)) - throw new ArgumentNullException(nameof(sourceZipFile)); - - if (string.IsNullOrWhiteSpace(destinationDirectory)) - throw new ArgumentNullException(nameof(destinationDirectory)); - - if (!File.Exists(sourceZipFile)) - throw new FileNotFoundException($"Source zip file not found: {sourceZipFile}"); - - // Ensure the destination directory exists - if (!Directory.Exists(destinationDirectory)) - { - Directory.CreateDirectory(destinationDirectory); - } - - // Get the normalized full path of the destination directory - var normalizedDestination = Path.GetFullPath(destinationDirectory); - - // Extract the zip archive with zip slip protection - using (var archive = System.IO.Compression.ZipFile.OpenRead(sourceZipFile)) - { - foreach (var entry in archive.Entries) - { - // Get the full path where the entry will be extracted - var entryPath = Path.Combine(destinationDirectory, entry.FullName); - var normalizedEntryPath = Path.GetFullPath(entryPath); - - // Validate that the entry path is within the destination directory (zip slip protection) - if (!normalizedEntryPath.StartsWith(normalizedDestination, StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException( - $"Zip entry '{entry.FullName}' attempts to extract outside the destination directory. " + - "This may indicate a zip slip attack."); - } - - // Create directory for the entry if needed - if (string.IsNullOrEmpty(entry.Name)) - { - // This is a directory entry - Directory.CreateDirectory(normalizedEntryPath); - } - else - { - // This is a file entry - var entryDirectory = Path.GetDirectoryName(normalizedEntryPath); - if (!string.IsNullOrEmpty(entryDirectory) && !Directory.Exists(entryDirectory)) - { - Directory.CreateDirectory(entryDirectory); - } - - // Extract the file - entry.ExtractToFile(normalizedEntryPath, overwriteFiles); - } - } - } - } - - /// - /// Extracts a zip file to a directory asynchronously - /// - /// Source zip file to extract - /// Destination directory for extraction - /// Whether to overwrite existing files - /// Task representing the asynchronous operation - /// Thrown when sourceZipFile or destinationDirectory is null or empty - /// Thrown when sourceZipFile does not exist - /// Thrown when a zip entry attempts to extract outside the destination directory (zip slip attack) - public static Task ExtractZipFileAsync( - string sourceZipFile, - string destinationDirectory, - bool overwriteFiles = true) - { - return Task.Run(() => ExtractZipFile(sourceZipFile, destinationDirectory, overwriteFiles)); - } - - /// - /// Adds a file to an existing zip archive - /// - /// Path to the zip file - /// Entry name in the archive - /// Content to add - /// Thrown when parameters are null or empty - /// Thrown when parameters are empty or whitespace - /// Thrown when zipFilePath does not exist - public static void AddFileToZip(string zipFilePath, string entryName, string content) - { - if (string.IsNullOrWhiteSpace(zipFilePath)) - throw new ArgumentException("Zip file path cannot be null or empty", nameof(zipFilePath)); - - if (string.IsNullOrWhiteSpace(entryName)) - throw new ArgumentException("Entry name cannot be null or empty", nameof(entryName)); - - if (content == null) - throw new ArgumentNullException(nameof(content)); - - if (!File.Exists(zipFilePath)) - throw new FileNotFoundException($"Zip file not found: {zipFilePath}"); - - using (var archive = System.IO.Compression.ZipFile.Open(zipFilePath, ZipArchiveMode.Update)) - { - // Remove existing entry if it exists - var existingEntry = archive.GetEntry(entryName); - existingEntry?.Delete(); - - // Create new entry - var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal); - using (var writer = new StreamWriter(entry.Open())) - { - writer.Write(content); - } - } - } - - /// - /// Adds a file to an existing zip archive asynchronously - /// - /// Path to the zip file - /// Entry name in the archive - /// Content to add - /// Task representing the asynchronous operation - /// Thrown when parameters are null - /// Thrown when parameters are empty or whitespace - /// Thrown when zipFilePath does not exist - public static Task AddFileToZipAsync(string zipFilePath, string entryName, string content) - { - return Task.Run(() => AddFileToZip(zipFilePath, entryName, content)); - } -} \ No newline at end of file diff --git a/src/GeneralUpdate.Tool.Avalonia.csproj b/src/GeneralUpdate.Tool.Avalonia.csproj deleted file mode 100644 index abbc6c7..0000000 --- a/src/GeneralUpdate.Tool.Avalonia.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - WinExe - net8.0 - enable - true - app.manifest - true - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/GeneralUpdate.Tool.Avalonia.sln b/src/GeneralUpdate.Tool.Avalonia.sln deleted file mode 100644 index a446de0..0000000 --- a/src/GeneralUpdate.Tool.Avalonia.sln +++ /dev/null @@ -1,16 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneralUpdate.Tool.Avalonia", "GeneralUpdate.Tool.Avalonia.csproj", "{4F819DD4-A905-4358-B338-6F6E759697BE}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {4F819DD4-A905-4358-B338-6F6E759697BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4F819DD4-A905-4358-B338-6F6E759697BE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4F819DD4-A905-4358-B338-6F6E759697BE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4F819DD4-A905-4358-B338-6F6E759697BE}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/src/GeneralUpdate.Tools.csproj b/src/GeneralUpdate.Tools.csproj new file mode 100644 index 0000000..528977c --- /dev/null +++ b/src/GeneralUpdate.Tools.csproj @@ -0,0 +1,32 @@ + + + WinExe + net10.0 + enable + app.manifest + true + GeneralUpdate.Tools + + + + + + + + + + + + + None + All + + + + + + + + + + diff --git a/src/GeneralUpdate.Tools.slnx b/src/GeneralUpdate.Tools.slnx new file mode 100644 index 0000000..b2aff54 --- /dev/null +++ b/src/GeneralUpdate.Tools.slnx @@ -0,0 +1,3 @@ + + + diff --git a/src/Models/AppTypeModel.cs b/src/Models/AppTypeModel.cs deleted file mode 100644 index 5ff843d..0000000 --- a/src/Models/AppTypeModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace GeneralUpdate.Tool.Avalonia.Models; - -public class AppTypeModel -{ - public string DisplayName { get; set; } - - public int Value { get; set; } - - public override string ToString() => DisplayName; -} \ No newline at end of file diff --git a/src/Models/CustomPropertyModel.cs b/src/Models/CustomPropertyModel.cs deleted file mode 100644 index a87526a..0000000 --- a/src/Models/CustomPropertyModel.cs +++ /dev/null @@ -1,27 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; - -namespace GeneralUpdate.Tool.Avalonia.Models; - -public class CustomPropertyModel : ObservableObject -{ - private string _key; - private string _value; - - /// - /// Property key - /// - public string Key - { - get => _key; - set => SetProperty(ref _key, value); - } - - /// - /// Property value - /// - public string Value - { - get => _value; - set => SetProperty(ref _value, value); - } -} \ No newline at end of file diff --git a/src/Models/EncodingModel.cs b/src/Models/EncodingModel.cs deleted file mode 100644 index f1a7a96..0000000 --- a/src/Models/EncodingModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Text; - -namespace GeneralUpdate.Tool.Avalonia.Models; - -public class EncodingModel -{ - public string DisplayName { get; set; } - - public Encoding Value { get; set; } - - public int Type { get; set; } - - public override string ToString() => DisplayName; -} \ No newline at end of file diff --git a/src/Models/ExtensionConfigModel.cs b/src/Models/ExtensionConfigModel.cs index 4b049cc..537441c 100644 --- a/src/Models/ExtensionConfigModel.cs +++ b/src/Models/ExtensionConfigModel.cs @@ -1,290 +1,21 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; -namespace GeneralUpdate.Tool.Avalonia.Models; +namespace GeneralUpdate.Tools.Models; -public class ExtensionConfigModel : ObservableObject +public partial class ExtensionConfigModel : ObservableObject { - private string _name; - private string _version; - private string _description; - private string _path; - private string _extensionDirectory; - private bool _isUploadToServer; - private PlatformModel _platform; - private string _dependencies; - private string _displayName; - private string _publisher; - private string _license; - private List _categories; - private string _minHostVersion; - private string _maxHostVersion; - private DateTime? _releaseDate; - private bool _isPreRelease; - private string _format; - private string _hash; - private Dictionary _customProperties; - private bool _showCustomProperties; - - /// - /// Extension name - /// - public string Name - { - get => _name; - set - { - SetProperty(ref _name, value); - } - } - - /// - /// Extension version - /// - public string Version - { - get => _version; - set - { - SetProperty(ref _version, value); - } - } - - /// - /// Extension description - /// - public string Description - { - get => _description; - set - { - SetProperty(ref _description, value); - } - } - - /// - /// Extension export path - /// - public string Path - { - get => _path; - set - { - SetProperty(ref _path, value); - } - } - - /// - /// Extension directory - /// - public string ExtensionDirectory - { - get => _extensionDirectory; - set - { - SetProperty(ref _extensionDirectory, value); - } - } - - /// - /// Whether to upload directly to server - /// - public bool IsUploadToServer - { - get => _isUploadToServer; - set - { - SetProperty(ref _isUploadToServer, value); - } - } - - /// - /// Platform - /// - public PlatformModel Platform - { - get => _platform ??= new PlatformModel(); - set => SetProperty(ref _platform, value); - } - - /// - /// Dependencies (comma-separated) - /// - public string Dependencies - { - get => _dependencies; - set - { - SetProperty(ref _dependencies, value); - } - } - - /// - /// Display name for UI - /// - public string DisplayName - { - get => _displayName; - set - { - SetProperty(ref _displayName, value); - } - } - - /// - /// Publisher name - /// - public string Publisher - { - get => _publisher; - set - { - SetProperty(ref _publisher, value); - } - } - - /// - /// License identifier (e.g., MIT, Apache-2.0) - /// - public string License - { - get => _license; - set - { - SetProperty(ref _license, value); - } - } - - /// - /// Categories list (comma-separated) - /// - public List Categories - { - get => _categories ??= new List(); - set - { - SetProperty(ref _categories, value); - } - } - - /// - /// Categories as a comma-separated string for UI binding - /// - public string CategoriesText - { - get => Categories != null && Categories.Count > 0 ? string.Join(", ", Categories) : string.Empty; - set - { - if (!string.IsNullOrWhiteSpace(value)) - { - Categories = value.Split(',', StringSplitOptions.RemoveEmptyEntries) - .Select(c => c.Trim()) - .Where(c => !string.IsNullOrWhiteSpace(c)) - .ToList(); - } - else - { - Categories = new List(); - } - OnPropertyChanged(new PropertyChangedEventArgs(nameof(CategoriesText))); - } - } - - /// - /// Minimum host version required - /// - public string MinHostVersion - { - get => _minHostVersion; - set - { - SetProperty(ref _minHostVersion, value); - } - } - - /// - /// Maximum host version supported - /// - public string MaxHostVersion - { - get => _maxHostVersion; - set - { - SetProperty(ref _maxHostVersion, value); - } - } - - /// - /// Release date - /// - public DateTime? ReleaseDate - { - get => _releaseDate; - set - { - SetProperty(ref _releaseDate, value); - } - } - - /// - /// Is this a pre-release version - /// - public bool IsPreRelease - { - get => _isPreRelease; - set - { - SetProperty(ref _isPreRelease, value); - } - } - - /// - /// File format (.dll, .zip, .so, .dylib, .exe) - /// - public string Format - { - get => _format; - set - { - SetProperty(ref _format, value); - } - } - - /// - /// File hash for integrity verification - /// - public string Hash - { - get => _hash; - set - { - SetProperty(ref _hash, value); - } - } - - /// - /// Custom properties (key-value pairs) - /// - public Dictionary CustomProperties - { - get => _customProperties ??= new Dictionary(); - set => SetProperty(ref _customProperties, value); - } - - /// - /// Controls visibility of CustomProperties input area - /// - public bool ShowCustomProperties - { - get => _showCustomProperties; - set => SetProperty(ref _showCustomProperties, value); - } - - /// - /// File size in bytes - /// - public long? FileSize { get; set; } -} \ No newline at end of file + [ObservableProperty] private string _name = ""; + [ObservableProperty] private string _version = "1.0.0.0"; + [ObservableProperty] private string _description = ""; + [ObservableProperty] private string _extensionDirectory = ""; + [ObservableProperty] private string _exportPath = ""; + [ObservableProperty] private string _dependencies = ""; + [ObservableProperty] private string _publisher = ""; + [ObservableProperty] private string _license = ""; + [ObservableProperty] private string _categoriesText = ""; + [ObservableProperty] private string _minHostVersion = ""; + [ObservableProperty] private string _maxHostVersion = ""; + [ObservableProperty] private int _platformValue = 1; + [ObservableProperty] private bool _isPreRelease; + [ObservableProperty] private string _outputPath = ""; +} diff --git a/src/Models/ExtensionDependencySelectionModel.cs b/src/Models/ExtensionDependencySelectionModel.cs deleted file mode 100644 index 8e9e4d1..0000000 --- a/src/Models/ExtensionDependencySelectionModel.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace GeneralUpdate.Tool.Avalonia.Models; - -/// -/// Model for extension dependency selection -/// -public class ExtensionDependencySelectionModel : ObservableObject -{ - private bool _isSelected; - - /// - /// Extension ID (GUID) - /// - public string Id { get; set; } = string.Empty; - - /// - /// Extension Name - /// - public string Name { get; set; } = string.Empty; - - /// - /// Extension Version - /// - public string Version { get; set; } = string.Empty; - - /// - /// Extension Description - /// - public string Description { get; set; } = string.Empty; - - /// - /// Release Date - /// - public DateTime? ReleaseDate { get; set; } - - /// - /// Whether this extension is selected as a dependency - /// - public bool IsSelected - { - get => _isSelected; - set => SetProperty(ref _isSelected, value); - } -} \ No newline at end of file diff --git a/src/Models/FormatModel.cs b/src/Models/FormatModel.cs deleted file mode 100644 index f90b4dd..0000000 --- a/src/Models/FormatModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace GeneralUpdate.Tool.Avalonia.Models; - -public class FormatModel -{ - public string DisplayName { get; set; } - - public int Type { get; set; } - - public string Value { get; set; } - - public override string ToString() => DisplayName; -} \ No newline at end of file diff --git a/src/Models/OSSConfigModel.cs b/src/Models/OSSConfigModel.cs index 4ffe1ae..bca72ef 100644 --- a/src/Models/OSSConfigModel.cs +++ b/src/Models/OSSConfigModel.cs @@ -1,79 +1,12 @@ -using System; -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; -namespace GeneralUpdate.Tool.Avalonia.Models; +namespace GeneralUpdate.Tools.Models; -public class OSSConfigModel : ObservableObject +public partial class OSSConfigModel : ObservableObject { - private string _packetName, _hash, _version, _url, _jsonContent; - private DateTime _date; - private TimeSpan _time; - - public string PacketName - { - get => _packetName; - set - { - SetProperty(ref _packetName, value); - } - } - - public string Hash - { - get => _hash; - set - { - SetProperty(ref _hash, value); - } - } - - public string Version - { - get => _version; - set - { - SetProperty(ref _version, value); - } - } - - public string Url - { - get => _url; - set - { - SetProperty(ref _url, value); - } - } - - public string JsonContent - { - get => _jsonContent; - set - { - SetProperty(ref _jsonContent, value); - } - } - - public DateTime Date - { - get => _date; - set - { - SetProperty(ref _date, value); - } - } - - public TimeSpan Time - { - get => _time; - set - { - SetProperty(ref _time, value); - } - } - - public DateTime PubTime - { - get => Date + Time; - } -} \ No newline at end of file + [ObservableProperty] private string _packetName = "Packet"; + [ObservableProperty] private string _hash = ""; + [ObservableProperty] private string _version = "1.0.0.0"; + [ObservableProperty] private string _url = "http://127.0.0.1"; + [ObservableProperty] private string _releaseDate = ""; +} diff --git a/src/Models/OperationType.cs b/src/Models/OperationType.cs deleted file mode 100644 index 204955e..0000000 --- a/src/Models/OperationType.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace GeneralUpdate.Tool.Avalonia.Models; - -public enum OperationType -{ - None, - Zip, -} \ No newline at end of file diff --git a/src/Models/PacketConfigModel.cs b/src/Models/PacketConfigModel.cs deleted file mode 100644 index 97ba3e2..0000000 --- a/src/Models/PacketConfigModel.cs +++ /dev/null @@ -1,192 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; - -namespace GeneralUpdate.Tool.Avalonia.Models; - -public class PacketConfigModel : ObservableObject -{ - private string _serverAddress; - private string _appDirectory, _releaseDirectory, _patchDirectory, _name, _path, _driverDirectory; - private string _reportUrl, _updateUrl, _appName, _mainAppName, _clientVersion; - private PlatformModel _platform; - private FormatModel _format; - private EncodingModel _encoding; - - public string ServerAddress - { - get => _serverAddress; - set - { - _serverAddress = value; - OnPropertyChanged(nameof(ServerAddress)); - - ReportUrl = $"{_serverAddress}/report"; - UpdateUrl = $"{_serverAddress}/update"; - } - } - - /// - /// 压缩包格式 - /// - public FormatModel Format - { - get => _format; - set - { - _format = value; - OnPropertyChanged(nameof(Format)); - } - } - - /// - /// 压缩包编码 - /// - public EncodingModel Encoding - { - get => _encoding; - set - { - _encoding = value; - OnPropertyChanged(nameof(Encoding)); - } - } - - /// - /// 最近一次发布的应用程序目录 - /// - public string AppDirectory - { - get => _appDirectory; - set - { - _appDirectory = value; - OnPropertyChanged(nameof(AppDirectory)); - } - } - - /// - /// 发布程序目录 - /// - public string ReleaseDirectory - { - get => _releaseDirectory; - set - { - _releaseDirectory = value; - OnPropertyChanged(nameof(ReleaseDirectory)); - } - } - - /// - /// 补丁包生成目录 - /// - public string PatchDirectory - { - get => _patchDirectory; - set - { - _patchDirectory = value; - OnPropertyChanged(nameof(PatchDirectory)); - } - } - - /// - /// 补丁包名称 - /// - public string Name - { - get => _name; - set - { - _name = value; - OnPropertyChanged(nameof(Name)); - } - } - - public string Path - { - get => _path; - set - { - _path = value; - OnPropertyChanged(nameof(Path)); - } - } - - /// - /// 驱动程序目录 - /// - public string DriverDirectory - { - get => _driverDirectory; - set - { - _driverDirectory = value; - 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/Models/PatchConfigModel.cs b/src/Models/PatchConfigModel.cs new file mode 100644 index 0000000..9f342fa --- /dev/null +++ b/src/Models/PatchConfigModel.cs @@ -0,0 +1,13 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace GeneralUpdate.Tools.Models; + +public partial class PatchConfigModel : ObservableObject +{ + [ObservableProperty] private string _oldDirectory = ""; + [ObservableProperty] private string _newDirectory = ""; + [ObservableProperty] private string _packageName = ""; + [ObservableProperty] private string _version = "1.0.0.0"; + [ObservableProperty] private string _format = ".zip"; + [ObservableProperty] private string _outputPath = ""; +} diff --git a/src/Models/PlatformModel.cs b/src/Models/PlatformModel.cs deleted file mode 100644 index d945aad..0000000 --- a/src/Models/PlatformModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace GeneralUpdate.Tool.Avalonia.Models; - -public class PlatformModel -{ - public string DisplayName { get; set; } - - public int Value { get; set; } - - public override string ToString() => DisplayName; -} \ No newline at end of file diff --git a/src/Models/TargetPlatform.cs b/src/Models/TargetPlatform.cs deleted file mode 100644 index e468468..0000000 --- a/src/Models/TargetPlatform.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; - -namespace GeneralUpdate.Tool.Avalonia.Models; - -[Flags] -public enum TargetPlatform -{ - /// - /// No platform specified. - /// - None = 0, - - /// - /// Windows operating system. - /// - Windows = 1, - - /// - /// Linux operating system. - /// - Linux = 2, - - /// - /// macOS operating system. - /// - MacOS = 4, - - /// - /// All supported platforms (Windows, Linux, and macOS). - /// - All = Windows | Linux | MacOS -} diff --git a/src/Program.cs b/src/Program.cs index 434ae17..0bd543a 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,23 +1,18 @@ using Avalonia; -using Avalonia.ReactiveUI; using System; -namespace GeneralUpdate.Tool.Avalonia; +namespace GeneralUpdate.Tools; sealed class Program { - // Initialization code. Don't use any Avalonia, third-party APIs or any - // SynchronizationContext-reliant code before AppMain is called: things aren't initialized - // yet and stuff might break. [STAThread] public static void Main(string[] args) => BuildAvaloniaApp() .StartWithClassicDesktopLifetime(args); - // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() .WithInterFont() .LogToTrace() .UseSkia(); -} \ No newline at end of file +} diff --git a/src/Services/DiffService.cs b/src/Services/DiffService.cs new file mode 100644 index 0000000..18a8a1d --- /dev/null +++ b/src/Services/DiffService.cs @@ -0,0 +1,17 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using GeneralUpdate.Differential; + +namespace GeneralUpdate.Tools.Services; + +public class DiffService +{ + public async Task GeneratePatchAsync(string oldDir, string newDir, string patchDir) + { + if (!Directory.Exists(oldDir)) throw new DirectoryNotFoundException("Old: " + oldDir); + if (!Directory.Exists(newDir)) throw new DirectoryNotFoundException("New: " + newDir); + Directory.CreateDirectory(patchDir); + await Task.Run(() => DifferentialCore.Clean(oldDir, newDir, patchDir).GetAwaiter().GetResult()); + } +} diff --git a/src/Services/PackageService.cs b/src/Services/PackageService.cs new file mode 100644 index 0000000..8f8cd9f --- /dev/null +++ b/src/Services/PackageService.cs @@ -0,0 +1,40 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace GeneralUpdate.Tools.Services; + +public class PackageService +{ + public async Task CompressDirectoryAsync(string sourceDir, string outputPath) + { + await Task.Run(() => { if (File.Exists(outputPath)) File.Delete(outputPath); ZipFile.CreateFromDirectory(sourceDir, outputPath, CompressionLevel.Optimal, false); }); + } + public async Task CreateManifestAsync(string zipPath, object manifest) + { + await Task.Run(() => { + var json = JsonConvert.SerializeObject(manifest, Formatting.Indented, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + using var archive = ZipFile.Open(zipPath, ZipArchiveMode.Update); + var entry = archive.CreateEntry("manifest.json", CompressionLevel.Optimal); + using var writer = new StreamWriter(entry.Open(), Encoding.UTF8); + writer.Write(json); + }); + } +} + +public class HashService +{ + public async Task ComputeHashAsync(string filePath) + { + return await Task.Run(() => { + using var sha256 = SHA256.Create(); + using var stream = File.OpenRead(filePath); + var hash = sha256.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + }); + } +} diff --git a/src/Storage/ClipboardUtility.cs b/src/Storage/ClipboardUtility.cs deleted file mode 100644 index 62a832d..0000000 --- a/src/Storage/ClipboardUtility.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Threading.Tasks; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Input.Platform; - -namespace GeneralUpdate.Tool.Avalonia; - -public class ClipboardUtility -{ - private static IClipboard? _clipboard = null; - - public static async Task SetText(string content) - { - var dataObject = new DataObject(); - dataObject.Set(DataFormats.Text, content); - await _clipboard?.SetDataObjectAsync(dataObject); - } - - public static void CreateClipboard(Visual visual) - { - _clipboard = TopLevel.GetTopLevel(visual)?.Clipboard; - } -} \ No newline at end of file diff --git a/src/Storage/Storage.cs b/src/Storage/Storage.cs deleted file mode 100644 index facbaf2..0000000 --- a/src/Storage/Storage.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Platform.Storage; - -namespace GeneralUpdate.Tool.Avalonia; - -public class Storage -{ - private IStorageProvider? _storageProvider; - private static Storage _instance; - private static readonly object _lock = new(); - - public static Storage Instance - { - get - { - if (_instance == null) - { - lock (_lock) - { - if (_instance == null) - { - _instance = new Storage(); - } - } - } - - return _instance; - } - } - - private Storage() - { - } - - /// - /// 打开文件 - /// - /// - public async Task> OpenFileDialog() => await _storageProvider!.OpenFilePickerAsync(new FilePickerOpenOptions() - { - Title = "Open File", - FileTypeFilter = GetFileTypes(), - AllowMultiple = true - }); - - /// - /// 选择文件目录 - /// - /// - public async Task> SelectFolderDialog() => await _storageProvider!.OpenFolderPickerAsync(new FolderPickerOpenOptions() - { - Title = "Select Folder", - AllowMultiple = true, - }); - - /// - /// 选择文件保存 - /// - /// - public async Task SaveFilePickerAsync() => await _storageProvider!.SaveFilePickerAsync(new FilePickerSaveOptions() - { - Title = "Save option", - }); - - /// - /// 初始化提供器 - /// - /// - public void SetStorageProvider(Visual visual) - { - var topLevel = TopLevel.GetTopLevel(visual); - _storageProvider = topLevel?.StorageProvider; - } - - private List? GetFileTypes() - { - return - [ - FilePickerFileTypes.All, - FilePickerFileTypes.TextPlain - ]; - } -} \ No newline at end of file diff --git a/src/ViewLocator.cs b/src/ViewLocator.cs new file mode 100644 index 0000000..5deaaa2 --- /dev/null +++ b/src/ViewLocator.cs @@ -0,0 +1,19 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using GeneralUpdate.Tools.ViewModels; + +namespace GeneralUpdate.Tools; + +public class ViewLocator : IDataTemplate +{ + public Control? Build(object? param) + { + if (param is null) return null; + var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); + var type = Type.GetType(name); + if (type != null) { var c = (Control)Activator.CreateInstance(type)!; c.DataContext = param; return c; } + return new TextBlock { Text = "Not Found: " + name }; + } + public bool Match(object? data) => data is ViewModelBase; +} diff --git a/src/ViewModels/ExtensionViewModel.cs b/src/ViewModels/ExtensionViewModel.cs index 7dd8f27..f0ea7c4 100644 --- a/src/ViewModels/ExtensionViewModel.cs +++ b/src/ViewModels/ExtensionViewModel.cs @@ -1,400 +1,60 @@ using System; using System.Collections.ObjectModel; -using System.ComponentModel; using System.IO; using System.Linq; using System.Threading.Tasks; -using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using GeneralUpdate.Tool.Avalonia.Common; -using GeneralUpdate.Tool.Avalonia.Models; -using Newtonsoft.Json; -using Nlnet.Avalonia.Controls; +using GeneralUpdate.Tools.Models; +using GeneralUpdate.Tools.Services; -namespace GeneralUpdate.Tool.Avalonia.ViewModels; +namespace GeneralUpdate.Tools.ViewModels; -public class ExtensionViewModel : ObservableObject +public partial class ExtensionViewModel : ViewModelBase { - #region Private Members - - private ExtensionConfigModel? _configModel; - private AsyncRelayCommand? _generateCommand; - private AsyncRelayCommand? _selectFolderCommand; - private RelayCommand? _loadedCommand; - private RelayCommand? _clearCommand; - private AsyncRelayCommand? _selectDependenciesCommand; - private ExtensionDependencySelectionModel? _selectedDependency; - private AsyncRelayCommand? _removeCustomPropertyCommand; - private AsyncRelayCommand? _addCustomPropertyCommand; - private string? _newCustomPropertyKey; - private string? _newCustomPropertyValue; - - #endregion - - public ExtensionViewModel() - { - LoadedCommand.Execute(null); - } - - #region Public Properties - - public RelayCommand LoadedCommand - { - get { return _loadedCommand ??= new RelayCommand(LoadedAction); } - } - - public AsyncRelayCommand SelectFolderCommand - { - get => _selectFolderCommand ??= new AsyncRelayCommand(SelectFolderAction); - } - - public AsyncRelayCommand GenerateCommand - { - get => _generateCommand ??= new AsyncRelayCommand(GeneratePackageAction); - } - - public RelayCommand ClearCommand - { - get => _clearCommand ??= new RelayCommand(ClearAction); - } - - public ObservableCollection Platforms { get; set; } = - [ - new PlatformModel { DisplayName = "Windows", Value = 1 }, - new PlatformModel { DisplayName = "Linux", Value = 2 }, - new PlatformModel { DisplayName = "MacOS", Value = 3 } - ]; - - public ExtensionConfigModel ConfigModel - { - get => _configModel ??= new ExtensionConfigModel(); - set - { - _configModel = value; - OnPropertyChanged(new PropertyChangedEventArgs(nameof(ConfigModel))); - } - } - - public ExtensionDependencySelectionModel? SelectedDependency - { - get => _selectedDependency; - set => SetProperty(ref _selectedDependency, value); - } - - public AsyncRelayCommand RemoveCustomPropertyCommand - { - get => _removeCustomPropertyCommand ??= new AsyncRelayCommand(RemoveCustomPropertyAction); - } - - public AsyncRelayCommand AddCustomPropertyCommand - { - get => _addCustomPropertyCommand ??= new AsyncRelayCommand(AddCustomPropertyAction, CanAddCustomProperty); - } - - public string? NewCustomPropertyKey - { - get => _newCustomPropertyKey; - set - { - if (SetProperty(ref _newCustomPropertyKey, value)) - { - AddCustomPropertyCommand.NotifyCanExecuteChanged(); - } - } - } - - public string? NewCustomPropertyValue - { - get => _newCustomPropertyValue; - set - { - if (SetProperty(ref _newCustomPropertyValue, value)) - { - AddCustomPropertyCommand.NotifyCanExecuteChanged(); - } - } - } - - public ObservableCollection CustomPropertiesCollection { get; set; } = new(); - - #endregion - - #region Private Methods - - /// - /// Maps legacy platform model value to TargetPlatform enum - /// - /// Platform model value (0=All, 1=Windows, 2=Linux, 3=MacOS) - /// Corresponding TargetPlatform enum value - private static TargetPlatform MapPlatformValue(int platformValue) - { - return platformValue switch - { - 1 => TargetPlatform.Windows, - 2 => TargetPlatform.Linux, - 3 => TargetPlatform.MacOS, - _ => TargetPlatform.All - }; - } - - private void LoadedAction() - { - ResetAction(); - } - - private void ResetAction() - { - Dispatcher.UIThread.Invoke(() => - { - ConfigModel.Name = string.Empty; - ConfigModel.Version = "1.0.0.0"; - ConfigModel.Description = string.Empty; - ConfigModel.ExtensionDirectory = string.Empty; - ConfigModel.Path = string.Empty; - ConfigModel.Dependencies = string.Empty; - ConfigModel.IsUploadToServer = false; - ConfigModel.Platform = Platforms.First(); - ConfigModel.DisplayName = string.Empty; - ConfigModel.Publisher = string.Empty; - ConfigModel.License = string.Empty; - ConfigModel.CategoriesText = string.Empty; - ConfigModel.MinHostVersion = string.Empty; - ConfigModel.MaxHostVersion = string.Empty; - ConfigModel.ReleaseDate = DateTime.Now; - ConfigModel.IsPreRelease = false; - ConfigModel.Format = ".zip"; - ConfigModel.Hash = string.Empty; - SelectedDependency = null; - ConfigModel.CustomProperties.Clear(); - ConfigModel.ShowCustomProperties = false; - CustomPropertiesCollection.Clear(); - NewCustomPropertyKey = string.Empty; - NewCustomPropertyValue = string.Empty; - }); - } - - private async Task SelectFolderAction(string? value) - { - try - { - if (string.IsNullOrWhiteSpace(value)) - { - await MessageBox.ShowAsync("Invalid folder selection parameter", "Error", Buttons.OK); - return; - } - - var folders = await Storage.Instance.SelectFolderDialog(); - if (!folders.Any()) return; - - var folder = folders.First(); - if (folder?.Path?.LocalPath == null) - { - await MessageBox.ShowAsync("Selected folder path is invalid", "Error", Buttons.OK); - return; - } - - switch (value) - { - case "ExtensionDirectory": - ConfigModel.ExtensionDirectory = folder.Path.LocalPath; - break; - case "ExportPath": - ConfigModel.Path = folder.Path.LocalPath; - break; - default: - await MessageBox.ShowAsync($"Unknown folder selection type: {value}", "Error", Buttons.OK); - break; - } - } - catch (Exception ex) - { - await MessageBox.ShowAsync($"Failed to select folder: {ex.Message}", "Error", Buttons.OK); - } - } - - /// - /// Generate update package (compress extension directory and optionally upload) - /// - private async Task GeneratePackageAction() - { - try - { - // Validate input - if (string.IsNullOrWhiteSpace(ConfigModel.Name)) - { - await MessageBox.ShowAsync("Extension name is required", "Validation Error", Buttons.OK); - return; - } - - if (string.IsNullOrWhiteSpace(ConfigModel.Version)) - { - await MessageBox.ShowAsync("Extension version is required", "Validation Error", Buttons.OK); - return; - } - - if (string.IsNullOrWhiteSpace(ConfigModel.ExtensionDirectory)) - { - await MessageBox.ShowAsync("Extension directory is required", "Validation Error", Buttons.OK); - return; - } - - if (!Directory.Exists(ConfigModel.ExtensionDirectory)) - { - await MessageBox.ShowAsync($"Extension directory does not exist: {ConfigModel.ExtensionDirectory}", "Validation Error", Buttons.OK); - return; - } - - if (string.IsNullOrWhiteSpace(ConfigModel.Path)) - { - await MessageBox.ShowAsync("Export path is required", "Validation Error", Buttons.OK); - return; - } - - // ConfigModel.Path is the export directory (not the final zip file path) - var exportDirectory = ConfigModel.Path; - - // Ensure export directory exists - if (!Directory.Exists(exportDirectory)) - { - Directory.CreateDirectory(exportDirectory); - } - - // Sanitize extension name and version to create a valid filename - var sanitizedName = ZipUtility.SanitizeFileName(ConfigModel.Name); - var sanitizedVersion = ZipUtility.SanitizeFileName(ConfigModel.Version); - - // Create zip file name: ExtensionName_Version.zip - var zipFileName = $"{sanitizedName}_{sanitizedVersion}.zip"; - var zipFilePath = Path.Combine(exportDirectory, zipFileName); - - // Compress the extension directory into a zip file - await ZipUtility.CompressDirectoryAsync( - ConfigModel.ExtensionDirectory, - zipFilePath, - System.IO.Compression.CompressionLevel.Optimal, - includeBaseDirectory: false); - - // Update the Path field to point to the compressed zip file for upload - ConfigModel.Path = zipFilePath; - - // Create manifest.json with all ExtensionDTO fields - var platformValue = ConfigModel.Platform?.Value ?? 0; - var targetPlatform = MapPlatformValue(platformValue); - ConfigModel.Platform = new PlatformModel { DisplayName = targetPlatform.ToString(), Value = platformValue }; - - // Get file info for the zip - var fileInfo = new FileInfo(zipFilePath); - ConfigModel.FileSize = fileInfo.Length; - - // Serialize manifest to JSON with explicit settings - var jsonSettings = new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore - }; - var manifestJson = JsonConvert.SerializeObject(ConfigModel, jsonSettings); - if (!string.IsNullOrEmpty(manifestJson)) - { - // Add manifest.json to the zip file - await ZipUtility.AddFileToZipAsync(zipFilePath, "manifest.json", manifestJson); - } - - var fileName = Path.GetFileName(zipFilePath); - var directory = Path.GetDirectoryName(zipFilePath); - await MessageBox.ShowAsync( - $"Extension package created successfully:\n\nFile: {fileName}\nLocation: {directory}", - "Success", - Buttons.OK); - } - catch (UnauthorizedAccessException ex) - { - await MessageBox.ShowAsync($"Access denied: {ex.Message}\nPlease check file permissions.", "Error", Buttons.OK); - } - catch (IOException ex) - { - await MessageBox.ShowAsync($"I/O error: {ex.Message}", "Error", Buttons.OK); - } - catch (Exception ex) - { - await MessageBox.ShowAsync($"Failed to generate package: {ex.Message}", "Error", Buttons.OK); - } - } - - private void ClearAction() => ResetAction(); - - private bool CanAddCustomProperty() - { - return !string.IsNullOrWhiteSpace(NewCustomPropertyKey) && - !string.IsNullOrWhiteSpace(NewCustomPropertyValue); - } - - private async Task AddCustomPropertyAction() - { + private readonly PackageService _pkg = new(); + public ExtensionConfigModel Config { get; } = new(); + [ObservableProperty] private bool _isBuilding; + [ObservableProperty] private string _status = "就绪"; + [ObservableProperty] private string _newPropKey = ""; + [ObservableProperty] private string _newPropValue = ""; + public ObservableCollection CustomProps { get; } = new(); + + async Task Pick() { var tl = Avalonia.Controls.TopLevel.GetTopLevel((Avalonia.Application.Current?.ApplicationLifetime as Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime)?.MainWindow); if (tl == null) return null; var r = await tl.StorageProvider.OpenFolderPickerAsync(new Avalonia.Platform.Storage.FolderPickerOpenOptions { Title = "选择目录", AllowMultiple = false }); return r.Count > 0 ? r[0].Path.LocalPath : null; } + + [RelayCommand] async Task SelectExt() { var p = await Pick(); if (p != null) Config.ExtensionDirectory = p; } + [RelayCommand] async Task SelectExport() { var p = await Pick(); if (p != null) Config.ExportPath = p; } + [RelayCommand] void AddProp() { if (!string.IsNullOrWhiteSpace(NewPropKey) && !string.IsNullOrWhiteSpace(NewPropValue)) { CustomProps.Add(new(NewPropKey, NewPropValue)); NewPropKey = ""; NewPropValue = ""; } } + [RelayCommand] void RemoveProp(CustomPropModel? item) { if (item != null) CustomProps.Remove(item); } + + [RelayCommand] async Task Generate() + { + if (string.IsNullOrWhiteSpace(Config.Name) || string.IsNullOrWhiteSpace(Config.Version)) { Status = "请填写扩展名称和版本"; return; } + if (string.IsNullOrWhiteSpace(Config.ExtensionDirectory) || !Directory.Exists(Config.ExtensionDirectory)) { Status = "请选择有效的扩展目录"; return; } + IsBuilding = true; Status = "正在生成扩展包..."; try { - if (string.IsNullOrWhiteSpace(NewCustomPropertyKey)) - { - await MessageBox.ShowAsync("Property key cannot be empty", "Validation Error", Buttons.OK); - return; - } - - if (string.IsNullOrWhiteSpace(NewCustomPropertyValue)) - { - await MessageBox.ShowAsync("Property value cannot be empty", "Validation Error", Buttons.OK); - return; - } - - // Check if key already exists - if (ConfigModel.CustomProperties.ContainsKey(NewCustomPropertyKey)) - { - await MessageBox.ShowAsync($"Property key '{NewCustomPropertyKey}' already exists", "Validation Error", Buttons.OK); - return; - } - - // Add to dictionary - ConfigModel.CustomProperties[NewCustomPropertyKey] = NewCustomPropertyValue; - - // Add to observable collection for UI binding - CustomPropertiesCollection.Add(new CustomPropertyModel - { - Key = NewCustomPropertyKey, - Value = NewCustomPropertyValue + var dir = string.IsNullOrWhiteSpace(Config.ExportPath) ? Environment.GetFolderPath(Environment.SpecialFolder.Desktop) : Config.ExportPath; + var zip = Path.Combine(dir, $"{Sanitize(Config.Name)}_{Config.Version}.zip"); + await _pkg.CompressDirectoryAsync(Config.ExtensionDirectory, zip); + await _pkg.CreateManifestAsync(zip, new { + name = Config.Name, version = Config.Version, description = Config.Description, + publisher = Config.Publisher, license = Config.License, dependencies = Config.Dependencies, + minHostVersion = Config.MinHostVersion, maxHostVersion = Config.MaxHostVersion, + isPreRelease = Config.IsPreRelease, + customProperties = CustomProps.ToDictionary(p => p.Key, p => p.Value) }); - - // Clear input fields - NewCustomPropertyKey = string.Empty; - NewCustomPropertyValue = string.Empty; - } - catch (Exception ex) - { - await MessageBox.ShowAsync($"Failed to add custom property: {ex.Message}", "Error", Buttons.OK); - } - } - - private async Task RemoveCustomPropertyAction(CustomPropertyModel? property) - { - try - { - if (property == null) - { - await MessageBox.ShowAsync("No property selected to remove", "Validation Error", Buttons.OK); - return; - } - - // Remove from dictionary - if (ConfigModel.CustomProperties.ContainsKey(property.Key)) - { - ConfigModel.CustomProperties.Remove(property.Key); - } - - // Remove from observable collection - CustomPropertiesCollection.Remove(property); - } - catch (Exception ex) - { - await MessageBox.ShowAsync($"Failed to remove custom property: {ex.Message}", "Error", Buttons.OK); + Config.OutputPath = zip; + Status = $"成功: {Path.GetFileName(zip)}"; } + catch (Exception ex) { Status = $"失败: {ex.Message}"; } + finally { IsBuilding = false; } } + static string Sanitize(string n) => string.Join("_", n.Split(Path.GetInvalidFileNameChars())); +} - #endregion -} \ No newline at end of file +public partial class CustomPropModel(string key, string value) : ObservableObject +{ + public string Key { get; set; } = key; + public string Value { get; set; } = value; +} diff --git a/src/ViewModels/MainWindowViewModel.cs b/src/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..801ce3e --- /dev/null +++ b/src/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,31 @@ +using System.Collections.ObjectModel; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using GeneralUpdate.Tools.Models; + +namespace GeneralUpdate.Tools.ViewModels; + +public partial class MainWindowViewModel : ViewModelBase +{ + [ObservableProperty] private ViewModelBase _currentPage = new PatchViewModel(); + public ObservableCollection NavItems { get; } = new(); + + public MainWindowViewModel() + { + NavItems.Add(new("Patch", "补丁包", typeof(PatchViewModel), true)); + NavItems.Add(new("Extension", "扩展包", typeof(ExtensionViewModel), false)); + NavItems.Add(new("OSS", "OSS配置", typeof(OSSViewModel), false)); + } + + [RelayCommand] private void Navigate(NavItem item) { foreach (var n in NavItems) n.IsSelected = false; item.IsSelected = true; CurrentPage = item.Key switch { "Patch" => new PatchViewModel(), "Extension" => new ExtensionViewModel(), _ => new OSSViewModel() }; } +} + +public partial class NavItem : ObservableObject +{ + public string Key { get; } + public string Title { get; } + public System.Type PageType { get; } + [ObservableProperty] private bool _isSelected; + public NavItem(string key, string title, System.Type pageType, bool selected) { Key = key; Title = title; PageType = pageType; _isSelected = selected; } +} diff --git a/src/ViewModels/OSSPacketViewModel.cs b/src/ViewModels/OSSPacketViewModel.cs deleted file mode 100644 index 9d5fd35..0000000 --- a/src/ViewModels/OSSPacketViewModel.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using GeneralUpdate.Common.HashAlgorithms; -using GeneralUpdate.Tool.Avalonia.Models; -using Newtonsoft.Json; -using Nlnet.Avalonia.Controls; - -namespace GeneralUpdate.Tool.Avalonia.ViewModels; - -public class OSSPacketViewModel : ObservableObject -{ - #region Private Members - - private OSSConfigModel? _currnetConfig; - - private AsyncRelayCommand? _copyCommand; - private AsyncRelayCommand? _buildCommand; - private AsyncRelayCommand? _hashCommand; - private RelayCommand? _appendCommand; - private RelayCommand? _clearCommand; - private RelayCommand? _loadedCommand; - - #endregion - - #region Public Properties - - public ObservableCollection Configs { get; set; } = new(); - - public OSSConfigModel CurrnetConfig - { - get => _currnetConfig; - set => SetProperty(ref _currnetConfig, value); - } - - public AsyncRelayCommand BuildCommand { get => _buildCommand ??= new AsyncRelayCommand(OSSBuildAction); } - - public RelayCommand AppendCommand { get => _appendCommand ??= new RelayCommand(AppendAction); } - - public AsyncRelayCommand CopyCommand { get => _copyCommand ??= new AsyncRelayCommand(CopyAction); } - - public AsyncRelayCommand HashCommand { get => _hashCommand ??= new AsyncRelayCommand(HashAction); } - - public RelayCommand ClearCommand { get => _clearCommand ??= new RelayCommand(ClearAction); } - - public RelayCommand LoadedCommand - { - get { return _loadedCommand ??= new (LoadedAction); } - } - - #endregion - - #region Private Methods - - private async Task OSSBuildAction() - { - try - { - var file = await Storage.Instance.SaveFilePickerAsync(); - if (file != null) - { - var json = JsonConvert.SerializeObject(Configs); - await File.WriteAllTextAsync(file.Path.AbsolutePath, json, System.Text.Encoding.UTF8); - var caption = string.Empty; - var message = string.Empty; - if (File.Exists(file.Path.AbsolutePath)) - { - caption = "Success"; - message = "Build success"; - } - else - { - caption = "Fail"; - message = "Build fail"; - } - - await MessageBox.ShowAsync(message, caption, Buttons.OK); - } - } - catch (Exception e) - { - await MessageBox.ShowAsync("Build fail", "Fail", Buttons.OK); - } - } - - private void AppendAction() - { - try - { - Configs.Add(new OSSConfigModel - { - Date = CurrnetConfig.Date, - Time = CurrnetConfig.Time, - Hash = CurrnetConfig.Hash, - PacketName = CurrnetConfig.PacketName, - Url = CurrnetConfig.Url, - Version = CurrnetConfig.Version - }); - var settings = new JsonSerializerSettings - { - Formatting = Formatting.Indented, NullValueHandling = NullValueHandling.Ignore - }; - CurrnetConfig.JsonContent = JsonConvert.SerializeObject(Configs, settings); - } - catch (Exception e) - { - MessageBox.Show("Append fail", "Fail", Buttons.OK); - } - } - - private async Task CopyAction() - { - try - { - await ClipboardUtility.SetText(CurrnetConfig.JsonContent); - await MessageBox.ShowAsync("Copy success", "Success", Buttons.OK); - } - catch (Exception e) - { - await MessageBox.ShowAsync("Copy fail", "Fail", Buttons.OK); - } - } - - private async Task HashAction() - { - var files = await Storage.Instance.OpenFileDialog(); - if (files is null || files.Count == 0) return; - - var file = files.First(); - if (file is not null) - { - Sha256HashAlgorithm hashAlgorithm = new(); - CurrnetConfig.Hash = hashAlgorithm.ComputeHash(file.Path.LocalPath); - } - } - - private void ClearAction() - { - CurrnetConfig.JsonContent = "{}"; - Configs.Clear(); - } - - private void LoadedAction() => Initialize(); - - private void Initialize() - { - DateTime dateTime = DateTime.Now; - CurrnetConfig = new OSSConfigModel - { - JsonContent = "{}", - PacketName = "Packet", - Hash = String.Empty, - Date = new DateTime(dateTime.Year, dateTime.Month, dateTime.Day), - Time = new TimeSpan(dateTime.Hour, dateTime.Minute, dateTime.Second), - Version = "1.0.0.0", - Url = "http://127.0.0.1" - }; - } - - #endregion -} \ No newline at end of file diff --git a/src/ViewModels/OSSViewModel.cs b/src/ViewModels/OSSViewModel.cs new file mode 100644 index 0000000..8e0302c --- /dev/null +++ b/src/ViewModels/OSSViewModel.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using GeneralUpdate.Tools.Models; +using GeneralUpdate.Tools.Services; +using Newtonsoft.Json; + +namespace GeneralUpdate.Tools.ViewModels; + +public partial class OSSViewModel : ViewModelBase +{ + private readonly HashService _hash = new(); + public ObservableCollection Configs { get; } = new(); + [ObservableProperty] private OSSConfigModel _current = new(); + [ObservableProperty] private string _status = "就绪"; + + [RelayCommand] async Task ComputeHash() { var tl = Avalonia.Controls.TopLevel.GetTopLevel((Avalonia.Application.Current?.ApplicationLifetime as Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime)?.MainWindow); if (tl == null) return; var files = await tl.StorageProvider.OpenFilePickerAsync(new Avalonia.Platform.Storage.FilePickerOpenOptions { Title = "选择文件", AllowMultiple = false }); if (files.Count > 0) { Current.Hash = await _hash.ComputeHashAsync(files[0].Path.LocalPath); Status = $"SHA256: {Current.Hash}"; } } + [RelayCommand] void Append() { Configs.Add(new() { PacketName = Current.PacketName, Hash = Current.Hash, Version = Current.Version, Url = Current.Url, ReleaseDate = Current.ReleaseDate }); Status = "已添加"; } + [RelayCommand] void Remove(OSSConfigModel? item) { if (item != null) Configs.Remove(item); } + [RelayCommand] void Clear() { Configs.Clear(); Status = "已清空"; } + [RelayCommand] async Task Export() { var tl = Avalonia.Controls.TopLevel.GetTopLevel((Avalonia.Application.Current?.ApplicationLifetime as Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime)?.MainWindow); if (tl == null) return; var file = await tl.StorageProvider.SaveFilePickerAsync(new Avalonia.Platform.Storage.FilePickerSaveOptions { Title = "导出JSON", DefaultExtension = ".json", SuggestedFileName = "oss_config.json" }); if (file != null) { await File.WriteAllTextAsync(file.Path.LocalPath, JsonConvert.SerializeObject(Configs, Formatting.Indented), System.Text.Encoding.UTF8); Status = $"导出: {Configs.Count} 条"; } } +} diff --git a/src/ViewModels/PacketViewModel.cs b/src/ViewModels/PacketViewModel.cs deleted file mode 100644 index 9c40e96..0000000 --- a/src/ViewModels/PacketViewModel.cs +++ /dev/null @@ -1,528 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; -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; -using Newtonsoft.Json; -using Nlnet.Avalonia.Controls; - -namespace GeneralUpdate.Tool.Avalonia.ViewModels; - -public class PacketViewModel : ObservableObject -{ - private PacketConfigModel? _configModel; - - private RelayCommand? _clearCommand; - private RelayCommand? _loadedCommand; - private AsyncRelayCommand? _buildCommand; - private AsyncRelayCommand? _selectFolderCommand; - - public ObservableCollection AppTypes { get; set; } = new(); - - public ObservableCollection Formats { get; set; } = - [ - new FormatModel { DisplayName = ".zip", Type = 1, Value = ".zip" } - ]; - - public ObservableCollection Encodings { get; set; } = - [ - new EncodingModel { DisplayName = "Default", Value = Encoding.Default, Type = 1 }, - new EncodingModel { DisplayName = "UTF-8", Value = Encoding.UTF8, Type = 2 }, - new EncodingModel { DisplayName = "UTF-7", Value = Encoding.UTF7, Type = 3 }, - new EncodingModel { DisplayName = "Unicode", Value = Encoding.GetEncoding("Unicode"), Type = 4 }, - new EncodingModel { DisplayName = "UTF-32", Value = Encoding.UTF32, Type = 5 }, - new EncodingModel { DisplayName = "BigEndianUnicode", Value = Encoding.BigEndianUnicode, Type = 6 }, - new EncodingModel { DisplayName = "Latin1", Value = Encoding.GetEncoding("Latin1"), Type = 7 }, - new EncodingModel { DisplayName = "ASCII", Value = Encoding.ASCII, Type = 8 } - ]; - - public ObservableCollection Platforms { get; set; } = - [ - new PlatformModel { DisplayName = "Windows", Value = 1 }, - new PlatformModel { DisplayName = "Linux", Value = 2 } - ]; - - public PacketConfigModel ConfigModel - { - get => _configModel ??= new PacketConfigModel() ; - set - { - _configModel = value; - SetProperty(ref _configModel, value); - } - } - - public RelayCommand LoadedCommand - { - get { return _loadedCommand ??= new (LoadedAction); } - } - - public AsyncRelayCommand SelectFolderCommand - { - get => _selectFolderCommand ??= new (SelectFolderAction); - } - - public AsyncRelayCommand BuildCommand - { - get => _buildCommand ??= new (BuildPacketAction); - } - - public RelayCommand ClearCommand - { - get => _clearCommand ??= new (ClearAction); - } - - private void LoadedAction() - { - AppTypes.Clear(); - AppTypes.Add(new AppTypeModel{ DisplayName = "ClientApp", Value = 1 }); - AppTypes.Add(new AppTypeModel{ DisplayName = "UpgradeApp", Value = 2 }); - ResetAction(); - } - - private void ResetAction() - { - ConfigModel.Name = GenerateFileName(); - ConfigModel.ReleaseDirectory = GetPlatformSpecificPath(); - ConfigModel.AppDirectory = GetPlatformSpecificPath(); - ConfigModel.PatchDirectory = GetPlatformSpecificPath(); - ConfigModel.DriverDirectory = string.Empty; - ConfigModel.ReportUrl = string.Empty; - ConfigModel.UpdateUrl = string.Empty; - ConfigModel.AppName = "Update"; - ConfigModel.MainAppName = string.Empty; - ConfigModel.ClientVersion = string.Empty; - ConfigModel.Encoding = Encodings.First(); - ConfigModel.Format = Formats.First(); - CreateDirectory(); - } - - /// - /// Choose a path - /// - /// - private async Task SelectFolderAction(string value) - { - try - { - var folders = await Storage.Instance.SelectFolderDialog(); - if (!folders.Any()) return; - - var folder = folders.First(); - switch (value) - { - case "App": - ConfigModel.AppDirectory = folder.Path.LocalPath; - break; - - case "Release": - ConfigModel.ReleaseDirectory = folder!.Path.LocalPath; - break; - - case "Patch": - ConfigModel.PatchDirectory = folder!.Path.LocalPath; - break; - - case "Driver": - ConfigModel.DriverDirectory = folder!.Path.LocalPath; - break; - } - } - catch (Exception e) - { - Trace.WriteLine(e.Message); - } - } - - /// - /// Build patch package - /// - private async Task BuildPacketAction() - { - try - { - // Validate required fields - if (!await ValidateRequiredFields()) - return; - - // Read configuration from .csproj - ReadProjectConfiguration(); - - await DifferentialCore.Instance.Clean(ConfigModel.AppDirectory, - ConfigModel.ReleaseDirectory, - ConfigModel.PatchDirectory); - - // Copy driver files to drivers folder if driver directory is specified - if (!string.IsNullOrWhiteSpace(ConfigModel.DriverDirectory) && - Directory.Exists(ConfigModel.DriverDirectory)) - { - try - { - var driversFolder = Path.Combine(ConfigModel.PatchDirectory, "drivers"); - Directory.CreateDirectory(driversFolder); - - CopyDriverFiles(ConfigModel.DriverDirectory, driversFolder); - } - catch (Exception ex) - { - await MessageBox.ShowAsync("Failed to copy driver files. Please check the driver directory permissions and available disk space.", "Warning", Buttons.OK); - } - } - - // Create and save ConfigInfo JSON file - var configFile = await CreateConfigInfoFile(); - - var directoryInfo = new DirectoryInfo(ConfigModel.PatchDirectory); - var parentDirectory = directoryInfo.Parent!.FullName; - var operationType = ConfigModel.Format.Value; - var encoding = ConfigModel.Encoding.Value; - - CompressProvider.Compress(operationType - , ConfigModel.PatchDirectory - , Path.Combine(parentDirectory,ConfigModel.Name + ConfigModel.Format.Value) - , false, encoding); - - if (Directory.Exists(ConfigModel.PatchDirectory)) - DeleteDirectoryRecursively(ConfigModel.PatchDirectory); - - var packetInfo = new FileInfo(Path.Combine(parentDirectory, $"{ConfigModel.Name}{ConfigModel.Format.Value}")); - if (packetInfo.Exists) - { - ConfigModel.Path = packetInfo.FullName; - await MessageBox.ShowAsync("Build success", "Success", Buttons.OK); - OpenFileDirectoryAndSelectFile(configFile); - } - else - { - await MessageBox.ShowAsync("Build fail", "Fail", Buttons.OK); - } - } - catch (Exception e) - { - await MessageBox.ShowAsync(e.Message, "Fail", Buttons.OK); - } - } - - private void ClearAction() => ResetAction(); - - private string GetPlatformSpecificPath() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - // Windows-specific path, defaulting to C: drive - return @"C:\"; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - // Linux-specific path, defaulting to /home/user - return "/home"; - } - - throw new PlatformNotSupportedException("Unsupported OS"); - } - - private string GenerateFileName() - { - var timestamp = DateTime.Now.ToString("yyyyMMddHHmmssfff"); - return $"packet_{timestamp}"; - } - - private void DeleteDirectoryRecursively(string targetDir) - { - foreach (var file in Directory.GetFiles(targetDir)) - { - File.SetAttributes(file, FileAttributes.Normal); - File.Delete(file); - } - - foreach (var dir in Directory.GetDirectories(targetDir)) - { - DeleteDirectoryRecursively(dir); - } - Directory.Delete(targetDir, false); - } - - private void CopyDriverFiles(string sourceDir, string targetDir) - { - // Copy all files from source to target - foreach (var file in Directory.GetFiles(sourceDir)) - { - var fileName = Path.GetFileName(file); - var destFile = Path.Combine(targetDir, fileName); - File.Copy(file, destFile, true); - } - - // Copy all subdirectories and their files recursively - foreach (var dir in Directory.GetDirectories(sourceDir)) - { - var dirName = Path.GetFileName(dir); - var destDir = Path.Combine(targetDir, dirName); - Directory.CreateDirectory(destDir); - CopyDriverFiles(dir, destDir); - } - } - - /// - /// Validate required fields - /// - private async Task 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)}"; - await MessageBox.ShowAsync(message, "Validation Error", Buttons.OK); - 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); - - // Read ReleaseDirectory - var directory = SearchExeFileAndGetDirectory(ConfigModel.ReleaseDirectory, ConfigModel.MainAppName); - var outputPath = CsprojReader.ReadOutputPath(ConfigModel.ReleaseDirectory); - - if (!string.IsNullOrWhiteSpace(outputPath)) - { - ConfigModel.ReleaseDirectory = outputPath; - } - else if (!string.IsNullOrWhiteSpace(directory)) - { - ConfigModel.ReleaseDirectory = SearchExeFileAndGetDirectory(ConfigModel.ReleaseDirectory, 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 - }; - - var json = JsonConvert.SerializeObject(configInfo, Formatting.Indented); - Directory.CreateDirectory(ConfigModel.PatchDirectory); - var configFilePath = Path.Combine(ConfigModel.PatchDirectory, "update_config.json"); - - await File.WriteAllTextAsync(configFilePath, json, Encoding.UTF8); - - return configFilePath; - } - catch (Exception ex) - { - Trace.WriteLine($"Error creating config info file: {ex.Message}"); - throw; - } - } - - /// - /// Searches for the specified exe file in release/debug directories under bin directories - /// in the specified root directory, and returns the directories of the found exe files as a concatenated string - /// - /// Root directory path - /// Name of the exe file to search (including extension) - /// Paths of directories containing matching exe files, separated by semicolons; - /// returns empty string if no files are found - /// Thrown when parameter is null or whitespace - /// Thrown when parameter format is invalid - /// Thrown when root directory does not exist - private static string SearchExeFileAndGetDirectory(string rootDirectory, string exeFileName) - { - // Temporarily store directories of found exe files - List exeDirectories = new List(); - - // ===================== Step 1: Comprehensive parameter validation ===================== - // 1. Validate if rootDirectory is null, empty or whitespace - if (string.IsNullOrWhiteSpace(rootDirectory)) - { - throw new ArgumentNullException(nameof(rootDirectory), "Root directory path cannot be null, empty or whitespace"); - } - - // 2. Validate if rootDirectory path format is legal (avoid invalid paths like "::/\\abc") - try - { - // Attempt to resolve the path, exception will be thrown if format is illegal - string fullPath = Path.GetFullPath(rootDirectory); - } - catch (Exception ex) - { - throw new ArgumentException($"Invalid root directory path format: {ex.Message}", nameof(rootDirectory), ex); - } - - // 3. Validate if exeFileName is null, empty or whitespace - if (string.IsNullOrWhiteSpace(exeFileName)) - { - throw new ArgumentNullException(nameof(exeFileName), "Exe file name to search cannot be null, empty or whitespace"); - } - - // 6. Basic validation: Check if root directory exists (kept after format validation) - if (!Directory.Exists(rootDirectory)) - { - throw new DirectoryNotFoundException($"Specified root directory does not exist: {rootDirectory}"); - } - - // ===================== Step 2: Original core search logic ===================== - try - { - // 1. Recursively find all bin directories (case-insensitive) - var binDirectories = Directory.EnumerateDirectories( - rootDirectory, - "bin", - SearchOption.AllDirectories) - .Where(dir => string.Equals(Path.GetFileName(dir), "bin", StringComparison.OrdinalIgnoreCase)); - - foreach (string binDir in binDirectories) - { - string targetDir = null; - - // 2. Prioritize checking release directory - string releaseDir = Path.Combine(binDir, "release"); - if (Directory.Exists(releaseDir)) - { - targetDir = releaseDir; - } - else - { - // Fallback to check debug directory - string debugDir = Path.Combine(binDir, "debug"); - if (Directory.Exists(debugDir)) - { - targetDir = debugDir; - } - } - - // 3. If release/debug directory is found, search for exe files - if (!string.IsNullOrEmpty(targetDir)) - { - var exeFiles = Directory.EnumerateFiles( - targetDir, - "*.exe", - SearchOption.AllDirectories); - - foreach (string filePath in exeFiles) - { - string fileName = Path.GetFileName(filePath); - if (fileName.Contains(exeFileName)) - { - string exeDir = Path.GetDirectoryName(filePath); - if (!exeDirectories.Contains(exeDir)) - { - exeDirectories.Add(exeDir); - } - } - } - } - } - } - catch (UnauthorizedAccessException ex) - { - Console.WriteLine($"Insufficient permissions to access directory: {ex.Message}"); - } - catch (PathTooLongException ex) - { - Console.WriteLine($"File path is too long: {ex.Message}"); - } - - // Concatenate directories into a string and return - return string.Join(";", exeDirectories); - } - - private void CreateDirectory() - { - var baseDir = GetApplicationDataDirectory(); - - var packetDir = Path.Combine(baseDir, "packets"); - if (!Directory.Exists(packetDir)) - { - Directory.CreateDirectory(packetDir); - } - - var patchDir = Path.Combine(packetDir, "patch"); - if (!Directory.Exists(patchDir)) - { - Directory.CreateDirectory(patchDir); - } - - ConfigModel.PatchDirectory = patchDir; - } - - private static string GetApplicationDataDirectory() - { - var localApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - if (string.IsNullOrWhiteSpace(localApplicationData)) - { - return AppContext.BaseDirectory; - } - - return Path.Combine(localApplicationData, "GeneralUpdate.Tool.Avalonia"); - } - - static void OpenFileDirectoryAndSelectFile(string filePath) - { - if (!File.Exists(filePath)) - { - throw new FileNotFoundException("The file is not exists!", filePath); - } - - Process.Start(new ProcessStartInfo - { - FileName = "explorer.exe", - Arguments = $"/select, \"{filePath}\"", // /select 参数用于选中文件 - UseShellExecute = true - }); - } -} diff --git a/src/ViewModels/PatchViewModel.cs b/src/ViewModels/PatchViewModel.cs new file mode 100644 index 0000000..9b06601 --- /dev/null +++ b/src/ViewModels/PatchViewModel.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using GeneralUpdate.Tools.Models; +using GeneralUpdate.Tools.Services; + +namespace GeneralUpdate.Tools.ViewModels; + +public partial class PatchViewModel : ViewModelBase +{ + private readonly DiffService _diff = new(); + private readonly PackageService _pkg = new(); + public PatchConfigModel Config { get; } = new(); + [ObservableProperty] private bool _isBuilding; + [ObservableProperty] private double _progress; + [ObservableProperty] private string _status = "就绪"; + [ObservableProperty] private ObservableCollection _log = new(); + + async Task Pick() { var tl = Avalonia.Controls.TopLevel.GetTopLevel((Avalonia.Application.Current?.ApplicationLifetime as Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime)?.MainWindow); if (tl == null) return null; var r = await tl.StorageProvider.OpenFolderPickerAsync(new Avalonia.Platform.Storage.FolderPickerOpenOptions { Title = "选择目录", AllowMultiple = false }); return r.Count > 0 ? r[0].Path.LocalPath : null; } + + [RelayCommand] async Task SelectOld() { var p = await Pick(); if (p != null) { Config.OldDirectory = p; L($"旧版本: {p}"); } } + [RelayCommand] async Task SelectNew() { var p = await Pick(); if (p != null) { Config.NewDirectory = p; L($"新版本: {p}"); } } + [RelayCommand] async Task SelectOut() { var p = await Pick(); if (p != null) Config.OutputPath = p; } + + [RelayCommand] async Task Build() + { + if (string.IsNullOrWhiteSpace(Config.OldDirectory) || string.IsNullOrWhiteSpace(Config.NewDirectory)) { Status = "请选择新旧版本目录"; return; } + IsBuilding = true; Log.Clear(); Progress = 0; Status = "正在生成差分补丁..."; + try + { + var tmp = Path.Combine(Path.GetTempPath(), $"gupatch_{DateTime.Now:yyyyMMddHHmmss}"); Directory.CreateDirectory(tmp); + L($"临时目录: {tmp}"); Progress = 20; + L("对比目录差异 + 生成 BSDiff40 补丁..."); + await _diff.GeneratePatchAsync(Config.OldDirectory, Config.NewDirectory, tmp); + Progress = 70; L("补丁生成完成"); + var outDir = string.IsNullOrWhiteSpace(Config.OutputPath) ? Environment.GetFolderPath(Environment.SpecialFolder.Desktop) : Config.OutputPath; + var name = string.IsNullOrWhiteSpace(Config.PackageName) ? $"patch_{DateTime.Now:yyyyMMddHHmmss}" : Config.PackageName; + var zip = Path.Combine(outDir, $"{name}.zip"); + L($"打包: {Path.GetFileName(zip)}"); + await _pkg.CompressDirectoryAsync(tmp, zip); + Directory.Delete(tmp, true); + Progress = 100; Config.OutputPath = zip; + Status = $"成功: {Path.GetFileName(zip)} ({new FileInfo(zip).Length/1024.0:F1} KB)"; + L(Status); + } + catch (Exception ex) { Status = $"失败: {ex.Message}"; L($"错误: {ex}"); } + finally { IsBuilding = false; } + } + void L(string m) => Log.Add($"[{DateTime.Now:HH:mm:ss}] {m}"); +} diff --git a/src/ViewModels/ViewModelBase.cs b/src/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..11d2f68 --- /dev/null +++ b/src/ViewModels/ViewModelBase.cs @@ -0,0 +1,5 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace GeneralUpdate.Tools.ViewModels; + +public partial class ViewModelBase : ObservableObject { } diff --git a/src/Views/ExtensionView.axaml b/src/Views/ExtensionView.axaml index 895da4b..9e70a46 100644 --- a/src/Views/ExtensionView.axaml +++ b/src/Views/ExtensionView.axaml @@ -1,370 +1,60 @@  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Views/PacketView.axaml.cs b/src/Views/PacketView.axaml.cs deleted file mode 100644 index 6397918..0000000 --- a/src/Views/PacketView.axaml.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using GeneralUpdate.Tool.Avalonia.ViewModels; - -namespace GeneralUpdate.Tool.Avalonia.Views; - -public partial class PacketView : UserControl -{ - public PacketView() - { - InitializeComponent(); - DataContext = new PacketViewModel(); - } -} \ No newline at end of file diff --git a/src/Views/PatchView.axaml b/src/Views/PatchView.axaml new file mode 100644 index 0000000..047a658 --- /dev/null +++ b/src/Views/PatchView.axaml @@ -0,0 +1,54 @@ + + + + + + + + + + +