diff --git a/src/GeneralUpdate.Tools.V12/App.axaml b/src/GeneralUpdate.Tools.V12/App.axaml new file mode 100644 index 0000000..16c7906 --- /dev/null +++ b/src/GeneralUpdate.Tools.V12/App.axaml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/src/GeneralUpdate.Tools.V12/App.axaml.cs b/src/GeneralUpdate.Tools.V12/App.axaml.cs new file mode 100644 index 0000000..6625bc1 --- /dev/null +++ b/src/GeneralUpdate.Tools.V12/App.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using GeneralUpdate.Tools.V12.ViewModels; +using GeneralUpdate.Tools.V12.Views; + +namespace GeneralUpdate.Tools.V12; + +public partial class App : Application +{ + public override void Initialize() { AvaloniaXamlLoader.Load(this); } + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + desktop.MainWindow = new MainWindow { DataContext = new MainWindowViewModel() }; + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/src/GeneralUpdate.Tools.V12/Assets/avalonia-logo.ico b/src/GeneralUpdate.Tools.V12/Assets/avalonia-logo.ico new file mode 100644 index 0000000..f7da8bb Binary files /dev/null and b/src/GeneralUpdate.Tools.V12/Assets/avalonia-logo.ico differ diff --git a/src/GeneralUpdate.Tools.V12/GeneralUpdate.Tools.V12.csproj b/src/GeneralUpdate.Tools.V12/GeneralUpdate.Tools.V12.csproj new file mode 100644 index 0000000..6c97196 --- /dev/null +++ b/src/GeneralUpdate.Tools.V12/GeneralUpdate.Tools.V12.csproj @@ -0,0 +1,32 @@ + + + WinExe + net10.0 + enable + app.manifest + true + GeneralUpdate.Tools.V12 + + + + + + + + + + + + + None + All + + + + + + + + + + diff --git a/src/GeneralUpdate.Tools.V12/Models/ExtensionConfigModel.cs b/src/GeneralUpdate.Tools.V12/Models/ExtensionConfigModel.cs new file mode 100644 index 0000000..822e269 --- /dev/null +++ b/src/GeneralUpdate.Tools.V12/Models/ExtensionConfigModel.cs @@ -0,0 +1,21 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace GeneralUpdate.Tools.V12.Models; + +public partial class ExtensionConfigModel : ObservableObject +{ + [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/GeneralUpdate.Tools.V12/Models/OSSConfigModel.cs b/src/GeneralUpdate.Tools.V12/Models/OSSConfigModel.cs new file mode 100644 index 0000000..153fbf8 --- /dev/null +++ b/src/GeneralUpdate.Tools.V12/Models/OSSConfigModel.cs @@ -0,0 +1,12 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace GeneralUpdate.Tools.V12.Models; + +public partial class OSSConfigModel : ObservableObject +{ + [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/GeneralUpdate.Tools.V12/Models/PatchConfigModel.cs b/src/GeneralUpdate.Tools.V12/Models/PatchConfigModel.cs new file mode 100644 index 0000000..dbafd04 --- /dev/null +++ b/src/GeneralUpdate.Tools.V12/Models/PatchConfigModel.cs @@ -0,0 +1,13 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace GeneralUpdate.Tools.V12.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/GeneralUpdate.Tools.V12/Program.cs b/src/GeneralUpdate.Tools.V12/Program.cs new file mode 100644 index 0000000..74be8c9 --- /dev/null +++ b/src/GeneralUpdate.Tools.V12/Program.cs @@ -0,0 +1,18 @@ +using Avalonia; +using System; + +namespace GeneralUpdate.Tools.V12; + +sealed class Program +{ + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace() + .UseSkia(); +} diff --git a/src/GeneralUpdate.Tools.V12/Services/DiffService.cs b/src/GeneralUpdate.Tools.V12/Services/DiffService.cs new file mode 100644 index 0000000..6e1418a --- /dev/null +++ b/src/GeneralUpdate.Tools.V12/Services/DiffService.cs @@ -0,0 +1,17 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using GeneralUpdate.Differential; + +namespace GeneralUpdate.Tools.V12.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/GeneralUpdate.Tools.V12/Services/PackageService.cs b/src/GeneralUpdate.Tools.V12/Services/PackageService.cs new file mode 100644 index 0000000..5352173 --- /dev/null +++ b/src/GeneralUpdate.Tools.V12/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.V12.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/GeneralUpdate.Tools.V12/ViewLocator.cs b/src/GeneralUpdate.Tools.V12/ViewLocator.cs new file mode 100644 index 0000000..e9169f4 --- /dev/null +++ b/src/GeneralUpdate.Tools.V12/ViewLocator.cs @@ -0,0 +1,19 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using GeneralUpdate.Tools.V12.ViewModels; + +namespace GeneralUpdate.Tools.V12; + +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/GeneralUpdate.Tools.V12/ViewModels/ExtensionViewModel.cs b/src/GeneralUpdate.Tools.V12/ViewModels/ExtensionViewModel.cs new file mode 100644 index 0000000..15f598e --- /dev/null +++ b/src/GeneralUpdate.Tools.V12/ViewModels/ExtensionViewModel.cs @@ -0,0 +1,60 @@ +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.Tools.V12.Models; +using GeneralUpdate.Tools.V12.Services; + +namespace GeneralUpdate.Tools.V12.ViewModels; + +public partial class ExtensionViewModel : ViewModelBase +{ + 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 + { + 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) + }); + 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())); +} + +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/GeneralUpdate.Tools.V12/ViewModels/MainWindowViewModel.cs b/src/GeneralUpdate.Tools.V12/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..d20098b --- /dev/null +++ b/src/GeneralUpdate.Tools.V12/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.V12.Models; + +namespace GeneralUpdate.Tools.V12.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/GeneralUpdate.Tools.V12/ViewModels/OSSViewModel.cs b/src/GeneralUpdate.Tools.V12/ViewModels/OSSViewModel.cs new file mode 100644 index 0000000..43f1cf4 --- /dev/null +++ b/src/GeneralUpdate.Tools.V12/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.V12.Models; +using GeneralUpdate.Tools.V12.Services; +using Newtonsoft.Json; + +namespace GeneralUpdate.Tools.V12.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/GeneralUpdate.Tools.V12/ViewModels/PatchViewModel.cs b/src/GeneralUpdate.Tools.V12/ViewModels/PatchViewModel.cs new file mode 100644 index 0000000..107e8c8 --- /dev/null +++ b/src/GeneralUpdate.Tools.V12/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.V12.Models; +using GeneralUpdate.Tools.V12.Services; + +namespace GeneralUpdate.Tools.V12.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/GeneralUpdate.Tools.V12/ViewModels/ViewModelBase.cs b/src/GeneralUpdate.Tools.V12/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..514935b --- /dev/null +++ b/src/GeneralUpdate.Tools.V12/ViewModels/ViewModelBase.cs @@ -0,0 +1,5 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace GeneralUpdate.Tools.V12.ViewModels; + +public partial class ViewModelBase : ObservableObject { } diff --git a/src/GeneralUpdate.Tools.V12/Views/ExtensionView.axaml b/src/GeneralUpdate.Tools.V12/Views/ExtensionView.axaml new file mode 100644 index 0000000..aaf62b9 --- /dev/null +++ b/src/GeneralUpdate.Tools.V12/Views/ExtensionView.axaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +