Skip to content

Commit 07358a7

Browse files
JusterZhuclaude
andauthored
fix: unify SemVer 2.0 version standards and improve config progress UI (#80)
- Set ConfigGeneratorModel/MainfestModel version defaults to '1.0.0' - Fix SimulationService hardcoded '1.0.0.0' (4-part) to SemVer '1.0.0' - Update XAML placeholders from '1.0.0.0' to '1.0.0 (semver)' - Add SemverValidator.Compare() and ParseCore() for full SemVer 2.0 comparison - Replace System.Version.TryParse with SemverValidator in LocalUpdateServer - Update ManifestGeneratorService.FromCsprojInfo fallback values - Add prominent ProgressBar + status text near action buttons in ConfigView - Disable Generate/GenerateSample buttons while busy to prevent double-click - Add _isPublishing state to distinguish Analyze vs GenerateSample operations Closes #79 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f296db3 commit 07358a7

10 files changed

Lines changed: 112 additions & 20 deletions

src/Models/ConfigGeneratorModel.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ public partial class ConfigGeneratorModel : ObservableObject
1111
// ── Analysis state ──
1212
[ObservableProperty] private bool _isAnalyzed;
1313
[ObservableProperty] private bool _isAnalyzing;
14+
[ObservableProperty] private bool _isPublishing;
1415

1516
// ── Editable fields (auto-filled + user input) ──
1617
[ObservableProperty] private string _mainAppName = "";
17-
[ObservableProperty] private string _clientVersion = "";
18+
[ObservableProperty] private string _clientVersion = "1.0.0";
1819
[ObservableProperty] private string _updateAppName = "Update.exe";
19-
[ObservableProperty] private string _upgradeClientVersion = "";
20+
[ObservableProperty] private string _upgradeClientVersion = "1.0.0";
2021
[ObservableProperty] private string _appType = "Client";
2122
[ObservableProperty] private string _productId = "";
2223
[ObservableProperty] private string _updatePath = "update/";

src/Models/ManifestModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ namespace GeneralUpdate.Tools.Models;
33
public class ManifestModel
44
{
55
public string MainAppName { get; set; } = "";
6-
public string ClientVersion { get; set; } = "";
6+
public string ClientVersion { get; set; } = "1.0.0";
77
public string AppType { get; set; } = "Client";
88
public string UpdateAppName { get; set; } = "Update.exe";
9-
public string UpgradeClientVersion { get; set; } = "";
9+
public string UpgradeClientVersion { get; set; } = "1.0.0";
1010
public string ProductId { get; set; } = "";
1111
public string UpdatePath { get; set; } = "update/";
1212
}

src/Services/LocalUpdateServer.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,12 @@ public async Task StartAsync(int port = 5000)
7878
!string.Equals(v.ProductId, productId, StringComparison.OrdinalIgnoreCase))
7979
return false;
8080
// Version filter: only return versions higher than client's.
81-
// Exclude unparseable versions — silently including them
82-
// would defeat the update-loop guard.
83-
if (!Version.TryParse(v.TargetVersion, out var itemVer)) return false;
84-
if (!Version.TryParse(clientVersion, out var clientVer)) return false;
85-
return itemVer > clientVer;
81+
// Uses SemVer 2.0 comparison — unparseable versions are silently skipped.
82+
if (!SemverValidator.IsValid(v.TargetVersion)) return false;
83+
if (!SemverValidator.IsValid(clientVersion)) return false;
84+
return SemverValidator.Compare(v.TargetVersion, clientVersion) > 0;
8685
})
87-
.OrderByDescending(v => Version.TryParse(v.TargetVersion, out var ver) ? ver : new Version(0, 0))
86+
.OrderByDescending(v => SemverValidator.IsValid(v.TargetVersion) ? SemverValidator.ParseCore(v.TargetVersion) : (0, 0, 0))
8887
.ToList();
8988

9089
if (matches.Count == 0)

src/Services/ManifestGeneratorService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ public static ManifestModel FromCsprojInfo(CsprojInfo client, CsprojInfo? upgrad
3434
MainAppName = !string.IsNullOrWhiteSpace(userInput?.MainAppName)
3535
? userInput.MainAppName
3636
: client.AssemblyName,
37-
ClientVersion = userInput?.ClientVersion ?? "",
37+
ClientVersion = userInput?.ClientVersion ?? "1.0.0",
3838
AppType = !string.IsNullOrWhiteSpace(userInput?.AppType)
3939
? userInput.AppType
4040
: "Client",
4141
UpdateAppName = !string.IsNullOrWhiteSpace(userInput?.UpdateAppName)
4242
? userInput.UpdateAppName
4343
: upgrade?.AssemblyName ?? "Update.exe",
44-
UpgradeClientVersion = userInput?.UpgradeClientVersion ?? "",
44+
UpgradeClientVersion = userInput?.UpgradeClientVersion ?? "1.0.0",
4545
ProductId = userInput?.ProductId ?? "",
4646
UpdatePath = !string.IsNullOrWhiteSpace(userInput?.UpdatePath)
4747
? userInput.UpdatePath

src/Services/SemverValidator.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Text.RegularExpressions;
23

34
namespace GeneralUpdate.Tools.Services;
@@ -13,5 +14,77 @@ public static partial class SemverValidator
1314
RegexOptions.Compiled)]
1415
private static partial Regex SemverRegex();
1516

