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/ExtensionView.axaml.cs b/src/Views/ExtensionView.axaml.cs
index aaa8ff3..b02f93c 100644
--- a/src/Views/ExtensionView.axaml.cs
+++ b/src/Views/ExtensionView.axaml.cs
@@ -1,15 +1 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
-using GeneralUpdate.Tool.Avalonia.ViewModels;
-
-namespace GeneralUpdate.Tool.Avalonia.Views;
-
-public partial class ExtensionView : UserControl
-{
- public ExtensionView()
- {
- InitializeComponent();
- DataContext = new ExtensionViewModel();
- }
-}
\ No newline at end of file
+using Avalonia.Controls; namespace GeneralUpdate.Tools.Views { public partial class ExtensionView : UserControl { public ExtensionView() { InitializeComponent(); } } }
diff --git a/src/Views/MainWindow.axaml b/src/Views/MainWindow.axaml
index 989f6e9..da7d3bb 100644
--- a/src/Views/MainWindow.axaml
+++ b/src/Views/MainWindow.axaml
@@ -1,21 +1,35 @@
-
-
-
-
-
-
-
-
-
-
-
-
+ xmlns:vm="using:GeneralUpdate.Tools.ViewModels"
+ x:Class="GeneralUpdate.Tools.Views.MainWindow"
+ x:Name="MainWin"
+ x:DataType="vm:MainWindowViewModel"
+ Title="GeneralUpdate Tools v12" Width="960" Height="640" MinWidth="780" MinHeight="540"
+ WindowStartupLocation="CenterScreen">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Views/MainWindow.axaml.cs b/src/Views/MainWindow.axaml.cs
index 566165c..23c77ce 100644
--- a/src/Views/MainWindow.axaml.cs
+++ b/src/Views/MainWindow.axaml.cs
@@ -1,14 +1 @@
-using Avalonia;
-using Avalonia.Controls;
-
-namespace GeneralUpdate.Tool.Avalonia.Views;
-
-public partial class MainWindow : Window
-{
- public MainWindow()
- {
- InitializeComponent();
- ClipboardUtility.CreateClipboard(this);
- Storage.Instance.SetStorageProvider(this);
- }
-}
\ No newline at end of file
+using Avalonia.Controls; namespace GeneralUpdate.Tools.Views { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } }
diff --git a/src/Views/OSSPacketView.axaml b/src/Views/OSSPacketView.axaml
deleted file mode 100644
index ed6ef33..0000000
--- a/src/Views/OSSPacketView.axaml
+++ /dev/null
@@ -1,111 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Views/OSSPacketView.axaml.cs b/src/Views/OSSPacketView.axaml.cs
deleted file mode 100644
index 64f4dd6..0000000
--- a/src/Views/OSSPacketView.axaml.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using Avalonia.Controls;
-using GeneralUpdate.Tool.Avalonia.ViewModels;
-
-namespace GeneralUpdate.Tool.Avalonia.Views;
-
-public partial class OSSPacketView : UserControl
-{
- public OSSPacketView()
- {
- InitializeComponent();
- DataContext = new OSSPacketViewModel();
- }
-}
\ No newline at end of file
diff --git a/src/Views/OSSView.axaml b/src/Views/OSSView.axaml
new file mode 100644
index 0000000..4d88b44
--- /dev/null
+++ b/src/Views/OSSView.axaml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Views/OSSView.axaml.cs b/src/Views/OSSView.axaml.cs
new file mode 100644
index 0000000..bb508c4
--- /dev/null
+++ b/src/Views/OSSView.axaml.cs
@@ -0,0 +1 @@
+using Avalonia.Controls; namespace GeneralUpdate.Tools.Views { public partial class OSSView : UserControl { public OSSView() { InitializeComponent(); } } }
diff --git a/src/Views/PacketView.axaml b/src/Views/PacketView.axaml
deleted file mode 100644
index 40afc7d..0000000
--- a/src/Views/PacketView.axaml
+++ /dev/null
@@ -1,90 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Views/PatchView.axaml.cs b/src/Views/PatchView.axaml.cs
new file mode 100644
index 0000000..362d30c
--- /dev/null
+++ b/src/Views/PatchView.axaml.cs
@@ -0,0 +1 @@
+using Avalonia.Controls; namespace GeneralUpdate.Tools.Views { public partial class PatchView : UserControl { public PatchView() { InitializeComponent(); } } }
diff --git a/src/app.manifest b/src/app.manifest
index 1936524..cde83fd 100644
--- a/src/app.manifest
+++ b/src/app.manifest
@@ -3,7 +3,7 @@
-
+