Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/GeneralUpdate.Tools.V12/App.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="GeneralUpdate.Tools.V12.App"
xmlns:local="using:GeneralUpdate.Tools.V12"
xmlns:semi="https://irihi.tech/semi"
RequestedThemeVariant="Default">

<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>

<Application.Styles>
<FluentTheme />
<semi:SemiTheme Locale="zh-CN" />
</Application.Styles>
</Application>
18 changes: 18 additions & 0 deletions src/GeneralUpdate.Tools.V12/App.axaml.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
Binary file not shown.
32 changes: 32 additions & 0 deletions src/GeneralUpdate.Tools.V12/GeneralUpdate.Tools.V12.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<RootNamespace>GeneralUpdate.Tools.V12</RootNamespace>
</PropertyGroup>

<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Avalonia" Version="12.0.3" />
<PackageReference Include="Avalonia.Desktop" Version="12.0.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.3" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.0.3" />
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.1">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="Semi.Avalonia" Version="12.0.1" />
<PackageReference Include="Semi.Avalonia.DataGrid" Version="12.0.0" />
<PackageReference Include="Irihi.Ursa" Version="2.0.0" />
<PackageReference Include="Irihi.Ursa.Themes.Semi" Version="2.0.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
<PackageReference Include="GeneralUpdate.Core" Version="10.4.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
21 changes: 21 additions & 0 deletions src/GeneralUpdate.Tools.V12/Models/ExtensionConfigModel.cs
Original file line number Diff line number Diff line change
@@ -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 = "";
}
12 changes: 12 additions & 0 deletions src/GeneralUpdate.Tools.V12/Models/OSSConfigModel.cs
Original file line number Diff line number Diff line change
@@ -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 = "";
}
13 changes: 13 additions & 0 deletions src/GeneralUpdate.Tools.V12/Models/PatchConfigModel.cs
Original file line number Diff line number Diff line change
@@ -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 = "";
}
18 changes: 18 additions & 0 deletions src/GeneralUpdate.Tools.V12/Program.cs
Original file line number Diff line number Diff line change
@@ -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<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace()
.UseSkia();
}
17 changes: 17 additions & 0 deletions src/GeneralUpdate.Tools.V12/Services/DiffService.cs
Original file line number Diff line number Diff line change
@@ -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());
}
}
40 changes: 40 additions & 0 deletions src/GeneralUpdate.Tools.V12/Services/PackageService.cs
Original file line number Diff line number Diff line change
@@ -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<string> 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();
});
}
}
19 changes: 19 additions & 0 deletions src/GeneralUpdate.Tools.V12/ViewLocator.cs
Original file line number Diff line number Diff line change
@@ -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;
}
60 changes: 60 additions & 0 deletions src/GeneralUpdate.Tools.V12/ViewModels/ExtensionViewModel.cs
Original file line number Diff line number Diff line change
@@ -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<CustomPropModel> CustomProps { get; } = new();

async Task<string?> 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;
}
31 changes: 31 additions & 0 deletions src/GeneralUpdate.Tools.V12/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
@@ -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<NavItem> 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; }
}
25 changes: 25 additions & 0 deletions src/GeneralUpdate.Tools.V12/ViewModels/OSSViewModel.cs
Original file line number Diff line number Diff line change
@@ -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<OSSConfigModel> 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} 条"; } }
}
53 changes: 53 additions & 0 deletions src/GeneralUpdate.Tools.V12/ViewModels/PatchViewModel.cs
Original file line number Diff line number Diff line change
@@ -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<string> _log = new();

async Task<string?> 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}");
}
5 changes: 5 additions & 0 deletions src/GeneralUpdate.Tools.V12/ViewModels/ViewModelBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using CommunityToolkit.Mvvm.ComponentModel;

namespace GeneralUpdate.Tools.V12.ViewModels;

public partial class ViewModelBase : ObservableObject { }
Loading
Loading