17+
// Captures: 1=MAJOR, 2=MINOR, 3=PATCH, 4=pre-release (including leading '-'), 5=build (including leading '+')
18+
[GeneratedRegex(
19+
@"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$",
20+
RegexOptions.Compiled)]
21+
private static partial Regex SemverPartsRegex();
22+
1623
public static bool IsValid(string version) => SemverRegex().IsMatch(version);
24+
25+
/// <summary>
26+
/// Extract the numeric (MAJOR, MINOR, PATCH) core for sorting.
27+
/// Returns (0,0,0) if the version cannot be parsed.
28+
/// </summary>
29+
public static (int Major, int Minor, int Patch) ParseCore(string version)
30+
{
31+
var m = SemverPartsRegex().Match(version);
32+
if (!m.Success) return (0, 0, 0);
33+
return (int.Parse(m.Groups[1].Value), int.Parse(m.Groups[2].Value), int.Parse(m.Groups[3].Value));
34+
}
35+
36+
/// <summary>
37+
/// Compare two SemVer 2.0 version strings.
38+
/// Returns negative if <paramref name="a"/> &lt; <paramref name="b"/>,
39+
/// zero if equal, positive if <paramref name="a"/> &gt; <paramref name="b"/>.
40+
/// </summary>
41+
public static int Compare(string a, string b)
42+
{
43+
var ma = SemverPartsRegex().Match(a);
44+
var mb = SemverPartsRegex().Match(b);
45+
if (!ma.Success || !mb.Success)
46+
throw new ArgumentException("Both versions must be valid SemVer 2.0.");
47+
48+
// Compare MAJOR.MINOR.PATCH numerically
49+
for (int i = 1; i <= 3; i++)
50+
{
51+
var cmp = int.Parse(ma.Groups[i].Value).CompareTo(int.Parse(mb.Groups[i].Value));
52+
if (cmp != 0) return cmp;
53+
}
54+
55+
// Pre-release comparison
56+
// A version without pre-release has higher precedence than one with pre-release
57+
var preA = ma.Groups[5].Value; // Group 5 = pre-release identifiers (without leading '-')
58+
var preB = mb.Groups[5].Value;
59+
var hasPreA = !string.IsNullOrEmpty(preA);
60+
var hasPreB = !string.IsNullOrEmpty(preB);
61+
62+
if (!hasPreA && !hasPreB) return 0;
63+
if (!hasPreA && hasPreB) return 1;
64+
if (hasPreA && !hasPreB) return -1;
65+
66+
// Both have pre-release — compare identifiers
67+
var idsA = preA.Split('.');
68+
var idsB = preB.Split('.');
69+
var count = Math.Min(idsA.Length, idsB.Length);
70+
for (int i = 0; i < count; i++)
71+
{
72+
var cmp = ComparePreReleaseIdentifier(idsA[i], idsB[i]);
73+
if (cmp != 0) return cmp;
74+
}
75+
76+
// All compared identifiers equal — longer pre-release has higher precedence
77+
return idsA.Length.CompareTo(idsB.Length);
78+
}
79+
80+
private static int ComparePreReleaseIdentifier(string a, string b)
81+
{
82+
var numA = int.TryParse(a, out var nA);
83+
var numB = int.TryParse(b, out var nB);
84+
85+
if (numA && numB) return nA.CompareTo(nB); // both numeric
86+
if (numA && !numB) return -1; // numeric < alphanumeric
87+
if (!numA && numB) return 1; // alphanumeric > numeric
88+
return string.CompareOrdinal(a, b); // both alphanumeric
89+
}
1790
}

src/Services/SimulationService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public async Task<SimulationResult> RunAsync(
7373
ClientVersion = config.CurrentVersion,
7474
AppType = config.AppType switch { 1 => "Client", 2 => "Upgrade", _ => "Client" },
7575
UpdateAppName = upgradeExeName,
76-
UpgradeClientVersion = "1.0.0.0",
76+
UpgradeClientVersion = "1.0.0",
7777
ProductId = config.ProductId,
7878
UpdatePath = config.UpdatePath ?? "update/"
7979
};

src/ViewModels/ConfigViewModel.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,16 @@ public partial class ConfigViewModel : ViewModelBase
1919
public ConfigGeneratorModel Model { get; } = new();
2020
public List<string> AppTypes { get; } = new() { "Client", "Upgrade", "OssClient", "OssUpgrade" };
2121

