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 // You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.7.1.10")] [assembly: AssemblyVersion("1.7.1.12")]
[assembly: AssemblyFileVersion("1.7.1.10")] [assembly: AssemblyFileVersion("1.7.1.12")]
+278 -257
View File
@@ -10,7 +10,9 @@ using System.Linq;
using System.Windows.Controls; using System.Windows.Controls;
using System.Text; using System.Text;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Ink_Canvas.Helpers 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> { 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延迟 // 检测URL延迟
private static async Task<long> GetUrlDelay(string 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 try
{ {
string localVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); string localVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
LogHelper.WriteLogToFile($"AutoUpdate | 本地版本: {localVersion}"); 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); var availableGroups = await GetAvailableLineGroupsOrdered(channel);
if (availableGroups.Count == 0) if (availableGroups.Count == 0)
{ {
LogHelper.WriteLogToFile("AutoUpdate | 所有线路组均不可用", LogHelper.LogType.Error); LogHelper.WriteLogToFile("AutoUpdate | 所有线路组均不可用", LogHelper.LogType.Error);
return (null, null); return (null, null, null);
} }
// 依次尝试每个线路组,直到成功获取版本信息
foreach (var group in availableGroups) foreach (var group in availableGroups)
{ {
LogHelper.WriteLogToFile($"AutoUpdate | 尝试使用线路组获取版本信息: {group.GroupName}"); LogHelper.WriteLogToFile($"AutoUpdate | 尝试使用线路组获取版本信息: {group.GroupName}");
string remoteVersion = await GetRemoteVersion(group.VersionUrl); string remoteVersion = await GetRemoteVersion(group.VersionUrl);
if (remoteVersion != null) if (remoteVersion != null)
{ {
LogHelper.WriteLogToFile($"AutoUpdate | 成功从线路组 {group.GroupName} 获取远程版本: {remoteVersion}"); LogHelper.WriteLogToFile($"AutoUpdate | 成功从线路组 {group.GroupName} 获取远程版本: {remoteVersion}");
Version local = new Version(localVersion); Version local = new Version(localVersion);
Version remote = new Version(remoteVersion); Version remote = new Version(remoteVersion);
if (remote > local || alwaysGetRemote) if (remote > local || alwaysGetRemote)
{ {
LogHelper.WriteLogToFile($"AutoUpdate | 发现新版本或强制获取: {remoteVersion}"); LogHelper.WriteLogToFile($"AutoUpdate | 发现新版本或强制获取: {remoteVersion}");
return (remoteVersion, group); return (remoteVersion, group, null);
} }
else else
{ {
LogHelper.WriteLogToFile($"AutoUpdate | 当前版本已是最新"); LogHelper.WriteLogToFile($"AutoUpdate | 当前版本已是最新");
return (null, group); return (null, group, null);
} }
} }
else else
@@ -387,14 +442,13 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"AutoUpdate | 线路组 {group.GroupName} 获取版本失败,尝试下一个线路组", LogHelper.LogType.Warning); LogHelper.WriteLogToFile($"AutoUpdate | 线路组 {group.GroupName} 获取版本失败,尝试下一个线路组", LogHelper.LogType.Warning);
} }
} }
LogHelper.WriteLogToFile("AutoUpdate | 所有线路组均无法获取版本信息", LogHelper.LogType.Error); LogHelper.WriteLogToFile("AutoUpdate | 所有线路组均无法获取版本信息", LogHelper.LogType.Error);
return (null, null); return (null, null, null);
} }
catch (Exception ex) catch (Exception ex)
{ {
LogHelper.WriteLogToFile($"AutoUpdate | CheckForUpdates错误: {ex.Message}", LogHelper.LogType.Error); 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 try
{ {
@@ -414,6 +468,7 @@ namespace Ink_Canvas.Helpers
if (File.Exists(statusFilePath) && File.ReadAllText(statusFilePath).Trim().ToLower() == "true") if (File.Exists(statusFilePath) && File.ReadAllText(statusFilePath).Trim().ToLower() == "true")
{ {
LogHelper.WriteLogToFile("AutoUpdate | 安装包已下载"); LogHelper.WriteLogToFile("AutoUpdate | 安装包已下载");
progressCallback?.Invoke(100, "已下载完成");
return true; return true;
} }
@@ -435,12 +490,13 @@ namespace Ink_Canvas.Helpers
string url = string.Format(group.DownloadUrlFormat, version); string url = string.Format(group.DownloadUrlFormat, version);
LogHelper.WriteLogToFile($"AutoUpdate | 尝试从线路组 {group.GroupName} 下载: {url}"); LogHelper.WriteLogToFile($"AutoUpdate | 尝试从线路组 {group.GroupName} 下载: {url}");
bool downloadSuccess = await DownloadFile(url, zipFilePath); bool downloadSuccess = await DownloadFile(url, zipFilePath, progressCallback);
if (downloadSuccess) if (downloadSuccess)
{ {
SaveDownloadStatus(true); SaveDownloadStatus(true);
LogHelper.WriteLogToFile($"AutoUpdate | 从线路组 {group.GroupName} 下载成功"); LogHelper.WriteLogToFile($"AutoUpdate | 从线路组 {group.GroupName} 下载成功");
progressCallback?.Invoke(100, "下载完成");
return true; return true;
} }
else else
@@ -450,6 +506,7 @@ namespace Ink_Canvas.Helpers
} }
LogHelper.WriteLogToFile("AutoUpdate | 所有线路组下载均失败", LogHelper.LogType.Error); LogHelper.WriteLogToFile("AutoUpdate | 所有线路组下载均失败", LogHelper.LogType.Error);
progressCallback?.Invoke(0, "所有线路组下载均失败");
return false; return false;
} }
catch (Exception ex) catch (Exception ex)
@@ -461,283 +518,185 @@ namespace Ink_Canvas.Helpers
} }
SaveDownloadStatus(false); SaveDownloadStatus(false);
progressCallback?.Invoke(0, $"下载异常: {ex.Message}");
return false; 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 LogHelper.WriteLogToFile($"AutoUpdate | 正在尝试多线程下载: {fileUrl}");
var osVersion = Environment.OSVersion; int maxRetry = 3;
bool isWindows7 = osVersion.Version.Major == 6 && osVersion.Version.Minor == 1; int[] threadOptions = new int[] { 32, 4 };
foreach (int threadCount in threadOptions)
if (isWindows7)
{ {
// Windows 7使用特殊配置 long totalSize = await GetContentLength(fileUrl);
using (var handler = new HttpClientHandler()) if (totalSize <= 0)
{ {
try progressCallback?.Invoke(0, "无法获取文件大小,取消下载");
{
// 配置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 { }
return false; return false;
} }
} int blockSize = (int)Math.Ceiling((double)totalSize / threadCount);
else int blockCount = (int)Math.Ceiling((double)totalSize / blockSize);
{ var blockQueue = new System.Collections.Concurrent.ConcurrentQueue<BlockTask>();
// 其他Windows版本使用标准配置 var finishedBlocks = new System.Collections.Concurrent.ConcurrentDictionary<int, bool>();
using (HttpClient client = new HttpClient()) 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); while (blockQueue.TryDequeue(out var block))
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); bool success = false;
} for (int retry = block.RetryCount; retry < maxRetry && !success; retry++)
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]; try
long totalBytesRead = 0; {
int bytesRead; using (var client = new HttpClient())
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); var req = new HttpRequestMessage(HttpMethod.Get, fileUrl);
totalBytesRead += bytesRead; req.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(block.Start, block.End);
if ((DateTime.Now - lastProgressUpdate).TotalSeconds >= 5) // 新增:分块下载超时机制
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; var stream = await resp.Content.ReadAsStreamAsync();
LogHelper.WriteLogToFile($"AutoUpdate | 下载进度: {percentage:F1}% ({(totalBytesRead / 1024.0 / 1024.0):F2} MB / {(totalBytes.Value / 1024.0 / 1024.0):F2} MB)"); 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 }
{ // 如果因超时break且未完成,success为false,重新入队
LogHelper.WriteLogToFile($"AutoUpdate | 已下载: {(totalBytesRead / 1024.0 / 1024.0):F2} MB"); if (!dataReceived)
} {
lastProgressUpdate = DateTime.Now; throw new IOException("分块下载超时无数据");
downloadTimeoutTask = Task.Delay(TimeSpan.FromSeconds(60));
} }
} }
return true; success = true;
});
if (await Task.WhenAny(readTask, downloadTimeoutTask) == downloadTimeoutTask)
{
LogHelper.WriteLogToFile($"AutoUpdate | 下载超时(60秒无数据传输)", LogHelper.LogType.Error);
return false;
} }
catch (Exception ex) when (ex is HttpRequestException || ex is IOException || ex is TaskCanceledException)
bool downloadCompleted = await readTask;
if (downloadCompleted)
{ {
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 (success)
if (File.Exists(tempFilePath))
{
if (File.Exists(destinationPath))
{ {
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; 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); string tempPath = destinationPath + $".part{i}";
} using (var input = new FileStream(tempPath, FileMode.Open, FileAccess.Read))
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 下载文件时出错: {ex.Message}", LogHelper.LogType.Error);
if (ex.InnerException != null)
{ {
LogHelper.WriteLogToFile($"AutoUpdate | 内部异常: {ex.InnerException.Message}", LogHelper.LogType.Error); await input.CopyToAsync(output);
} }
File.Delete(tempPath);
} }
}
try progressCallback?.Invoke(100, $"多线程下载完成({threadCount}线程)");
{ return true;
string tempFilePath = destinationPath + ".tmp"; }
if (File.Exists(tempFilePath)) // 理论上不会到这里
{ return false;
File.Delete(tempFilePath); }
// 获取文件总大小
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 { } catch { }
return -1;
return false;
}
}
} }
// 保存下载状态 // 保存下载状态
@@ -1065,7 +1024,7 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"AutoUpdate | 开始修复版本,通道: {channel}"); LogHelper.WriteLogToFile($"AutoUpdate | 开始修复版本,通道: {channel}");
// 获取远程版本号(自动选择最快线路组,始终下载远程版本) // 获取远程版本号(自动选择最快线路组,始终下载远程版本)
var (remoteVersion, group) = await CheckForUpdates(channel, true); var (remoteVersion, group, _) = await CheckForUpdates(channel, true);
if (string.IsNullOrEmpty(remoteVersion) || group == null) if (string.IsNullOrEmpty(remoteVersion) || group == null)
{ {
LogHelper.WriteLogToFile("AutoUpdate | 修复版本时获取远程版本失败", LogHelper.LogType.Error); 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连接的方法 // 测试Windows 7 TLS连接的方法
public static async Task<bool> TestWindows7TlsConnection() public static async Task<bool> TestWindows7TlsConnection()
{ {
@@ -1145,6 +1135,37 @@ namespace Ink_Canvas.Helpers
return false; 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 internal class AutoUpdateWithSilenceTimeComboBox
+18 -6
View File
@@ -645,8 +645,10 @@
Width="120" HorizontalAlignment="Left" Click="FixVersionButton_Click"/> Width="120" HorizontalAlignment="Left" Click="FixVersionButton_Click"/>
<TextBlock Text="# 版本修复会根据当前选择的通道下载最新版本并执行安装,可用于修复损坏的安装" <TextBlock Text="# 版本修复会根据当前选择的通道下载最新版本并执行安装,可用于修复损坏的安装"
TextWrapping="Wrap" Foreground="#a1a1aa" /> TextWrapping="Wrap" Foreground="#a1a1aa" />
<Button x:Name="HistoryRollbackButton" Content="历史版本回滚" Width="120" Margin="0,10,0,0" Click="HistoryRollbackButton_Click"/>
<Border BorderBrush="Black" BorderThickness="1" CornerRadius="5" Padding="12" <TextBlock Text="# 历史版本回滚,点击后会弹出相应页面供用户手动回滚到之前的版本"
TextWrapping="Wrap" Foreground="#a1a1aa" />
<Border BorderBrush="White" BorderThickness="1" CornerRadius="5" Padding="12"
Visibility="{Binding ElementName=ToggleSwitchIsAutoUpdateWithSilence, Path=IsOn, Converter={StaticResource BooleanToVisibilityConverter}}"> Visibility="{Binding ElementName=ToggleSwitchIsAutoUpdateWithSilence, Path=IsOn, Converter={StaticResource BooleanToVisibilityConverter}}">
<ui:SimpleStackPanel Spacing="12"> <ui:SimpleStackPanel Spacing="12">
<TextBlock <TextBlock
@@ -655,12 +657,12 @@
<ui:SimpleStackPanel x:Name="AutoUpdateTimePeriodBlock" Spacing="12"> <ui:SimpleStackPanel x:Name="AutoUpdateTimePeriodBlock" Spacing="12">
<ui:SimpleStackPanel Spacing="12"> <ui:SimpleStackPanel Spacing="12">
<TextBlock Text="静默更新时间段" FontSize="15" FontWeight="Bold" <TextBlock Text="静默更新时间段" FontSize="15" FontWeight="Bold"
TextWrapping="Wrap" Foreground="Black" /> TextWrapping="Wrap" Foreground="#fafafa" />
<ui:SimpleStackPanel Orientation="Horizontal" Spacing="12"> <ui:SimpleStackPanel Orientation="Horizontal" Spacing="12">
<ui:SimpleStackPanel Orientation="Horizontal"> <ui:SimpleStackPanel Orientation="Horizontal">
<TextBlock Margin="0,0,10,0" VerticalAlignment="Center" <TextBlock Margin="0,0,10,0" VerticalAlignment="Center"
Text="起始时间" FontSize="14" TextWrapping="Wrap" Text="起始时间" FontSize="14" TextWrapping="Wrap"
Foreground="Black" /> Foreground="#fafafa" />
<ComboBox x:Name="AutoUpdateWithSilenceStartTimeComboBox" <ComboBox x:Name="AutoUpdateWithSilenceStartTimeComboBox"
Width="90" Width="90"
SelectionChanged="AutoUpdateWithSilenceStartTimeComboBox_SelectionChanged" /> SelectionChanged="AutoUpdateWithSilenceStartTimeComboBox_SelectionChanged" />
@@ -668,7 +670,7 @@
<ui:SimpleStackPanel Orientation="Horizontal"> <ui:SimpleStackPanel Orientation="Horizontal">
<TextBlock Margin="0,0,10,0" VerticalAlignment="Center" <TextBlock Margin="0,0,10,0" VerticalAlignment="Center"
Text="终止时间" FontSize="14" TextWrapping="Wrap" Text="终止时间" FontSize="14" TextWrapping="Wrap"
Foreground="Black" /> Foreground="#fafafa" />
<ComboBox x:Name="AutoUpdateWithSilenceEndTimeComboBox" <ComboBox x:Name="AutoUpdateWithSilenceEndTimeComboBox"
Width="90" Width="90"
SelectionChanged="AutoUpdateWithSilenceEndTimeComboBox_SelectionChanged" /> SelectionChanged="AutoUpdateWithSilenceEndTimeComboBox_SelectionChanged" />
@@ -849,6 +851,15 @@
</ui:SimpleStackPanel> </ui:SimpleStackPanel>
<TextBlock Text="# 允许选中墨迹后对墨迹进行双指或多指缩放操作(此设置不受“允许双指旋转”设置的影响)" TextWrapping="Wrap" <TextBlock Text="# 允许选中墨迹后对墨迹进行双指或多指缩放操作(此设置不受“允许双指旋转”设置的影响)" TextWrapping="Wrap"
Foreground="#a1a1aa" /> 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> </ui:SimpleStackPanel>
</GroupBox> </GroupBox>
<GroupBox Name="GroupBoxInkRecognition"> <GroupBox Name="GroupBoxInkRecognition">
@@ -1206,6 +1217,7 @@
Toggled="ToggleSwitchEnableWppProcessKill_Toggled" /> Toggled="ToggleSwitchEnableWppProcessKill_Toggled" />
</ui:SimpleStackPanel> </ui:SimpleStackPanel>
<TextBlock Text="# 关闭后将不会自动查杀WPP残留进程,可能导致WPP关闭卡顿或无法彻底退出。" TextWrapping="Wrap" Foreground="#a1a1aa" /> <TextBlock Text="# 关闭后将不会自动查杀WPP残留进程,可能导致WPP关闭卡顿或无法彻底退出。" TextWrapping="Wrap" Foreground="#a1a1aa" />
<TextBlock Text="# 如果您只使用PowerPoint请不要打开WPS联动开关,如果使用WPS建议不要使用PowerPoint" TextWrapping="Wrap" Foreground="#a1a1aa" />
<Border BorderBrush="#ef4444" <Border BorderBrush="#ef4444"
BorderThickness="2" Padding="8" CornerRadius="6" Background="#991b1b"> BorderThickness="2" Padding="8" CornerRadius="6" Background="#991b1b">
<ui:SimpleStackPanel Orientation="Horizontal" Spacing="4"> <ui:SimpleStackPanel Orientation="Horizontal" Spacing="4">
@@ -2888,7 +2900,7 @@
</ui:ScrollViewerEx> </ui:ScrollViewerEx>
<!-- 底部按钮区域 --> <!-- 底部按钮区域 -->
<Grid Grid.Row="2" VerticalAlignment="Bottom" Height="50"> <Grid Grid.Row="2" VerticalAlignment="Bottom" Height="65">
<Button FontFamily="Microsoft YaHei UI" <Button FontFamily="Microsoft YaHei UI"
Width="120" Margin="10" Width="120" Margin="10"
HorizontalAlignment="Right" HorizontalAlignment="Right"
+66 -19
View File
@@ -35,6 +35,14 @@ namespace Ink_Canvas {
private static extern int GetWindowLong(IntPtr hWnd, int nIndex); private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
private const int GWL_EXSTYLE = -20; private const int GWL_EXSTYLE = -20;
private const int WS_EX_NOACTIVATE = 0x08000000; 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) private void SetTopmostWithNoActivate(bool topmost)
@@ -45,6 +53,15 @@ namespace Ink_Canvas {
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle & ~WS_EX_NOACTIVATE); SetWindowLong(hwnd, GWL_EXSTYLE, exStyle & ~WS_EX_NOACTIVATE);
// 设置 Topmost // 设置 Topmost
this.Topmost = 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 // 再加回 WS_EX_NOACTIVATE
exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_NOACTIVATE); SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_NOACTIVATE);
@@ -248,10 +265,25 @@ namespace Ink_Canvas {
catch { } catch { }
} }
// 新增:记录上一个模式
private InkCanvasEditingMode lastEditingMode = InkCanvasEditingMode.Ink;
private void inkCanvas_EditingModeChanged(object sender, RoutedEventArgs e) { private void inkCanvas_EditingModeChanged(object sender, RoutedEventArgs e) {
var inkCanvas1 = sender as InkCanvas; var inkCanvas1 = sender as InkCanvas;
if (inkCanvas1 == null) return; 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); SetCursorBasedOnEditingMode(inkCanvas1);
if (Settings.Canvas.IsShowCursor) { if (Settings.Canvas.IsShowCursor) {
@@ -262,31 +294,13 @@ namespace Ink_Canvas {
else else
inkCanvas1.ForceCursor = false; inkCanvas1.ForceCursor = false;
} else { } else {
// 套索选择模式下始终强制显示光标,即使用户设置不显示光标
if (inkCanvas1.EditingMode == InkCanvasEditingMode.Select) { if (inkCanvas1.EditingMode == InkCanvasEditingMode.Select) {
inkCanvas1.ForceCursor = true; inkCanvas1.ForceCursor = true;
} else { } else {
inkCanvas1.ForceCursor = false; inkCanvas1.ForceCursor = false;
} }
} }
if (inkCanvas1.EditingMode == InkCanvasEditingMode.Ink) forcePointEraser = !forcePointEraser; 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 #endregion Ink Canvas
@@ -297,11 +311,16 @@ namespace Ink_Canvas {
public static string settingsFileName = "Settings.json"; public static string settingsFileName = "Settings.json";
private bool isLoaded = false; private bool isLoaded = false;
private bool forcePointEraser = false; private bool forcePointEraser = false;
public static bool EnablePalmEraser = true;
private void Window_Loaded(object sender, RoutedEventArgs e) { private void Window_Loaded(object sender, RoutedEventArgs e) {
loadPenCanvas(); loadPenCanvas();
//加载设置 //加载设置
LoadSettings(true); LoadSettings(true);
// 同步手掌擦开关
EnablePalmEraser = Settings.Canvas.EnablePalmEraser;
if (ToggleSwitchEnablePalmEraser != null)
ToggleSwitchEnablePalmEraser.IsOn = EnablePalmEraser;
// 加载自定义背景颜色 // 加载自定义背景颜色
LoadCustomBackgroundColor(); LoadCustomBackgroundColor();
@@ -381,6 +400,14 @@ namespace Ink_Canvas {
// 初始化插件系统 // 初始化插件系统
InitializePluginSystem(); 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) { private void SystemEventsOnDisplaySettingsChanged(object sender, EventArgs e) {
@@ -535,7 +562,7 @@ namespace Ink_Canvas {
AvailableLatestLineGroup = null; AvailableLatestLineGroup = null;
// 使用当前选择的更新通道检查更新 // 使用当前选择的更新通道检查更新
var (remoteVersion, lineGroup) = await AutoUpdateHelper.CheckForUpdates(Settings.Startup.UpdateChannel); var (remoteVersion, lineGroup, apiReleaseNotes) = await AutoUpdateHelper.CheckForUpdates(Settings.Startup.UpdateChannel);
AvailableLatestVersion = remoteVersion; AvailableLatestVersion = remoteVersion;
AvailableLatestLineGroup = lineGroup; AvailableLatestLineGroup = lineGroup;
@@ -1312,5 +1339,25 @@ namespace Ink_Canvas {
// 直接调用PPT放映结束按钮的逻辑 // 直接调用PPT放映结束按钮的逻辑
BtnPPTSlideShowEnd_Click(BtnPPTSlideShowEnd, null); 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) => { overlay.MouseDown += (sender, e) => {
if (!MainWindow.EnablePalmEraser) return;
var inkCanvas = this.FindName("inkCanvas") as InkCanvas;
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint) { if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint) {
lastEditingMode = inkCanvas.EditingMode;
overlay.CaptureMouse(); overlay.CaptureMouse();
inkCanvas.EditingMode = InkCanvasEditingMode.None;
StartAdvancedEraserOperation(sender); StartAdvancedEraserOperation(sender);
} }
}; };
overlay.MouseUp += (sender, e) => { 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(); overlay.ReleaseMouseCapture();
EndAdvancedEraserOperation(sender); EndAdvancedEraserOperation(sender);
inkCanvas.EditingMode = InkCanvasEditingMode.Ink; // 抬手后自动回到画笔
} }
}; };
overlay.MouseMove += (sender, e) => { 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")); var position = e.GetPosition((UIElement)this.FindName("inkCanvas"));
Trace.WriteLine($"Advanced Eraser: Mouse move event triggered at ({position.X:F1}, {position.Y:F1})"); Trace.WriteLine($"Advanced Eraser: Mouse move event triggered at ({position.X:F1}, {position.Y:F1})");
UpdateAdvancedEraserPosition(sender, position); UpdateAdvancedEraserPosition(sender, position);
} else {
Trace.WriteLine($"Advanced Eraser: Mouse move ignored - not in eraser mode, current mode: {inkCanvas.EditingMode}");
} }
}; };
// 触控笔事件 // 触控笔事件
overlay.StylusDown += (sender, e) => { overlay.StylusDown += (sender, e) => {
if (!MainWindow.EnablePalmEraser) return;
var inkCanvas = this.FindName("inkCanvas") as InkCanvas;
if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint) { if (inkCanvas.EditingMode == InkCanvasEditingMode.EraseByPoint) {
e.Handled = true; e.Handled = true;
if (e.StylusDevice.TabletDevice.Type == TabletDeviceType.Stylus) { if (e.StylusDevice.TabletDevice.Type == TabletDeviceType.Stylus) {
overlay.CaptureStylus(); overlay.CaptureStylus();
} }
lastEditingMode = inkCanvas.EditingMode;
inkCanvas.EditingMode = InkCanvasEditingMode.None;
StartAdvancedEraserOperation(sender); StartAdvancedEraserOperation(sender);
} }
}; };
overlay.StylusUp += (sender, e) => { 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; e.Handled = true;
if (e.StylusDevice.TabletDevice.Type == TabletDeviceType.Stylus) { if (e.StylusDevice.TabletDevice.Type == TabletDeviceType.Stylus) {
overlay.ReleaseStylusCapture(); overlay.ReleaseStylusCapture();
} }
EndAdvancedEraserOperation(sender); EndAdvancedEraserOperation(sender);
inkCanvas.EditingMode = InkCanvasEditingMode.Ink; // 抬手后自动回到画笔
} }
}; };
overlay.StylusMove += (sender, e) => { 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; e.Handled = true;
var position = e.GetPosition((UIElement)this.FindName("inkCanvas")); var position = e.GetPosition((UIElement)this.FindName("inkCanvas"));
UpdateAdvancedEraserPosition(sender, position); UpdateAdvancedEraserPosition(sender, position);
+41 -3
View File
@@ -9,6 +9,9 @@ using System.IO;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Controls; using System.Windows.Controls;
using System.Net;
using System.Net.Sockets;
using System.Linq;
namespace Ink_Canvas { namespace Ink_Canvas {
public class TimeViewModel : INotifyPropertyChanged { public class TimeViewModel : INotifyPropertyChanged {
@@ -55,6 +58,35 @@ namespace Ink_Canvas {
private TimeViewModel nowTimeVM = new TimeViewModel(); 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() { private void InitTimers() {
timerCheckPPT.Elapsed += TimerCheckPPT_Elapsed; timerCheckPPT.Elapsed += TimerCheckPPT_Elapsed;
timerCheckPPT.Interval = 500; timerCheckPPT.Interval = 500;
@@ -66,7 +98,7 @@ namespace Ink_Canvas {
timerCheckAutoUpdateWithSilence.Interval = 1000 * 60 * 10; timerCheckAutoUpdateWithSilence.Interval = 1000 * 60 * 10;
WaterMarkTime.DataContext = nowTimeVM; WaterMarkTime.DataContext = nowTimeVM;
WaterMarkDate.DataContext = nowTimeVM; WaterMarkDate.DataContext = nowTimeVM;
timerDisplayTime.Elapsed += TimerDisplayTime_Elapsed; timerDisplayTime.Elapsed += async (s, e) => await TimerDisplayTime_ElapsedAsync();
timerDisplayTime.Interval = 1000; timerDisplayTime.Interval = 1000;
timerDisplayTime.Start(); timerDisplayTime.Start();
timerDisplayDate.Elapsed += TimerDisplayDate_Elapsed; timerDisplayDate.Elapsed += TimerDisplayDate_Elapsed;
@@ -77,8 +109,14 @@ namespace Ink_Canvas {
nowTimeVM.nowTime = DateTime.Now.ToShortTimeString().ToString(); nowTimeVM.nowTime = DateTime.Now.ToShortTimeString().ToString();
} }
private void TimerDisplayTime_Elapsed(object sender, ElapsedEventArgs e) { private async Task TimerDisplayTime_ElapsedAsync()
nowTimeVM.nowTime = DateTime.Now.ToShortTimeString().ToString(); {
DateTime now = await GetNetworkTimeAsync();
// 只更新时间,日期由原有逻辑定时更新即可
Dispatcher.Invoke(() =>
{
nowTimeVM.nowTime = now.ToShortTimeString();
});
} }
private void TimerDisplayDate_Elapsed(object sender, ElapsedEventArgs e) { 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 // You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.7.1.10")] [assembly: AssemblyVersion("1.7.1.12")]
[assembly: AssemblyFileVersion("1.7.1.10")] [assembly: AssemblyFileVersion("1.7.1.12")]
+2
View File
@@ -77,6 +77,8 @@ namespace Ink_Canvas
public OptionalOperation HyperbolaAsymptoteOption { get; set; } = OptionalOperation.Ask; public OptionalOperation HyperbolaAsymptoteOption { get; set; } = OptionalOperation.Ask;
[JsonProperty("isCompressPicturesUploaded")] [JsonProperty("isCompressPicturesUploaded")]
public bool IsCompressPicturesUploaded { get; set; } = false; public bool IsCompressPicturesUploaded { get; set; } = false;
[JsonProperty("enablePalmEraser")]
public bool EnablePalmEraser { get; set; } = true;
} }
public enum OptionalOperation public enum OptionalOperation
@@ -78,6 +78,12 @@
Background="#f8fafc" BorderBrush="#cbd5e1" ToolTip="跳过此版本更新" Visibility="Visible" IsEnabled="True"/> Background="#f8fafc" BorderBrush="#cbd5e1" ToolTip="跳过此版本更新" Visibility="Visible" IsEnabled="True"/>
</ui:SimpleStackPanel> </ui:SimpleStackPanel>
</Border> </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> </ui:SimpleStackPanel>
</Grid> </Grid>
</ScrollViewer> </ScrollViewer>
+68 -4
View File
@@ -7,6 +7,9 @@ using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Interop; using System.Windows.Interop;
using System.Windows.Media; using System.Windows.Media;
using System.Threading.Tasks;
using System.IO;
using System.Threading;
namespace Ink_Canvas namespace Ink_Canvas
{ {
@@ -129,16 +132,59 @@ namespace Ink_Canvas
LogHelper.WriteLogToFile("AutoUpdate | Update dialog buttons visibility ensured"); 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"); 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; Result = UpdateResult.UpdateNow;
// 关闭窗口,返回到MainWindow处理后续下载和安装流程
DialogResult = true; DialogResult = true;
Close(); Close();
}
else
{
DownloadProgressText.Text = "下载失败,请检查网络后重试。";
UpdateNowButton.IsEnabled = true;
UpdateLaterButton.IsEnabled = true;
SkipVersionButton.IsEnabled = true;
}
} }
private void UpdateLaterButton_Click(object sender, RoutedEventArgs e) 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 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 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 E:\ICC CE\ICC CE main\community\Ink Canvas\App.xaml
21348134359 22-2143008179
752071346691 76-141727233
471037513499 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 False