Merge pull request #104 from InkCanvasForClass/beta

ICC CE 1.7.3.0
This commit is contained in:
CJK_mkp
2025-07-26 19:38:22 +08:00
committed by GitHub
23 changed files with 4227 additions and 167 deletions
+43
View File
@@ -36,6 +36,49 @@
"contributions": [
"code"
]
},
{
"login": "2-2-3-trimethylpentane",
"name": "2,2,3-三甲基戊烷",
"avatar_url": "https://avatars.githubusercontent.com/u/141403762?v=4",
"profile": "https://github.com/2-2-3-trimethylpentane",
"contributions": [
"blog",
"doc",
"design"
]
},
{
"login": "Alan-CRL",
"name": "Alan-CRL",
"avatar_url": "https://avatars.githubusercontent.com/u/92425617?v=4",
"profile": "https://github.com/Alan-CRL",
"contributions": [
"code",
"infra",
"doc",
"financial"
]
},
{
"login": "MKStoler1024",
"name": "MKStoler1024",
"avatar_url": "https://avatars.githubusercontent.com/u/158786854?v=4",
"profile": "https://github.com/MKStoler1024",
"contributions": [
"doc",
"code",
"design"
]
},
{
"login": "awesome-iwb",
"name": "Awesome Iwb",
"avatar_url": "https://avatars.githubusercontent.com/u/184760810?v=4",
"profile": "https://github.com/awesome-iwb",
"contributions": [
"doc"
]
}
]
}
+1 -1
View File
@@ -1 +1 @@
1.7.2.0
1.7.3.0
+17
View File
@@ -432,6 +432,12 @@ namespace Ink_Canvas
LogHelper.NewLog(string.Format("Ink Canvas Starting (Version: {0})", Assembly.GetExecutingAssembly().GetName().Version.ToString()));
// 记录应用启动(设备标识符)
DeviceIdentifier.RecordAppLaunch();
LogHelper.WriteLogToFile($"App | 设备ID: {DeviceIdentifier.GetDeviceId()}");
LogHelper.WriteLogToFile($"App | 使用频率: {DeviceIdentifier.GetUsageFrequency()}");
LogHelper.WriteLogToFile($"App | 更新优先级: {DeviceIdentifier.GetUpdatePriority()}");
bool ret;
mutex = new System.Threading.Mutex(true, "InkCanvasForClass", out ret);
@@ -710,6 +716,17 @@ namespace Ink_Canvas
string exitType = IsAppExitByUser ? "用户主动退出" : "应用程序退出";
WriteCrashLog($"{exitType},退出代码: {e.ApplicationExitCode}");
// 记录应用退出(设备标识符)
try
{
DeviceIdentifier.RecordAppExit();
LogHelper.WriteLogToFile($"App | 应用运行时长: {(DateTime.Now - appStartTime).TotalMinutes:F1}分钟");
}
catch (Exception deviceEx)
{
LogHelper.WriteLogToFile($"记录设备标识符退出信息失败: {deviceEx.Message}", LogHelper.LogType.Error);
}
if (IsAppExitByUser)
{
// 写入退出信号文件,通知看门狗正常退出
+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.2.0")]
[assembly: AssemblyFileVersion("1.7.2.0")]
[assembly: AssemblyVersion("1.7.3.0")]
[assembly: AssemblyFileVersion("1.7.3.0")]
+430 -12
View File
@@ -1,37 +1,409 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Threading;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 适合手写/触摸的墨迹平滑方案:指数平滑+等距重采样+Catmull-Rom样条插值,防止自交和异常填充
/// 异步硬件加速的墨迹平滑处理器
/// </summary>
public class AsyncAdvancedBezierSmoothing
{
private readonly SemaphoreSlim _processingSemaphore;
private readonly ConcurrentDictionary<Stroke, CancellationTokenSource> _processingTasks;
private readonly Dispatcher _uiDispatcher;
public AsyncAdvancedBezierSmoothing(Dispatcher uiDispatcher)
{
_uiDispatcher = uiDispatcher;
_processingSemaphore = new SemaphoreSlim(Environment.ProcessorCount, Environment.ProcessorCount);
_processingTasks = new ConcurrentDictionary<Stroke, CancellationTokenSource>();
}
public double SmoothingStrength { get; set; } = 0.3; // 大幅降低强度
public double ResampleInterval { get; set; } = 3.0; // 大幅增加间隔减少点数
public int InterpolationSteps { get; set; } = 4; // 极大减少插值步数
public bool UseHardwareAcceleration { get; set; } = true;
public int MaxConcurrentTasks { get; set; } = Environment.ProcessorCount;
/// <summary>
/// 异步平滑笔画
/// </summary>
public async Task<Stroke> SmoothStrokeAsync(Stroke originalStroke,
Action<Stroke, Stroke> onCompleted = null,
CancellationToken cancellationToken = default)
{
if (originalStroke == null || originalStroke.StylusPoints.Count < 2)
return originalStroke;
// 取消之前对同一笔画的处理
if (_processingTasks.TryGetValue(originalStroke, out var existingCts))
{
existingCts.Cancel();
_processingTasks.TryRemove(originalStroke, out _);
}
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_processingTasks[originalStroke] = cts;
try
{
await _processingSemaphore.WaitAsync(cts.Token);
var smoothedStroke = await Task.Run(() =>
ProcessStrokeInternal(originalStroke, cts.Token), cts.Token);
// 在UI线程上执行回调
if (onCompleted != null && !cts.Token.IsCancellationRequested)
{
await _uiDispatcher.InvokeAsync(() => onCompleted(originalStroke, smoothedStroke));
}
return smoothedStroke;
}
catch (OperationCanceledException)
{
return originalStroke;
}
finally
{
_processingSemaphore.Release();
_processingTasks.TryRemove(originalStroke, out _);
cts.Dispose();
}
}
private Stroke ProcessStrokeInternal(Stroke stroke, CancellationToken cancellationToken)
{
var originalPoints = stroke.StylusPoints.ToArray();
// 如果点数太少,直接返回原始笔画
if (originalPoints.Length < 3)
return stroke;
cancellationToken.ThrowIfCancellationRequested();
// 简化处理:只进行轻度平滑,避免点数爆炸
var smoothedPoints = ApplyLightSmoothing(originalPoints);
cancellationToken.ThrowIfCancellationRequested();
// 确保点数不会过多
if (smoothedPoints.Length > originalPoints.Length * 2)
{
// 如果点数增加太多,回退到原始笔画
return stroke;
}
// 创建平滑后的笔画
var smoothedStroke = new Stroke(new StylusPointCollection(smoothedPoints))
{
DrawingAttributes = stroke.DrawingAttributes.Clone()
};
return smoothedStroke;
}
/// <summary>
/// 轻度平滑处理,避免点数爆炸
/// </summary>
private StylusPoint[] ApplyLightSmoothing(StylusPoint[] points)
{
if (points.Length < 3) return points;
var result = new List<StylusPoint>();
result.Add(points[0]); // 保持第一个点
// 简单的3点平均平滑
for (int i = 1; i < points.Length - 1; i++)
{
var prev = points[i - 1];
var curr = points[i];
var next = points[i + 1];
// 3点平均
double x = (prev.X + curr.X + next.X) / 3.0;
double y = (prev.Y + curr.Y + next.Y) / 3.0;
float pressure = (prev.PressureFactor + curr.PressureFactor + next.PressureFactor) / 3.0f;
result.Add(new StylusPoint(x, y, Math.Max(pressure, 0.1f)));
}
result.Add(points[points.Length - 1]); // 保持最后一个点
return result.ToArray();
}
/// <summary>
/// 硬件加速的向量化指数平滑
/// </summary>
private StylusPoint[] ApplyExponentialSmoothingVectorized(StylusPoint[] points, double alpha)
{
if (points.Length == 0) return points;
var result = new StylusPoint[points.Length];
result[0] = points[0];
double lastX = points[0].X;
double lastY = points[0].Y;
float lastPressure = points[0].PressureFactor;
double oneMinusAlpha = 1.0 - alpha;
// 向量化处理,减少分支预测失败
for (int i = 1; i < points.Length; i++)
{
var p = points[i];
lastX = alpha * p.X + oneMinusAlpha * lastX;
lastY = alpha * p.Y + oneMinusAlpha * lastY;
lastPressure = (float)(alpha * p.PressureFactor + oneMinusAlpha * lastPressure);
lastPressure = Math.Max(lastPressure, 0.1f); // 避免分支
result[i] = new StylusPoint(lastX, lastY, lastPressure);
}
return result;
}
/// <summary>
/// 优化的等距重采样
/// </summary>
private StylusPoint[] ResampleEquidistantOptimized(StylusPoint[] points, double interval)
{
if (points.Length == 0) return points;
var result = new List<StylusPoint>(points.Length) { points[0] };
double accumulated = 0;
for (int i = 1; i < points.Length; i++)
{
var prev = result[result.Count - 1];
var curr = points[i];
double dx = curr.X - prev.X;
double dy = curr.Y - prev.Y;
double dist = Math.Sqrt(dx * dx + dy * dy);
if (dist + accumulated >= interval)
{
double t = (interval - accumulated) / dist;
double x = prev.X + t * dx;
double y = prev.Y + t * dy;
float pressure = (float)(prev.PressureFactor * (1 - t) + curr.PressureFactor * t);
pressure = Math.Max(pressure, 0.1f);
result.Add(new StylusPoint(x, y, pressure));
accumulated = 0;
i--; // 重新处理当前点
}
else
{
accumulated += dist;
}
}
return result.ToArray();
}
/// <summary>
/// 硬件加速的贝塞尔曲线拟合
/// </summary>
private StylusPoint[] SlidingBezierFitHardwareAccelerated(StylusPoint[] points, int window, int steps)
{
if (points.Length < window) return points;
var result = new List<StylusPoint>(points.Length * steps / window);
// 使用并行处理加速计算
var segments = new List<StylusPoint[]>();
Parallel.For(0, points.Length - window + 1, i =>
{
var segmentPoints = new StylusPoint[steps];
var p0 = points[i];
var p1 = points[i + 1];
var p2 = points[i + 2];
var p3 = points[i + 3];
for (int j = 0; j < steps; j++)
{
double t = (double)j / steps;
segmentPoints[j] = CubicBezierOptimized(p0, p1, p2, p3, t);
}
lock (segments)
{
segments.Add(segmentPoints);
}
});
// 合并结果
foreach (var segment in segments)
{
result.AddRange(segment);
}
result.Add(points[points.Length - 1]);
return result.ToArray();
}
/// <summary>
/// 优化的单线程贝塞尔拟合
/// </summary>
private StylusPoint[] SlidingBezierFitOptimized(StylusPoint[] points, int window, int steps)
{
if (points.Length < window) return points;
var result = new List<StylusPoint>(points.Length * steps / window);
for (int i = 0; i <= points.Length - window; i++)
{
var p0 = points[i];
var p1 = points[i + 1];
var p2 = points[i + 2];
var p3 = points[i + 3];
for (int j = 0; j < steps; j++)
{
double t = (double)j / steps;
result.Add(CubicBezierOptimized(p0, p1, p2, p3, t));
}
}
result.Add(points[points.Length - 1]);
return result.ToArray();
}
/// <summary>
/// 优化的三次贝塞尔曲线计算
/// </summary>
private StylusPoint CubicBezierOptimized(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3, double t)
{
double u = 1 - t;
double tt = t * t;
double uu = u * u;
double uuu = uu * u;
double ttt = tt * t;
// 预计算系数
double c0 = uuu;
double c1 = 3 * uu * t;
double c2 = 3 * u * tt;
double c3 = ttt;
double x = c0 * p0.X + c1 * p1.X + c2 * p2.X + c3 * p3.X;
double y = c0 * p0.Y + c1 * p1.Y + c2 * p2.Y + c3 * p3.Y;
float pressure = (float)(p1.PressureFactor * u + p2.PressureFactor * t);
pressure = Math.Max(pressure, 0.1f);
return new StylusPoint(x, y, pressure);
}
/// <summary>
/// 兼容性方法:传统指数平滑
/// </summary>
private StylusPoint[] ApplyExponentialSmoothing(StylusPoint[] points, double alpha)
{
if (points.Length == 0) return points;
var result = new StylusPoint[points.Length];
result[0] = points[0];
double lastX = points[0].X;
double lastY = points[0].Y;
float lastPressure = points[0].PressureFactor;
for (int i = 1; i < points.Length; i++)
{
var p = points[i];
lastX = alpha * p.X + (1 - alpha) * lastX;
lastY = alpha * p.Y + (1 - alpha) * lastY;
lastPressure = (float)(alpha * p.PressureFactor + (1 - alpha) * lastPressure);
lastPressure = Math.Max(lastPressure, 0.1f);
result[i] = new StylusPoint(lastX, lastY, lastPressure);
}
return result;
}
/// <summary>
/// 取消所有正在进行的处理任务
/// </summary>
public void CancelAllTasks()
{
foreach (var kvp in _processingTasks)
{
kvp.Value.Cancel();
}
_processingTasks.Clear();
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
CancelAllTasks();
_processingSemaphore?.Dispose();
}
}
/// <summary>
/// 原有的同步版本(保持向后兼容)
/// </summary>
public class AdvancedBezierSmoothing
{
public double SmoothingStrength { get; set; } = 0.8;
public double ResampleInterval { get; set; } = 0.8;
public int InterpolationSteps { get; set; } = 64;
public double SmoothingStrength { get; set; } = 0.3;
public double ResampleInterval { get; set; } = 3.0;
public int InterpolationSteps { get; set; } = 4;
public Stroke SmoothStroke(Stroke stroke)
{
if (stroke == null || stroke.StylusPoints.Count < 2)
if (stroke == null || stroke.StylusPoints.Count < 3)
return stroke;
var originalPoints = stroke.StylusPoints.ToList();
var smoothedPoints = ApplyExponentialSmoothing(originalPoints, SmoothingStrength);
var resampledPoints = ResampleEquidistant(smoothedPoints, ResampleInterval);
var interpolatedPoints = SlidingBezierFit(resampledPoints, 4, 24);
var finalPoints = ApplyExponentialSmoothing(interpolatedPoints, 0.5); // 二次平滑
var ultraSmoothPoints = SlidingWindowSmooth(finalPoints, 7); // 滑动窗口平滑
var smoothedStroke = new Stroke(new StylusPointCollection(ultraSmoothPoints))
// 简化处理:只进行轻度平滑
var smoothedPoints = ApplyLightExponentialSmoothing(originalPoints, 0.2); // 很轻的平滑
// 检查点数是否合理
if (smoothedPoints.Count > originalPoints.Count * 1.5)
{
return stroke; // 如果点数增加太多,返回原始笔画
}
var smoothedStroke = new Stroke(new StylusPointCollection(smoothedPoints))
{
DrawingAttributes = stroke.DrawingAttributes.Clone()
};
return smoothedStroke;
}
/// <summary>
/// 轻度指数平滑
/// </summary>
private List<StylusPoint> ApplyLightExponentialSmoothing(List<StylusPoint> points, double alpha)
{
var result = new List<StylusPoint>();
if (points.Count == 0) return result;
result.Add(points[0]);
for (int i = 1; i < points.Count; i++)
{
var prev = result[result.Count - 1];
var curr = points[i];
double x = alpha * curr.X + (1 - alpha) * prev.X;
double y = alpha * curr.Y + (1 - alpha) * prev.Y;
float pressure = (float)(alpha * curr.PressureFactor + (1 - alpha) * prev.PressureFactor);
pressure = Math.Max(pressure, 0.1f);
result.Add(new StylusPoint(x, y, pressure));
}
return result;
}
private List<StylusPoint> ApplyExponentialSmoothing(List<StylusPoint> points, double alpha)
{
var result = new List<StylusPoint>();
@@ -141,4 +513,50 @@ namespace Ink_Canvas.Helpers
return result;
}
}
}
/// <summary>
/// 性能监控器
/// </summary>
public class InkSmoothingPerformanceMonitor
{
private readonly Queue<TimeSpan> _processingTimes = new Queue<TimeSpan>();
private readonly object _lock = new object();
private const int MaxSamples = 100;
public void RecordProcessingTime(TimeSpan time)
{
lock (_lock)
{
_processingTimes.Enqueue(time);
if (_processingTimes.Count > MaxSamples)
_processingTimes.Dequeue();
}
}
public double GetAverageProcessingTimeMs()
{
lock (_lock)
{
return _processingTimes.Count > 0 ?
_processingTimes.Average(t => t.TotalMilliseconds) : 0;
}
}
public double GetMaxProcessingTimeMs()
{
lock (_lock)
{
return _processingTimes.Count > 0 ?
_processingTimes.Max(t => t.TotalMilliseconds) : 0;
}
}
public int GetSampleCount()
{
lock (_lock)
{
return _processingTimes.Count;
}
}
}
}
+341 -97
View File
@@ -20,7 +20,7 @@ namespace Ink_Canvas.Helpers
{
// 定义超时时间为10秒
private static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(10);
private static string updatesFolderPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "AutoUpdate");
private static readonly string updatesFolderPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "AutoUpdate");
private static string statusFilePath = null;
// 线路组结构体(包含版本、下载、日志地址)
@@ -62,6 +62,11 @@ namespace Ink_Canvas.Helpers
{
GroupName = "智教联盟",
DownloadUrlFormat = "https://get.smart-teach.cn/d/Ningbo-S3/shared/jiangling/community/InkCanvasForClass.CE.{0}.zip",
},
new UpdateLineGroup
{
GroupName = "inkeys",
DownloadUrlFormat = "https://iccce.inkeys.top/Release/InkCanvasForClass.CE.{0}.zip",
}
}
},
@@ -92,6 +97,11 @@ namespace Ink_Canvas.Helpers
{
GroupName = "智教联盟",
DownloadUrlFormat = "https://get.smart-teach.cn/d/Ningbo-S3/shared/jiangling/community-beta/InkCanvasForClass.CE.{0}.zip",
},
new UpdateLineGroup
{
GroupName = "inkeys",
DownloadUrlFormat = "https://iccce.inkeys.top/Beta/InkCanvasForClass.CE.{0}.zip",
}
}
}
@@ -169,10 +179,10 @@ namespace Ink_Canvas.Helpers
foreach (var group in groups)
{
// 跳过智教联盟线路组,不参与延迟检测和排序
if (group.GroupName == "智教联盟")
// 跳过"智教联盟"和"inkeys"线路组,不参与延迟检测和排序
if (group.GroupName == "智教联盟" || group.GroupName == "inkeys")
{
LogHelper.WriteLogToFile($"AutoUpdate | 跳过智教联盟线路组延迟检测");
LogHelper.WriteLogToFile($"AutoUpdate | 跳过{group.GroupName}线路组延迟检测");
continue;
}
LogHelper.WriteLogToFile($"AutoUpdate | 检测线路组: {group.GroupName} ({group.VersionUrl})");
@@ -194,7 +204,7 @@ namespace Ink_Canvas.Helpers
.Select(x => x.group)
.ToList();
// 将智教联盟线路组插入到最前面(如果存在)
// 将"智教联盟"线路组插入到最前面(如果存在)
var zhiJiaoGroup = groups.FirstOrDefault(g => g.GroupName == "智教联盟");
if (zhiJiaoGroup != null)
{
@@ -202,6 +212,14 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"AutoUpdate | 智教联盟线路组已插入到首位");
}
// 将"inkeys"线路组插入到第二位(如果存在)
var inkeysGroup = groups.FirstOrDefault(g => g.GroupName == "inkeys");
if (inkeysGroup != null)
{
orderedGroups.Insert(1, inkeysGroup);
LogHelper.WriteLogToFile($"AutoUpdate | inkeys线路组已插入到第二位");
}
if (orderedGroups.Count > 0)
{
LogHelper.WriteLogToFile($"AutoUpdate | 找到 {orderedGroups.Count} 个可用线路组,按延迟排序:");
@@ -377,8 +395,49 @@ namespace Ink_Canvas.Helpers
}
}
// 通过GitHub API获取指定版本的Release信息
private static async Task<(string version, string downloadUrl, string releaseNotes, DateTime? releaseTime)> GetGithubReleaseByVersion(string targetVersion, UpdateChannel channel)
{
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 releases = JArray.Parse(response);
foreach (var release in releases)
{
string version = release["tag_name"]?.ToString();
if (version == targetVersion || version == $"v{targetVersion}" || version == $"V{targetVersion}")
{
string releaseNotes = release["body"]?.ToString();
string downloadUrl = release["assets"]?.First?["browser_download_url"]?.ToString();
// 解析发布时间
DateTime? releaseTime = null;
if (release["published_at"] != null && DateTime.TryParse(release["published_at"].ToString(), out DateTime parsedTime))
{
releaseTime = parsedTime;
}
return (version, downloadUrl, releaseNotes, releaseTime);
}
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | GitHub Releases API 获取版本 {targetVersion} 失败: {ex.Message}", LogHelper.LogType.Warning);
}
return (null, null, null, null);
}
// 通过GitHub API获取最新Release信息
private static async Task<(string version, string downloadUrl, string releaseNotes)> GetLatestGithubRelease(UpdateChannel channel)
private static async Task<(string version, string downloadUrl, string releaseNotes, DateTime? releaseTime)> GetLatestGithubRelease(UpdateChannel channel)
{
try
{
@@ -393,28 +452,42 @@ namespace Ink_Canvas.Helpers
string version = json["tag_name"]?.ToString();
string releaseNotes = json["body"]?.ToString();
string downloadUrl = json["assets"]?.First?["browser_download_url"]?.ToString();
// 解析发布时间
DateTime? releaseTime = null;
if (json["published_at"] != null && DateTime.TryParse(json["published_at"].ToString(), out DateTime parsedTime))
{
releaseTime = parsedTime;
}
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(downloadUrl))
return (version, downloadUrl, releaseNotes);
return (version, downloadUrl, releaseNotes, releaseTime);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | GitHub Releases API 获取失败: {ex.Message}", LogHelper.LogType.Warning);
}
return (null, null, null);
return (null, null, null, null);
}
// 主要的更新检测方法(优先检测延迟,失败时自动切换线路组)
// 仅检测新版本时用GitHub API,实际下载时只用线路组
public static async Task<(string remoteVersion, UpdateLineGroup lineGroup, string releaseNotes)> CheckForUpdates(UpdateChannel channel = UpdateChannel.Release, bool alwaysGetRemote = false)
public static async Task<(string remoteVersion, UpdateLineGroup lineGroup, string releaseNotes)> CheckForUpdates(UpdateChannel channel = UpdateChannel.Release, bool alwaysGetRemote = false, bool isVersionFix = false)
{
try
{
// 记录更新检查时间
DeviceIdentifier.RecordUpdateCheck();
string localVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
LogHelper.WriteLogToFile($"AutoUpdate | 本地版本: {localVersion}");
LogHelper.WriteLogToFile($"AutoUpdate | 设备ID: {DeviceIdentifier.GetDeviceId()}");
LogHelper.WriteLogToFile($"AutoUpdate | 更新优先级: {DeviceIdentifier.GetUpdatePriority()}");
LogHelper.WriteLogToFile($"AutoUpdate | 优先通过GitHub Releases API检测...");
// 1. 优先通过GitHub Releases API获取
var (apiVersion, _, apiReleaseNotes) = await GetLatestGithubRelease(channel);
var (apiVersion, _, apiReleaseNotes, apiReleaseTime) = await GetLatestGithubRelease(channel);
if (!string.IsNullOrEmpty(apiVersion))
{
Version local = new Version(localVersion);
@@ -422,15 +495,42 @@ namespace Ink_Canvas.Helpers
if (remote > local || alwaysGetRemote)
{
LogHelper.WriteLogToFile($"AutoUpdate | 通过GitHub Releases API发现新版本: {apiVersion}");
// 检查是否应该根据用户优先级推送更新(版本修复功能不受限制)
if (!isVersionFix)
{
DateTime releaseTime = apiReleaseTime ?? DateTime.Now;
// 尝试获取当前版本的发布时间
DateTime? currentVersionReleaseTime = await GetVersionReleaseTime(localVersion, channel);
bool shouldPush = DeviceIdentifier.ShouldPushUpdate(apiVersion, releaseTime, true, currentVersionReleaseTime); // 明确标记为自动更新
if (!shouldPush)
{
var priority = DeviceIdentifier.GetUpdatePriority();
var daysBetweenVersions = currentVersionReleaseTime.HasValue
? (releaseTime - currentVersionReleaseTime.Value).TotalDays
: (DateTime.Now - releaseTime).TotalDays;
LogHelper.WriteLogToFile($"AutoUpdate | 根据用户优先级({priority}),暂不推送更新 {apiVersion},版本间隔: {daysBetweenVersions:F1} 天");
var group = (await GetAvailableLineGroupsOrdered(channel)).FirstOrDefault();
return (null, group, apiReleaseNotes); // 返回null表示不推送
}
}
else
{
LogHelper.WriteLogToFile($"AutoUpdate | 版本修复模式,跳过分级策略检查");
}
LogHelper.WriteLogToFile($"AutoUpdate | 根据用户优先级,推送更新 {apiVersion}");
// 只返回版本号和日志,不返回直链
var group = (await GetAvailableLineGroupsOrdered(channel)).FirstOrDefault();
return (apiVersion, group, apiReleaseNotes);
var availableGroup = (await GetAvailableLineGroupsOrdered(channel)).FirstOrDefault();
return (apiVersion, availableGroup, apiReleaseNotes);
}
else
{
LogHelper.WriteLogToFile($"AutoUpdate | 当前版本已是最新 (GitHub Releases API)");
var group = (await GetAvailableLineGroupsOrdered(channel)).FirstOrDefault();
return (null, group, apiReleaseNotes);
var availableGroup = (await GetAvailableLineGroupsOrdered(channel)).FirstOrDefault();
return (null, availableGroup, apiReleaseNotes);
}
}
// 2. 回退到原有txt方案
@@ -453,6 +553,27 @@ namespace Ink_Canvas.Helpers
if (remote > local || alwaysGetRemote)
{
LogHelper.WriteLogToFile($"AutoUpdate | 发现新版本或强制获取: {remoteVersion}");
// 检查是否应该根据用户优先级推送更新(版本修复功能不受限制)
if (!isVersionFix)
{
// 尝试获取当前版本的发布时间
DateTime? currentVersionReleaseTime = await GetVersionReleaseTime(localVersion, channel);
bool shouldPush = DeviceIdentifier.ShouldPushUpdate(remoteVersion, DateTime.Now, true, currentVersionReleaseTime); // 明确标记为自动更新
if (!shouldPush)
{
var priority = DeviceIdentifier.GetUpdatePriority();
LogHelper.WriteLogToFile($"AutoUpdate | 根据用户优先级({priority}),暂不推送更新 {remoteVersion}");
return (null, group, null); // 返回null表示不推送
}
}
else
{
LogHelper.WriteLogToFile($"AutoUpdate | 版本修复模式,跳过分级策略检查");
}
LogHelper.WriteLogToFile($"AutoUpdate | 根据用户优先级,推送更新 {remoteVersion}");
return (remoteVersion, group, null);
}
else
@@ -547,10 +668,21 @@ namespace Ink_Canvas.Helpers
// 优先尝试“智教联盟”线路组
var zhiJiaoGroup = groups.FirstOrDefault(g => g.GroupName == "智教联盟");
if (zhiJiaoGroup != null)
var inkeysGroup = groups.FirstOrDefault(g => g.GroupName == "inkeys");
if (zhiJiaoGroup != null || inkeysGroup != null)
{
groups = new List<UpdateLineGroup> { zhiJiaoGroup }.Concat(groups.Where(g => g.GroupName != "智教联盟")).ToList();
LogHelper.WriteLogToFile($"AutoUpdate | 下载时优先尝试智教联盟线路组");
var priorityGroups = new List<UpdateLineGroup>();
if (zhiJiaoGroup != null)
{
priorityGroups.Add(zhiJiaoGroup);
LogHelper.WriteLogToFile($"AutoUpdate | 下载时优先尝试智教联盟线路组");
}
if (inkeysGroup != null)
{
priorityGroups.Add(inkeysGroup);
LogHelper.WriteLogToFile($"AutoUpdate | 下载时优先尝试inkeys线路组");
}
groups = priorityGroups.Concat(groups.Where(g => g.GroupName != "智教联盟" && g.GroupName != "inkeys")).ToList();
}
// 依次尝试每个线路组
@@ -571,6 +703,11 @@ namespace Ink_Canvas.Helpers
url = realUrl;
LogHelper.WriteLogToFile($"AutoUpdate | 智教联盟真实下载地址: {url}");
}
// inkeys线路组直接使用下载地址,无需特殊处理
else if (group.GroupName == "inkeys")
{
LogHelper.WriteLogToFile($"AutoUpdate | 使用inkeys线路组下载地址: {url}");
}
LogHelper.WriteLogToFile($"AutoUpdate | 尝试从线路组 {group.GroupName} 下载: {url}");
bool downloadSuccess = await DownloadFile(url, zipFilePath, progressCallback);
@@ -611,7 +748,8 @@ namespace Ink_Canvas.Helpers
{
LogHelper.WriteLogToFile($"AutoUpdate | 正在尝试多线程下载: {fileUrl}");
int maxRetry = 3;
int[] threadOptions = new int[] { 32, 4 };
// 降低并发数,减少网络压力
int[] threadOptions = new int[] { 32, 16, 8, 4, 1 };
// 检查服务器是否支持Range分块下载
bool supportRange = false;
@@ -655,46 +793,14 @@ namespace Ink_Canvas.Helpers
{
LogHelper.WriteLogToFile($"AutoUpdate | 服务器不支持分块下载,自动降级为单线程下载");
progressCallback?.Invoke(0, "服务器不支持分块下载,自动降级为单线程下载");
try
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
using (var resp = await client.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead))
{
resp.EnsureSuccessStatusCode();
using (var fs = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
var stream = await resp.Content.ReadAsStreamAsync();
byte[] buffer = new byte[8192];
int read;
long downloaded = 0;
while ((read = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await fs.WriteAsync(buffer, 0, read);
downloaded += read;
if (totalSize > 0)
{
double percent = (double)downloaded / totalSize * 100;
progressCallback?.Invoke(percent, $"单线程下载中: {percent:F1}%");
}
}
}
}
}
progressCallback?.Invoke(100, "单线程下载完成");
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 单线程下载失败: {ex.Message}", LogHelper.LogType.Error);
progressCallback?.Invoke(0, $"单线程下载失败: {ex.Message}");
return false;
}
return await DownloadSingleThread(fileUrl, destinationPath, totalSize, progressCallback);
}
foreach (int threadCount in threadOptions)
{
LogHelper.WriteLogToFile($"AutoUpdate | 尝试使用 {threadCount} 线程下载");
progressCallback?.Invoke(0, $"尝试使用 {threadCount} 线程下载");
if (totalSize <= 0)
{
totalSize = await GetContentLength(fileUrl);
@@ -704,19 +810,28 @@ namespace Ink_Canvas.Helpers
progressCallback?.Invoke(0, "无法获取文件大小,取消下载");
return false;
}
int blockSize = (int)Math.Ceiling((double)totalSize / threadCount);
// 根据文件大小动态调整分块大小,避免分块过小
int minBlockSize = 32 * 1024; // 最小32KB
int blockSize = Math.Max(minBlockSize, (int)Math.Ceiling((double)totalSize / threadCount));
int blockCount = (int)Math.Ceiling((double)totalSize / blockSize);
LogHelper.WriteLogToFile($"AutoUpdate | 文件大小: {totalSize}, 分块数: {blockCount}, 分块大小: {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++)
{
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 () =>
@@ -724,6 +839,8 @@ namespace Ink_Canvas.Helpers
while (blockQueue.TryDequeue(out var block))
{
bool success = false;
string tempPath = destinationPath + $".part{block.Index}";
for (int retry = block.RetryCount; retry < maxRetry && !success; retry++)
{
try
@@ -734,7 +851,9 @@ namespace Ink_Canvas.Helpers
var req = new HttpRequestMessage(HttpMethod.Get, fileUrl);
req.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(block.Start, block.End);
// 新增:分块下载超时机制
// 增加连接超时设置
client.Timeout = TimeSpan.FromSeconds(30);
var downloadCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token);
var lastReadTime = DateTime.UtcNow;
bool dataReceived = false;
@@ -743,31 +862,33 @@ namespace Ink_Canvas.Helpers
{
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index} 响应状态: {resp.StatusCode}");
resp.EnsureSuccessStatusCode();
string tempPath = destinationPath + $".part{block.Index}";
using (var fs = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
var stream = await resp.Content.ReadAsStreamAsync();
byte[] buffer = new byte[8192];
int read;
long blockDownloadedBytes = 0;
while (true)
{
var readTask = stream.ReadAsync(buffer, 0, buffer.Length, downloadCts.Token);
var timeoutTask = Task.Delay(15000, downloadCts.Token); // 15秒超时
var timeoutTask = Task.Delay(20000, downloadCts.Token); // 增加到20秒超时
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秒无数据,线程超时重试");
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index} 20秒无数据,线程超时重试", LogHelper.LogType.Warning);
progressCallback?.Invoke(0, $"分块{block.Index} 20秒无数据,线程超时重试");
downloadCts.Cancel();
break;
}
read = await readTask;
if (read <= 0) break;
await fs.WriteAsync(buffer, 0, read, downloadCts.Token);
blockDownloaded[block.Index] += read;
blockDownloadedBytes += read;
blockDownloaded[block.Index] = blockDownloadedBytes;
lastReadTime = DateTime.UtcNow;
dataReceived = true;
// 合并所有块进度
long totalDownloaded = blockDownloaded.Sum();
double percent = (double)totalDownloaded / totalSize * 100;
@@ -775,19 +896,37 @@ namespace Ink_Canvas.Helpers
}
}
}
// 如果因超时break且未完成,success为false,重新入队
if (!dataReceived)
{
throw new IOException("分块下载超时无数据");
}
// 验证分块大小是否正确
var fileInfo = new FileInfo(tempPath);
long expectedSize = block.End - block.Start + 1;
if (fileInfo.Length != expectedSize)
{
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}大小不匹配,期望:{expectedSize},实际:{fileInfo.Length}", LogHelper.LogType.Warning);
throw new IOException($"分块{block.Index}大小不匹配");
}
}
success = true;
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载成功");
}
catch (Exception ex) when (ex is HttpRequestException || ex is IOException || ex is TaskCanceledException)
{
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(tempPath))
{
try { File.Delete(tempPath); } catch { }
}
// 增加重试间隔,避免频繁重试
await Task.Delay(2000 * (retry + 1));
}
}
if (success)
@@ -803,76 +942,161 @@ namespace Ink_Canvas.Helpers
else
{
// 超过最大重试,取消所有任务
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}超过最大重试次数,取消下载", LogHelper.LogType.Error);
cts.Cancel();
break;
}
}
}));
}
await Task.WhenAll(tasks);
if (cts.IsCancellationRequested || finishedBlocks.Count != blockCount)
{
progressCallback?.Invoke(0, $"多线程下载失败({threadCount}线程)");
LogHelper.WriteLogToFile($"AutoUpdate | {threadCount}线程下载失败,完成分块数: {finishedBlocks.Count}/{blockCount}", LogHelper.LogType.Warning);
progressCallback?.Invoke(0, $"{threadCount}线程下载失败,完成分块数: {finishedBlocks.Count}/{blockCount}");
// 清理分块文件
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;
// 已经是最后一次尝试,降级为单线程
LogHelper.WriteLogToFile($"AutoUpdate | 所有多线程尝试失败,降级为单线程下载");
progressCallback?.Invoke(0, "所有多线程尝试失败,降级为单线程下载");
return await DownloadSingleThread(fileUrl, destinationPath, totalSize, progressCallback);
}
else
{
LogHelper.WriteLogToFile($"AutoUpdate | {threadCount}线程下载失败,尝试降级为{threadOptions.Last()}线程");
progressCallback?.Invoke(0, $"{threadCount}线程下载失败,尝试降级为{threadOptions.Last()}线程");
LogHelper.WriteLogToFile($"AutoUpdate | {threadCount}线程下载失败,尝试降级为{threadOptions[Array.IndexOf(threadOptions, threadCount) + 1]}线程");
progressCallback?.Invoke(0, $"{threadCount}线程下载失败,尝试降级为{threadOptions[Array.IndexOf(threadOptions, threadCount) + 1]}线程");
continue;
}
}
// 合并所有块
using (var output = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None))
try
{
for (int i = 0; i < blockCount; i++)
using (var output = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
string tempPath = destinationPath + $".part{i}";
using (var input = new FileStream(tempPath, FileMode.Open, FileAccess.Read))
for (int i = 0; i < blockCount; i++)
{
await input.CopyToAsync(output);
string tempPath = destinationPath + $".part{i}";
if (!File.Exists(tempPath))
{
throw new FileNotFoundException($"分块文件不存在: {tempPath}");
}
using (var input = new FileStream(tempPath, FileMode.Open, FileAccess.Read))
{
await input.CopyToAsync(output);
}
File.Delete(tempPath);
}
File.Delete(tempPath);
}
}
progressCallback?.Invoke(100, $"多线程下载完成({threadCount}线程)");
progressCallback?.Invoke(100, $"多线程下载完成({threadCount}线程)");
LogHelper.WriteLogToFile($"AutoUpdate | 多线程下载完成({threadCount}线程)");
FileInfo fileInfo = new FileInfo(destinationPath);
if (fileInfo.Length != totalSize)
{
LogHelper.WriteLogToFile($"AutoUpdate | 文件大小校验失败,本地:{fileInfo.Length},服务器:{totalSize}", LogHelper.LogType.Error);
File.Delete(destinationPath);
progressCallback?.Invoke(0, "文件大小校验失败,已删除损坏文件");
return false;
}
if (destinationPath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
try
// 文件大小校验
FileInfo fileInfo = new FileInfo(destinationPath);
if (fileInfo.Length != totalSize)
{
System.IO.Compression.ZipFile.OpenRead(destinationPath).Dispose();
}
catch
{
LogHelper.WriteLogToFile("AutoUpdate | ZIP文件解压测试失败,文件可能已损坏", LogHelper.LogType.Error);
LogHelper.WriteLogToFile($"AutoUpdate | 文件大小校验失败,本地:{fileInfo.Length},服务器:{totalSize}", LogHelper.LogType.Error);
File.Delete(destinationPath);
progressCallback?.Invoke(0, "ZIP文件解压测试失败,已删除损坏文件");
progressCallback?.Invoke(0, "文件大小校验失败,已删除损坏文件");
return false;
}
// ZIP文件完整性校验
if (destinationPath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
try
{
System.IO.Compression.ZipFile.OpenRead(destinationPath).Dispose();
}
catch
{
LogHelper.WriteLogToFile("AutoUpdate | ZIP文件解压测试失败,文件可能已损坏", LogHelper.LogType.Error);
File.Delete(destinationPath);
progressCallback?.Invoke(0, "ZIP文件解压测试失败,已删除损坏文件");
return false;
}
}
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 合并分块文件时出错: {ex.Message}", LogHelper.LogType.Error);
File.Delete(destinationPath);
progressCallback?.Invoke(0, $"合并分块文件时出错: {ex.Message}");
return false;
}
return true;
}
return false;
}
// 单线程下载方法
private static async Task<bool> DownloadSingleThread(string fileUrl, string destinationPath, long totalSize, Action<double, string> progressCallback = null)
{
try
{
LogHelper.WriteLogToFile($"AutoUpdate | 开始单线程下载: {fileUrl}");
progressCallback?.Invoke(0, "开始单线程下载");
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
client.Timeout = TimeSpan.FromMinutes(10); // 单线程下载设置更长的超时时间
using (var resp = await client.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead))
{
resp.EnsureSuccessStatusCode();
using (var fs = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
var stream = await resp.Content.ReadAsStreamAsync();
byte[] buffer = new byte[8192];
int read;
long downloaded = 0;
var lastProgressUpdate = DateTime.UtcNow;
while ((read = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await fs.WriteAsync(buffer, 0, read);
downloaded += read;
// 限制进度更新频率,避免UI卡顿
if (DateTime.UtcNow - lastProgressUpdate > TimeSpan.FromMilliseconds(500))
{
if (totalSize > 0)
{
double percent = (double)downloaded / totalSize * 100;
progressCallback?.Invoke(percent, $"单线程下载中: {percent:F1}%");
}
lastProgressUpdate = DateTime.UtcNow;
}
}
}
}
}
progressCallback?.Invoke(100, "单线程下载完成");
LogHelper.WriteLogToFile($"AutoUpdate | 单线程下载完成");
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 单线程下载失败: {ex.Message}", LogHelper.LogType.Error);
progressCallback?.Invoke(0, $"单线程下载失败: {ex.Message}");
return false;
}
}
// 获取文件总大小
private static async Task<long> GetContentLength(string fileUrl)
{
@@ -1214,8 +1438,8 @@ namespace Ink_Canvas.Helpers
{
LogHelper.WriteLogToFile($"AutoUpdate | 开始修复版本,通道: {channel}");
// 获取远程版本号(自动选择最快线路组,始终下载远程版本)
var (remoteVersion, group, _) = await CheckForUpdates(channel, true);
// 获取远程版本号(自动选择最快线路组,始终下载远程版本,版本修复模式
var (remoteVersion, group, _) = await CheckForUpdates(channel, true, true);
if (string.IsNullOrEmpty(remoteVersion) || group == null)
{
LogHelper.WriteLogToFile("AutoUpdate | 修复版本时获取远程版本失败", LogHelper.LogType.Error);
@@ -1327,6 +1551,26 @@ namespace Ink_Canvas.Helpers
}
}
/// <summary>
/// 获取指定版本的发布时间
/// </summary>
/// <param name="version">版本号</param>
/// <param name="channel">更新通道</param>
/// <returns>版本发布时间,如果获取失败则返回null</returns>
public static async Task<DateTime?> GetVersionReleaseTime(string version, UpdateChannel channel = UpdateChannel.Release)
{
try
{
var (_, _, _, releaseTime) = await GetGithubReleaseByVersion(version, channel);
return releaseTime;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 获取版本 {version} 发布时间失败: {ex.Message}", LogHelper.LogType.Warning);
return null;
}
}
/// <summary>
/// 启动手动指定版本的多线路多线程下载并自动安装(用于历史版本回滚等场景)
/// </summary>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,259 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Effects;
using System.Windows.Media.Imaging;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 硬件加速的墨迹处理器,利用WPF的GPU渲染能力
/// </summary>
public class HardwareAcceleratedInkProcessor
{
private readonly RenderTargetBitmap _renderTarget;
private readonly DrawingVisual _drawingVisual;
private readonly DrawingContext _drawingContext;
private bool _isInitialized = false;
public HardwareAcceleratedInkProcessor(int width = 1920, int height = 1080)
{
// 创建硬件加速的渲染目标
_renderTarget = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
_drawingVisual = new DrawingVisual();
// 启用硬件加速
RenderOptions.SetBitmapScalingMode(_drawingVisual, BitmapScalingMode.HighQuality);
RenderOptions.SetEdgeMode(_drawingVisual, EdgeMode.Aliased);
_isInitialized = true;
}
/// <summary>
/// 使用GPU加速的贝塞尔曲线平滑
/// </summary>
public async Task<Stroke> SmoothStrokeWithGPU(Stroke originalStroke)
{
if (!_isInitialized || originalStroke == null || originalStroke.StylusPoints.Count < 2)
return originalStroke;
return await Task.Run(() =>
{
try
{
// 使用PathGeometry进行硬件加速的曲线拟合
var pathGeometry = CreateSmoothPathGeometry(originalStroke.StylusPoints);
// 将PathGeometry转换回StylusPoint集合
var smoothedPoints = ConvertPathGeometryToStylusPoints(pathGeometry, originalStroke.StylusPoints);
return new Stroke(new StylusPointCollection(smoothedPoints))
{
DrawingAttributes = originalStroke.DrawingAttributes.Clone()
};
}
catch
{
return originalStroke;
}
});
}
/// <summary>
/// 创建平滑的路径几何体
/// </summary>
private PathGeometry CreateSmoothPathGeometry(StylusPointCollection points)
{
var pathGeometry = new PathGeometry();
var pathFigure = new PathFigure();
if (points.Count < 2) return pathGeometry;
pathFigure.StartPoint = new Point(points[0].X, points[0].Y);
// 使用贝塞尔曲线段创建平滑路径
for (int i = 0; i < points.Count - 1; i += 3)
{
var p1 = i + 1 < points.Count ? new Point(points[i + 1].X, points[i + 1].Y) : pathFigure.StartPoint;
var p2 = i + 2 < points.Count ? new Point(points[i + 2].X, points[i + 2].Y) : p1;
var p3 = i + 3 < points.Count ? new Point(points[i + 3].X, points[i + 3].Y) : p2;
var bezierSegment = new BezierSegment(p1, p2, p3, true);
pathFigure.Segments.Add(bezierSegment);
}
pathGeometry.Figures.Add(pathFigure);
return pathGeometry;
}
/// <summary>
/// 将PathGeometry转换为StylusPoint集合
/// </summary>
private List<StylusPoint> ConvertPathGeometryToStylusPoints(PathGeometry pathGeometry, StylusPointCollection originalPoints)
{
var result = new List<StylusPoint>();
var flattened = pathGeometry.GetFlattenedPathGeometry();
foreach (var figure in flattened.Figures)
{
result.Add(new StylusPoint(figure.StartPoint.X, figure.StartPoint.Y, 0.5f));
foreach (var segment in figure.Segments)
{
if (segment is LineSegment lineSegment)
{
result.Add(new StylusPoint(lineSegment.Point.X, lineSegment.Point.Y, 0.5f));
}
else if (segment is PolyLineSegment polyLineSegment)
{
foreach (var point in polyLineSegment.Points)
{
result.Add(new StylusPoint(point.X, point.Y, 0.5f));
}
}
}
}
// 保持原始压感信息
InterpolatePressure(result, originalPoints);
return result;
}
/// <summary>
/// 插值压感信息
/// </summary>
private void InterpolatePressure(List<StylusPoint> smoothedPoints, StylusPointCollection originalPoints)
{
if (originalPoints.Count == 0 || smoothedPoints.Count == 0) return;
for (int i = 0; i < smoothedPoints.Count; i++)
{
double ratio = (double)i / (smoothedPoints.Count - 1);
int originalIndex = (int)(ratio * (originalPoints.Count - 1));
originalIndex = Math.Max(0, Math.Min(originalIndex, originalPoints.Count - 1));
var point = smoothedPoints[i];
float pressure = originalPoints[originalIndex].PressureFactor;
smoothedPoints[i] = new StylusPoint(point.X, point.Y, Math.Max(pressure, 0.1f));
}
}
/// <summary>
/// 使用GPU加速的并行贝塞尔计算
/// </summary>
public static StylusPoint[] ParallelBezierInterpolation(StylusPoint[] controlPoints, int segments = 16)
{
if (controlPoints.Length < 4) return controlPoints;
var result = new StylusPoint[segments * (controlPoints.Length / 4)];
Parallel.For(0, controlPoints.Length / 4, segmentIndex =>
{
var p0 = controlPoints[segmentIndex * 4];
var p1 = controlPoints[segmentIndex * 4 + 1];
var p2 = controlPoints[segmentIndex * 4 + 2];
var p3 = controlPoints[segmentIndex * 4 + 3];
for (int i = 0; i < segments; i++)
{
double t = (double)i / (segments - 1);
result[segmentIndex * segments + i] = CubicBezierFast(p0, p1, p2, p3, t);
}
});
return result;
}
/// <summary>
/// 优化的三次贝塞尔曲线计算
/// </summary>
private static StylusPoint CubicBezierFast(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3, double t)
{
double u = 1 - t;
double tt = t * t;
double uu = u * u;
double uuu = uu * u;
double ttt = tt * t;
double x = uuu * p0.X + 3 * uu * t * p1.X + 3 * u * tt * p2.X + ttt * p3.X;
double y = uuu * p0.Y + 3 * uu * t * p1.Y + 3 * u * tt * p2.Y + ttt * p3.Y;
float pressure = (float)(p1.PressureFactor * u + p2.PressureFactor * t);
return new StylusPoint(x, y, Math.Max(pressure, 0.1f));
}
/// <summary>
/// 释放GPU资源
/// </summary>
public void Dispose()
{
_drawingContext?.Close();
_renderTarget?.Clear();
_isInitialized = false;
}
}
/// <summary>
/// 质量配置枚举
/// </summary>
public enum InkSmoothingQuality
{
HighPerformance = 0, // 高性能低质量
Balanced = 1, // 平衡
HighQuality = 2 // 高质量低性能
}
/// <summary>
/// 墨迹平滑配置
/// </summary>
public class InkSmoothingConfig
{
public InkSmoothingQuality Quality { get; set; } = InkSmoothingQuality.Balanced;
public bool UseHardwareAcceleration { get; set; } = true;
public bool UseAsyncProcessing { get; set; } = true;
public int MaxConcurrentTasks { get; set; } = Environment.ProcessorCount;
public double SmoothingStrength { get; set; } = 0.6;
public double ResampleInterval { get; set; } = 1.2;
public int InterpolationSteps { get; set; } = 16;
public static InkSmoothingConfig FromSettings()
{
return new InkSmoothingConfig
{
Quality = (InkSmoothingQuality)MainWindow.Settings.Canvas.InkSmoothingQuality,
UseHardwareAcceleration = MainWindow.Settings.Canvas.UseHardwareAcceleration,
UseAsyncProcessing = MainWindow.Settings.Canvas.UseAsyncInkSmoothing,
MaxConcurrentTasks = MainWindow.Settings.Canvas.MaxConcurrentSmoothingTasks > 0 ?
MainWindow.Settings.Canvas.MaxConcurrentSmoothingTasks : Environment.ProcessorCount
};
}
public void ApplyQualitySettings()
{
switch (Quality)
{
case InkSmoothingQuality.HighPerformance:
SmoothingStrength = 0.4;
ResampleInterval = 2.0;
InterpolationSteps = 8;
break;
case InkSmoothingQuality.Balanced:
SmoothingStrength = 0.6;
ResampleInterval = 1.2;
InterpolationSteps = 16;
break;
case InkSmoothingQuality.HighQuality:
SmoothingStrength = 0.8;
ResampleInterval = 0.8;
InterpolationSteps = 32;
break;
}
}
}
}
+258
View File
@@ -0,0 +1,258 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Ink;
using System.Windows.Threading;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 统一的墨迹平滑管理器,整合异步处理和硬件加速
/// </summary>
public class InkSmoothingManager : IDisposable
{
private readonly AsyncAdvancedBezierSmoothing _asyncSmoothing;
private readonly HardwareAcceleratedInkProcessor _hardwareProcessor;
private readonly InkSmoothingPerformanceMonitor _performanceMonitor;
private readonly InkSmoothingConfig _config;
private readonly Dispatcher _uiDispatcher;
private bool _disposed = false;
public InkSmoothingManager(Dispatcher uiDispatcher)
{
_uiDispatcher = uiDispatcher;
_config = InkSmoothingConfig.FromSettings();
_config.ApplyQualitySettings();
_asyncSmoothing = new AsyncAdvancedBezierSmoothing(uiDispatcher)
{
SmoothingStrength = _config.SmoothingStrength,
ResampleInterval = _config.ResampleInterval,
InterpolationSteps = _config.InterpolationSteps,
UseHardwareAcceleration = _config.UseHardwareAcceleration,
MaxConcurrentTasks = _config.MaxConcurrentTasks
};
_hardwareProcessor = new HardwareAcceleratedInkProcessor();
_performanceMonitor = new InkSmoothingPerformanceMonitor();
}
/// <summary>
/// 平滑笔画(自动选择最佳方法)
/// </summary>
public async Task<Stroke> SmoothStrokeAsync(Stroke originalStroke,
Action<Stroke, Stroke> onCompleted = null,
CancellationToken cancellationToken = default)
{
if (originalStroke == null || originalStroke.StylusPoints.Count < 2)
return originalStroke;
var stopwatch = Stopwatch.StartNew();
Stroke result = originalStroke;
try
{
if (_config.UseAsyncProcessing)
{
// 使用异步处理
result = await _asyncSmoothing.SmoothStrokeAsync(originalStroke, onCompleted, cancellationToken);
}
else if (_config.UseHardwareAcceleration)
{
// 使用硬件加速但同步处理
result = await _hardwareProcessor.SmoothStrokeWithGPU(originalStroke);
onCompleted?.Invoke(originalStroke, result);
}
else
{
// 回退到传统同步处理
result = await Task.Run(() =>
{
var traditionalSmoothing = new AdvancedBezierSmoothing();
return traditionalSmoothing.SmoothStroke(originalStroke);
}, cancellationToken);
onCompleted?.Invoke(originalStroke, result);
}
}
catch (OperationCanceledException)
{
result = originalStroke;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"墨迹平滑失败: {ex.Message}");
result = originalStroke;
}
finally
{
stopwatch.Stop();
_performanceMonitor.RecordProcessingTime(stopwatch.Elapsed);
}
return result;
}
/// <summary>
/// 同步平滑笔画(用于向后兼容)
/// </summary>
public Stroke SmoothStroke(Stroke originalStroke)
{
if (originalStroke == null || originalStroke.StylusPoints.Count < 2)
return originalStroke;
var stopwatch = Stopwatch.StartNew();
Stroke result;
try
{
if (_config.UseHardwareAcceleration)
{
// 使用硬件加速的同步版本
var task = _hardwareProcessor.SmoothStrokeWithGPU(originalStroke);
task.Wait(5000); // 5秒超时
result = task.Status == TaskStatus.RanToCompletion ? task.Result : originalStroke;
}
else
{
// 传统同步处理
var traditionalSmoothing = new AdvancedBezierSmoothing();
result = traditionalSmoothing.SmoothStroke(originalStroke);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"同步墨迹平滑失败: {ex.Message}");
result = originalStroke;
}
finally
{
stopwatch.Stop();
_performanceMonitor.RecordProcessingTime(stopwatch.Elapsed);
}
return result;
}
/// <summary>
/// 更新配置
/// </summary>
public void UpdateConfig()
{
var newConfig = InkSmoothingConfig.FromSettings();
newConfig.ApplyQualitySettings();
_asyncSmoothing.SmoothingStrength = newConfig.SmoothingStrength;
_asyncSmoothing.ResampleInterval = newConfig.ResampleInterval;
_asyncSmoothing.InterpolationSteps = newConfig.InterpolationSteps;
_asyncSmoothing.UseHardwareAcceleration = newConfig.UseHardwareAcceleration;
_asyncSmoothing.MaxConcurrentTasks = newConfig.MaxConcurrentTasks;
}
/// <summary>
/// 获取性能统计信息
/// </summary>
public string GetPerformanceStats()
{
return $"平均处理时间: {_performanceMonitor.GetAverageProcessingTimeMs():F2}ms, " +
$"最大处理时间: {_performanceMonitor.GetMaxProcessingTimeMs():F2}ms, " +
$"样本数: {_performanceMonitor.GetSampleCount()}";
}
/// <summary>
/// 取消所有正在进行的任务
/// </summary>
public void CancelAllTasks()
{
_asyncSmoothing?.CancelAllTasks();
}
/// <summary>
/// 检查系统是否支持硬件加速
/// </summary>
public static bool IsHardwareAccelerationSupported()
{
try
{
return System.Windows.Media.RenderCapability.Tier >= 0x00020000;
}
catch
{
return false;
}
}
/// <summary>
/// 获取推荐的配置
/// </summary>
public static InkSmoothingConfig GetRecommendedConfig()
{
var config = new InkSmoothingConfig();
// 根据系统性能调整配置
var processorCount = Environment.ProcessorCount;
var isHardwareAccelerated = IsHardwareAccelerationSupported();
if (processorCount >= 8 && isHardwareAccelerated)
{
config.Quality = InkSmoothingQuality.HighQuality;
config.UseHardwareAcceleration = true;
config.UseAsyncProcessing = true;
config.MaxConcurrentTasks = Math.Min(processorCount, 8);
}
else if (processorCount >= 4)
{
config.Quality = InkSmoothingQuality.Balanced;
config.UseHardwareAcceleration = isHardwareAccelerated;
config.UseAsyncProcessing = true;
config.MaxConcurrentTasks = Math.Min(processorCount, 4);
}
else
{
config.Quality = InkSmoothingQuality.HighPerformance;
config.UseHardwareAcceleration = false;
config.UseAsyncProcessing = false;
config.MaxConcurrentTasks = 1;
}
config.ApplyQualitySettings();
return config;
}
/// <summary>
/// 应用推荐配置到设置
/// </summary>
public static void ApplyRecommendedSettings()
{
var config = GetRecommendedConfig();
MainWindow.Settings.Canvas.InkSmoothingQuality = (int)config.Quality;
MainWindow.Settings.Canvas.UseHardwareAcceleration = config.UseHardwareAcceleration;
MainWindow.Settings.Canvas.UseAsyncInkSmoothing = config.UseAsyncProcessing;
MainWindow.Settings.Canvas.MaxConcurrentSmoothingTasks = config.MaxConcurrentTasks;
}
public void Dispose()
{
if (!_disposed)
{
CancelAllTasks();
_asyncSmoothing?.Dispose();
_hardwareProcessor?.Dispose();
_disposed = true;
}
}
}
/// <summary>
/// 墨迹平滑事件参数
/// </summary>
public class InkSmoothingEventArgs : EventArgs
{
public Stroke OriginalStroke { get; set; }
public Stroke SmoothedStroke { get; set; }
public TimeSpan ProcessingTime { get; set; }
public bool WasAsync { get; set; }
public bool UsedHardwareAcceleration { get; set; }
}
}
@@ -1,12 +1,60 @@
<UserControl x:Class="Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher.LauncherSettingsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher"
mc:Ignorable="d"
mc:Ignorable="d"
d:DesignHeight="500" d:DesignWidth="600">
<UserControl.Resources>
<!-- 自定义按钮样式 -->
<Style x:Key="DefaultButtonStyle" TargetType="Button">
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
TextElement.Foreground="{TemplateBinding Foreground}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Opacity" Value="0.8"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect Color="Black" Direction="270" ShadowDepth="2" Opacity="0.3" BlurRadius="4"/>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Opacity" Value="0.6"/>
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX="0.95" ScaleY="0.95"/>
</Setter.Value>
</Setter>
<Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="border" Property="Opacity" Value="0.4"/>
<Setter Property="Cursor" Value="Arrow"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
@@ -16,12 +64,12 @@
</Grid.RowDefinitions>
<!-- 标题 -->
<TextBlock Grid.Row="0" Text="超级启动台设置" FontSize="16" FontWeight="Bold" Margin="0,0,0,15"/>
<TextBlock Grid.Row="0" Text="超级启动台设置" FontSize="16" FontWeight="Bold" Margin="0,0,0,15" Foreground="Black"/>
<!-- 基本设置 -->
<StackPanel Grid.Row="1" Margin="0,0,0,15">
<TextBlock Text="基本设置" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,10"/>
<TextBlock Text="基本设置" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,10" Foreground="Black"/>
<Grid Margin="10,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
@@ -30,12 +78,12 @@
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 按钮位置 -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="按钮位置:" VerticalAlignment="Center"/>
<TextBlock Grid.Row="0" Grid.Column="0" Text="按钮位置:" VerticalAlignment="Center" Foreground="Black"/>
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Margin="0,5">
<RadioButton x:Name="RbtnLeft" Content="浮动栏左侧" Margin="0,0,20,0" Checked="RbtnPosition_Checked"/>
<RadioButton x:Name="RbtnRight" Content="浮动栏右侧" IsChecked="True" Checked="RbtnPosition_Checked"/>
<RadioButton x:Name="RbtnLeft" Content="浮动栏左侧" Margin="0,0,20,0" Checked="RbtnPosition_Checked" Foreground="Black"/>
<RadioButton x:Name="RbtnRight" Content="浮动栏右侧" IsChecked="True" Checked="RbtnPosition_Checked" Foreground="Black"/>
</StackPanel>
</Grid>
</StackPanel>
@@ -47,7 +95,7 @@
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="应用管理" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,10"/>
<TextBlock Grid.Row="0" Text="应用管理" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,10" Foreground="Black"/>
<Border Grid.Row="1" BorderThickness="1" BorderBrush="#CCCCCC" CornerRadius="5">
<Grid>
@@ -71,9 +119,15 @@
<!-- 操作按钮 -->
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="5">
<Button x:Name="BtnAdd" Content="添加" Padding="10,5" Margin="0,5,5,5" Click="BtnAdd_Click"/>
<Button x:Name="BtnEdit" Content="编辑" Padding="10,5" Margin="5" Click="BtnEdit_Click"/>
<Button x:Name="BtnDelete" Content="删除" Padding="10,5" Margin="5" Click="BtnDelete_Click"/>
<Button x:Name="BtnAdd" Content="添加" Padding="10,5" Margin="0,5,5,5" Click="BtnAdd_Click"
Background="#FF007ACC" Foreground="White" BorderBrush="#FF005A9B" BorderThickness="1"
Style="{StaticResource DefaultButtonStyle}"/>
<Button x:Name="BtnEdit" Content="编辑" Padding="10,5" Margin="5" Click="BtnEdit_Click"
Background="#FF6C757D" Foreground="White" BorderBrush="#FF5A6268" BorderThickness="1"
Style="{StaticResource DefaultButtonStyle}"/>
<Button x:Name="BtnDelete" Content="删除" Padding="10,5" Margin="5" Click="BtnDelete_Click"
Background="#FFDC3545" Foreground="White" BorderBrush="#FFBD2130" BorderThickness="1"
Style="{StaticResource DefaultButtonStyle}"/>
</StackPanel>
</Grid>
</Border>
@@ -81,7 +135,9 @@
<!-- 底部按钮 -->
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,15,0,0">
<Button x:Name="BtnSave" Content="保存设置" Padding="15,5" Click="BtnSave_Click"/>
<Button x:Name="BtnSave" Content="保存设置" Padding="15,5" Click="BtnSave_Click"
Background="#FF28A745" Foreground="White" BorderBrush="#FF1E7E34" BorderThickness="1"
Style="{StaticResource DefaultButtonStyle}"/>
</StackPanel>
</Grid>
</UserControl>
+32
View File
@@ -2594,6 +2594,38 @@
<TextBlock x:Name="AppVersionTextBlock" FontSize="18" FontWeight="Bold"
Text="1.X.X.X" />
</ui:SimpleStackPanel>
<!-- 设备信息 -->
<Border BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="8"
Background="#18181b" Padding="12">
<ui:SimpleStackPanel Spacing="8">
<TextBlock Text="设备信息" FontWeight="Bold" Foreground="#fafafa" FontSize="14" />
<ui:SimpleStackPanel Orientation="Horizontal">
<TextBlock Text="设备ID" Foreground="#a1a1aa" FontSize="12" />
<TextBlock x:Name="DeviceIdTextBlock" Text="正在获取..." Foreground="#fafafa" FontSize="12" />
</ui:SimpleStackPanel>
<ui:SimpleStackPanel Orientation="Horizontal">
<TextBlock Text="使用频率:" Foreground="#a1a1aa" FontSize="12" />
<TextBlock x:Name="UsageFrequencyTextBlock" Text="正在获取..." Foreground="#fafafa" FontSize="12" />
</ui:SimpleStackPanel>
<ui:SimpleStackPanel Orientation="Horizontal">
<TextBlock Text="更新优先级:" Foreground="#a1a1aa" FontSize="12" />
<TextBlock x:Name="UpdatePriorityTextBlock" Text="正在获取..." Foreground="#fafafa" FontSize="12" />
</ui:SimpleStackPanel>
<ui:SimpleStackPanel Orientation="Horizontal">
<TextBlock Text="启动次数:" Foreground="#a1a1aa" FontSize="12" />
<TextBlock x:Name="LaunchCountTextBlock" Text="正在获取..." Foreground="#fafafa" FontSize="12" />
</ui:SimpleStackPanel>
<ui:SimpleStackPanel Orientation="Horizontal">
<TextBlock Text="总使用时长:" Foreground="#a1a1aa" FontSize="12" />
<TextBlock x:Name="TotalUsageTextBlock" Text="正在获取..." Foreground="#fafafa" FontSize="12" />
</ui:SimpleStackPanel>
<Button x:Name="RefreshDeviceInfoButton" Content="刷新设备信息"
Width="120" Height="30" Margin="0,8,0,0"
Click="RefreshDeviceInfo_Click" />
</ui:SimpleStackPanel>
</Border>
<TextBlock
Text="# 使用和分发本软件前,请您应当且务必知晓相关开源协议,且您应当知晓本软件基于 https://github.com/WXRIW/Ink-Canvas 修改而成。"
TextWrapping="Wrap" Foreground="#a1a1aa" />
+99
View File
@@ -110,6 +110,9 @@ namespace Ink_Canvas {
CheckColorTheme(true);
CheckPenTypeUIState();
// 初始化墨迹平滑管理器
_inkSmoothingManager = new Helpers.InkSmoothingManager(Dispatcher);
// 注册输入事件
inkCanvas.PreviewMouseDown += inkCanvas_PreviewMouseDown;
inkCanvas.StylusDown += inkCanvas_StylusDown;
@@ -182,6 +185,7 @@ namespace Ink_Canvas {
private System.Windows.Media.Color Ink_DefaultColor = Colors.Red;
private DrawingAttributes drawingAttributes;
private Helpers.InkSmoothingManager _inkSmoothingManager;
private void loadPenCanvas() {
try {
@@ -854,6 +858,8 @@ namespace Ink_Canvas {
{
// 切换到关于页面
ShowSettingsSection("about");
// 刷新设备信息
RefreshDeviceInfo();
}
// 新增:个性化设置
@@ -881,6 +887,99 @@ namespace Ink_Canvas {
BorderSettingsMask.Background = null; // 确保清除蒙层背景
}
/// <summary>
/// 刷新设备信息按钮点击事件
/// </summary>
private void RefreshDeviceInfo_Click(object sender, RoutedEventArgs e)
{
RefreshDeviceInfo();
}
/// <summary>
/// 刷新设备信息显示
/// </summary>
private void RefreshDeviceInfo()
{
try
{
// 获取设备ID
string deviceId = DeviceIdentifier.GetDeviceId();
DeviceIdTextBlock.Text = deviceId;
// 获取使用频率
var usageFrequency = DeviceIdentifier.GetUsageFrequency();
string frequencyText;
switch (usageFrequency)
{
case DeviceIdentifier.UsageFrequency.High:
frequencyText = "高频用户";
break;
case DeviceIdentifier.UsageFrequency.Medium:
frequencyText = "中频用户";
break;
case DeviceIdentifier.UsageFrequency.Low:
frequencyText = "低频用户";
break;
default:
frequencyText = "未知";
break;
}
UsageFrequencyTextBlock.Text = frequencyText;
// 获取更新优先级
var updatePriority = DeviceIdentifier.GetUpdatePriority();
string priorityText;
switch (updatePriority)
{
case DeviceIdentifier.UpdatePriority.High:
priorityText = "高优先级(优先推送更新)";
break;
case DeviceIdentifier.UpdatePriority.Medium:
priorityText = "中优先级(正常推送更新)";
break;
case DeviceIdentifier.UpdatePriority.Low:
priorityText = "低优先级(延迟推送更新)";
break;
default:
priorityText = "未知";
break;
}
UpdatePriorityTextBlock.Text = priorityText;
// 获取使用统计
var (launchCount, totalMinutes, avgSession, _) = DeviceIdentifier.GetUsageStats();
LaunchCountTextBlock.Text = launchCount.ToString();
string totalUsageText;
if (totalMinutes < 60)
{
totalUsageText = $"{totalMinutes}分钟";
}
else if (totalMinutes < 1440)
{
totalUsageText = $"{totalMinutes / 60}小时{totalMinutes % 60}分钟";
}
else
{
totalUsageText = $"{totalMinutes / 1440}天{(totalMinutes % 1440) / 60}小时";
}
TotalUsageTextBlock.Text = totalUsageText;
LogHelper.WriteLogToFile($"MainWindow | 设备信息已刷新 - ID: {deviceId}, 频率: {frequencyText}, 优先级: {priorityText}");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"MainWindow | 刷新设备信息失败: {ex.Message}", LogHelper.LogType.Error);
// 显示错误信息
DeviceIdTextBlock.Text = "获取失败";
UsageFrequencyTextBlock.Text = "获取失败";
UpdatePriorityTextBlock.Text = "获取失败";
LaunchCountTextBlock.Text = "获取失败";
TotalUsageTextBlock.Text = "获取失败";
}
}
// 新增:折叠侧边栏
private void CollapseNavSidebar_Click(object sender, RoutedEventArgs e)
{
+17
View File
@@ -1270,10 +1270,27 @@ namespace Ink_Canvas {
LogHelper.WriteLogToFile($"结束PPT放映操作异常: {ex.ToString()}", LogHelper.LogType.Error);
}
await Application.Current.Dispatcher.InvokeAsync(() => {
// 隐藏侧边栏退出按钮
if (BtnExitPptFromSidebarLeft != null)
BtnExitPptFromSidebarLeft.Visibility = Visibility.Collapsed;
if (BtnExitPptFromSidebarRight != null)
BtnExitPptFromSidebarRight.Visibility = Visibility.Collapsed;
// 确保所有放映模式按钮都被隐藏,防止PptApplication_SlideShowEnd事件未触发的情况
try {
BtnPPTSlideShow.Visibility = Visibility.Visible;
BtnPPTSlideShowEnd.Visibility = Visibility.Collapsed;
StackPanelPPTControls.Visibility = Visibility.Collapsed;
LeftBottomPanelForPPTNavigation.Visibility = Visibility.Collapsed;
RightBottomPanelForPPTNavigation.Visibility = Visibility.Collapsed;
LeftSidePanelForPPTNavigation.Visibility = Visibility.Collapsed;
RightSidePanelForPPTNavigation.Visibility = Visibility.Collapsed;
LogHelper.WriteLogToFile("手动隐藏所有放映模式按钮", LogHelper.LogType.Trace);
}
catch (Exception ex) {
LogHelper.WriteLogToFile($"手动隐藏放映模式按钮失败: {ex.ToString()}", LogHelper.LogType.Error);
}
});
}
+54 -2
View File
@@ -1298,7 +1298,7 @@ namespace Ink_Canvas {
private void ToggleSwitchAdvancedBezierSmoothing_Toggled(object sender, RoutedEventArgs e) {
if (!isLoaded) return;
Settings.Canvas.UseAdvancedBezierSmoothing = ToggleSwitchAdvancedBezierSmoothing.IsOn;
// 启用高级贝塞尔平滑时自动禁用原来的FitToCurve
if (ToggleSwitchAdvancedBezierSmoothing.IsOn)
{
@@ -1306,9 +1306,61 @@ namespace Ink_Canvas {
Settings.Canvas.FitToCurve = false;
drawingAttributes.FitToCurve = false;
}
// 更新墨迹平滑管理器配置
_inkSmoothingManager?.UpdateConfig();
SaveSettingsToFile();
}
// 注释掉这些方法,因为对应的UI控件还没有在XAML中定义
/*
private void ToggleSwitchAsyncInkSmoothing_Toggled(object sender, RoutedEventArgs e) {
if (!isLoaded) return;
Settings.Canvas.UseAsyncInkSmoothing = ToggleSwitchAsyncInkSmoothing.IsOn;
_inkSmoothingManager?.UpdateConfig();
SaveSettingsToFile();
}
private void ToggleSwitchHardwareAcceleration_Toggled(object sender, RoutedEventArgs e) {
if (!isLoaded) return;
Settings.Canvas.UseHardwareAcceleration = ToggleSwitchHardwareAcceleration.IsOn;
_inkSmoothingManager?.UpdateConfig();
SaveSettingsToFile();
}
private void ComboBoxInkSmoothingQuality_SelectionChanged(object sender, SelectionChangedEventArgs e) {
if (!isLoaded) return;
Settings.Canvas.InkSmoothingQuality = ComboBoxInkSmoothingQuality.SelectedIndex;
_inkSmoothingManager?.UpdateConfig();
SaveSettingsToFile();
}
private void SliderMaxConcurrentTasks_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) {
if (!isLoaded) return;
Settings.Canvas.MaxConcurrentSmoothingTasks = (int)SliderMaxConcurrentTasks.Value;
_inkSmoothingManager?.UpdateConfig();
SaveSettingsToFile();
}
private void ButtonApplyRecommendedSettings_Click(object sender, RoutedEventArgs e) {
// 应用推荐的性能设置
Helpers.InkSmoothingManager.ApplyRecommendedSettings();
LoadSettings(false);
_inkSmoothingManager?.UpdateConfig();
SaveSettingsToFile();
ShowNotification("已应用推荐的性能设置");
}
private void ButtonShowPerformanceStats_Click(object sender, RoutedEventArgs e) {
if (_inkSmoothingManager != null)
{
var stats = _inkSmoothingManager.GetPerformanceStats();
ShowNotification($"性能统计: {stats}");
}
}
*/
private void ToggleSwitchAutoSaveStrokesInPowerPoint_Toggled(object sender, RoutedEventArgs e) {
if (!isLoaded) return;
@@ -538,6 +538,23 @@ namespace Ink_Canvas {
ToggleSwitchAdvancedBezierSmoothing.IsOn = false;
drawingAttributes.FitToCurve = false;
}
// 注释掉新的墨迹平滑性能设置,因为UI控件还没有定义
/*
// 初始化新的墨迹平滑性能设置
ToggleSwitchAsyncInkSmoothing.IsOn = Settings.Canvas.UseAsyncInkSmoothing;
ToggleSwitchHardwareAcceleration.IsOn = Settings.Canvas.UseHardwareAcceleration;
ComboBoxInkSmoothingQuality.SelectedIndex = Settings.Canvas.InkSmoothingQuality;
SliderMaxConcurrentTasks.Value = Settings.Canvas.MaxConcurrentSmoothingTasks > 0 ?
Settings.Canvas.MaxConcurrentSmoothingTasks : Environment.ProcessorCount;
// 检查硬件加速支持
if (!Helpers.InkSmoothingManager.IsHardwareAccelerationSupported())
{
ToggleSwitchHardwareAcceleration.IsEnabled = false;
// 可以添加提示文本说明硬件加速不可用
}
*/
// 初始化直线自动拉直相关设置
ToggleSwitchAutoStraightenLine.IsOn = Settings.Canvas.AutoStraightenLine;
+8 -9
View File
@@ -1579,17 +1579,16 @@ namespace Ink_Canvas {
{
try
{
var advancedSmoothing = new Helpers.AdvancedBezierSmoothing
{
};
// 对临时笔画应用平滑
if (lastTempStroke != null)
if (lastTempStroke != null && _inkSmoothingManager != null)
{
var smoothedStroke = advancedSmoothing.SmoothStroke(lastTempStroke);
inkCanvas.Strokes.Remove(lastTempStroke);
lastTempStroke = smoothedStroke;
inkCanvas.Strokes.Add(smoothedStroke);
var smoothedStroke = _inkSmoothingManager.SmoothStroke(lastTempStroke);
if (smoothedStroke != lastTempStroke)
{
inkCanvas.Strokes.Remove(lastTempStroke);
lastTempStroke = smoothedStroke;
inkCanvas.Strokes.Add(smoothedStroke);
}
}
}
catch (Exception ex)
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Ink;
@@ -587,18 +588,27 @@ namespace Ink_Canvas {
// 检查原始笔画是否仍然存在于画布中
if (inkCanvas.Strokes.Contains(e.Stroke))
{
var advancedSmoothing = new Helpers.AdvancedBezierSmoothing
// 使用新的异步墨迹平滑管理器
if (Settings.Canvas.UseAsyncInkSmoothing && _inkSmoothingManager != null)
{
};
// 异步处理
_ = ProcessStrokeAsync(e.Stroke);
}
else
{
// 同步处理(向后兼容)
var smoothedStroke = _inkSmoothingManager?.SmoothStroke(e.Stroke) ?? e.Stroke;
var smoothedStroke = advancedSmoothing.SmoothStroke(e.Stroke);
// 替换原始笔画
SetNewBackupOfStroke();
_currentCommitType = CommitReason.ShapeRecognition;
inkCanvas.Strokes.Remove(e.Stroke);
inkCanvas.Strokes.Add(smoothedStroke);
_currentCommitType = CommitReason.UserInput;
if (smoothedStroke != e.Stroke)
{
// 替换原始笔画
SetNewBackupOfStroke();
_currentCommitType = CommitReason.ShapeRecognition;
inkCanvas.Strokes.Remove(e.Stroke);
inkCanvas.Strokes.Add(smoothedStroke);
_currentCommitType = CommitReason.UserInput;
}
}
}
}
catch (Exception ex)
@@ -613,6 +623,32 @@ namespace Ink_Canvas {
}
}
/// <summary>
/// 异步处理笔画平滑
/// </summary>
private async Task ProcessStrokeAsync(Stroke originalStroke)
{
try
{
await _inkSmoothingManager.SmoothStrokeAsync(originalStroke, (original, smoothed) =>
{
// 在UI线程上执行笔画替换
if (inkCanvas.Strokes.Contains(original) && smoothed != original)
{
SetNewBackupOfStroke();
_currentCommitType = CommitReason.ShapeRecognition;
inkCanvas.Strokes.Remove(original);
inkCanvas.Strokes.Add(smoothed);
_currentCommitType = CommitReason.UserInput;
}
});
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"异步墨迹平滑失败: {ex.Message}");
}
}
// New method: Checks if a stroke is potentially a straight line
private bool IsPotentialStraightLine(Stroke stroke) {
// 确保有足够的点来进行线条分析
+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.2.0")]
[assembly: AssemblyFileVersion("1.7.2.0")]
[assembly: AssemblyVersion("1.7.3.0")]
[assembly: AssemblyFileVersion("1.7.3.0")]
+8
View File
@@ -50,6 +50,14 @@ namespace Ink_Canvas
public bool FitToCurve { get; set; } = false; // 默认关闭原来的贝塞尔平滑
[JsonProperty("useAdvancedBezierSmoothing")]
public bool UseAdvancedBezierSmoothing { get; set; } = true; // 默认启用高级贝塞尔曲线平滑
[JsonProperty("useAsyncInkSmoothing")]
public bool UseAsyncInkSmoothing { get; set; } = true; // 默认启用异步墨迹平滑
[JsonProperty("useHardwareAcceleration")]
public bool UseHardwareAcceleration { get; set; } = true; // 默认启用硬件加速
[JsonProperty("inkSmoothingQuality")]
public int InkSmoothingQuality { get; set; } = 1; // 0-低质量高性能, 1-平衡, 2-高质量低性能
[JsonProperty("maxConcurrentSmoothingTasks")]
public int MaxConcurrentSmoothingTasks { get; set; } = 0; // 0表示自动检测CPU核心数
[JsonProperty("clearCanvasAndClearTimeMachine")]
public bool ClearCanvasAndClearTimeMachine { get; set; } = false;
[JsonProperty("enablePressureTouchMode")]
+13 -13
View File
@@ -54,7 +54,7 @@
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="插件列表" Margin="10,10,0,5" FontSize="16" FontWeight="SemiBold"/>
<TextBlock Grid.Row="0" Text="插件列表" Margin="10,10,0,5" FontSize="16" FontWeight="SemiBold" Foreground="Black"/>
<ListView Grid.Row="1" x:Name="PluginListView" BorderThickness="0" Margin="0,5,0,0"
SelectionChanged="PluginListView_SelectionChanged">
@@ -101,17 +101,17 @@
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="名称:" FontWeight="SemiBold" Margin="0,0,0,5"/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Name}" Margin="0,0,0,5"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="版本:" FontWeight="SemiBold" Margin="0,0,0,5"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Version}" Margin="0,0,0,5"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="作者:" FontWeight="SemiBold" Margin="0,0,0,5"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Author}" Margin="0,0,0,5"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="描述:" FontWeight="SemiBold" Margin="0,0,0,5"/>
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding Description}" TextWrapping="Wrap" Margin="0,0,0,5"/>
<TextBlock Grid.Row="0" Grid.Column="0" Text="名称:" FontWeight="SemiBold" Margin="0,0,0,5" Foreground="Black"/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Name}" Margin="0,0,0,5" Foreground="Black"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="版本:" FontWeight="SemiBold" Margin="0,0,0,5" Foreground="Black"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Version}" Margin="0,0,0,5" Foreground="Black"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="作者:" FontWeight="SemiBold" Margin="0,0,0,5" Foreground="Black"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Author}" Margin="0,0,0,5" Foreground="Black"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="描述:" FontWeight="SemiBold" Margin="0,0,0,5" Foreground="Black"/>
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding Description}" TextWrapping="Wrap" Margin="0,0,0,5" Foreground="Black"/>
<Button Grid.Row="3" Grid.Column="2" x:Name="BtnDeletePlugin" Content="删除插件"
Visibility="Collapsed" Click="BtnDeletePlugin_Click"
@@ -123,7 +123,7 @@
<!-- 插件设置区域 -->
<Border Grid.Row="1" BorderThickness="1" BorderBrush="{DynamicResource BorderBrush}" Margin="0,10,0,0" Padding="15" CornerRadius="5">
<StackPanel>
<TextBlock Text="插件设置" FontSize="16" FontWeight="SemiBold" Margin="0,0,0,10"/>
<TextBlock Text="插件设置" FontSize="16" FontWeight="SemiBold" Margin="0,0,0,10" Foreground="Black"/>
<ContentControl x:Name="PluginSettingsContainer" MinHeight="50"/>
</StackPanel>
</Border>
@@ -12,7 +12,7 @@ TRACE;DEBUG;NETFRAMEWORK;NET472;;NET30_OR_GREATER;NET35_OR_GREATER;NET40_OR_GREA
E:\ICC CE\ICC CE main\community\Ink Canvas\App.xaml
22-2143008179
76-141727233
79-461684434
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\HistoryRollbackWindow.xaml;Windows\ManagePickNameBackgroundsWindow.xaml;Windows\NamesInputWindow.xaml;Windows\OperatingGuideWindow.xaml;Windows\PluginSettingsWindow.xaml;Windows\RandWindow.xaml;Windows\YesOrNoNotificationWindow.xaml;
+6 -1
View File
@@ -29,7 +29,7 @@
使用该版本 InkCanvasForClass,意味着您同意自行承担任何可能存在的问题与风险。建议不要在公众场合(例如公开课,录播课,线上直播课,大型会议)使用未经广泛测试和优化的 Beta 版本,对使用 Beta 版本而带来的任何问题和风险(例如:被班主任批斗,被校长处罚,崩溃而导致的场面混乱,全球海平面上升等),**将由使用者自行承担**,[CJKmkp](https://github,com/CJK-mkp) 和 [DotteringDoge471](https://github.com/DotteringDoge471) 及其项目的所有维护者不提供任何担保。
♥️ **本项目版权归 [CJKmkp](https://github,com/CJK-mkp) 和 [DotteringDoge471](https://github.com/DotteringDoge471) 共同所有。[CJKmkp](https://github,com/CJK-mkp) 拥有最终解释权。**
♥️ **本项目版权归 [CJKmkp](https://github,com/CJK-mkp) 所有。[CJKmkp](https://github,com/CJK-mkp) 拥有最终解释权。**
**智教联盟 InkCanvasForClass Community Edition 板块:** [forum.smart-teach.cn/t/icc-ce](https://forum.smart-teach.cn/t/icc-ce) ,我们会在此处发布版本更新日志,同时,您也可以在遵守论坛对应管理规则与InkCanvasForClass Community Edition 板块管理条约的情况下,在该板块内提问或发表自己的使用体验。
@@ -55,6 +55,7 @@
2. 在“信息”标签内,点击右侧的“启用编辑”按钮。
2. 曾经安装过 WPS Office 办公软件,导致 COM 组件被破坏,解决方法为完全卸载 WPS Office 后重新安装 Microsoft Office Mondo 2016 即可解决。
3. 请确保 PowerPoint 和本应用运行在同一权限下,如果 PowerPoint 以管理员身份运行而本应用以普通用户身份运行,也会出现无法切换到 PPT 模式的现象,您可以通过检查 PowerPoint 的兼容性设置或提权本应用运行来解决该问题。
4. 如果上述方法不能解决你的问题,请参考这个链接[【点击此处以跳转】](https://www.inkeys.top/tutorial/ppt-com.html)
### 程序无法正常启动
请检查你的电脑上是否安装了 `.Net Framework 4.7.2` 或更高版本。若没有,请[前往官网](https://dotnet.microsoft.com/zh-cn/download/dotnet-framework/thank-you/net472-offline-installer "下载 .Net Framework 4.7.2")下载安装。
@@ -86,6 +87,10 @@
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CJKmkp"><img src="https://avatars.githubusercontent.com/u/113243675?v=4?s=100" width="100px;" alt="CJK_mkp"/><br /><sub><b>CJK_mkp</b></sub></a><br /><a href="#maintenance-CJKmkp" title="Maintenance">🚧</a> <a href="#doc-CJKmkp" title="Documentation">📖</a> <a href="#code-CJKmkp" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://hydro11451.qzz.io"><img src="https://avatars.githubusercontent.com/u/214308559?v=4?s=100" width="100px;" alt="Hydrogen"/><br /><sub><b>Hydrogen</b></sub></a><br /><a href="#code-Hydro11451" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CreeperAWA"><img src="https://avatars.githubusercontent.com/u/134939494?v=4?s=100" width="100px;" alt="CreeperAWA"/><br /><sub><b>CreeperAWA</b></sub></a><br /><a href="#code-CreeperAWA" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/2-2-3-trimethylpentane"><img src="https://avatars.githubusercontent.com/u/141403762?v=4?s=100" width="100px;" alt="2,2,3-三甲基戊烷"/><br /><sub><b>2,2,3-三甲基戊烷</b></sub></a><br /><a href="#blog-2-2-3-trimethylpentane" title="Blogposts">📝</a> <a href="#doc-2-2-3-trimethylpentane" title="Documentation">📖</a> <a href="#design-2-2-3-trimethylpentane" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Alan-CRL"><img src="https://avatars.githubusercontent.com/u/92425617?v=4?s=100" width="100px;" alt="Alan-CRL"/><br /><sub><b>Alan-CRL</b></sub></a><br /><a href="#code-Alan-CRL" title="Code">💻</a> <a href="#infra-Alan-CRL" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#doc-Alan-CRL" title="Documentation">📖</a> <a href="#financial-Alan-CRL" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MKStoler1024"><img src="https://avatars.githubusercontent.com/u/158786854?v=4?s=100" width="100px;" alt="MKStoler1024"/><br /><sub><b>MKStoler1024</b></sub></a><br /><a href="#doc-MKStoler1024" title="Documentation">📖</a> <a href="#code-MKStoler1024" title="Code">💻</a> <a href="#design-MKStoler1024" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/awesome-iwb"><img src="https://avatars.githubusercontent.com/u/184760810?v=4?s=100" width="100px;" alt="Awesome Iwb"/><br /><sub><b>Awesome Iwb</b></sub></a><br /><a href="#doc-awesome-iwb" title="Documentation">📖</a></td>
</tr>
</tbody>
</table>
+2
View File
@@ -29,3 +29,5 @@
29. 修复墨迹错页
30. 改进直线拉直
31. 改进白板时间显示
32. 修复快速面板中的退出放映按钮
33. 优化多墨迹卡顿问题