Compare commits

..

38 Commits

Author SHA1 Message Date
CJK_mkp 6058cb9cff Merge pull request #104 from InkCanvasForClass/beta
ICC CE 1.7.3.0
2025-07-26 19:38:22 +08:00
CJKmkp 6b20d3e268 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-07-26 19:37:12 +08:00
CJKmkp ce037a437a 更新版本号 2025-07-26 19:36:39 +08:00
2,2,3-三甲基戊烷 1349dab6d4 fix: Update README.md 2025-07-26 19:35:16 +08:00
CJKmkp d3d31925ee Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-07-26 19:34:22 +08:00
CJKmkp d9fa77c86b 更新版本号 2025-07-26 19:34:06 +08:00
CJKmkp dad01235b0 fix:侧边栏中的退出放映按钮退出不完全的问题 2025-07-26 19:30:56 +08:00
2,2,3-三甲基戊烷 52d9c26076 Merge pull request #103 from InkCanvasForClass/all-contributors/add-awesome-iwb
docs: add awesome-iwb as a contributor for doc
2025-07-26 19:26:01 +08:00
allcontributors[bot] 1a0236237d docs: update .all-contributorsrc 2025-07-26 11:25:43 +00:00
allcontributors[bot] 58bb7a5ebc docs: update README.md 2025-07-26 11:25:42 +00:00
CJK_mkp efbab58bca Merge pull request #102 from InkCanvasForClass/all-contributors/add-Alan-CRL
docs: add Alan-CRL as a contributor for financial
2025-07-26 19:20:37 +08:00
2,2,3-三甲基戊烷 eedbc7a863 Merge branch 'beta' into all-contributors/add-Alan-CRL 2025-07-26 19:20:26 +08:00
allcontributors[bot] 626cda63ff docs: update .all-contributorsrc 2025-07-26 11:17:27 +00:00
allcontributors[bot] 7b7a9d93aa docs: update README.md 2025-07-26 11:17:26 +00:00
2,2,3-三甲基戊烷 f5c68dac61 Merge pull request #101 from InkCanvasForClass/all-contributors/add-MKStoler1024
docs: add MKStoler1024 as a contributor for doc, code, and design
2025-07-26 19:16:22 +08:00
allcontributors[bot] 2b6b106771 docs: update .all-contributorsrc 2025-07-26 11:15:47 +00:00
allcontributors[bot] 047586883e docs: update README.md 2025-07-26 11:15:46 +00:00
2,2,3-三甲基戊烷 0c1a25dd6b feat: Merge pull request #100 from InkCanvasForClass/all-contributors/add-Alan-CRL
docs: add Alan-CRL as a contributor for code, infra, and doc
2025-07-26 19:11:45 +08:00
allcontributors[bot] 7840a9a713 docs: update .all-contributorsrc 2025-07-26 11:11:01 +00:00
allcontributors[bot] 28f96ffcd3 docs: update README.md 2025-07-26 11:11:00 +00:00
CJKmkp 04b2663183 improve:墨迹平滑 2025-07-26 19:03:07 +08:00
2,2,3-三甲基戊烷 1a11027871 feat: Merge pull request #99 from InkCanvasForClass/all-contributors/add-2-2-3-trimethylpentane
docs: add 2-2-3-trimethylpentane as a contributor for blog, doc, and design
2025-07-26 19:00:48 +08:00
allcontributors[bot] 854a00803e docs: update .all-contributorsrc 2025-07-26 11:00:10 +00:00
allcontributors[bot] 11ae5f157f docs: update README.md 2025-07-26 11:00:09 +00:00
CJKmkp ecfe05139e improve:插件功能 2025-07-26 18:37:08 +08:00
CJKmkp 4a392e03a7 更新版本号 2025-07-26 16:24:30 +08:00
CJKmkp a66037f886 improve:用户体验分级 2025-07-26 16:22:55 +08:00
CJKmkp 58c399dcbe improve:用户体验分级 2025-07-26 16:08:22 +08:00
CJKmkp f33e617f44 improve:用户体验分级 2025-07-26 15:57:28 +08:00
CJKmkp 3e976c1026 improve:用户体验分级 2025-07-26 15:01:00 +08:00
CJKmkp c15c75075c improve:用户体验分级 2025-07-26 15:00:31 +08:00
CJKmkp bf8d988c75 improve:用户体验分级 2025-07-26 14:58:10 +08:00
CJKmkp 6fb7af3d46 add:用户体验分级 2025-07-26 14:29:24 +08:00
CJKmkp 1c2860c180 更新版本号 2025-07-26 12:41:44 +08:00
CJKmkp fc41f10c37 improve:自动更新 2025-07-26 12:37:39 +08:00
CJKmkp dedf366866 improve:自动更新 2025-07-26 12:33:08 +08:00
CJKmkp edb10096d6 improve:自动更新 2025-07-26 12:20:23 +08:00
CJKmkp ba42a1e6c9 improve:自动更新 2025-07-26 12:02:58 +08:00
23 changed files with 4227 additions and 167 deletions
+43
View File
@@ -36,6 +36,49 @@
"contributions": [ "contributions": [
"code" "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())); 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; bool ret;
mutex = new System.Threading.Mutex(true, "InkCanvasForClass", out ret); mutex = new System.Threading.Mutex(true, "InkCanvasForClass", out ret);
@@ -710,6 +716,17 @@ namespace Ink_Canvas
string exitType = IsAppExitByUser ? "用户主动退出" : "应用程序退出"; string exitType = IsAppExitByUser ? "用户主动退出" : "应用程序退出";
WriteCrashLog($"{exitType},退出代码: {e.ApplicationExitCode}"); 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) 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 // 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.2.0")] [assembly: AssemblyVersion("1.7.3.0")]
[assembly: AssemblyFileVersion("1.7.2.0")] [assembly: AssemblyFileVersion("1.7.3.0")]
+430 -12
View File
@@ -1,37 +1,409 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Ink; using System.Windows.Ink;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Threading;
namespace Ink_Canvas.Helpers namespace Ink_Canvas.Helpers
{ {
/// <summary> /// <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> /// </summary>
public class AdvancedBezierSmoothing public class AdvancedBezierSmoothing
{ {
public double SmoothingStrength { get; set; } = 0.8; public double SmoothingStrength { get; set; } = 0.3;
public double ResampleInterval { get; set; } = 0.8; public double ResampleInterval { get; set; } = 3.0;
public int InterpolationSteps { get; set; } = 64; public int InterpolationSteps { get; set; } = 4;
public Stroke SmoothStroke(Stroke stroke) public Stroke SmoothStroke(Stroke stroke)
{ {
if (stroke == null || stroke.StylusPoints.Count < 2) if (stroke == null || stroke.StylusPoints.Count < 3)
return stroke; return stroke;
var originalPoints = stroke.StylusPoints.ToList(); var originalPoints = stroke.StylusPoints.ToList();
var smoothedPoints = ApplyExponentialSmoothing(originalPoints, SmoothingStrength);
var resampledPoints = ResampleEquidistant(smoothedPoints, ResampleInterval); // 简化处理:只进行轻度平滑
var interpolatedPoints = SlidingBezierFit(resampledPoints, 4, 24); var smoothedPoints = ApplyLightExponentialSmoothing(originalPoints, 0.2); // 很轻的平滑
var finalPoints = ApplyExponentialSmoothing(interpolatedPoints, 0.5); // 二次平滑
var ultraSmoothPoints = SlidingWindowSmooth(finalPoints, 7); // 滑动窗口平滑 // 检查点数是否合理
var smoothedStroke = new Stroke(new StylusPointCollection(ultraSmoothPoints)) if (smoothedPoints.Count > originalPoints.Count * 1.5)
{
return stroke; // 如果点数增加太多,返回原始笔画
}
var smoothedStroke = new Stroke(new StylusPointCollection(smoothedPoints))
{ {
DrawingAttributes = stroke.DrawingAttributes.Clone() DrawingAttributes = stroke.DrawingAttributes.Clone()
}; };
return smoothedStroke; 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) private List<StylusPoint> ApplyExponentialSmoothing(List<StylusPoint> points, double alpha)
{ {
var result = new List<StylusPoint>(); var result = new List<StylusPoint>();
@@ -141,4 +513,50 @@ namespace Ink_Canvas.Helpers
return result; 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秒 // 定义超时时间为10秒
private static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(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; private static string statusFilePath = null;
// 线路组结构体(包含版本、下载、日志地址) // 线路组结构体(包含版本、下载、日志地址)
@@ -62,6 +62,11 @@ namespace Ink_Canvas.Helpers
{ {
GroupName = "智教联盟", GroupName = "智教联盟",
DownloadUrlFormat = "https://get.smart-teach.cn/d/Ningbo-S3/shared/jiangling/community/InkCanvasForClass.CE.{0}.zip", 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 = "智教联盟", GroupName = "智教联盟",
DownloadUrlFormat = "https://get.smart-teach.cn/d/Ningbo-S3/shared/jiangling/community-beta/InkCanvasForClass.CE.{0}.zip", 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) foreach (var group in groups)
{ {
// 跳过智教联盟线路组,不参与延迟检测和排序 // 跳过"智教联盟"和"inkeys"线路组,不参与延迟检测和排序
if (group.GroupName == "智教联盟") if (group.GroupName == "智教联盟" || group.GroupName == "inkeys")
{ {
LogHelper.WriteLogToFile($"AutoUpdate | 跳过智教联盟线路组延迟检测"); LogHelper.WriteLogToFile($"AutoUpdate | 跳过{group.GroupName}线路组延迟检测");
continue; continue;
} }
LogHelper.WriteLogToFile($"AutoUpdate | 检测线路组: {group.GroupName} ({group.VersionUrl})"); LogHelper.WriteLogToFile($"AutoUpdate | 检测线路组: {group.GroupName} ({group.VersionUrl})");
@@ -194,7 +204,7 @@ namespace Ink_Canvas.Helpers
.Select(x => x.group) .Select(x => x.group)
.ToList(); .ToList();
// 将智教联盟线路组插入到最前面(如果存在) // 将"智教联盟"线路组插入到最前面(如果存在)
var zhiJiaoGroup = groups.FirstOrDefault(g => g.GroupName == "智教联盟"); var zhiJiaoGroup = groups.FirstOrDefault(g => g.GroupName == "智教联盟");
if (zhiJiaoGroup != null) if (zhiJiaoGroup != null)
{ {
@@ -202,6 +212,14 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"AutoUpdate | 智教联盟线路组已插入到首位"); 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) if (orderedGroups.Count > 0)
{ {
LogHelper.WriteLogToFile($"AutoUpdate | 找到 {orderedGroups.Count} 个可用线路组,按延迟排序:"); 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信息 // 通过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 try
{ {
@@ -393,28 +452,42 @@ namespace Ink_Canvas.Helpers
string version = json["tag_name"]?.ToString(); string version = json["tag_name"]?.ToString();
string releaseNotes = json["body"]?.ToString(); string releaseNotes = json["body"]?.ToString();
string downloadUrl = json["assets"]?.First?["browser_download_url"]?.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)) if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(downloadUrl))
return (version, downloadUrl, releaseNotes); return (version, downloadUrl, releaseNotes, releaseTime);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
LogHelper.WriteLogToFile($"AutoUpdate | GitHub Releases API 获取失败: {ex.Message}", LogHelper.LogType.Warning); LogHelper.WriteLogToFile($"AutoUpdate | GitHub Releases API 获取失败: {ex.Message}", LogHelper.LogType.Warning);
} }
return (null, null, null); return (null, null, null, null);
} }
// 主要的更新检测方法(优先检测延迟,失败时自动切换线路组) // 主要的更新检测方法(优先检测延迟,失败时自动切换线路组)
// 仅检测新版本时用GitHub API,实际下载时只用线路组 // 仅检测新版本时用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 try
{ {
// 记录更新检查时间
DeviceIdentifier.RecordUpdateCheck();
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 | 设备ID: {DeviceIdentifier.GetDeviceId()}");
LogHelper.WriteLogToFile($"AutoUpdate | 更新优先级: {DeviceIdentifier.GetUpdatePriority()}");
LogHelper.WriteLogToFile($"AutoUpdate | 优先通过GitHub Releases API检测..."); LogHelper.WriteLogToFile($"AutoUpdate | 优先通过GitHub Releases API检测...");
// 1. 优先通过GitHub Releases API获取 // 1. 优先通过GitHub Releases API获取
var (apiVersion, _, apiReleaseNotes) = await GetLatestGithubRelease(channel); var (apiVersion, _, apiReleaseNotes, apiReleaseTime) = await GetLatestGithubRelease(channel);
if (!string.IsNullOrEmpty(apiVersion)) if (!string.IsNullOrEmpty(apiVersion))
{ {
Version local = new Version(localVersion); Version local = new Version(localVersion);
@@ -422,15 +495,42 @@ namespace Ink_Canvas.Helpers
if (remote > local || alwaysGetRemote) if (remote > local || alwaysGetRemote)
{ {
LogHelper.WriteLogToFile($"AutoUpdate | 通过GitHub Releases API发现新版本: {apiVersion}"); 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(); var availableGroup = (await GetAvailableLineGroupsOrdered(channel)).FirstOrDefault();
return (apiVersion, group, apiReleaseNotes); return (apiVersion, availableGroup, apiReleaseNotes);
} }
else else
{ {
LogHelper.WriteLogToFile($"AutoUpdate | 当前版本已是最新 (GitHub Releases API)"); LogHelper.WriteLogToFile($"AutoUpdate | 当前版本已是最新 (GitHub Releases API)");
var group = (await GetAvailableLineGroupsOrdered(channel)).FirstOrDefault(); var availableGroup = (await GetAvailableLineGroupsOrdered(channel)).FirstOrDefault();
return (null, group, apiReleaseNotes); return (null, availableGroup, apiReleaseNotes);
} }
} }
// 2. 回退到原有txt方案 // 2. 回退到原有txt方案
@@ -453,6 +553,27 @@ namespace Ink_Canvas.Helpers
if (remote > local || alwaysGetRemote) if (remote > local || alwaysGetRemote)
{ {
LogHelper.WriteLogToFile($"AutoUpdate | 发现新版本或强制获取: {remoteVersion}"); 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); return (remoteVersion, group, null);
} }
else else
@@ -547,10 +668,21 @@ namespace Ink_Canvas.Helpers
// 优先尝试“智教联盟”线路组 // 优先尝试“智教联盟”线路组
var zhiJiaoGroup = groups.FirstOrDefault(g => g.GroupName == "智教联盟"); 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(); var priorityGroups = new List<UpdateLineGroup>();
LogHelper.WriteLogToFile($"AutoUpdate | 下载时优先尝试智教联盟线路组"); 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; url = realUrl;
LogHelper.WriteLogToFile($"AutoUpdate | 智教联盟真实下载地址: {url}"); LogHelper.WriteLogToFile($"AutoUpdate | 智教联盟真实下载地址: {url}");
} }
// inkeys线路组直接使用下载地址,无需特殊处理
else if (group.GroupName == "inkeys")
{
LogHelper.WriteLogToFile($"AutoUpdate | 使用inkeys线路组下载地址: {url}");
}
LogHelper.WriteLogToFile($"AutoUpdate | 尝试从线路组 {group.GroupName} 下载: {url}"); LogHelper.WriteLogToFile($"AutoUpdate | 尝试从线路组 {group.GroupName} 下载: {url}");
bool downloadSuccess = await DownloadFile(url, zipFilePath, progressCallback); bool downloadSuccess = await DownloadFile(url, zipFilePath, progressCallback);
@@ -611,7 +748,8 @@ namespace Ink_Canvas.Helpers
{ {
LogHelper.WriteLogToFile($"AutoUpdate | 正在尝试多线程下载: {fileUrl}"); LogHelper.WriteLogToFile($"AutoUpdate | 正在尝试多线程下载: {fileUrl}");
int maxRetry = 3; int maxRetry = 3;
int[] threadOptions = new int[] { 32, 4 }; // 降低并发数,减少网络压力
int[] threadOptions = new int[] { 32, 16, 8, 4, 1 };
// 检查服务器是否支持Range分块下载 // 检查服务器是否支持Range分块下载
bool supportRange = false; bool supportRange = false;
@@ -655,46 +793,14 @@ namespace Ink_Canvas.Helpers
{ {
LogHelper.WriteLogToFile($"AutoUpdate | 服务器不支持分块下载,自动降级为单线程下载"); LogHelper.WriteLogToFile($"AutoUpdate | 服务器不支持分块下载,自动降级为单线程下载");
progressCallback?.Invoke(0, "服务器不支持分块下载,自动降级为单线程下载"); progressCallback?.Invoke(0, "服务器不支持分块下载,自动降级为单线程下载");
try return await DownloadSingleThread(fileUrl, destinationPath, totalSize, progressCallback);
{
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;
}
} }
foreach (int threadCount in threadOptions) foreach (int threadCount in threadOptions)
{ {
LogHelper.WriteLogToFile($"AutoUpdate | 尝试使用 {threadCount} 线程下载");
progressCallback?.Invoke(0, $"尝试使用 {threadCount} 线程下载");
if (totalSize <= 0) if (totalSize <= 0)
{ {
totalSize = await GetContentLength(fileUrl); totalSize = await GetContentLength(fileUrl);
@@ -704,19 +810,28 @@ namespace Ink_Canvas.Helpers
progressCallback?.Invoke(0, "无法获取文件大小,取消下载"); progressCallback?.Invoke(0, "无法获取文件大小,取消下载");
return false; 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); int blockCount = (int)Math.Ceiling((double)totalSize / blockSize);
LogHelper.WriteLogToFile($"AutoUpdate | 文件大小: {totalSize}, 分块数: {blockCount}, 分块大小: {blockSize}");
var blockQueue = new System.Collections.Concurrent.ConcurrentQueue<BlockTask>(); var blockQueue = new System.Collections.Concurrent.ConcurrentQueue<BlockTask>();
var finishedBlocks = new System.Collections.Concurrent.ConcurrentDictionary<int, bool>(); var finishedBlocks = new System.Collections.Concurrent.ConcurrentDictionary<int, bool>();
long[] blockDownloaded = new long[blockCount]; long[] blockDownloaded = new long[blockCount];
for (int i = 0; i < blockCount; i++) for (int i = 0; i < blockCount; i++)
{ {
long start = i * blockSize; long start = i * blockSize;
long end = Math.Min(start + blockSize - 1, totalSize - 1); long end = Math.Min(start + blockSize - 1, totalSize - 1);
blockQueue.Enqueue(new BlockTask { Index = i, Start = start, End = end, RetryCount = 0 }); blockQueue.Enqueue(new BlockTask { Index = i, Start = start, End = end, RetryCount = 0 });
} }
CancellationTokenSource cts = new CancellationTokenSource(); CancellationTokenSource cts = new CancellationTokenSource();
var tasks = new List<Task>(); var tasks = new List<Task>();
for (int t = 0; t < threadCount; t++) for (int t = 0; t < threadCount; t++)
{ {
tasks.Add(Task.Run(async () => tasks.Add(Task.Run(async () =>
@@ -724,6 +839,8 @@ namespace Ink_Canvas.Helpers
while (blockQueue.TryDequeue(out var block)) while (blockQueue.TryDequeue(out var block))
{ {
bool success = false; bool success = false;
string tempPath = destinationPath + $".part{block.Index}";
for (int retry = block.RetryCount; retry < maxRetry && !success; retry++) for (int retry = block.RetryCount; retry < maxRetry && !success; retry++)
{ {
try try
@@ -734,7 +851,9 @@ namespace Ink_Canvas.Helpers
var req = new HttpRequestMessage(HttpMethod.Get, fileUrl); var req = new HttpRequestMessage(HttpMethod.Get, fileUrl);
req.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(block.Start, block.End); 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 downloadCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token);
var lastReadTime = DateTime.UtcNow; var lastReadTime = DateTime.UtcNow;
bool dataReceived = false; bool dataReceived = false;
@@ -743,31 +862,33 @@ namespace Ink_Canvas.Helpers
{ {
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index} 响应状态: {resp.StatusCode}"); LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index} 响应状态: {resp.StatusCode}");
resp.EnsureSuccessStatusCode(); resp.EnsureSuccessStatusCode();
string tempPath = destinationPath + $".part{block.Index}";
using (var fs = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None)) using (var fs = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None))
{ {
var stream = await resp.Content.ReadAsStreamAsync(); var stream = await resp.Content.ReadAsStreamAsync();
byte[] buffer = new byte[8192]; byte[] buffer = new byte[8192];
int read; int read;
long blockDownloadedBytes = 0;
while (true) while (true)
{ {
var readTask = stream.ReadAsync(buffer, 0, buffer.Length, downloadCts.Token); 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); var completed = await Task.WhenAny(readTask, timeoutTask);
if (completed == timeoutTask) if (completed == timeoutTask)
{ {
// 超时未收到数据,取消本线程,重新入队 LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index} 20秒无数据,线程超时重试", LogHelper.LogType.Warning);
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index} 15秒无数据,线程超时重试", LogHelper.LogType.Warning); progressCallback?.Invoke(0, $"分块{block.Index} 20秒无数据,线程超时重试");
progressCallback?.Invoke(0, $"分块{block.Index} 15秒无数据,线程超时重试");
downloadCts.Cancel(); downloadCts.Cancel();
break; break;
} }
read = await readTask; read = await readTask;
if (read <= 0) break; if (read <= 0) break;
await fs.WriteAsync(buffer, 0, read, downloadCts.Token); await fs.WriteAsync(buffer, 0, read, downloadCts.Token);
blockDownloaded[block.Index] += read; blockDownloadedBytes += read;
blockDownloaded[block.Index] = blockDownloadedBytes;
lastReadTime = DateTime.UtcNow; lastReadTime = DateTime.UtcNow;
dataReceived = true; dataReceived = true;
// 合并所有块进度 // 合并所有块进度
long totalDownloaded = blockDownloaded.Sum(); long totalDownloaded = blockDownloaded.Sum();
double percent = (double)totalDownloaded / totalSize * 100; double percent = (double)totalDownloaded / totalSize * 100;
@@ -775,19 +896,37 @@ namespace Ink_Canvas.Helpers
} }
} }
} }
// 如果因超时break且未完成,success为false,重新入队
if (!dataReceived) if (!dataReceived)
{ {
throw new IOException("分块下载超时无数据"); 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; success = true;
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载成功");
} }
catch (Exception ex) when (ex is HttpRequestException || ex is IOException || ex is TaskCanceledException) 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); LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载失败,第{retry + 1}次: {ex.Message}", LogHelper.LogType.Warning);
progressCallback?.Invoke(0, $"分块{block.Index}下载失败,第{retry + 1}次: {ex.Message}"); 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) if (success)
@@ -803,76 +942,161 @@ namespace Ink_Canvas.Helpers
else else
{ {
// 超过最大重试,取消所有任务 // 超过最大重试,取消所有任务
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}超过最大重试次数,取消下载", LogHelper.LogType.Error);
cts.Cancel(); cts.Cancel();
break; break;
} }
} }
})); }));
} }
await Task.WhenAll(tasks); await Task.WhenAll(tasks);
if (cts.IsCancellationRequested || finishedBlocks.Count != blockCount) 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++) for (int i = 0; i < blockCount; i++)
{ {
string tempPath = destinationPath + $".part{i}"; string tempPath = destinationPath + $".part{i}";
if (File.Exists(tempPath)) File.Delete(tempPath); if (File.Exists(tempPath)) File.Delete(tempPath);
} }
if (threadCount == threadOptions.Last()) if (threadCount == threadOptions.Last())
{ {
// 已经是最后一次尝试 // 已经是最后一次尝试,降级为单线程
return false; LogHelper.WriteLogToFile($"AutoUpdate | 所有多线程尝试失败,降级为单线程下载");
progressCallback?.Invoke(0, "所有多线程尝试失败,降级为单线程下载");
return await DownloadSingleThread(fileUrl, destinationPath, totalSize, progressCallback);
} }
else else
{ {
LogHelper.WriteLogToFile($"AutoUpdate | {threadCount}线程下载失败,尝试降级为{threadOptions.Last()}线程"); LogHelper.WriteLogToFile($"AutoUpdate | {threadCount}线程下载失败,尝试降级为{threadOptions[Array.IndexOf(threadOptions, threadCount) + 1]}线程");
progressCallback?.Invoke(0, $"{threadCount}线程下载失败,尝试降级为{threadOptions.Last()}线程"); progressCallback?.Invoke(0, $"{threadCount}线程下载失败,尝试降级为{threadOptions[Array.IndexOf(threadOptions, threadCount) + 1]}线程");
continue; 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}"; for (int i = 0; i < blockCount; i++)
using (var input = new FileStream(tempPath, FileMode.Open, FileAccess.Read))
{ {
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) 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
{ {
System.IO.Compression.ZipFile.OpenRead(destinationPath).Dispose(); LogHelper.WriteLogToFile($"AutoUpdate | 文件大小校验失败,本地:{fileInfo.Length},服务器:{totalSize}", LogHelper.LogType.Error);
}
catch
{
LogHelper.WriteLogToFile("AutoUpdate | ZIP文件解压测试失败,文件可能已损坏", LogHelper.LogType.Error);
File.Delete(destinationPath); File.Delete(destinationPath);
progressCallback?.Invoke(0, "ZIP文件解压测试失败,已删除损坏文件"); progressCallback?.Invoke(0, "文件大小校验失败,已删除损坏文件");
return false; 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; 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) private static async Task<long> GetContentLength(string fileUrl)
{ {
@@ -1214,8 +1438,8 @@ 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, 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);
@@ -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>
/// 启动手动指定版本的多线路多线程下载并自动安装(用于历史版本回滚等场景) /// 启动手动指定版本的多线路多线程下载并自动安装(用于历史版本回滚等场景)
/// </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" <UserControl x:Class="Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher.LauncherSettingsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher" xmlns:local="clr-namespace:Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="500" d:DesignWidth="600"> 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 Margin="10">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
@@ -16,12 +64,12 @@
</Grid.RowDefinitions> </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"> <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 Margin="10,0,0,0">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/> <ColumnDefinition Width="120"/>
@@ -30,12 +78,12 @@
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </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"> <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="RbtnLeft" Content="浮动栏左侧" Margin="0,0,20,0" Checked="RbtnPosition_Checked" Foreground="Black"/>
<RadioButton x:Name="RbtnRight" Content="浮动栏右侧" IsChecked="True" Checked="RbtnPosition_Checked"/> <RadioButton x:Name="RbtnRight" Content="浮动栏右侧" IsChecked="True" Checked="RbtnPosition_Checked" Foreground="Black"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
</StackPanel> </StackPanel>
@@ -47,7 +95,7 @@
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
</Grid.RowDefinitions> </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"> <Border Grid.Row="1" BorderThickness="1" BorderBrush="#CCCCCC" CornerRadius="5">
<Grid> <Grid>
@@ -71,9 +119,15 @@
<!-- 操作按钮 --> <!-- 操作按钮 -->
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="5"> <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="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"/> Background="#FF007ACC" Foreground="White" BorderBrush="#FF005A9B" BorderThickness="1"
<Button x:Name="BtnDelete" Content="删除" Padding="10,5" Margin="5" Click="BtnDelete_Click"/> 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> </StackPanel>
</Grid> </Grid>
</Border> </Border>
@@ -81,7 +135,9 @@
<!-- 底部按钮 --> <!-- 底部按钮 -->
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,15,0,0"> <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> </StackPanel>
</Grid> </Grid>
</UserControl> </UserControl>
+32
View File
@@ -2594,6 +2594,38 @@
<TextBlock x:Name="AppVersionTextBlock" FontSize="18" FontWeight="Bold" <TextBlock x:Name="AppVersionTextBlock" FontSize="18" FontWeight="Bold"
Text="1.X.X.X" /> Text="1.X.X.X" />
</ui:SimpleStackPanel> </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 <TextBlock
Text="# 使用和分发本软件前,请您应当且务必知晓相关开源协议,且您应当知晓本软件基于 https://github.com/WXRIW/Ink-Canvas 修改而成。" Text="# 使用和分发本软件前,请您应当且务必知晓相关开源协议,且您应当知晓本软件基于 https://github.com/WXRIW/Ink-Canvas 修改而成。"
TextWrapping="Wrap" Foreground="#a1a1aa" /> TextWrapping="Wrap" Foreground="#a1a1aa" />
+99
View File
@@ -110,6 +110,9 @@ namespace Ink_Canvas {
CheckColorTheme(true); CheckColorTheme(true);
CheckPenTypeUIState(); CheckPenTypeUIState();
// 初始化墨迹平滑管理器
_inkSmoothingManager = new Helpers.InkSmoothingManager(Dispatcher);
// 注册输入事件 // 注册输入事件
inkCanvas.PreviewMouseDown += inkCanvas_PreviewMouseDown; inkCanvas.PreviewMouseDown += inkCanvas_PreviewMouseDown;
inkCanvas.StylusDown += inkCanvas_StylusDown; inkCanvas.StylusDown += inkCanvas_StylusDown;
@@ -182,6 +185,7 @@ namespace Ink_Canvas {
private System.Windows.Media.Color Ink_DefaultColor = Colors.Red; private System.Windows.Media.Color Ink_DefaultColor = Colors.Red;
private DrawingAttributes drawingAttributes; private DrawingAttributes drawingAttributes;
private Helpers.InkSmoothingManager _inkSmoothingManager;
private void loadPenCanvas() { private void loadPenCanvas() {
try { try {
@@ -854,6 +858,8 @@ namespace Ink_Canvas {
{ {
// 切换到关于页面 // 切换到关于页面
ShowSettingsSection("about"); ShowSettingsSection("about");
// 刷新设备信息
RefreshDeviceInfo();
} }
// 新增:个性化设置 // 新增:个性化设置
@@ -881,6 +887,99 @@ namespace Ink_Canvas {
BorderSettingsMask.Background = null; // 确保清除蒙层背景 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) 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); LogHelper.WriteLogToFile($"结束PPT放映操作异常: {ex.ToString()}", LogHelper.LogType.Error);
} }
await Application.Current.Dispatcher.InvokeAsync(() => { await Application.Current.Dispatcher.InvokeAsync(() => {
// 隐藏侧边栏退出按钮
if (BtnExitPptFromSidebarLeft != null) if (BtnExitPptFromSidebarLeft != null)
BtnExitPptFromSidebarLeft.Visibility = Visibility.Collapsed; BtnExitPptFromSidebarLeft.Visibility = Visibility.Collapsed;
if (BtnExitPptFromSidebarRight != null) if (BtnExitPptFromSidebarRight != null)
BtnExitPptFromSidebarRight.Visibility = Visibility.Collapsed; 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) { private void ToggleSwitchAdvancedBezierSmoothing_Toggled(object sender, RoutedEventArgs e) {
if (!isLoaded) return; if (!isLoaded) return;
Settings.Canvas.UseAdvancedBezierSmoothing = ToggleSwitchAdvancedBezierSmoothing.IsOn; Settings.Canvas.UseAdvancedBezierSmoothing = ToggleSwitchAdvancedBezierSmoothing.IsOn;
// 启用高级贝塞尔平滑时自动禁用原来的FitToCurve // 启用高级贝塞尔平滑时自动禁用原来的FitToCurve
if (ToggleSwitchAdvancedBezierSmoothing.IsOn) if (ToggleSwitchAdvancedBezierSmoothing.IsOn)
{ {
@@ -1306,9 +1306,61 @@ namespace Ink_Canvas {
Settings.Canvas.FitToCurve = false; Settings.Canvas.FitToCurve = false;
drawingAttributes.FitToCurve = false; drawingAttributes.FitToCurve = false;
} }
// 更新墨迹平滑管理器配置
_inkSmoothingManager?.UpdateConfig();
SaveSettingsToFile(); 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) { private void ToggleSwitchAutoSaveStrokesInPowerPoint_Toggled(object sender, RoutedEventArgs e) {
if (!isLoaded) return; if (!isLoaded) return;
@@ -538,6 +538,23 @@ namespace Ink_Canvas {
ToggleSwitchAdvancedBezierSmoothing.IsOn = false; ToggleSwitchAdvancedBezierSmoothing.IsOn = false;
drawingAttributes.FitToCurve = 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; ToggleSwitchAutoStraightenLine.IsOn = Settings.Canvas.AutoStraightenLine;
+8 -9
View File
@@ -1579,17 +1579,16 @@ namespace Ink_Canvas {
{ {
try try
{ {
var advancedSmoothing = new Helpers.AdvancedBezierSmoothing
{
};
// 对临时笔画应用平滑 // 对临时笔画应用平滑
if (lastTempStroke != null) if (lastTempStroke != null && _inkSmoothingManager != null)
{ {
var smoothedStroke = advancedSmoothing.SmoothStroke(lastTempStroke); var smoothedStroke = _inkSmoothingManager.SmoothStroke(lastTempStroke);
inkCanvas.Strokes.Remove(lastTempStroke); if (smoothedStroke != lastTempStroke)
lastTempStroke = smoothedStroke; {
inkCanvas.Strokes.Add(smoothedStroke); inkCanvas.Strokes.Remove(lastTempStroke);
lastTempStroke = smoothedStroke;
inkCanvas.Strokes.Add(smoothedStroke);
}
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -2,6 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Ink; using System.Windows.Ink;
@@ -587,18 +588,27 @@ namespace Ink_Canvas {
// 检查原始笔画是否仍然存在于画布中 // 检查原始笔画是否仍然存在于画布中
if (inkCanvas.Strokes.Contains(e.Stroke)) 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); if (smoothedStroke != e.Stroke)
{
// 替换原始笔画 // 替换原始笔画
SetNewBackupOfStroke(); SetNewBackupOfStroke();
_currentCommitType = CommitReason.ShapeRecognition; _currentCommitType = CommitReason.ShapeRecognition;
inkCanvas.Strokes.Remove(e.Stroke); inkCanvas.Strokes.Remove(e.Stroke);
inkCanvas.Strokes.Add(smoothedStroke); inkCanvas.Strokes.Add(smoothedStroke);
_currentCommitType = CommitReason.UserInput; _currentCommitType = CommitReason.UserInput;
}
}
} }
} }
catch (Exception ex) 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 // New method: Checks if a stroke is potentially a straight line
private bool IsPotentialStraightLine(Stroke stroke) { 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 // 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.2.0")] [assembly: AssemblyVersion("1.7.3.0")]
[assembly: AssemblyFileVersion("1.7.2.0")] [assembly: AssemblyFileVersion("1.7.3.0")]
+8
View File
@@ -50,6 +50,14 @@ namespace Ink_Canvas
public bool FitToCurve { get; set; } = false; // 默认关闭原来的贝塞尔平滑 public bool FitToCurve { get; set; } = false; // 默认关闭原来的贝塞尔平滑
[JsonProperty("useAdvancedBezierSmoothing")] [JsonProperty("useAdvancedBezierSmoothing")]
public bool UseAdvancedBezierSmoothing { get; set; } = true; // 默认启用高级贝塞尔曲线平滑 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")] [JsonProperty("clearCanvasAndClearTimeMachine")]
public bool ClearCanvasAndClearTimeMachine { get; set; } = false; public bool ClearCanvasAndClearTimeMachine { get; set; } = false;
[JsonProperty("enablePressureTouchMode")] [JsonProperty("enablePressureTouchMode")]
+13 -13
View File
@@ -54,7 +54,7 @@
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
</Grid.RowDefinitions> </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" <ListView Grid.Row="1" x:Name="PluginListView" BorderThickness="0" Margin="0,5,0,0"
SelectionChanged="PluginListView_SelectionChanged"> SelectionChanged="PluginListView_SelectionChanged">
@@ -101,17 +101,17 @@
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="名称:" FontWeight="SemiBold" 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"/> <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"/> <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"/> <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"/> <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"/> <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"/> <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"/> <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="删除插件" <Button Grid.Row="3" Grid.Column="2" x:Name="BtnDeletePlugin" Content="删除插件"
Visibility="Collapsed" Click="BtnDeletePlugin_Click" 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"> <Border Grid.Row="1" BorderThickness="1" BorderBrush="{DynamicResource BorderBrush}" Margin="0,10,0,0" Padding="15" CornerRadius="5">
<StackPanel> <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"/> <ContentControl x:Name="PluginSettingsContainer" MinHeight="50"/>
</StackPanel> </StackPanel>
</Border> </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 E:\ICC CE\ICC CE main\community\Ink Canvas\App.xaml
22-2143008179 22-2143008179
76-141727233 79-461684434
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\HistoryRollbackWindow.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;
+6 -1
View File
@@ -29,7 +29,7 @@
使用该版本 InkCanvasForClass,意味着您同意自行承担任何可能存在的问题与风险。建议不要在公众场合(例如公开课,录播课,线上直播课,大型会议)使用未经广泛测试和优化的 Beta 版本,对使用 Beta 版本而带来的任何问题和风险(例如:被班主任批斗,被校长处罚,崩溃而导致的场面混乱,全球海平面上升等),**将由使用者自行承担**,[CJKmkp](https://github,com/CJK-mkp) 和 [DotteringDoge471](https://github.com/DotteringDoge471) 及其项目的所有维护者不提供任何担保。 使用该版本 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 板块管理条约的情况下,在该板块内提问或发表自己的使用体验。 **智教联盟 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. 在“信息”标签内,点击右侧的“启用编辑”按钮。
2. 曾经安装过 WPS Office 办公软件,导致 COM 组件被破坏,解决方法为完全卸载 WPS Office 后重新安装 Microsoft Office Mondo 2016 即可解决。 2. 曾经安装过 WPS Office 办公软件,导致 COM 组件被破坏,解决方法为完全卸载 WPS Office 后重新安装 Microsoft Office Mondo 2016 即可解决。
3. 请确保 PowerPoint 和本应用运行在同一权限下,如果 PowerPoint 以管理员身份运行而本应用以普通用户身份运行,也会出现无法切换到 PPT 模式的现象,您可以通过检查 PowerPoint 的兼容性设置或提权本应用运行来解决该问题。 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")下载安装。 请检查你的电脑上是否安装了 `.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="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="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/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> </tr>
</tbody> </tbody>
</table> </table>
+2
View File
@@ -29,3 +29,5 @@
29. 修复墨迹错页 29. 修复墨迹错页
30. 改进直线拉直 30. 改进直线拉直
31. 改进白板时间显示 31. 改进白板时间显示
32. 修复快速面板中的退出放映按钮
33. 优化多墨迹卡顿问题