Skip to content

Commit cf3ab2e

Browse files
JusterZhuclaude
andauthored
feat: add mobile packaging tool with real-time BuildResultWindow (#91)
* feat: add mobile packaging tool with real-time BuildResultWindow - Add Mobile (移动端) tab supporting APK/AAB file mode and .csproj project mode - AXML parser for AndroidManifest.xml metadata extraction (package, versionName, versionCode) - Format auto-detection (APK vs AAB) by extension + ZIP internal structure - MobileCsprojParser for MAUI/Avalonia Android project metadata extraction - Shared BuildResultWindow (AvaloniaEdit) for real-time log streaming across all modules - Converted Patch/Extension/OSS/Simulate/Config modules to use async BuildResultWindow pattern - HttpUploadService now supports metadata form fields via Dictionary overload - Platform ComboBox with fixed Android option, AppType removed (mobile has no Upgrade concept) - Full i18n (en-US/zh-CN) for all Mobile tab strings Co-Authored-By: Claude <noreply@anthropic.com> * fix: address all 11 Copilot review comments from PR #91 - #1: Add Upload Project Mode FilePath null check with user feedback - #2: Fix CsprojParser TargetFramework null returning Success=true; support multi-TFM by picking the -android variant - #3: Unsubscribe CollectionChanged on BuildResultWindow Closed - #4-7: Show validation dialog on silent failures in Simulate, Patch, Extension, and Config ViewModels - #8: Localize hardcoded Chinese strings in BuildResultWindow - #9: Remove stale AppType fields from MobilePackageModel, AppConfig, and MobileVersionRecord - #10: Replace unbound Platform ComboBox with readonly TextBox - #11: Clarify DialogHelper XML doc on threading behavior Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 95afa99 commit cf3ab2e

32 files changed

Lines changed: 2104 additions & 355 deletions

src/App.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@
1111
<FluentTheme />
1212
<semi:SemiTheme Locale="zh-CN" />
1313
<semi:UrsaSemiTheme />
14+
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
1415
</Application.Styles>
1516
</Application>

src/Configuration/AppConfig.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,26 @@ public class AppConfig
6565
[JsonProperty("lastExtensionOutputDir")]
6666
public string LastExtensionOutputDir { get; set; } = string.Empty;
6767

68+
// ── Mobile / Android Path Memory ───────────────────────────
69+
70+
[JsonProperty("lastMobileFilePath")]
71+
public string LastMobileFilePath { get; set; } = string.Empty;
72+
73+
[JsonProperty("lastMobileProjectPath")]
74+
public string LastMobileProjectPath { get; set; } = string.Empty;
75+
76+
[JsonProperty("lastMobileOutputDir")]
77+
public string LastMobileOutputDir { get; set; } = string.Empty;
78+
79+
[JsonProperty("lastMobileProductId")]
80+
public string LastMobileProductId { get; set; } = string.Empty;
81+
82+
[JsonProperty("lastMobilePlatform")]
83+
public int LastMobilePlatform { get; set; } = 4;
84+
85+
[JsonProperty("lastMobileUseProjectMode")]
86+
public bool LastMobileUseProjectMode { get; set; } = true;
87+
6888
// ── Upload Configuration ──────────────────────────────────
6989

7090
[JsonProperty("uploadServerUrl")]

src/GeneralUpdate.Tools.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
<ItemGroup>
1919
<PackageReference Include="Avalonia" Version="12.0.3" />
20+
<PackageReference Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
2021
<PackageReference Include="Avalonia.Desktop" Version="12.0.3" />
2122
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.3" />
2223
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.0.3" />

src/Models/MobilePackageModel.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using CommunityToolkit.Mvvm.ComponentModel;
2+
3+
namespace GeneralUpdate.Tools.Models;
4+
5+
public partial class MobilePackageModel : ObservableObject
6+
{
7+
// ── Mode ───────────────────────────────────────────────
8+
// true = project mode (.csproj), false = file mode (.apk/.aab)
9+
[ObservableProperty] private bool _useProjectMode = true;
10+
11+
// ── File mode ──────────────────────────────────────────
12+
[ObservableProperty] private string _filePath = "";
13+
[ObservableProperty] private PackageFormat _format = PackageFormat.Unknown;
14+
[ObservableProperty] private string _formatDisplay = "";
15+
16+
// ── Project mode ───────────────────────────────────────
17+
[ObservableProperty] private string _projectPath = "";
18+
[ObservableProperty] private string _projectType = ""; // "MAUI" / "Avalonia" / ".NET Android"
19+
[ObservableProperty] private string _projectBuildOutput = ""; // path to published APK/AAB
20+
21+
// ── Metadata (auto-detected, editable) ─────────────────
22+
[ObservableProperty] private string _packageName = "";
23+
[ObservableProperty] private string _versionName = "";
24+
[ObservableProperty] private string _versionCode = "";
25+
26+
// ── Auto-computed (read-only display) ─────────────────
27+
[ObservableProperty] private string _sha256Hash = "";
28+
[ObservableProperty] private long _fileSize;
29+
[ObservableProperty] private string _fileSizeDisplay = "";
30+
31+
// ── Upload config ──────────────────────────────────────
32+
[ObservableProperty] private int _platform = 4; // default Android
33+
[ObservableProperty] private string _productId = "";
34+
[ObservableProperty] private string _productName = "";
35+
[ObservableProperty] private string _releaseNotes = "";
36+
[ObservableProperty] private bool _isForcibly;
37+
[ObservableProperty] private string _outputDirectory = "";
38+
}

src/Models/MobileVersionRecord.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Newtonsoft.Json;
2+
3+
namespace GeneralUpdate.Tools.Models;
4+
5+
public class MobileVersionRecord
6+
{
7+
[JsonProperty("name")] public string Name { get; set; } = string.Empty;
8+
[JsonProperty("version")] public string Version { get; set; } = string.Empty;
9+
[JsonProperty("hash")] public string Hash { get; set; } = string.Empty;
10+
[JsonProperty("url")] public string Url { get; set; } = string.Empty;
11+
[JsonProperty("packageName")] public string PackageName { get; set; } = string.Empty;
12+
[JsonProperty("fileSize")] public long FileSize { get; set; }
13+
[JsonProperty("format")] public string Format { get; set; } = string.Empty;
14+
[JsonProperty("platform")] public int Platform { get; set; } = 4;
15+
[JsonProperty("productId")] public string ProductId { get; set; } = string.Empty;
16+
[JsonProperty("isForcibly")] public bool IsForcibly { get; set; }
17+
[JsonProperty("releaseDate")] public string ReleaseDate { get; set; } = string.Empty;
18+
}

src/Models/PackageFormat.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace GeneralUpdate.Tools.Models;
2+
3+
public enum PackageFormat
4+
{
5+
Unknown,
6+
Apk,
7+
Aab
8+
}

src/Resources/Locales/en-US.json

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,5 +184,85 @@
184184
"Patch.Upload": "📤 Upload",
185185
"Upload.Success": "Upload successful",
186186
"Upload.Failed": "Upload failed: {0}",
187-
"Upload.Uploading": "Uploading..."
187+
"Upload.Uploading": "Uploading...",
188+
189+
"Result.OpenOutput": "Open Output Directory",
190+
"Result.Exit": "Exit",
191+
"Result.Running": "Running...",
192+
"Result.ValidationTitle": "Validation",
193+
"Result.BuildFirst": "Please build the project first to locate the output package file.",
194+
195+
"Nav.Mobile": "Mobile",
196+
"Mobile.Title": "📱 Mobile Package Upload",
197+
"Mobile.Mode": "Mode",
198+
"Mobile.FileMode": "📦 Package File",
199+
"Mobile.ProjectMode": "📁 Project (.csproj)",
200+
"Mobile.PackageFile": "Package File Selection",
201+
"Mobile.ProjectFile": "Project File Selection",
202+
"Mobile.FilePath": "File Path",
203+
"Mobile.CsprojPath": "Project Path",
204+
"Mobile.FilePlaceholder": "Select .apk or .aab file...",
205+
"Mobile.ProjectPlaceholder": "Select .csproj file...",
206+
"Mobile.Browse": "Browse",
207+
"Mobile.SelectFile": "Select mobile package file",
208+
"Mobile.SelectProject": "Select .csproj file",
209+
"Mobile.SelectOutput": "Select output directory",
210+
"Mobile.Format": "Format",
211+
"Mobile.ProjectType": "Project Type",
212+
"Mobile.Analyze": "🔍 Analyze Metadata",
213+
"Mobile.BuildAndLocate": "🔨 Build & Locate Output",
214+
"Mobile.Metadata": "Detected Metadata",
215+
"Mobile.PackageName": "Package Name",
216+
"Mobile.VersionName": "Version Name",
217+
"Mobile.VersionCode": "Version Code",
218+
"Mobile.Sha256": "SHA256",
219+
"Mobile.FileSize": "File Size",
220+
"Mobile.EditableHint": "Auto-detected, editable",
221+
"Mobile.UploadConfig": "Upload Configuration",
222+
"Mobile.Platform": "Platform",
223+
"Mobile.AppType": "App Type",
224+
"Mobile.AppTypeClient": "Client App (1)",
225+
"Mobile.AppTypeUpgrade": "Upgrade App (2)",
226+
"Mobile.ProductId": "Product ID",
227+
"Mobile.ProductName": "Product Name",
228+
"Mobile.ReleaseNotes": "Release Notes",
229+
"Mobile.ForcedUpdate": "Force update",
230+
"Mobile.OutputDir": "Output Directory",
231+
"Mobile.OutputPlaceholder": "Desktop (default)",
232+
"Mobile.UploadAndGenerate": "📤 Upload & Generate Version Record",
233+
"Mobile.ExportRecord": "Export Record Only",
234+
"Mobile.OpenOutput": "Open Output",
235+
"Mobile.Ready": "Ready",
236+
"Mobile.AnalyzePrompt": "Select file and click Analyze",
237+
"Mobile.BuildPrompt": "Project parsed. Click Build & Locate or edit fields manually.",
238+
"Mobile.SelectFileFirst": "Please select a package file first",
239+
"Mobile.SelectProjectFirst": "Please select a .csproj file first",
240+
"Mobile.ValidateProductId": "Product ID is required",
241+
"Mobile.Analyzing": "Analyzing package...",
242+
"Mobile.ComputingHash": "Computing SHA256...",
243+
"Mobile.AnalyzeDone": "Analysis complete",
244+
"Mobile.AnalyzeFailed": "Analysis failed: {0}",
245+
"Mobile.FormatDetected": "Format detected: {0}",
246+
"Mobile.FormatDetectFailed": "Format detection failed: {0}",
247+
"Mobile.FileSelected": "Selected: {0}",
248+
"Mobile.ExtractedInfo": "Package: {0}, Version: {1} (Code: {2})",
249+
"Mobile.MetadataExtractFailed": "Metadata auto-extraction failed: {0}. You can fill in manually.",
250+
"Mobile.HashResult": "SHA256: {0}",
251+
"Mobile.SizeResult": "File Size: {0}",
252+
"Mobile.ProjectParsed": "Parsed {0} project: {1}",
253+
"Mobile.ProjectParseFailed": "Project parse failed: {0}",
254+
"Mobile.PackageNameLabel": "Package: {0}",
255+
"Mobile.VersionNameLabel": "Version: {0}",
256+
"Mobile.VersionCodeLabel": "Version Code: {0}",
257+
"Mobile.Publishing": "Building project...",
258+
"Mobile.PublishingDetail": "Publishing {0} via dotnet publish...",
259+
"Mobile.PublishDone": "dotnet publish completed: {0}",
260+
"Mobile.BuildOutputNotFound": "Build output not found in: {0}",
261+
"Mobile.BuildOutputFound": "Build output: {0}",
262+
"Mobile.BuildLocateDone": "Build & locate complete",
263+
"Mobile.BuildFailed": "Build failed: {0}",
264+
"Mobile.Uploading": "Uploading package...",
265+
"Mobile.RecordGenerated": "Version record saved: {0}",
266+
"Mobile.RecordFailed": "Failed to save version record: {0}",
267+
"Mobile.ResultTitle": "📋 Build & Upload Result"
188268
}

