Merge pull request #96 from InkCanvasForClass/beta

ICC CE 1.7.1.12
This commit is contained in:
CJK_mkp
2025-07-23 19:52:01 +08:00
committed by GitHub
13 changed files with 674 additions and 302 deletions
+2 -2
View File
@@ -49,5 +49,5 @@ using System.Windows;
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.7.1.10")]
[assembly: AssemblyFileVersion("1.7.1.10")]
[assembly: AssemblyVersion("1.7.1.12")]
[assembly: AssemblyFileVersion("1.7.1.12")]
+278 -257
View File
@@ -10,7 +10,9 @@ using System.Linq;
using System.Windows.Controls;
using System.Text;
using System.Collections.Generic;
using System.Threading;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Ink_Canvas.Helpers
{
@@ -31,7 +33,7 @@ namespace Ink_Canvas.Helpers
}
// 通道-线路组映射
private static readonly Dictionary<UpdateChannel, List<UpdateLineGroup>> ChannelLineGroups = new Dictionary<UpdateChannel, List<UpdateLineGroup>>
public static readonly Dictionary<UpdateChannel, List<UpdateLineGroup>> ChannelLineGroups = new Dictionary<UpdateChannel, List<UpdateLineGroup>>
{
{ UpdateChannel.Release, new List<UpdateLineGroup>
{
@@ -85,6 +87,15 @@ namespace Ink_Canvas.Helpers
}
};
// 区块任务结构体(移到类体内)
private class BlockTask
{
public int Index;
public long Start;
public long End;
public int RetryCount;
}
// 检测URL延迟
private static async Task<long> GetUrlDelay(string url)
{
@@ -342,44 +353,88 @@ namespace Ink_Canvas.Helpers
}
}
// 通过GitHub API获取最新Release信息
private static async Task<(string version, string downloadUrl, string releaseNotes)> GetLatestGithubRelease(UpdateChannel channel)
{
try
{
string apiUrl = channel == UpdateChannel.Beta
? "https://api.github.com/repos/InkCanvasForClass/community-beta/releases/latest"
: "https://api.github.com/repos/InkCanvasForClass/community/releases/latest";
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("User-Agent", "ICC-CE Auto Updater");
var response = await client.GetStringAsync(apiUrl);
var json = JObject.Parse(response);
string version = json["tag_name"]?.ToString();
string releaseNotes = json["body"]?.ToString();
string downloadUrl = json["assets"]?.First?["browser_download_url"]?.ToString();
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(downloadUrl))
return (version, downloadUrl, releaseNotes);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | GitHub Releases API 获取失败: {ex.Message}", LogHelper.LogType.Warning);
}
return (null, null, null);
}
// 主要的更新检测方法(优先检测延迟,失败时自动切换线路组)
public static async Task<(string remoteVersion, UpdateLineGroup lineGroup)> CheckForUpdates(UpdateChannel channel = UpdateChannel.Release, bool alwaysGetRemote = false)
// 仅检测新版本时用GitHub API,实际下载时只用线路组
public static async Task<(string remoteVersion, UpdateLineGroup lineGroup, string releaseNotes)> CheckForUpdates(UpdateChannel channel = UpdateChannel.Release, bool alwaysGetRemote = false)
{
try
{
string localVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
LogHelper.WriteLogToFile($"AutoUpdate | 本地版本: {localVersion}");
LogHelper.WriteLogToFile($"AutoUpdate | 检测通道 {channel} 下最快线路组...");
// 获取所有可用线路组(按延迟排序)
LogHelper.WriteLogToFile($"AutoUpdate | 优先通过GitHub Releases API检测...");
// 1. 优先通过GitHub Releases API获取
var (apiVersion, _, apiReleaseNotes) = await GetLatestGithubRelease(channel);
if (!string.IsNullOrEmpty(apiVersion))
{
Version local = new Version(localVersion);
Version remote = new Version(apiVersion.TrimStart('v', 'V'));
if (remote > local || alwaysGetRemote)
{
LogHelper.WriteLogToFile($"AutoUpdate | 通过GitHub Releases API发现新版本: {apiVersion}");
// 只返回版本号和日志,不返回直链
var group = (await GetAvailableLineGroupsOrdered(channel)).FirstOrDefault();
return (apiVersion, group, apiReleaseNotes);
}
else
{
LogHelper.WriteLogToFile($"AutoUpdate | 当前版本已是最新 (GitHub Releases API)");
var group = (await GetAvailableLineGroupsOrdered(channel)).FirstOrDefault();
return (null, group, apiReleaseNotes);
}
}
// 2. 回退到原有txt方案
LogHelper.WriteLogToFile($"AutoUpdate | GitHub Releases API获取失败,回退到txt方案...");
var availableGroups = await GetAvailableLineGroupsOrdered(channel);
if (availableGroups.Count == 0)
{
LogHelper.WriteLogToFile("AutoUpdate | 所有线路组均不可用", LogHelper.LogType.Error);
return (null, null);
return (null, null, null);
}
// 依次尝试每个线路组,直到成功获取版本信息
foreach (var group in availableGroups)
{
LogHelper.WriteLogToFile($"AutoUpdate | 尝试使用线路组获取版本信息: {group.GroupName}");
string remoteVersion = await GetRemoteVersion(group.VersionUrl);
if (remoteVersion != null)
{
LogHelper.WriteLogToFile($"AutoUpdate | 成功从线路组 {group.GroupName} 获取远程版本: {remoteVersion}");
Version local = new Version(localVersion);
Version remote = new Version(remoteVersion);
if (remote > local || alwaysGetRemote)
{
LogHelper.WriteLogToFile($"AutoUpdate | 发现新版本或强制获取: {remoteVersion}");
return (remoteVersion, group);
return (remoteVersion, group, null);
}
else
{
LogHelper.WriteLogToFile($"AutoUpdate | 当前版本已是最新");
return (null, group);
return (null, group, null);
}
}
else
@@ -387,14 +442,13 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"AutoUpdate | 线路组 {group.GroupName} 获取版本失败,尝试下一个线路组", LogHelper.LogType.Warning);
}
}
LogHelper.WriteLogToFile("AutoUpdate | 所有线路组均无法获取版本信息", LogHelper.LogType.Error);
return (null, null);
return (null, null, null);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | CheckForUpdates错误: {ex.Message}", LogHelper.LogType.Error);
return (null, null);
return (null, null, null);
}
}
@@ -405,7 +459,7 @@ namespace Ink_Canvas.Helpers
}
// 使用多线路组下载新版(支持自动切换)
public static async Task<bool> DownloadSetupFileWithFallback(string version, List<UpdateLineGroup> groups)
public static async Task<bool> DownloadSetupFileWithFallback(string version, List<UpdateLineGroup> groups, Action<double, string> progressCallback = null)
{
try
{
@@ -414,6 +468,7 @@ namespace Ink_Canvas.Helpers
if (File.Exists(statusFilePath) && File.ReadAllText(statusFilePath).Trim().ToLower() == "true")
{
LogHelper.WriteLogToFile("AutoUpdate | 安装包已下载");
progressCallback?.Invoke(100, "已下载完成");
return true;
}
@@ -435,12 +490,13 @@ namespace Ink_Canvas.Helpers
string url = string.Format(group.DownloadUrlFormat, version);
LogHelper.WriteLogToFile($"AutoUpdate | 尝试从线路组 {group.GroupName} 下载: {url}");
bool downloadSuccess = await DownloadFile(url, zipFilePath);
bool downloadSuccess = await DownloadFile(url, zipFilePath, progressCallback);
if (downloadSuccess)
{
SaveDownloadStatus(true);
LogHelper.WriteLogToFile($"AutoUpdate | 从线路组 {group.GroupName} 下载成功");
progressCallback?.Invoke(100, "下载完成");
return true;
}
else
@@ -450,6 +506,7 @@ namespace Ink_Canvas.Helpers
}
LogHelper.WriteLogToFile("AutoUpdate | 所有线路组下载均失败", LogHelper.LogType.Error);
progressCallback?.Invoke(0, "所有线路组下载均失败");
return false;
}
catch (Exception ex)
@@ -461,283 +518,185 @@ namespace Ink_Canvas.Helpers
}
SaveDownloadStatus(false);
progressCallback?.Invoke(0, $"下载异常: {ex.Message}");
return false;
}
}
// 下载文件的具体实现
private static async Task<bool> DownloadFile(string fileUrl, string destinationPath)
public static async Task<bool> DownloadFile(string fileUrl, string destinationPath, Action<double, string> progressCallback = null)
{
// 检测是否为Windows 7
var osVersion = Environment.OSVersion;
bool isWindows7 = osVersion.Version.Major == 6 && osVersion.Version.Minor == 1;
if (isWindows7)
LogHelper.WriteLogToFile($"AutoUpdate | 正在尝试多线程下载: {fileUrl}");
int maxRetry = 3;
int[] threadOptions = new int[] { 32, 4 };
foreach (int threadCount in threadOptions)
{
// Windows 7使用特殊配置
using (var handler = new HttpClientHandler())
long totalSize = await GetContentLength(fileUrl);
if (totalSize <= 0)
{
try
{
// 配置HttpClientHandler以支持Windows 7
handler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true;
using (HttpClient client = new HttpClient(handler))
{
client.Timeout = TimeSpan.FromMinutes(5);
client.DefaultRequestHeaders.Add("User-Agent", "ICC-CE Auto Updater");
LogHelper.WriteLogToFile($"AutoUpdate | 开始下载: {fileUrl}");
string tempFilePath = destinationPath + ".tmp";
string directory = Path.GetDirectoryName(destinationPath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var downloadTask = client.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead);
var initialTimeoutTask = Task.Delay(RequestTimeout);
var completedTask = await Task.WhenAny(downloadTask, initialTimeoutTask);
if (completedTask == initialTimeoutTask)
{
LogHelper.WriteLogToFile($"AutoUpdate | 初始连接超时", LogHelper.LogType.Error);
return false;
}
HttpResponseMessage response = await downloadTask;
LogHelper.WriteLogToFile($"AutoUpdate | HTTP响应状态: {response.StatusCode}");
response.EnsureSuccessStatusCode();
long? totalBytes = response.Content.Headers.ContentLength;
LogHelper.WriteLogToFile($"AutoUpdate | 文件大小: {(totalBytes.HasValue ? (totalBytes.Value / 1024.0 / 1024.0).ToString("F2") + " MB" : "")}");
using (var fileStream = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None))
{
using (var downloadStream = await response.Content.ReadAsStreamAsync())
{
byte[] buffer = new byte[8192];
long totalBytesRead = 0;
int bytesRead;
DateTime lastProgressUpdate = DateTime.Now;
var downloadTimeoutTask = Task.Delay(TimeSpan.FromSeconds(60));
var readTask = Task.Run(async () => {
while ((bytesRead = await downloadStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
if ((DateTime.Now - lastProgressUpdate).TotalSeconds >= 5)
{
if (totalBytes.HasValue)
{
double percentage = (double)totalBytesRead / totalBytes.Value * 100;
LogHelper.WriteLogToFile($"AutoUpdate | 下载进度: {percentage:F1}% ({(totalBytesRead / 1024.0 / 1024.0):F2} MB / {(totalBytes.Value / 1024.0 / 1024.0):F2} MB)");
}
else
{
LogHelper.WriteLogToFile($"AutoUpdate | 已下载: {(totalBytesRead / 1024.0 / 1024.0):F2} MB");
}
lastProgressUpdate = DateTime.Now;
downloadTimeoutTask = Task.Delay(TimeSpan.FromSeconds(60));
}
}
return true;
});
if (await Task.WhenAny(readTask, downloadTimeoutTask) == downloadTimeoutTask)
{
LogHelper.WriteLogToFile($"AutoUpdate | 下载超时(60秒无数据传输)", LogHelper.LogType.Error);
return false;
}
bool downloadCompleted = await readTask;
if (downloadCompleted)
{
LogHelper.WriteLogToFile($"AutoUpdate | 下载完成: {(totalBytesRead / 1024.0 / 1024.0):F2} MB");
}
}
}
if (File.Exists(tempFilePath))
{
if (File.Exists(destinationPath))
{
File.Delete(destinationPath);
}
File.Move(tempFilePath, destinationPath);
LogHelper.WriteLogToFile($"AutoUpdate | 文件保存到: {destinationPath}");
return true;
}
return false;
}
}
catch (HttpRequestException ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | HTTP请求错误: {ex.Message}", LogHelper.LogType.Error);
}
catch (TaskCanceledException ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 下载超时: {ex.Message}", LogHelper.LogType.Error);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 下载文件时出错: {ex.Message}", LogHelper.LogType.Error);
if (ex.InnerException != null)
{
LogHelper.WriteLogToFile($"AutoUpdate | 内部异常: {ex.InnerException.Message}", LogHelper.LogType.Error);
}
}
try
{
string tempFilePath = destinationPath + ".tmp";
if (File.Exists(tempFilePath))
{
File.Delete(tempFilePath);
}
}
catch { }
progressCallback?.Invoke(0, "无法获取文件大小,取消下载");
return false;
}
}
else
{
// 其他Windows版本使用标准配置
using (HttpClient client = new HttpClient())
int blockSize = (int)Math.Ceiling((double)totalSize / threadCount);
int blockCount = (int)Math.Ceiling((double)totalSize / blockSize);
var blockQueue = new System.Collections.Concurrent.ConcurrentQueue<BlockTask>();
var finishedBlocks = new System.Collections.Concurrent.ConcurrentDictionary<int, bool>();
long[] blockDownloaded = new long[blockCount];
for (int i = 0; i < blockCount; i++)
{
try
long start = i * blockSize;
long end = Math.Min(start + blockSize - 1, totalSize - 1);
blockQueue.Enqueue(new BlockTask { Index = i, Start = start, End = end, RetryCount = 0 });
}
CancellationTokenSource cts = new CancellationTokenSource();
var tasks = new List<Task>();
for (int t = 0; t < threadCount; t++)
{
tasks.Add(Task.Run(async () =>
{
client.Timeout = TimeSpan.FromMinutes(5);
client.DefaultRequestHeaders.Add("User-Agent", "ICC-CE Auto Updater");
LogHelper.WriteLogToFile($"AutoUpdate | 开始下载: {fileUrl}");
string tempFilePath = destinationPath + ".tmp";
string directory = Path.GetDirectoryName(destinationPath);
if (!Directory.Exists(directory))
while (blockQueue.TryDequeue(out var block))
{
Directory.CreateDirectory(directory);
}
var downloadTask = client.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead);
var initialTimeoutTask = Task.Delay(RequestTimeout);
var completedTask = await Task.WhenAny(downloadTask, initialTimeoutTask);
if (completedTask == initialTimeoutTask)
{
LogHelper.WriteLogToFile($"AutoUpdate | 初始连接超时", LogHelper.LogType.Error);
return false;
}
HttpResponseMessage response = await downloadTask;
LogHelper.WriteLogToFile($"AutoUpdate | HTTP响应状态: {response.StatusCode}");
response.EnsureSuccessStatusCode();
long? totalBytes = response.Content.Headers.ContentLength;
LogHelper.WriteLogToFile($"AutoUpdate | 文件大小: {(totalBytes.HasValue ? (totalBytes.Value / 1024.0 / 1024.0).ToString("F2") + " MB" : "")}");
using (var fileStream = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None))
{
using (var downloadStream = await response.Content.ReadAsStreamAsync())
bool success = false;
for (int retry = block.RetryCount; retry < maxRetry && !success; retry++)
{
byte[] buffer = new byte[8192];
long totalBytesRead = 0;
int bytesRead;
DateTime lastProgressUpdate = DateTime.Now;
var downloadTimeoutTask = Task.Delay(TimeSpan.FromSeconds(60));
var readTask = Task.Run(async () => {
while ((bytesRead = await downloadStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
try
{
using (var client = new HttpClient())
{
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
if ((DateTime.Now - lastProgressUpdate).TotalSeconds >= 5)
var req = new HttpRequestMessage(HttpMethod.Get, fileUrl);
req.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(block.Start, block.End);
// 新增:分块下载超时机制
var downloadCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token);
var lastReadTime = DateTime.UtcNow;
bool dataReceived = false;
using (var resp = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, downloadCts.Token))
{
if (totalBytes.HasValue)
resp.EnsureSuccessStatusCode();
string tempPath = destinationPath + $".part{block.Index}";
using (var fs = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
double percentage = (double)totalBytesRead / totalBytes.Value * 100;
LogHelper.WriteLogToFile($"AutoUpdate | 下载进度: {percentage:F1}% ({(totalBytesRead / 1024.0 / 1024.0):F2} MB / {(totalBytes.Value / 1024.0 / 1024.0):F2} MB)");
var stream = await resp.Content.ReadAsStreamAsync();
byte[] buffer = new byte[8192];
int read;
while (true)
{
var readTask = stream.ReadAsync(buffer, 0, buffer.Length, downloadCts.Token);
var timeoutTask = Task.Delay(15000, downloadCts.Token); // 15秒超时
var completed = await Task.WhenAny(readTask, timeoutTask);
if (completed == timeoutTask)
{
// 超时未收到数据,取消本线程,重新入队
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index} 15秒无数据,线程超时重试", LogHelper.LogType.Warning);
progressCallback?.Invoke(0, $"分块{block.Index} 15秒无数据,线程超时重试");
downloadCts.Cancel();
break;
}
read = await readTask;
if (read <= 0) break;
await fs.WriteAsync(buffer, 0, read, downloadCts.Token);
blockDownloaded[block.Index] += read;
lastReadTime = DateTime.UtcNow;
dataReceived = true;
// 合并所有块进度
long totalDownloaded = blockDownloaded.Sum();
double percent = (double)totalDownloaded / totalSize * 100;
progressCallback?.Invoke(percent, $"多线程下载中({threadCount}线程): {percent:F1}%");
}
}
else
{
LogHelper.WriteLogToFile($"AutoUpdate | 已下载: {(totalBytesRead / 1024.0 / 1024.0):F2} MB");
}
lastProgressUpdate = DateTime.Now;
downloadTimeoutTask = Task.Delay(TimeSpan.FromSeconds(60));
}
// 如果因超时break且未完成,success为false,重新入队
if (!dataReceived)
{
throw new IOException("分块下载超时无数据");
}
}
return true;
});
if (await Task.WhenAny(readTask, downloadTimeoutTask) == downloadTimeoutTask)
{
LogHelper.WriteLogToFile($"AutoUpdate | 下载超时(60秒无数据传输)", LogHelper.LogType.Error);
return false;
success = true;
}
bool downloadCompleted = await readTask;
if (downloadCompleted)
catch (Exception ex) when (ex is HttpRequestException || ex is IOException || ex is TaskCanceledException)
{
LogHelper.WriteLogToFile($"AutoUpdate | 下载完成: {(totalBytesRead / 1024.0 / 1024.0):F2} MB");
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载失败,第{retry + 1}次: {ex.Message}", LogHelper.LogType.Warning);
progressCallback?.Invoke(0, $"分块{block.Index}下载失败,第{retry + 1}次: {ex.Message}");
await Task.Delay(15000);
}
}
}
if (File.Exists(tempFilePath))
{
if (File.Exists(destinationPath))
if (success)
{
File.Delete(destinationPath);
finishedBlocks[block.Index] = true;
}
else if (block.RetryCount + 1 < maxRetry)
{
// 失败但未超最大重试,重新入队
block.RetryCount++;
blockQueue.Enqueue(block);
}
else
{
// 超过最大重试,取消所有任务
cts.Cancel();
break;
}
File.Move(tempFilePath, destinationPath);
LogHelper.WriteLogToFile($"AutoUpdate | 文件保存到: {destinationPath}");
return true;
}
}));
}
await Task.WhenAll(tasks);
if (cts.IsCancellationRequested || finishedBlocks.Count != blockCount)
{
progressCallback?.Invoke(0, $"多线程下载失败({threadCount}线程)");
// 清理分块文件
for (int i = 0; i < blockCount; i++)
{
string tempPath = destinationPath + $".part{i}";
if (File.Exists(tempPath)) File.Delete(tempPath);
}
if (threadCount == threadOptions.Last())
{
// 已经是最后一次尝试
return false;
}
catch (HttpRequestException ex)
else
{
LogHelper.WriteLogToFile($"AutoUpdate | HTTP请求错误: {ex.Message}", LogHelper.LogType.Error);
LogHelper.WriteLogToFile($"AutoUpdate | {threadCount}线程下载失败,尝试降级为{threadOptions.Last()}线程");
progressCallback?.Invoke(0, $"{threadCount}线程下载失败,尝试降级为{threadOptions.Last()}线程");
continue;
}
catch (TaskCanceledException ex)
}
// 合并所有块
using (var output = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
for (int i = 0; i < blockCount; i++)
{
LogHelper.WriteLogToFile($"AutoUpdate | 下载超时: {ex.Message}", LogHelper.LogType.Error);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 下载文件时出错: {ex.Message}", LogHelper.LogType.Error);
if (ex.InnerException != null)
string tempPath = destinationPath + $".part{i}";
using (var input = new FileStream(tempPath, FileMode.Open, FileAccess.Read))
{
LogHelper.WriteLogToFile($"AutoUpdate | 内部异常: {ex.InnerException.Message}", LogHelper.LogType.Error);
await input.CopyToAsync(output);
}
File.Delete(tempPath);
}
try
{
string tempFilePath = destinationPath + ".tmp";
if (File.Exists(tempFilePath))
{
File.Delete(tempFilePath);
}
progressCallback?.Invoke(100, $"多线程下载完成({threadCount}线程)");
return true;
}
// 理论上不会到这里
return false;
}
// 获取文件总大小
private static async Task<long> GetContentLength(string fileUrl)
{
try
{
using (var client = new HttpClient())
{
var req = new HttpRequestMessage(HttpMethod.Head, fileUrl);
var resp = await client.SendAsync(req);
if (resp.IsSuccessStatusCode && resp.Content.Headers.ContentLength.HasValue)
return resp.Content.Headers.ContentLength.Value;
}
}
catch { }
return false;
}
}
return -1;
}
// 保存下载状态
@@ -1065,7 +1024,7 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"AutoUpdate | 开始修复版本,通道: {channel}");
// 获取远程版本号(自动选择最快线路组,始终下载远程版本)
var (remoteVersion, group) = await CheckForUpdates(channel, true);
var (remoteVersion, group, _) = await CheckForUpdates(channel, true);
if (string.IsNullOrEmpty(remoteVersion) || group == null)
{
LogHelper.WriteLogToFile("AutoUpdate | 修复版本时获取远程版本失败", LogHelper.LogType.Error);
@@ -1097,6 +1056,37 @@ namespace Ink_Canvas.Helpers
}
}
// 获取所有GitHub历史版本(Release
public static async Task<List<(string version, string downloadUrl, string releaseNotes)>> GetAllGithubReleases(UpdateChannel channel = UpdateChannel.Release)
{
var result = new List<(string, string, string)>();
try
{
string apiUrl = channel == UpdateChannel.Beta
? "https://api.github.com/repos/InkCanvasForClass/community-beta/releases"
: "https://api.github.com/repos/InkCanvasForClass/community/releases";
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("User-Agent", "ICC-CE Auto Updater");
var response = await client.GetStringAsync(apiUrl);
var arr = JArray.Parse(response);
foreach (var item in arr)
{
string version = item["tag_name"]?.ToString();
string releaseNotes = item["body"]?.ToString();
string downloadUrl = item["assets"]?.First?["browser_download_url"]?.ToString();
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(downloadUrl))
result.Add((version, downloadUrl, releaseNotes));
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 获取历史版本失败: {ex.Message}", LogHelper.LogType.Error);
}
return result;
}
// 测试Windows 7 TLS连接的方法
public static async Task<bool> TestWindows7TlsConnection()
{
@@ -1145,6 +1135,37 @@ namespace Ink_Canvas.Helpers
return false;
}
}
/// <summary>
/// 启动手动指定版本的多线路多线程下载并自动安装(用于历史版本回滚等场景)
/// </summary>
public static async Task<bool> StartManualDownloadAndInstall(string version, UpdateChannel channel, Action<double, string> progressCallback = null)
{
try
{
// 先检测并排序所有可用线路组
var groups = await GetAvailableLineGroupsOrdered(channel);
bool downloadSuccess = await DownloadSetupFileWithFallback(version, groups, progressCallback);
if (!downloadSuccess)
{
LogHelper.WriteLogToFile($"AutoUpdate | 手动下载版本{version}失败");
return false;
}
LogHelper.WriteLogToFile($"AutoUpdate | 手动安装版本: {version}");
InstallNewVersionApp(version, false);
App.IsAppExitByUser = true;
Application.Current.Dispatcher.Invoke(() => {
Application.Current.Shutdown();
});
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 手动下载或安装异常: {ex.Message}", LogHelper.LogType.Error);
progressCallback?.Invoke(0, $"下载异常: {ex.Message}");
return false;
}
}
}
internal class AutoUpdateWithSilenceTimeComboBox
+18 -6
View File
@@ -645,8 +645,10 @@
Width="120" HorizontalAlignment="Left" Click="FixVersionButton_Click"/>
<TextBlock Text="# 版本修复会根据当前选择的通道下载最新版本并执行安装,可用于修复损坏的安装"
TextWrapping="Wrap" Foreground="#a1a1aa" />
<Border BorderBrush="Black" BorderThickness="1" CornerRadius="5" Padding="12"
<Button x:Name="HistoryRollbackButton" Content="历史版本回滚" Width="120" Margin="0,10,0,0" Click="HistoryRollbackButton_Click"/>
<TextBlock Text="# 历史版本回滚,点击后会弹出相应页面供用户手动回滚到之前的版本"
TextWrapping="Wrap" Foreground="#a1a1aa" />
<Border BorderBrush="White" BorderThickness="1" CornerRadius="5" Padding="12"
Visibility="{Binding ElementName=ToggleSwitchIsAutoUpdateWithSilence, Path=IsOn, Converter={StaticResource BooleanToVisibilityConverter}}">
<ui:SimpleStackPanel Spacing="12">
<TextBlock
@@ -655,12 +657,12 @@
<ui:SimpleStackPanel x:Name="AutoUpdateTimePeriodBlock" Spacing="12">
<ui:SimpleStackPanel Spacing="12">
<TextBlock Text="静默更新时间段" FontSize="15" FontWeight="Bold"
TextWrapping="Wrap" Foreground="Black" />
TextWrapping="Wrap" Foreground="#fafafa" />
<ui:SimpleStackPanel Orientation="Horizontal" Spacing="12">
<ui:SimpleStackPanel Orientation="Horizontal">
<TextBlock Margin="0,0,10,0" VerticalAlignment="Center"
Text="起始时间" FontSize="14" TextWrapping="Wrap"
Foreground="Black" />
Foreground="#fafafa" />
<ComboBox x:Name="AutoUpdateWithSilenceStartTimeComboBox"
Width="90"
SelectionChanged="AutoUpdateWithSilenceStartTimeComboBox_SelectionChanged" />
@@ -668,7 +670,7 @@
<ui:SimpleStackPanel Orientation="Horizontal">
<TextBlock Margin="0,0,10,0" VerticalAlignment="Center"
Text="终止时间" FontSize="14" TextWrapping="Wrap"
Foreground="Black" />
Foreground="#fafafa" />
<ComboBox x:Name="AutoUpdateWithSilenceEndTimeComboBox"
Width="90"
SelectionChanged="AutoUpdateWithSilenceEndTimeComboBox_SelectionChanged" />
@@ -849,6 +851,15 @@
</ui:SimpleStackPanel>
<TextBlock Text="# 允许选中墨迹后对墨迹进行双指或多指缩放操作(此设置不受“允许双指旋转”设置的影响)" TextWrapping="Wrap"
Foreground="#a1a1aa" />
<Line HorizontalAlignment="Center" X1="0" Y1="0" X2="400" Y2="0" Stroke="#3f3f46"
StrokeThickness="1" Margin="0,4,0,4" />
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Foreground="#fafafa" Text="启用手掌擦" VerticalAlignment="Center" FontSize="14" Margin="0,0,16,0" />
<ui:ToggleSwitch OnContent="" OffContent="" Name="ToggleSwitchEnablePalmEraser" IsOn="True"
FontFamily="Microsoft YaHei UI" FontWeight="Bold"
Toggled="ToggleSwitchEnablePalmEraser_Toggled" />
</ui:SimpleStackPanel>
<TextBlock Text="# 关闭后,手掌将无法触发橡皮擦功能" TextWrapping="Wrap" Foreground="#a1a1aa" />
</ui:SimpleStackPanel>
</GroupBox>
<GroupBox Name="GroupBoxInkRecognition">
@@ -1206,6 +1217,7 @@
Toggled="ToggleSwitchEnableWppProcessKill_Toggled" />
</ui:SimpleStackPanel>
<TextBlock Text="# 关闭后将不会自动查杀WPP残留进程,可能导致WPP关闭卡顿或无法彻底退出。" TextWrapping="Wrap" Foreground="#a1a1aa" />
<TextBlock Text="# 如果您只使用PowerPoint请不要打开WPS联动开关,如果使用WPS建议不要使用PowerPoint" TextWrapping="Wrap" Foreground="#a1a1aa" />
<Border BorderBrush="#ef4444"
BorderThickness="2" Padding="8" CornerRadius="6" Background="#991b1b">
<ui:SimpleStackPanel Orientation="Horizontal" Spacing="4">
@@ -2888,7 +2900,7 @@
</ui:ScrollViewerEx>
<!-- 底部按钮区域 -->
<Grid Grid.Row="2" VerticalAlignment="Bottom" Height="50">
<Grid Grid.Row="2" VerticalAlignment="Bottom" Height="65">
<Button FontFamily="Microsoft YaHei UI"
Width="120" Margin="10"
HorizontalAlignment="Right"
+66 -19
View File
@@ -35,6 +35,14 @@ namespace Ink_Canvas {
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
private const int GWL_EXSTYLE = -20;
private const int WS_EX_NOACTIVATE = 0x08000000;
[DllImport("user32.dll")]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
private static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
private const uint SWP_NOSIZE = 0x0001;
private const uint SWP_NOMOVE = 0x0002;
private const uint SWP_NOACTIVATE = 0x0010;
private const uint SWP_SHOWWINDOW = 0x0040;
// 新增:设置窗口置顶并兼容无焦点
private void SetTopmostWithNoActivate(bool topmost)
@@ -45,6 +53,15 @@ namespace Ink_Canvas {
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle & ~WS_EX_NOACTIVATE);
// 设置 Topmost
this.Topmost = topmost;
// 使用SetWindowPos确保无焦点置顶
if (topmost)
{
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
}
else
{
SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
}
// 再加回 WS_EX_NOACTIVATE
exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_NOACTIVATE);
@@ -248,10 +265,25 @@ namespace Ink_Canvas {
catch { }
}
// 新增:记录上一个模式
private InkCanvasEditingMode lastEditingMode = InkCanvasEditingMode.Ink;
private void inkCanvas_EditingModeChanged(object sender, RoutedEventArgs e) {
var inkCanvas1 = sender as InkCanvas;
if (inkCanvas1 == null) return;
// 只负责显示/隐藏覆盖层,不再强制切换模式
var eraserOverlay = this.FindName("AdvancedEraserOverlay") as Border;
if (eraserOverlay != null) {
if (inkCanvas1.EditingMode == InkCanvasEditingMode.EraseByPoint) {
eraserOverlay.IsHitTestVisible = true;
Trace.WriteLine("Advanced Eraser: Overlay enabled in eraser mode");
} else {
eraserOverlay.IsHitTestVisible = false;
DisableAdvancedEraserSystem();
Trace.WriteLine("Advanced Eraser: Overlay disabled in non-eraser mode");
}
}
// 使用辅助方法设置光标
SetCursorBasedOnEditingMode(inkCanvas1);
if (Settings.Canvas.IsShowCursor) {
@@ -262,31 +294,13 @@ namespace Ink_Canvas {
else
inkCanvas1.ForceCursor = false;
} else {
// 套索选择模式下始终强制显示光标,即使用户设置不显示光标
if (inkCanvas1.EditingMode == InkCanvasEditingMode.Select) {
inkCanvas1.ForceCursor = true;
} else {
inkCanvas1.ForceCursor = false;
}
}
if (inkCanvas1.EditingMode == InkCanvasEditingMode.Ink) forcePointEraser = !forcePointEraser;
// 处理高级橡皮擦覆盖层的启用/禁用
var eraserOverlay = this.FindName("AdvancedEraserOverlay") as Border;
if (eraserOverlay != null) {
if (inkCanvas1.EditingMode == InkCanvasEditingMode.EraseByPoint) {
// 橡皮擦模式下启用覆盖层
eraserOverlay.IsHitTestVisible = true;
Trace.WriteLine("Advanced Eraser: Overlay enabled in eraser mode");
} else {
// 其他模式下禁用覆盖层
eraserOverlay.IsHitTestVisible = false;
// 同时禁用高级橡皮擦系统
DisableAdvancedEraserSystem();
Trace.WriteLine("Advanced Eraser: Overlay disabled in non-eraser mode");
}
}
}
#endregion Ink Canvas
@@ -297,11 +311,16 @@ namespace Ink_Canvas {
public static string settingsFileName = "Settings.json";
private bool isLoaded = false;
private bool forcePointEraser = false;
public static bool EnablePalmEraser = true;
private void Window_Loaded(object sender, RoutedEventArgs e) {
loadPenCanvas();
//加载设置
LoadSettings(true);
// 同步手掌擦开关
EnablePalmEraser = Settings.Canvas.EnablePalmEraser;
if (ToggleSwitchEnablePalmEraser != null)
ToggleSwitchEnablePalmEraser.IsOn = EnablePalmEraser;
// 加载自定义背景颜色
LoadCustomBackgroundColor();
@@ -381,6 +400,14 @@ namespace Ink_Canvas {
// 初始化插件系统
InitializePluginSystem();
// 新增:确保EditingModeChanged事件已绑定
var inkCanvas = this.FindName("inkCanvas") as InkCanvas;
if (inkCanvas != null)
{
inkCanvas.EditingModeChanged -= inkCanvas_EditingModeChanged;
inkCanvas.EditingModeChanged += inkCanvas_EditingModeChanged;
}
}
private void SystemEventsOnDisplaySettingsChanged(object sender, EventArgs e) {
@@ -535,7 +562,7 @@ namespace Ink_Canvas {
AvailableLatestLineGroup = null;
// 使用当前选择的更新通道检查更新
var (remoteVersion, lineGroup) = await AutoUpdateHelper.CheckForUpdates(Settings.Startup.UpdateChannel);
var (remoteVersion, lineGroup, apiReleaseNotes) = await AutoUpdateHelper.CheckForUpdates(Settings.Startup.UpdateChannel);
AvailableLatestVersion = remoteVersion;
AvailableLatestLineGroup = lineGroup;
@@ -1312,5 +1339,25 @@ namespace Ink_Canvas {
// 直接调用PPT放映结束按钮的逻辑
BtnPPTSlideShowEnd_Click(BtnPPTSlideShowEnd, null);
}
private void HistoryRollbackButton_Click(object sender, RoutedEventArgs e)
{
// 收起设置面板(与插件面板一致)
BorderSettings.Visibility = Visibility.Hidden;
BorderSettingsMask.Visibility = Visibility.Hidden;
var win = new HistoryRollbackWindow(Settings.Startup.UpdateChannel);
win.ShowDialog();
// 可选:回滚窗口关闭后恢复设置面板显示
BorderSettings.Visibility = Visibility.Visible;
BorderSettingsMask.Visibility = Visibility.Visible;
}
private void ToggleSwitchEnablePalmEraser_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
EnablePalmEraser = ToggleSwitchEnablePalmEraser.IsOn;
Settings.Canvas.EnablePalmEraser = EnablePalmEraser;
SaveSettingsToFile();
}
}
}
+22 -6
View File
@@ -634,52 +634,68 @@ namespace Ink_Canvas {
// 绑定事件处理
overlay.MouseDown += (sender, e) => {
if (!MainWindow.EnablePalmEraser) return;
var inkCanvas = this.FindName("inkCanvas") as InkCanvas;
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint) {
lastEditingMode = inkCanvas.EditingMode;
overlay.CaptureMouse();
inkCanvas.EditingMode = InkCanvasEditingMode.None;
StartAdvancedEraserOperation(sender);
}
};
overlay.MouseUp += (sender, e) => {
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint) {
if (!MainWindow.EnablePalmEraser) return;
var inkCanvas = this.FindName("inkCanvas") as InkCanvas;
if (inkCanvas.EditingMode == InkCanvasEditingMode.None) {
overlay.ReleaseMouseCapture();
EndAdvancedEraserOperation(sender);
inkCanvas.EditingMode = InkCanvasEditingMode.Ink; // 抬手后自动回到画笔
}
};
overlay.MouseMove += (sender, e) => {
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint) {
if (!MainWindow.EnablePalmEraser) return;
var inkCanvas = this.FindName("inkCanvas") as InkCanvas;
if (inkCanvas.EditingMode == InkCanvasEditingMode.None) {
var position = e.GetPosition((UIElement)this.FindName("inkCanvas"));
Trace.WriteLine($"Advanced Eraser: Mouse move event triggered at ({position.X:F1}, {position.Y:F1})");
UpdateAdvancedEraserPosition(sender, position);
} else {
Trace.WriteLine($"Advanced Eraser: Mouse move ignored - not in eraser mode, current mode: {inkCanvas.EditingMode}");
}
};
// 触控笔事件
overlay.StylusDown += (sender, e) => {
if (!MainWindow.EnablePalmEraser) return;
var inkCanvas = this.FindName("inkCanvas") as InkCanvas;
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint) {
e.Handled = true;
if (e.StylusDevice.TabletDevice.Type == TabletDeviceType.Stylus) {
overlay.CaptureStylus();
}
lastEditingMode = inkCanvas.EditingMode;
inkCanvas.EditingMode = InkCanvasEditingMode.None;
StartAdvancedEraserOperation(sender);
}
};
overlay.StylusUp += (sender, e) => {
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint) {
if (!MainWindow.EnablePalmEraser) return;
var inkCanvas = this.FindName("inkCanvas") as InkCanvas;
if (inkCanvas.EditingMode == InkCanvasEditingMode.None) {
e.Handled = true;
if (e.StylusDevice.TabletDevice.Type == TabletDeviceType.Stylus) {
overlay.ReleaseStylusCapture();
}
EndAdvancedEraserOperation(sender);
inkCanvas.EditingMode = InkCanvasEditingMode.Ink; // 抬手后自动回到画笔
}
};
overlay.StylusMove += (sender, e) => {
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint) {
if (!MainWindow.EnablePalmEraser) return;
var inkCanvas = this.FindName("inkCanvas") as InkCanvas;
if (inkCanvas.EditingMode == InkCanvasEditingMode.None) {
e.Handled = true;
var position = e.GetPosition((UIElement)this.FindName("inkCanvas"));
UpdateAdvancedEraserPosition(sender, position);
+41 -3
View File
@@ -9,6 +9,9 @@ using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Net;
using System.Net.Sockets;
using System.Linq;
namespace Ink_Canvas {
public class TimeViewModel : INotifyPropertyChanged {
@@ -55,6 +58,35 @@ namespace Ink_Canvas {
private TimeViewModel nowTimeVM = new TimeViewModel();
private async Task<DateTime> GetNetworkTimeAsync()
{
try
{
const string ntpServer = "ntp.ntsc.ac.cn";
var ntpData = new byte[48];
ntpData[0] = 0x1B;
var addresses = await Dns.GetHostAddressesAsync(ntpServer);
var ipEndPoint = new IPEndPoint(addresses[0], 123);
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp))
{
socket.ReceiveTimeout = 2000;
socket.Connect(ipEndPoint);
await Task.Factory.FromAsync(socket.BeginSend(ntpData, 0, ntpData.Length, SocketFlags.None, null, socket), socket.EndSend);
await Task.Factory.FromAsync(socket.BeginReceive(ntpData, 0, ntpData.Length, SocketFlags.None, null, socket), socket.EndReceive);
}
const byte serverReplyTime = 40;
ulong intPart = BitConverter.ToUInt32(ntpData.Skip(serverReplyTime).Take(4).Reverse().ToArray(), 0);
ulong fractPart = BitConverter.ToUInt32(ntpData.Skip(serverReplyTime + 4).Take(4).Reverse().ToArray(), 0);
var milliseconds = (intPart * 1000) + ((fractPart * 1000) / 0x100000000L);
var networkDateTime = (new DateTime(1900, 1, 1)).AddMilliseconds((long)milliseconds);
return networkDateTime.ToLocalTime();
}
catch
{
return DateTime.Now;
}
}
private void InitTimers() {
timerCheckPPT.Elapsed += TimerCheckPPT_Elapsed;
timerCheckPPT.Interval = 500;
@@ -66,7 +98,7 @@ namespace Ink_Canvas {
timerCheckAutoUpdateWithSilence.Interval = 1000 * 60 * 10;
WaterMarkTime.DataContext = nowTimeVM;
WaterMarkDate.DataContext = nowTimeVM;
timerDisplayTime.Elapsed += TimerDisplayTime_Elapsed;
timerDisplayTime.Elapsed += async (s, e) => await TimerDisplayTime_ElapsedAsync();
timerDisplayTime.Interval = 1000;
timerDisplayTime.Start();
timerDisplayDate.Elapsed += TimerDisplayDate_Elapsed;
@@ -77,8 +109,14 @@ namespace Ink_Canvas {
nowTimeVM.nowTime = DateTime.Now.ToShortTimeString().ToString();
}
private void TimerDisplayTime_Elapsed(object sender, ElapsedEventArgs e) {
nowTimeVM.nowTime = DateTime.Now.ToShortTimeString().ToString();
private async Task TimerDisplayTime_ElapsedAsync()
{
DateTime now = await GetNetworkTimeAsync();
// 只更新时间,日期由原有逻辑定时更新即可
Dispatcher.Invoke(() =>
{
nowTimeVM.nowTime = now.ToShortTimeString();
});
}
private void TimerDisplayDate_Elapsed(object sender, ElapsedEventArgs e) {
+2 -2
View File
@@ -49,5 +49,5 @@ using System.Windows;
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.7.1.10")]
[assembly: AssemblyFileVersion("1.7.1.10")]
[assembly: AssemblyVersion("1.7.1.12")]
[assembly: AssemblyFileVersion("1.7.1.12")]
+2
View File
@@ -77,6 +77,8 @@ namespace Ink_Canvas
public OptionalOperation HyperbolaAsymptoteOption { get; set; } = OptionalOperation.Ask;
[JsonProperty("isCompressPicturesUploaded")]
public bool IsCompressPicturesUploaded { get; set; } = false;
[JsonProperty("enablePalmEraser")]
public bool EnablePalmEraser { get; set; } = true;
}
public enum OptionalOperation
@@ -78,6 +78,12 @@
Background="#f8fafc" BorderBrush="#cbd5e1" ToolTip="跳过此版本更新" Visibility="Visible" IsEnabled="True"/>
</ui:SimpleStackPanel>
</Border>
<!-- 下载进度条和状态 -->
<StackPanel x:Name="DownloadProgressPanel" Orientation="Vertical" HorizontalAlignment="Center" Margin="0,10,0,0" Visibility="Collapsed">
<ProgressBar x:Name="DownloadProgressBar" Width="360" Height="18" Minimum="0" Maximum="100" Value="0"/>
<TextBlock x:Name="DownloadProgressText" Text="正在下载..." FontSize="14" Foreground="#2563eb" HorizontalAlignment="Center" Margin="0,6,0,0"/>
</StackPanel>
</ui:SimpleStackPanel>
</Grid>
</ScrollViewer>
+68 -4
View File
@@ -7,6 +7,9 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media;
using System.Threading.Tasks;
using System.IO;
using System.Threading;
namespace Ink_Canvas
{
@@ -129,16 +132,59 @@ namespace Ink_Canvas
LogHelper.WriteLogToFile("AutoUpdate | Update dialog buttons visibility ensured");
}
private void UpdateNowButton_Click(object sender, RoutedEventArgs e)
private async void UpdateNowButton_Click(object sender, RoutedEventArgs e)
{
LogHelper.WriteLogToFile("AutoUpdate | Update Now button clicked");
// 禁用按钮,显示进度条
UpdateNowButton.IsEnabled = false;
UpdateLaterButton.IsEnabled = false;
SkipVersionButton.IsEnabled = false;
DownloadProgressPanel.Visibility = Visibility.Visible;
DownloadProgressBar.Value = 0;
DownloadProgressText.Text = "正在准备下载...";
// 启动多线路下载
bool downloadSuccess = false;
try
{
// 获取当前通道的所有线路组
var groups = AutoUpdateHelper.ChannelLineGroups[MainWindow.Settings.Startup.UpdateChannel];
downloadSuccess = await AutoUpdateHelper.DownloadSetupFileWithFallback(NewVersion, groups, (percent, text) =>
{
Dispatcher.Invoke(() => {
DownloadProgressBar.Value = percent;
DownloadProgressText.Text = text;
});
});
if (downloadSuccess)
{
// 下载完成后自动安装
await DownloadAndInstallVersion(NewVersion, null, CancellationToken.None);
}
}
catch (Exception ex)
{
DownloadProgressText.Text = $"下载失败: {ex.Message}";
LogHelper.WriteLogToFile($"AutoUpdate | 下载异常: {ex.Message}", LogHelper.LogType.Error);
}
if (downloadSuccess)
{
DownloadProgressBar.Value = 100;
DownloadProgressText.Text = "下载完成,准备安装...";
await Task.Delay(800);
// 设置结果为立即更新
Result = UpdateResult.UpdateNow;
// 关闭窗口,返回到MainWindow处理后续下载和安装流程
DialogResult = true;
Close();
}
else
{
DownloadProgressText.Text = "下载失败,请检查网络后重试。";
UpdateNowButton.IsEnabled = true;
UpdateLaterButton.IsEnabled = true;
SkipVersionButton.IsEnabled = true;
}
}
private void UpdateLaterButton_Click(object sender, RoutedEventArgs e)
@@ -279,5 +325,23 @@ namespace Ink_Canvas
}
}
}
// 多线程分块下载并自动安装
private async Task<bool> DownloadAndInstallVersion(string version, string downloadUrl, CancellationToken token)
{
if (string.IsNullOrEmpty(downloadUrl))
{
// 自动更新场景下,downloadUrl为null,直接用主下载目录
string updatesFolderPath = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "AutoUpdate");
downloadUrl = Path.Combine(updatesFolderPath, $"InkCanvasForClass.CE.{version}.zip");
}
LogHelper.WriteLogToFile($"AutoUpdate | 开始安装版本: {version}");
AutoUpdateHelper.InstallNewVersionApp(version, false);
App.IsAppExitByUser = true;
Application.Current.Dispatcher.Invoke(() => {
Application.Current.Shutdown();
});
return true;
}
}
}
@@ -0,0 +1,24 @@
<Window x:Class="Ink_Canvas.HistoryRollbackWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
xmlns:mdxam="clr-namespace:MdXaml;assembly=MdXaml"
mc:Ignorable="d"
Title="历史版本回滚" Height="600" Width="850" ResizeMode="NoResize" WindowStartupLocation="CenterScreen">
<Grid Background="#fafafa">
<ui:SimpleStackPanel VerticalAlignment="Stretch" Spacing="0">
<TextBlock Text="选择历史版本进行回滚" FontSize="24" FontWeight="Bold" Foreground="#2563eb" Margin="24,24,0,12"/>
<ComboBox x:Name="VersionComboBox" Width="400" Height="36" Margin="24,0,0,0" DisplayMemberPath="Version" SelectionChanged="VersionComboBox_SelectionChanged"/>
<Border BorderBrush="#3f3f46" Background="White" BorderThickness="1" CornerRadius="4" Margin="24,16,24,0" Height="180">
<mdxam:MarkdownScrollViewer x:Name="ReleaseNotesViewer" Foreground="Black" MarkdownStyleName="GithubLike"/>
</Border>
<Button x:Name="RollbackButton" Content="回滚到此版本" Width="360" Height="48" Margin="24,24,0,0" Click="RollbackButton_Click"/>
<StackPanel x:Name="DownloadProgressPanel" Orientation="Vertical" HorizontalAlignment="Center" Margin="0,10,0,0" Visibility="Collapsed">
<ProgressBar x:Name="DownloadProgressBar" Width="360" Height="18" Minimum="0" Maximum="100" Value="0"/>
<TextBlock x:Name="DownloadProgressText" Text="正在下载..." FontSize="14" Foreground="#2563eb" HorizontalAlignment="Center" Margin="0,6,0,0"/>
</StackPanel>
</ui:SimpleStackPanel>
</Grid>
</Window>
@@ -0,0 +1,142 @@
using Ink_Canvas.Helpers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Linq; // Added for OrderByDescending
using System.ComponentModel;
using System.Threading;
namespace Ink_Canvas
{
public partial class HistoryRollbackWindow : Window
{
private class VersionItem
{
public string Version { get; set; }
public string DownloadUrl { get; set; }
public string ReleaseNotes { get; set; }
}
private List<VersionItem> versionList = new List<VersionItem>();
private VersionItem selectedItem = null;
private UpdateChannel channel = UpdateChannel.Release;
private CancellationTokenSource downloadCts = null;
public HistoryRollbackWindow(UpdateChannel channel = UpdateChannel.Release)
{
InitializeComponent();
this.channel = channel;
LoadVersions();
}
private async void LoadVersions()
{
LogHelper.WriteLogToFile($"HistoryRollback | 开始加载历史版本,通道: {channel}");
RollbackButton.IsEnabled = false;
VersionComboBox.Items.Clear();
DownloadProgressPanel.Visibility = Visibility.Collapsed;
DownloadProgressBar.Value = 0;
DownloadProgressText.Text = "";
ReleaseNotesViewer.Markdown = "正在获取历史版本...";
var releases = await AutoUpdateHelper.GetAllGithubReleases(channel);
versionList.Clear();
foreach (var (version, url, notes) in releases)
{
versionList.Add(new VersionItem { Version = version, DownloadUrl = url, ReleaseNotes = notes });
}
// 按版本号数字降序排列
versionList = versionList.OrderByDescending(v => ParseVersionForSort(v.Version)).ToList();
VersionComboBox.ItemsSource = versionList;
if (versionList.Count > 0)
{
VersionComboBox.SelectedIndex = 0;
RollbackButton.IsEnabled = true;
LogHelper.WriteLogToFile($"HistoryRollback | 加载到 {versionList.Count} 个历史版本");
}
else
{
ReleaseNotesViewer.Markdown = "未获取到历史版本信息。";
LogHelper.WriteLogToFile($"HistoryRollback | 未获取到历史版本信息", LogHelper.LogType.Warning);
}
}
// 辅助方法:解析版本号用于排序
private Version ParseVersionForSort(string version)
{
var v = version.TrimStart('v', 'V');
Version result;
if (Version.TryParse(v, out result))
return result;
return new Version(0, 0, 0, 0);
}
private void VersionComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
selectedItem = VersionComboBox.SelectedItem as VersionItem;
if (selectedItem != null)
{
ReleaseNotesViewer.Markdown = selectedItem.ReleaseNotes ?? "无更新日志";
LogHelper.WriteLogToFile($"HistoryRollback | 用户选择版本: {selectedItem.Version}");
}
// 取消聚焦,防止父级自动滚动
Keyboard.ClearFocus();
}
private async void RollbackButton_Click(object sender, RoutedEventArgs e)
{
if (selectedItem == null) return;
LogHelper.WriteLogToFile($"HistoryRollback | 用户点击回滚,目标版本: {selectedItem.Version}");
RollbackButton.IsEnabled = false;
VersionComboBox.IsEnabled = false;
DownloadProgressPanel.Visibility = Visibility.Visible;
DownloadProgressBar.Value = 0;
DownloadProgressText.Text = "正在准备下载...";
bool downloadSuccess = false;
try
{
downloadSuccess = await AutoUpdateHelper.StartManualDownloadAndInstall(
selectedItem.Version,
channel,
(percent, text) =>
{
Dispatcher.Invoke(() => {
DownloadProgressBar.Value = percent;
DownloadProgressText.Text = text;
});
}
);
}
catch (Exception ex)
{
DownloadProgressText.Text = $"下载失败: {ex.Message}";
LogHelper.WriteLogToFile($"HistoryRollback | 下载异常: {ex.Message}", LogHelper.LogType.Error);
}
if (downloadSuccess)
{
DownloadProgressBar.Value = 100;
DownloadProgressText.Text = "下载完成,准备安装...";
await Task.Delay(800);
this.DialogResult = true;
this.Close();
}
else
{
DownloadProgressText.Text = "下载失败,请检查网络后重试。";
RollbackButton.IsEnabled = true;
VersionComboBox.IsEnabled = true;
}
}
protected override void OnClosing(CancelEventArgs e)
{
downloadCts?.Cancel();
base.OnClosing(e);
}
}
}
@@ -10,11 +10,11 @@ none
false
TRACE;DEBUG;NETFRAMEWORK;NET472;;NET30_OR_GREATER;NET35_OR_GREATER;NET40_OR_GREATER;NET45_OR_GREATER;NET451_OR_GREATER;NET452_OR_GREATER;NET46_OR_GREATER;NET461_OR_GREATER;NET462_OR_GREATER;NET47_OR_GREATER;NET471_OR_GREATER;NET472_OR_GREATER
E:\ICC CE\ICC CE main\community\Ink Canvas\App.xaml
21348134359
22-2143008179
752071346691
76-141727233
471037513499
Helpers\Plugins\BuiltIn\SuperLauncher\LauncherSettingsControl.xaml;Helpers\Plugins\BuiltIn\SuperLauncher\LauncherWindow.xaml;MainWindow.xaml;MainWindow_cs\MW_Eraser.xaml;Resources\DrawShapeImageDictionary.xaml;Resources\IconImageDictionary.xaml;Resources\SeewoImageDictionary.xaml;Resources\Styles\Dark.xaml;Resources\Styles\Light.xaml;Windows\AddCustomIconWindow.xaml;Windows\AddPickNameBackgroundWindow.xaml;Windows\CountdownTimerWindow.xaml;Windows\CustomIconWindow.xaml;Windows\CycleProcessBar.xaml;Windows\HasNewUpdateWindow.xaml;Windows\ManagePickNameBackgroundsWindow.xaml;Windows\NamesInputWindow.xaml;Windows\OperatingGuideWindow.xaml;Windows\PluginSettingsWindow.xaml;Windows\RandWindow.xaml;Windows\YesOrNoNotificationWindow.xaml;
Helpers\Plugins\BuiltIn\SuperLauncher\LauncherSettingsControl.xaml;Helpers\Plugins\BuiltIn\SuperLauncher\LauncherWindow.xaml;MainWindow.xaml;MainWindow_cs\MW_Eraser.xaml;Resources\DrawShapeImageDictionary.xaml;Resources\IconImageDictionary.xaml;Resources\SeewoImageDictionary.xaml;Resources\Styles\Dark.xaml;Resources\Styles\Light.xaml;Windows\AddCustomIconWindow.xaml;Windows\AddPickNameBackgroundWindow.xaml;Windows\CountdownTimerWindow.xaml;Windows\CustomIconWindow.xaml;Windows\CycleProcessBar.xaml;Windows\HasNewUpdateWindow.xaml;Windows\HistoryRollbackWindow.xaml;Windows\ManagePickNameBackgroundsWindow.xaml;Windows\NamesInputWindow.xaml;Windows\OperatingGuideWindow.xaml;Windows\PluginSettingsWindow.xaml;Windows\RandWindow.xaml;Windows\YesOrNoNotificationWindow.xaml;
False