22+
public bool IsBusy => Model.IsAnalyzing || Model.IsPublishing;
23+
2224
public ConfigViewModel()
2325
{
24-
Model.PropertyChanged += (_, _) => UpdatePreview();
26+
Model.PropertyChanged += (_, e) =>
27+
{
28+
UpdatePreview();
29+
if (e.PropertyName is nameof(ConfigGeneratorModel.IsAnalyzing) or nameof(ConfigGeneratorModel.IsPublishing))
30+
OnPropertyChanged(nameof(IsBusy));
31+
};
2532
}
2633

2734
private static Avalonia.Controls.TopLevel? GetTopLevel()
@@ -209,7 +216,7 @@ private async Task GenerateSample()
209216
return;
210217
}
211218

212-
Model.IsAnalyzing = true;
219+
Model.IsPublishing = true;
213220
Model.StatusText = _loc["Config.Publishing"];
214221

215222
try
@@ -263,7 +270,7 @@ await DialogHelper.ShowInfoAsync(_loc["Config.Title"],
263270
}
264271
finally
265272
{
266-
Model.IsAnalyzing = false;
273+
Model.IsPublishing = false;
267274
}
268275
}
269276

src/Views/ConfigView.axaml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,23 @@
113113
<!-- Action buttons -->
114114
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10">
115115
<Button Content="{Binding Source={x:Static svc:LocalizationService.Instance}, Path=[Config.Generate]}"
116-
Command="{Binding GenerateCommand}" MinWidth="140"/>
116+
Command="{Binding GenerateCommand}" MinWidth="140"
117+
IsEnabled="{Binding IsBusy, Converter={x:Static BoolConverters.Not}}"/>
117118
<Button Content="{Binding Source={x:Static svc:LocalizationService.Instance}, Path=[Config.GenerateSample]}"
118-
Command="{Binding GenerateSampleCommand}" MinWidth="160"/>
119+
Command="{Binding GenerateSampleCommand}" MinWidth="160"
120+
IsEnabled="{Binding IsBusy, Converter={x:Static BoolConverters.Not}}"/>
119121
</StackPanel>
120122

123+
<!-- Progress (visible during analysis / publishing) -->
124+
<Border Padding="16,12" CornerRadius="8" IsVisible="{Binding IsBusy}"
125+
Background="{DynamicResource SystemControlBackgroundChromeMediumBrush}">
126+
<StackPanel Spacing="10">
127+
<ProgressBar IsIndeterminate="True" Height="8"/>
128+
<TextBlock Text="{Binding Model.StatusText}" FontSize="14" FontWeight="SemiBold"
129+
HorizontalAlignment="Center" TextWrapping="Wrap"/>
130+
</StackPanel>
131+
</Border>
132+
121133
<!-- Preview -->
122134
<Border Padding="16" CornerRadius="8" Background="{DynamicResource SystemControlBackgroundChromeMediumBrush}">
123135
<StackPanel Spacing="6">

src/Views/ExtensionView.axaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<TextBlock Grid.Column="0" Text="{Binding Source={x:Static svc:LocalizationService.Instance}, Path=[Ext.Name]}" VerticalAlignment="Center"/>
1818
<TextBox Grid.Column="1" Text="{Binding Config.Name}" PlaceholderText="MyExtension" Margin="8,0,16,0"/>
1919
<TextBlock Grid.Column="2" Text="{Binding Source={x:Static svc:LocalizationService.Instance}, Path=[Ext.Version]}" VerticalAlignment="Center"/>
20-
<TextBox Grid.Column="3" Text="{Binding Config.Version}" PlaceholderText="1.0.0.0" Margin="8,0"/>
20+
<TextBox Grid.Column="3" Text="{Binding Config.Version}" PlaceholderText="1.0.0 (semver)" Margin="8,0"/>
2121
</Grid>
2222
<TextBlock Text="{Binding Source={x:Static svc:LocalizationService.Instance}, Path=[Ext.Description]}"/>
2323
<TextBox Text="{Binding Config.Description}" PlaceholderText="{Binding Source={x:Static svc:LocalizationService.Instance}, Path=[Ext.DescPlaceholder]}"

src/Views/PatchView.axaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
<TextBlock Grid.Column="0" Text="{Binding Source={x:Static svc:LocalizationService.Instance}, Path=[Patch.PackageName]}" VerticalAlignment="Center"/>
4343
<TextBox Grid.Column="1" Text="{Binding Config.PackageName}" PlaceholderText="patch_v1.0_to_v1.1" Margin="8,0,16,0"/>
4444
<TextBlock Grid.Column="2" Text="{Binding Source={x:Static svc:LocalizationService.Instance}, Path=[Patch.Version]}" VerticalAlignment="Center"/>
45-
<TextBox Grid.Column="3" Text="{Binding Config.Version}" PlaceholderText="1.0.0.0" Margin="8,0"/>
45+
<TextBox Grid.Column="3" Text="{Binding Config.Version}" PlaceholderText="1.0.0 (semver)" Margin="8,0"/>
4646
</Grid>
4747
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
4848
<TextBlock Grid.Column="0" Text="{Binding Source={x:Static svc:LocalizationService.Instance}, Path=[Patch.OutputDir]}" VerticalAlignment="Center"/>

0 commit comments

Comments
 (0)