src/Resources/Locales/zh-CN.json

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,5 +184,85 @@
184184
"Patch.Upload": "📤 上传",
185185
"Upload.Success": "上传成功",
186186
"Upload.Failed": "上传失败: {0}",
187-
"Upload.Uploading": "正在上传..."
187+
"Upload.Uploading": "正在上传...",
188+
189+
"Result.OpenOutput": "打开输出目录",
190+
"Result.Exit": "退出",
191+
"Result.Running": "执行中...",
192+
"Result.ValidationTitle": "验证",
193+
"Result.BuildFirst": "请先构建项目以找到输出安装包文件。",
194+
195+
"Nav.Mobile": "移动端",
196+
"Mobile.Title": "📱 移动端安装包上传",
197+
"Mobile.Mode": "模式",
198+
"Mobile.FileMode": "📦 安装包文件",
199+
"Mobile.ProjectMode": "📁 项目 (.csproj)",
200+
"Mobile.PackageFile": "安装包文件选择",
201+
"Mobile.ProjectFile": "项目文件选择",
202+
"Mobile.FilePath": "文件路径",
203+
"Mobile.CsprojPath": "项目路径",
204+
"Mobile.FilePlaceholder": "选择 .apk 或 .aab 文件...",
205+
"Mobile.ProjectPlaceholder": "选择 .csproj 文件...",
206+
"Mobile.Browse": "选择",
207+
"Mobile.SelectFile": "选择移动端安装包",
208+
"Mobile.SelectProject": "选择 .csproj 项目文件",
209+
"Mobile.SelectOutput": "选择输出目录",
210+
"Mobile.Format": "格式",
211+
"Mobile.ProjectType": "项目类型",
212+
"Mobile.Analyze": "🔍 分析元数据",
213+
"Mobile.BuildAndLocate": "🔨 构建并定位输出",
214+
"Mobile.Metadata": "检测到的元数据",
215+
"Mobile.PackageName": "包名",
216+
"Mobile.VersionName": "版本名",
217+
"Mobile.VersionCode": "版本号",
218+
"Mobile.Sha256": "SHA256",
219+
"Mobile.FileSize": "文件大小",
220+
"Mobile.EditableHint": "自动检测,可编辑",
221+
"Mobile.UploadConfig": "上传配置",
222+
"Mobile.Platform": "平台",
223+
"Mobile.AppType": "应用类型",
224+
"Mobile.AppTypeClient": "客户端 (1)",
225+
"Mobile.AppTypeUpgrade": "升级器 (2)",
226+
"Mobile.ProductId": "产品 ID",
227+
"Mobile.ProductName": "产品名称",
228+
"Mobile.ReleaseNotes": "发布说明",
229+
"Mobile.ForcedUpdate": "强制更新",
230+
"Mobile.OutputDir": "输出目录",
231+
"Mobile.OutputPlaceholder": "桌面 (默认)",
232+
"Mobile.UploadAndGenerate": "📤 上传并生成版本记录",
233+
"Mobile.ExportRecord": "仅导出记录",
234+
"Mobile.OpenOutput": "打开输出目录",
235+
"Mobile.Ready": "就绪",
236+
"Mobile.AnalyzePrompt": "选择文件后点击分析",
237+
"Mobile.BuildPrompt": "项目已解析。可点击构建定位或手动编辑字段。",
238+
"Mobile.SelectFileFirst": "请先选择安装包文件",
239+
"Mobile.SelectProjectFirst": "请先选择 .csproj 文件",
240+
"Mobile.ValidateProductId": "产品 ID 为必填项",
241+
"Mobile.Analyzing": "正在分析安装包...",
242+
"Mobile.ComputingHash": "正在计算 SHA256...",
243+
"Mobile.AnalyzeDone": "分析完成",
244+
"Mobile.AnalyzeFailed": "分析失败: {0}",
245+
"Mobile.FormatDetected": "检测到格式: {0}",
246+
"Mobile.FormatDetectFailed": "格式检测失败: {0}",
247+
"Mobile.FileSelected": "已选择: {0}",
248+
"Mobile.ExtractedInfo": "包名: {0}, 版本: {1} (版本号: {2})",
249+
"Mobile.MetadataExtractFailed": "元数据自动提取失败: {0}。您可以手动填写。",
250+
"Mobile.HashResult": "SHA256: {0}",
251+
"Mobile.SizeResult": "文件大小: {0}",
252+
"Mobile.ProjectParsed": "已解析 {0} 项目: {1}",
253+
"Mobile.ProjectParseFailed": "项目解析失败: {0}",
254+
"Mobile.PackageNameLabel": "包名: {0}",
255+
"Mobile.VersionNameLabel": "版本: {0}",
256+
"Mobile.VersionCodeLabel": "版本号: {0}",
257+
"Mobile.Publishing": "正在构建项目...",
258+
"Mobile.PublishingDetail": "通过 dotnet publish 发布 {0}...",
259+
"Mobile.PublishDone": "dotnet publish 完成: {0}",
260+
"Mobile.BuildOutputNotFound": "未在以下目录找到构建输出: {0}",
261+
"Mobile.BuildOutputFound": "构建输出: {0}",
262+
"Mobile.BuildLocateDone": "构建并定位完成",
263+
"Mobile.BuildFailed": "构建失败: {0}",
264+
"Mobile.Uploading": "正在上传安装包...",
265+
"Mobile.RecordGenerated": "版本记录已保存: {0}",
266+
"Mobile.RecordFailed": "版本记录保存失败: {0}",
267+
"Mobile.ResultTitle": "📋 构建与上传结果"
188268
}

