diff --git a/src/Common/ZipUtility.cs b/src/Common/ZipUtility.cs
new file mode 100644
index 0000000..1f08392
--- /dev/null
+++ b/src/Common/ZipUtility.cs
@@ -0,0 +1,244 @@
+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/Models/CustomPropertyModel.cs b/src/Models/CustomPropertyModel.cs
new file mode 100644
index 0000000..a87526a
--- /dev/null
+++ b/src/Models/CustomPropertyModel.cs
@@ -0,0 +1,27 @@
+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/ExtensionConfigModel.cs b/src/Models/ExtensionConfigModel.cs
new file mode 100644
index 0000000..4b049cc
--- /dev/null
+++ b/src/Models/ExtensionConfigModel.cs
@@ -0,0 +1,290 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace GeneralUpdate.Tool.Avalonia.Models;
+
+public 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
diff --git a/src/Models/ExtensionDependencySelectionModel.cs b/src/Models/ExtensionDependencySelectionModel.cs
new file mode 100644
index 0000000..8e9e4d1
--- /dev/null
+++ b/src/Models/ExtensionDependencySelectionModel.cs
@@ -0,0 +1,46 @@
+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/TargetPlatform.cs b/src/Models/TargetPlatform.cs
new file mode 100644
index 0000000..e468468
--- /dev/null
+++ b/src/Models/TargetPlatform.cs
@@ -0,0 +1,32 @@
+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/ViewModels/ExtensionViewModel.cs b/src/ViewModels/ExtensionViewModel.cs
new file mode 100644
index 0000000..faea085
--- /dev/null
+++ b/src/ViewModels/ExtensionViewModel.cs
@@ -0,0 +1,336 @@
+using System;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using GeneralUpdate.Tool.Avalonia.Common;
+using GeneralUpdate.Tool.Avalonia.Models;
+using Newtonsoft.Json;
+
+namespace GeneralUpdate.Tool.Avalonia.ViewModels;
+
+public class ExtensionViewModel : ObservableObject
+{
+ #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 RelayCommand? _removeCustomPropertyCommand;
+ private RelayCommand? _addCustomPropertyCommand;
+ private string? _newCustomPropertyKey;
+ private string? _newCustomPropertyValue;
+
+ #endregion
+
+ #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 RelayCommand RemoveCustomPropertyCommand
+ {
+ get => _removeCustomPropertyCommand ??= new RelayCommand(RemoveCustomPropertyAction);
+ }
+
+ public RelayCommand AddCustomPropertyCommand
+ {
+ get => _addCustomPropertyCommand ??= new RelayCommand(AddCustomPropertyAction, CanAddCustomProperty);
+ }
+
+ public string? NewCustomPropertyKey
+ {
+ get => _newCustomPropertyKey;
+ set
+ {
+ SetProperty(ref _newCustomPropertyKey, value);
+ }
+ }
+
+ public string? NewCustomPropertyValue
+ {
+ get => _newCustomPropertyValue;
+ set
+ {
+ SetProperty(ref _newCustomPropertyValue, value);
+ }
+ }
+
+ 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()
+ {
+ 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
+ {
+ var folders = await Storage.Instance.SelectFolderDialog();
+ if (!folders.Any()) return;
+
+ var folder = folders.First();
+ switch (value)
+ {
+ case "ExtensionDirectory":
+ ConfigModel.ExtensionDirectory = folder!.Path.LocalPath;
+ break;
+ case "ExportPath":
+ ConfigModel.Path = folder!.Path.LocalPath;
+ break;
+ }
+ }
+ catch (Exception e)
+ {
+ }
+ }
+
+ ///
+ /// Generate update package (compress extension directory and optionally upload)
+ ///
+ private async Task GeneratePackageAction()
+ {
+ try
+ {
+ // Validate input
+ if (string.IsNullOrWhiteSpace(ConfigModel.Name))
+ {
+ //eventAggregator.PublishWarning("Extension name is required");
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(ConfigModel.Version))
+ {
+ //eventAggregator.PublishWarning("Extension version is required");
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(ConfigModel.ExtensionDirectory) ||
+ !Directory.Exists(ConfigModel.ExtensionDirectory))
+ {
+ //eventAggregator.PublishWarning("Extension directory is invalid");
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(ConfigModel.Path))
+ {
+ //eventAggregator.PublishWarning("Export path is required");
+ 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);
+
+ //eventAggregator.PublishSuccess("Starting extension compression...");
+
+ // 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
+ var manifestJson = JsonConvert.SerializeObject(ConfigModel);
+ if (!string.IsNullOrEmpty(manifestJson))
+ {
+ // Add manifest.json to the zip file
+ await ZipUtility.AddFileToZipAsync(zipFilePath, "manifest.json", manifestJson);
+ }
+ }
+ catch (Exception ex)
+ {
+ }
+ }
+
+ private void ClearAction() => ResetAction();
+
+ private bool CanAddCustomProperty()
+ {
+ return !string.IsNullOrWhiteSpace(NewCustomPropertyKey) &&
+ !string.IsNullOrWhiteSpace(NewCustomPropertyValue);
+ }
+
+ private void AddCustomPropertyAction()
+ {
+ try
+ {
+ if (string.IsNullOrWhiteSpace(NewCustomPropertyKey) ||
+ string.IsNullOrWhiteSpace(NewCustomPropertyValue))
+ {
+ return;
+ }
+
+ // Check if key already exists
+ if (ConfigModel.CustomProperties.ContainsKey(NewCustomPropertyKey))
+ {
+ return;
+ }
+
+ // Add to dictionary
+ ConfigModel.CustomProperties[NewCustomPropertyKey] = NewCustomPropertyValue;
+
+ // Add to observable collection for UI binding
+ CustomPropertiesCollection.Add(new CustomPropertyModel
+ {
+ Key = NewCustomPropertyKey,
+ Value = NewCustomPropertyValue
+ });
+
+ // Clear input fields
+ NewCustomPropertyKey = string.Empty;
+ NewCustomPropertyValue = string.Empty;
+
+ }
+ catch (Exception ex)
+ {
+ }
+ }
+
+ private void RemoveCustomPropertyAction(CustomPropertyModel? property)
+ {
+ try
+ {
+ if (property == null)
+ {
+ return;
+ }
+
+ // Remove from dictionary - use TryGetValue for safety
+ if (ConfigModel.CustomProperties.ContainsKey(property.Key))
+ {
+ ConfigModel.CustomProperties.Remove(property.Key);
+ }
+
+ // Remove from observable collection
+ CustomPropertiesCollection.Remove(property);
+
+ }
+ catch (Exception ex)
+ {
+ }
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Views/ExtensionView.axaml b/src/Views/ExtensionView.axaml
new file mode 100644
index 0000000..f14d53b
--- /dev/null
+++ b/src/Views/ExtensionView.axaml
@@ -0,0 +1,371 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Views/ExtensionView.axaml.cs b/src/Views/ExtensionView.axaml.cs
new file mode 100644
index 0000000..77d1ac7
--- /dev/null
+++ b/src/Views/ExtensionView.axaml.cs
@@ -0,0 +1,13 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace GeneralUpdate.Tool.Avalonia.Views;
+
+public partial class ExtensionView : UserControl
+{
+ public ExtensionView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Views/MainWindow.axaml b/src/Views/MainWindow.axaml
index 15f2b7a..989f6e9 100644
--- a/src/Views/MainWindow.axaml
+++ b/src/Views/MainWindow.axaml
@@ -9,10 +9,13 @@
Title="GeneralUpdate.Tool.Avalonia">
-
+
-
+
+
+
+