3 Commits

Author SHA1 Message Date
ZerkyLiu 8ae90f81b4 上传文件至「/」
同步最新发布版1.2.3
2026-01-27 13:43:10 +01:00
ZerkyLiu 024cdfb2d5 更新 README.md 2026-01-27 13:42:27 +01:00
ZerkyLiu b09dbbe63f 更新 README.md 2026-01-27 12:01:47 +01:00
2 changed files with 495 additions and 9 deletions
+493 -7
View File
@@ -26,6 +26,7 @@ using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
@@ -39,6 +40,9 @@ namespace 善学教育积分卡汇率计算器
private static readonly string[] NtpServers = { "ntp.aliyun.com", "ntp.ntsc.ac.cn", "pool.ntp.org" };
private static readonly string RateTextHorizontal = BuildRateText(true);
private static readonly string RateTextVertical = BuildRateText(false);
private const string CurrentVersion = "1.2.3";
private const string ReleasePageUrl = "https://gittea.dev/ZerkyLiu/Shanxue-Education-Points-Coin-Converter/releases";
private const string DownloadUrlTemplate = "https://gittea.dev/ZerkyLiu/Shanxue-Education-Points-Coin-Converter/releases/download/{0}/善学教育积分卡汇率计算器.exe";
private readonly Label _currencyLabel;
private readonly Label _timeLabel;
private readonly Label _timeSourceLabel;
@@ -62,6 +66,8 @@ namespace 善学教育积分卡汇率计算器
private bool _usingUiAccessTopMost;
private bool _suppressPointsTextChange;
private readonly bool _forceFallbackTopMost;
private Form? _aboutDialog;
private Form? _floatingPrompt;
private const string UiAccessRelaunchArg = "--uiaccess-relaunch";
private const string UiAccessResourceSuffix = ".dll.uiaccess.dll";
private const string IconResourceSuffix = ".ico.logo_256x256.ico";
@@ -691,16 +697,35 @@ namespace 善学教育积分卡汇率计算器
private void ShowAboutDialog()
{
if (_aboutDialog != null && !_aboutDialog.IsDisposed)
{
_aboutDialog.Show();
_aboutDialog.BringToFront();
return;
}
var dialog = new Form
{
Text = "关于",
StartPosition = FormStartPosition.CenterParent,
StartPosition = FormStartPosition.Manual,
FormBorderStyle = FormBorderStyle.FixedDialog,
MaximizeBox = false,
MinimizeBox = false,
ShowInTaskbar = false,
ClientSize = new Size(520, 420)
};
_aboutDialog = dialog;
dialog.FormClosed += (_, __) => _aboutDialog = null;
var layout = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 2
};
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
var textBox = new TextBox
{
@@ -720,7 +745,7 @@ namespace 善学教育积分卡汇率计算器
textBox.Text = string.Join(Environment.NewLine, new[]
{
"善学教育积分卡汇率计算器",
"版本号: 1.0.0",
$"版本号: {CurrentVersion}",
"作者: ZerkyLiu",
$"构建时间:{buildTime}",
"运行环境:.NET 4.7",
@@ -732,14 +757,454 @@ namespace 善学教育积分卡汇率计算器
"• 网络时间同步",
"• 高精度阶梯换算",
"• UIAccess 置顶支持",
"• 联网自动更新",
"",
"未来计划:",
"• 联网自动更新",
"• 修复已知问题"
});
dialog.Controls.Add(textBox);
dialog.ShowDialog(this);
var updateButton = new Button
{
Text = "检查更新",
AutoSize = true,
Anchor = AnchorStyles.Right
};
updateButton.Click += async (_, __) => await HandleUpdateCheckAsync(updateButton);
var buttonPanel = new Panel
{
Dock = DockStyle.Fill,
Height = updateButton.Height + 12,
Padding = new Padding(0, 8, 0, 0)
};
updateButton.Location = new Point(buttonPanel.Width - updateButton.Width, 0);
updateButton.Anchor = AnchorStyles.Top | AnchorStyles.Right;
buttonPanel.Controls.Add(updateButton);
buttonPanel.Resize += (_, __) =>
{
updateButton.Location = new Point(buttonPanel.Width - updateButton.Width, 0);
};
layout.Controls.Add(textBox, 0, 0);
layout.Controls.Add(buttonPanel, 0, 1);
dialog.Controls.Add(layout);
CenterFormOnScreen(dialog, this);
dialog.Show();
dialog.Activate();
}
private async Task HandleUpdateCheckAsync(Button button)
{
if (!button.Enabled)
{
return;
}
var owner = button.FindForm() ?? this;
var originalText = button.Text;
button.Enabled = false;
button.Text = "检查更新中…";
try
{
var releaseInfo = await GetLatestReleaseInfoAsync();
if (releaseInfo == null || string.IsNullOrEmpty(releaseInfo.Tag))
{
await ShowNonModalMessageAsync(owner, "无法获取最新版本信息,请稍后重试。", "更新检查", false);
return;
}
var latestVersion = NormalizeVersion(releaseInfo.Tag);
var currentVersion = NormalizeVersion(CurrentVersion);
if (CompareVersions(currentVersion, latestVersion) >= 0)
{
await ShowNonModalMessageAsync(owner, "已经是最新版本。", "更新检查", false);
return;
}
var consent = await ShowNonModalConfirmAsync(owner, $"检测到新版本 {releaseInfo.Tag},是否下载?", "发现新版本");
if (consent != DialogResult.Yes)
{
return;
}
var downloadUrl = releaseInfo.DownloadUrl ?? string.Empty;
if (string.IsNullOrEmpty(downloadUrl))
{
downloadUrl = string.Format(CultureInfo.InvariantCulture, DownloadUrlTemplate, Uri.EscapeDataString(releaseInfo.Tag));
}
var targetPath = GetDownloadTargetPath(releaseInfo.Tag);
button.Text = "正在下载中…";
var progressPercent = 0;
PaintEventHandler painter = (_, e) =>
{
var rect = button.ClientRectangle;
var pad = 4;
var barH = 4;
var x = rect.X + pad;
var y = rect.Bottom - barH - pad;
var w = Math.Max(0, rect.Width - pad * 2);
using (var back = new SolidBrush(Color.FromArgb(220, 220, 220)))
{
e.Graphics.FillRectangle(back, x, y, w, barH);
}
var fillW = (int)Math.Round(w * progressPercent / 100.0);
using (var fill = new SolidBrush(Color.FromArgb(76, 175, 80)))
{
e.Graphics.FillRectangle(fill, x, y, fillW, barH);
}
};
button.Paint += painter;
try
{
await DownloadFileAsync(downloadUrl, targetPath, new Progress<int>(p =>
{
progressPercent = Math.Max(0, Math.Min(100, p));
button.Invalidate();
}));
}
finally
{
button.Paint -= painter;
}
await ShowNonModalMessageAsync(owner, $"已下载到:{targetPath}", "下载完成", false);
Process.Start(new ProcessStartInfo
{
FileName = Path.GetDirectoryName(targetPath) ?? GetDownloadFolder(),
UseShellExecute = true
});
}
catch (Exception ex)
{
await ShowNonModalMessageAsync(owner, $"更新失败:{ex.Message}", "更新失败", false);
}
finally
{
button.Text = originalText;
button.Enabled = true;
}
}
private static void CenterFormOnScreen(Form dialog, Form reference)
{
var screen = Screen.FromControl(reference);
var area = screen.WorkingArea;
var x = area.X + (area.Width - dialog.Width) / 2;
var y = area.Y + (area.Height - dialog.Height) / 2;
dialog.Location = new Point(Math.Max(area.X, x), Math.Max(area.Y, y));
}
private async Task ShowNonModalMessageAsync(Form owner, string text, string title, bool yesNo)
{
var tcs = new TaskCompletionSource<bool>();
var prompt = CreateFloatingPrompt(owner, title, text, yesNo, out var yesButton, out var noButton);
_floatingPrompt = prompt;
prompt.FormClosed += (_, __) =>
{
_floatingPrompt = null;
tcs.TrySetResult(true);
};
prompt.Show();
CenterChildOnOwner(prompt, owner);
yesButton.Click += (_, __) => prompt.Close();
if (yesNo)
{
noButton.Click += (_, __) => prompt.Close();
}
await tcs.Task.ConfigureAwait(true);
}
private async Task<DialogResult> ShowNonModalConfirmAsync(Form owner, string text, string title)
{
var tcs = new TaskCompletionSource<DialogResult>();
var prompt = CreateFloatingPrompt(owner, title, text, true, out var yesButton, out var noButton);
_floatingPrompt = prompt;
prompt.FormClosed += (_, __) =>
{
_floatingPrompt = null;
if (!tcs.Task.IsCompleted)
{
tcs.TrySetResult(DialogResult.No);
}
};
prompt.Show();
CenterChildOnOwner(prompt, owner);
yesButton.Click += (_, __) =>
{
tcs.TrySetResult(DialogResult.Yes);
prompt.Close();
};
noButton.Click += (_, __) =>
{
tcs.TrySetResult(DialogResult.No);
prompt.Close();
};
return await tcs.Task.ConfigureAwait(true);
}
private static Form CreateFloatingPrompt(Form owner, string title, string text, bool yesNo, out Button yesButton, out Button noButton)
{
var f = new Form
{
Text = title,
FormBorderStyle = FormBorderStyle.FixedDialog,
StartPosition = FormStartPosition.Manual,
MaximizeBox = false,
MinimizeBox = false,
ShowInTaskbar = false,
ClientSize = new Size(380, 160)
};
var layout = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 2
};
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
var label = new Label
{
Text = text,
Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleLeft,
AutoEllipsis = true
};
var panel = new FlowLayoutPanel
{
FlowDirection = FlowDirection.RightToLeft,
Dock = DockStyle.Fill,
Padding = new Padding(0, 8, 0, 0),
AutoSize = true
};
yesButton = new Button { Text = yesNo ? "是" : "确定", AutoSize = true };
noButton = new Button { Text = "否", AutoSize = true };
panel.Controls.Add(yesButton);
if (yesNo)
{
panel.Controls.Add(noButton);
}
layout.Controls.Add(label, 0, 0);
layout.Controls.Add(panel, 0, 1);
f.Controls.Add(layout);
return f;
}
private static void CenterChildOnOwner(Form child, Form owner)
{
var rect = owner.Bounds;
var x = rect.X + (rect.Width - child.Width) / 2;
var y = rect.Y + (rect.Height - child.Height) / 2;
child.Location = new Point(Math.Max(0, x), Math.Max(0, y));
}
private static string GetDownloadFolder()
{
var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var path = string.IsNullOrWhiteSpace(userProfile) ? string.Empty : Path.Combine(userProfile, "Downloads");
if (string.IsNullOrWhiteSpace(path))
{
path = Path.GetTempPath();
}
Directory.CreateDirectory(path);
return path;
}
private static string GetDownloadTargetPath(string versionTag)
{
var folder = GetDownloadFolder();
return Path.Combine(folder, $"善学教育积分卡汇率计算器_{versionTag}.exe");
}
private sealed class ReleaseInfo
{
public ReleaseInfo(string tag, string? downloadUrl)
{
Tag = tag;
DownloadUrl = downloadUrl;
}
public string Tag { get; }
public string? DownloadUrl { get; }
}
private async Task<ReleaseInfo?> GetLatestReleaseInfoAsync()
{
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
var result = await TryGetLatestReleaseInfoAsync(false);
if (result != null)
{
return result;
}
return await TryGetLatestReleaseInfoAsync(true);
}
private async Task<ReleaseInfo?> TryGetLatestReleaseInfoAsync(bool useProxy)
{
try
{
using (var client = CreateWebClient(useProxy))
{
var html = await client.DownloadStringTaskAsync(ReleasePageUrl);
var tag = ParseLatestReleaseTag(html) ?? string.Empty;
if (tag.Length == 0)
{
return null;
}
var tagValue = tag;
var downloadUrl = ParseLatestDownloadUrl(html);
if (string.IsNullOrEmpty(downloadUrl))
{
var tagUrl = BuildReleaseTagUrl(tagValue);
var tagHtml = await client.DownloadStringTaskAsync(tagUrl);
downloadUrl = ParseLatestDownloadUrl(tagHtml);
}
return new ReleaseInfo(tagValue, downloadUrl);
}
}
catch
{
return null;
}
}
private static string? ParseLatestReleaseTag(string html)
{
if (string.IsNullOrEmpty(html))
{
return null;
}
var match = Regex.Match(html, "/releases/tag/([^\"/<>]+)");
if (!match.Success)
{
return null;
}
return Uri.UnescapeDataString(match.Groups[1].Value);
}
private static string? ParseLatestDownloadUrl(string html)
{
if (string.IsNullOrEmpty(html))
{
return null;
}
var hrefMatches = Regex.Matches(html, "href=\"([^\"]+/releases/download/[^\"]+)\"", RegexOptions.IgnoreCase);
foreach (Match match in hrefMatches)
{
var link = WebUtility.HtmlDecode(match.Groups[1].Value);
if (link.IndexOf(".exe", StringComparison.OrdinalIgnoreCase) < 0)
{
continue;
}
return NormalizeDownloadUrl(link);
}
var rawMatch = Regex.Match(html, "/releases/download/[^\"'<>\\s]+\\.exe", RegexOptions.IgnoreCase);
if (rawMatch.Success)
{
return NormalizeDownloadUrl(rawMatch.Value);
}
return null;
}
private static string BuildReleaseTagUrl(string tag)
{
var escaped = Uri.EscapeDataString(tag);
return $"{ReleasePageUrl}/tag/{escaped}";
}
private static string NormalizeDownloadUrl(string link)
{
var cleaned = Uri.UnescapeDataString(link);
if (cleaned.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return cleaned;
}
var baseUri = new Uri(ReleasePageUrl);
var resolved = new Uri(baseUri, cleaned);
return resolved.ToString();
}
private static string NormalizeVersion(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
return value.Trim().TrimStart('v', 'V');
}
private static int CompareVersions(string left, string right)
{
if (Version.TryParse(left, out var leftVersion) && Version.TryParse(right, out var rightVersion))
{
return leftVersion.CompareTo(rightVersion);
}
return string.CompareOrdinal(left, right);
}
private static async Task DownloadFileAsync(string url, string targetPath, IProgress<int>? progress = null)
{
using (var client = CreateWebClient(true))
{
var tcs = new TaskCompletionSource<bool>();
Exception? error = null;
client.DownloadProgressChanged += (_, e) =>
{
try
{
progress?.Report(e.ProgressPercentage);
}
catch
{
}
};
client.DownloadFileCompleted += (_, e) =>
{
error = e.Error;
tcs.TrySetResult(true);
};
client.DownloadFileAsync(new Uri(url), targetPath);
await tcs.Task.ConfigureAwait(true);
if (error != null)
{
throw error;
}
}
}
private static WebClient CreateWebClient(bool useProxy)
{
var client = new WebClient();
if (useProxy)
{
var proxy = WebRequest.DefaultWebProxy;
if (proxy != null)
{
proxy.Credentials = CredentialCache.DefaultCredentials;
client.Proxy = proxy;
}
}
client.Headers[HttpRequestHeader.UserAgent] = "Shanxue-Updater";
return client;
}
private bool TryRelaunchWithUiAccess()
@@ -966,8 +1431,29 @@ namespace 善学教育积分卡汇率计算器
return;
}
TopMost = true;
SetWindowPos(Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
var target = GetActiveTopMostTarget();
if (target == null || !target.IsHandleCreated)
{
return;
}
SetWindowPos(target.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
}
private Form? GetActiveTopMostTarget()
{
var active = Form.ActiveForm;
if (active == null || active.IsDisposed || !active.Visible)
{
return this;
}
if (active == this || active == _aboutDialog || active == _floatingPrompt)
{
return active;
}
return this;
}
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
+2 -2
View File
@@ -9,7 +9,7 @@
## 快速开始
从[发布](http://https://gittea.dev/ZerkyLiu/Shanxue-Education-Points-Coin-Converter/releases "发布")页下载程序本体。
从[发布](http://https://gittea.dev/ZerkyLiu/Shanxue-Education-Points-Coin-Converter/releases "发布")页下载程序本体,需要.Net4.7
> https://gittea.dev/ZerkyLiu/Shanxue-Education-Points-Coin-Converter/releases
@@ -19,7 +19,7 @@
## 特别鸣谢
使用了[RunUIAccess](http://https://github.com/shc0743/RunUIAccess "RunUIAccess")以实现“超级置顶”的主要功能。
使用了[RunUIAccess](https://github.com/shc0743/RunUIAccess "RunUIAccess")以实现“超级置顶”的主要功能。
## 许可证