src/Services/DialogHelper.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
using System;
12
using System.Threading.Tasks;
23
using Avalonia;
34
using Avalonia.Controls;
45
using Avalonia.Layout;
6+
using GeneralUpdate.Tools.ViewModels;
7+
using GeneralUpdate.Tools.Views;
58

69
namespace GeneralUpdate.Tools.Services;
710

@@ -44,4 +47,41 @@ public static async Task ShowInfoAsync(string title, string message)
4447
dialog.Show();
4548
await tcs.Task;
4649
}
50+
51+
/// <summary>
52+
/// Show a result window that runs an operation and streams logs in real time.
53+
/// The window appears immediately; logs stream via <see cref="IProgress{String}"/>.
54+
/// Note: the operation is awaited on the UI thread so it should yield appropriately
55+
/// (e.g. await I/O, Process, or file operations); CPU-bound work should be offloaded
56+
/// via <c>Task.Run</c> inside the operation delegate.
57+
/// </summary>
58+
/// <param name="title">Window title.</param>
59+
/// <param name="operation">Work to perform. Call <c>progress.Report(line)</c> to stream log lines.</param>
60+
/// <param name="outputDirectory">Optional output directory for the "Open Output" button.</param>
61+
public static async Task ShowResultWindowAsync(
62+
string title,
63+
Func<IProgress<string>, Task> operation,
64+
string? outputDirectory = null)
65+
{
66+
var owner = (Application.Current?.ApplicationLifetime as
67+
Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime)?.MainWindow;
68+
if (owner == null) return;
69+
70+
var vm = new BuildResultWindowViewModel
71+
{
72+
WindowTitle = title,
73+
OutputDirectory = outputDirectory,
74+
};
75+
76+
var window = new BuildResultWindow(vm);
77+
78+
// Kick off the background operation (don't await yet — show window first)
79+
var task = vm.RunAsync(operation);
80+
81+
// Show the window — this blocks until the user clicks "退出"
82+
await window.ShowDialog(owner);
83+
84+
// Ensure the background task completed (it may have finished before the user closed)
85+
await task;
86+
}
4787
}

src/Services/LocalizationService.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,11 @@ private void OnPropertyChanged([CallerMemberName] string? name = null) =>
261261
["Patch.Upload"] = "📤 上传",
262262
["Upload.Success"] = "上传成功", ["Upload.Failed"] = "上传失败: {0}",
263263
["Upload.Uploading"] = "正在上传...",
264+
["Result.OpenOutput"] = "打开输出目录",
265+
["Result.Exit"] = "退出",
266+
["Result.Running"] = "执行中...",
267+
["Result.ValidationTitle"] = "验证",
268+
["Result.BuildFirst"] = "请先构建项目以找到输出安装包文件。",
264269
};
265270

266271
private static Dictionary<string, string> BuildEnUS() => new()
@@ -385,5 +390,10 @@ private void OnPropertyChanged([CallerMemberName] string? name = null) =>
385390
["Patch.Upload"] = "📤 Upload",
386391
["Upload.Success"] = "Upload successful", ["Upload.Failed"] = "Upload failed: {0}",
387392
["Upload.Uploading"] = "Uploading...",
393+
["Result.OpenOutput"] = "Open Output Directory",
394+
["Result.Exit"] = "Exit",
395+
["Result.Running"] = "Running...",
396+
["Result.ValidationTitle"] = "Validation",
397+
["Result.BuildFirst"] = "Please build the project first to locate the output package file.",
388398
};
389399
}

0 commit comments

Comments
 (0)