feat(Upload/WebDav):迁移Dlass并添加WebDav管理 (#381)

* feat(Upload/Common): 重构上传功能以添加通用设置管理

- 新增UploadSettings类用于管理上传通用设置
- 重构上传逻辑,将延迟上传功能移至UploadHelper
- 在Dlass设置窗口添加通用设置标签页
- 支持多上传提供者管理及取消操作
- 增强文件上传前的验证和错误处理

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* feat(upload): 添加WebDav文件上传支持

- 新增WebDavUploader工具类实现文件上传功能
- 添加WebDavUploadProvider作为上传提供者
- 在设置界面增加WebDav配置选项
- 添加WebDav.Client NuGet包依赖

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* feat(WebDAV): 实现WebDAV上传队列管理

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* feat(Upload): 重命名Dlass设置项为云存储以支持WebDav保存

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* feat(Dlass):迁移Dlass注册位置

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* refactor(Upload): 优化上传逻辑和界面交互

- 修改Dlass标签页检测逻辑,使用Tag属性替代Header
- 限制WebDav上传队列的批量处理大小
- 移除多处上传延迟逻辑,统一在通用设置中配置
- 更新Dlass设置界面提示文本

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* chore:修改窗口命名

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* refactor(upload): 重构上传队列为统一管理架构

重构上传队列系统,引入BaseUploadQueue基类实现通用队列管理逻辑,创建UploadQueueHelper统一管理所有上传队列。将DlassUploadQueue和WebDavUploadQueue重构为继承自BaseUploadQueue的具体实现,简化代码并提高可维护性。修改MainWindow初始化代码以使用新的统一初始化方法。

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* refactor(Upload): 重构上传队列系统,改进错误处理和资源管理

- 将上传队列改为可释放资源,实现IDisposable接口
- 移除硬编码的文件验证逻辑,改为可重写方法
- 改进API客户端,支持取消操作和更好的资源管理
- 优化队列初始化流程,增加错误处理
- 统一上传提供者的队列注册方式
- 改进日志记录和错误信息

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* Update Settings.cs

* refactor(UpLoad/Queue): 移除冗余的上传成功/失败日志记录

优化WebDav上传逻辑,增加目录创建重试机制

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* refactor(MW_Settings): 重构全选复选框状态更新逻辑

将直接设置全选复选框状态的逻辑拆分为两步,先计算所有分类复选框状态,再更新全选复选框,提高代码可读性

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

---------

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
Co-authored-by: CJK_mkp <113243675+CJKmkp@users.noreply.github.com>
This commit is contained in:
doudou0720
2026-02-24 14:08:57 +08:00
committed by GitHub
parent c76021194a
commit 0ad74d9f7f
18 changed files with 2729 additions and 1447 deletions
+246 -131
View File
@@ -95,7 +95,125 @@ namespace Ink_Canvas
//savePathWithName = savePath + @"\" + DateTime.Now.ToString("u").Replace(':', '-') + ".icstk";
savePathWithName = savePath + @"\" + DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss-fff") + ".icstk";
if (Settings.Automation.IsSaveFullPageStrokes)
if (Settings.Automation.IsSaveStrokesAsXML)
{
// XML保存模式 - 检查是否存在多页面墨迹
bool hasMultiplePages = false;
List<StrokeCollection> allPageStrokes = new List<StrokeCollection>();
// 检查PPT放映模式下的多页面墨迹
if (BtnPPTSlideShowEnd.Visibility == Visibility.Visible && _pptManager?.IsConnected == true)
{
hasMultiplePages = true;
var totalSlides = _pptManager.SlidesCount;
var currentSlide = _pptManager.GetCurrentSlideNumber();
for (int i = 1; i <= totalSlides; i++)
{
var slideStrokes = _singlePPTInkManager?.LoadSlideStrokes(i);
if (slideStrokes != null && slideStrokes.Count > 0)
{
allPageStrokes.Add(slideStrokes);
}
else if (i == currentSlide && inkCanvas.Strokes.Count > 0)
{
allPageStrokes.Add(inkCanvas.Strokes.Clone());
}
else
{
allPageStrokes.Add(new StrokeCollection());
}
}
}
// 检查白板模式下的多页面墨迹
else if (currentMode != 0 && WhiteboardTotalCount > 1)
{
hasMultiplePages = true;
for (int i = 1; i <= WhiteboardTotalCount; i++)
{
if (TimeMachineHistories[i] != null)
{
var strokes = ApplyHistoriesToNewStrokeCollection(TimeMachineHistories[i]);
allPageStrokes.Add(strokes);
}
else
{
allPageStrokes.Add(new StrokeCollection());
}
}
}
if (hasMultiplePages && allPageStrokes.Count > 0)
{
// 检查是否是PPT模式
bool isPPTMode = BtnPPTSlideShowEnd.Visibility == Visibility.Visible && _pptManager?.IsConnected == true;
if (isPPTMode)
{
// PPT模式:保存为多个XML文件
string basePath = Path.GetDirectoryName(savePathWithName);
string baseFileName = Path.GetFileNameWithoutExtension(savePathWithName);
int savedCount = 0;
for (int i = 0; i < allPageStrokes.Count; i++)
{
var strokes = allPageStrokes[i];
if (strokes.Count > 0)
{
string pageFileName = Path.Combine(basePath, $"{baseFileName}_Page-{i + 1}.xml");
SaveStrokesAsXML(strokes, pageFileName, false);
savedCount++;
// 异步上传每个XML文件
_ = Task.Run(async () =>
{
try
{
await Helpers.UploadHelper.UploadFileAsync(pageFileName);
}
catch (Exception)
{
}
});
}
}
if (newNotice)
{
Task.Delay(100).ContinueWith(t =>
{
Dispatcher.Invoke(() =>
{
ShowNotification($"多页面XML墨迹成功保存为 {savedCount} 个XML文件");
});
});
}
}
else
{
// 非PPT模式:保存为XML压缩包
string zipFileName = Path.ChangeExtension(savePathWithName, "zip");
SaveMultiPageStrokesAsXMLZip(allPageStrokes, zipFileName, newNotice);
}
}
else
{
// 单页面XML保存
string xmlPath = Path.ChangeExtension(savePathWithName, ".xml");
SaveStrokesAsXML(inkCanvas.Strokes, xmlPath);
if (newNotice)
{
Task.Delay(100).ContinueWith(t =>
{
Dispatcher.Invoke(() =>
{
ShowNotification("墨迹成功保存为XML格式至 " + xmlPath);
});
});
}
}
}
else if (Settings.Automation.IsSaveFullPageStrokes)
{
// 全页面保存模式 - 检查是否存在多页面墨迹
bool hasMultiplePages = false;
@@ -159,9 +277,9 @@ namespace Ink_Canvas
SaveSinglePageStrokesAsImage(savePathWithName, newNotice);
}
}
else if (Settings.Automation.IsSaveStrokesAsXML)
else
{
// XML保存模式 - 检查是否存在多页面墨迹
// 常规保存模式 - 检查是否存在多页面墨迹
bool hasMultiplePages = false;
List<StrokeCollection> allPageStrokes = new List<StrokeCollection>();
@@ -209,99 +327,117 @@ namespace Ink_Canvas
if (hasMultiplePages && allPageStrokes.Count > 0)
{
// 多页面XML保存为压缩包
string zipFileName = Path.ChangeExtension(savePathWithName, "zip");
SaveMultiPageStrokesAsXMLZip(allPageStrokes, zipFileName, newNotice);
}
else
{
// 单页面XML保存
string xmlPath = Path.ChangeExtension(savePathWithName, ".xml");
SaveStrokesAsXML(inkCanvas.Strokes, xmlPath);
if (newNotice)
// 多页面保存为多个icstk文件
string basePath = Path.GetDirectoryName(savePathWithName);
string baseFileName = Path.GetFileNameWithoutExtension(savePathWithName);
for (int i = 0; i < allPageStrokes.Count; i++)
{
Task.Delay(100).ContinueWith(t =>
var strokes = allPageStrokes[i];
if (strokes.Count > 0)
{
Dispatcher.Invoke(() =>
string pageFileName = Path.Combine(basePath, $"{baseFileName}_Page-{i + 1}.icstk");
using (var fs = new FileStream(pageFileName, FileMode.Create))
{
ShowNotification("墨迹成功保存为XML格式至 " + xmlPath);
});
});
}
}
}
else
{
// 常规保存模式 - 仅保存墨迹对象
if (Settings.Automation.IsSaveStrokesAsXML)
{
// 保存为XML格式
string xmlPath = Path.ChangeExtension(savePathWithName, ".xml");
SaveStrokesAsXML(inkCanvas.Strokes, xmlPath);
if (newNotice)
{
Task.Delay(100).ContinueWith(t =>
{
Dispatcher.Invoke(() =>
strokes.Save(fs);
}
// 异步上传每个icstk文件
_ = Task.Run(async () =>
{
ShowNotification("墨迹成功保存为XML格式至 " + xmlPath);
try
{
await Helpers.UploadHelper.UploadFileAsync(pageFileName);
}
catch (Exception)
{
}
});
});
}
}
else
{
// 保存为二进制格式
var fs = new FileStream(savePathWithName, FileMode.Create);
inkCanvas.Strokes.Save(fs);
fs.Close();
if (newNotice)
{
Task.Delay(100).ContinueWith(t =>
{
Dispatcher.Invoke(() =>
{
ShowNotification("墨迹成功保存至 " + savePathWithName);
});
});
}
}
_ = Task.Run(async () =>
{
try
{
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
if (delayMinutes > 0)
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
}
await Helpers.DlassNoteUploader.UploadNoteFileAsync(savePathWithName);
}
catch (Exception)
{
}
});
// 保存元素信息
var elementInfos = new List<CanvasElementInfo>();
foreach (var child in inkCanvas.Children)
{
if (child is Image img && img.Source is BitmapImage bmp)
if (newNotice)
{
elementInfos.Add(new CanvasElementInfo
Task.Delay(100).ContinueWith(t =>
{
Type = "Image",
SourcePath = bmp.UriSource?.LocalPath ?? "",
Left = InkCanvas.GetLeft(img),
Top = InkCanvas.GetTop(img),
Width = img.Width,
Height = img.Height,
Stretch = img.Stretch.ToString()
Dispatcher.Invoke(() =>
{
ShowNotification($"多页面墨迹成功保存为 {allPageStrokes.Count} 个icstk文件");
});
});
}
}
File.WriteAllText(Path.ChangeExtension(savePathWithName, ".elements.json"), JsonConvert.SerializeObject(elementInfos, Newtonsoft.Json.Formatting.Indented));
else
{
// 单页面保存
if (Settings.Automation.IsSaveStrokesAsXML)
{
// 保存为XML格式
string xmlPath = Path.ChangeExtension(savePathWithName, ".xml");
SaveStrokesAsXML(inkCanvas.Strokes, xmlPath);
if (newNotice)
{
Task.Delay(100).ContinueWith(t =>
{
Dispatcher.Invoke(() =>
{
ShowNotification("墨迹成功保存为XML格式至 " + xmlPath);
});
});
}
}
else
{
// 保存为二进制格式
var fs = new FileStream(savePathWithName, FileMode.Create);
inkCanvas.Strokes.Save(fs);
fs.Close();
if (newNotice)
{
Task.Delay(100).ContinueWith(t =>
{
Dispatcher.Invoke(() =>
{
ShowNotification("墨迹成功保存至 " + savePathWithName);
});
});
}
}
// 异步上传文件
_ = Task.Run(async () =>
{
try
{
string uploadPath = Settings.Automation.IsSaveStrokesAsXML ? Path.ChangeExtension(savePathWithName, ".xml") : savePathWithName;
await Helpers.UploadHelper.UploadFileAsync(uploadPath);
}
catch (Exception)
{
}
});
// 保存元素信息
var elementInfos = new List<CanvasElementInfo>();
foreach (var child in inkCanvas.Children)
{
if (child is Image img && img.Source is BitmapImage bmp)
{
elementInfos.Add(new CanvasElementInfo
{
Type = "Image",
SourcePath = bmp.UriSource?.LocalPath ?? "",
Left = InkCanvas.GetLeft(img),
Top = InkCanvas.GetTop(img),
Width = img.Width,
Height = img.Height,
Stretch = img.Stretch.ToString()
});
}
}
string elementsPath = Settings.Automation.IsSaveStrokesAsXML ? Path.ChangeExtension(savePathWithName, ".elements.json") : Path.ChangeExtension(savePathWithName, ".elements.json");
File.WriteAllText(elementsPath, JsonConvert.SerializeObject(elementInfos, Newtonsoft.Json.Formatting.Indented));
}
}
}
catch (Exception ex)
@@ -314,7 +450,7 @@ namespace Ink_Canvas
/// <summary>
/// 将StrokeCollection保存为XML格式
/// </summary>
private void SaveStrokesAsXML(StrokeCollection strokes, string xmlPath)
private void SaveStrokesAsXML(StrokeCollection strokes, string xmlPath, bool triggerUpload = true)
{
try
{
@@ -368,22 +504,19 @@ namespace Ink_Canvas
File.WriteAllText(Path.ChangeExtension(xmlPath, ".elements.json"), JsonConvert.SerializeObject(elementInfos, Newtonsoft.Json.Formatting.Indented));
// 异步上传到Dlass
_ = Task.Run(async () =>
if (triggerUpload)
{
try
_ = Task.Run(async () =>
{
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
if (delayMinutes > 0)
try
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
await Helpers.UploadHelper.UploadFileAsync(xmlPath);
}
await Helpers.DlassNoteUploader.UploadNoteFileAsync(xmlPath);
}
catch (Exception)
{
}
});
catch (Exception)
{
}
});
}
}
catch (Exception ex)
{
@@ -427,9 +560,9 @@ namespace Ink_Canvas
var strokes = allPageStrokes[i];
if (strokes.Count > 0)
{
// 保存XML文件
// 保存XML文件(临时文件,不触发上传)
string xmlFileName = Path.Combine(tempDir, $"page_{i + 1:D4}.xml");
SaveStrokesAsXML(strokes, xmlFileName);
SaveStrokesAsXML(strokes, xmlFileName, false);
}
}
@@ -460,28 +593,22 @@ namespace Ink_Canvas
}
// 创建ZIP文件
if (File.Exists(zipFileName))
File.Delete(zipFileName);
if (File.Exists(zipFileName))
File.Delete(zipFileName);
ZipFile.CreateFromDirectory(tempDir, zipFileName);
ZipFile.CreateFromDirectory(tempDir, zipFileName);
// 异步上传ZIP文件到Dlass
_ = Task.Run(async () =>
// 异步上传ZIP文件到Dlass
_ = Task.Run(async () =>
{
try
{
try
{
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
if (delayMinutes > 0)
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
}
await Helpers.DlassNoteUploader.UploadNoteFileAsync(zipFileName);
}
catch (Exception)
{
}
});
await Helpers.UploadHelper.UploadFileAsync(zipFileName);
}
catch (Exception)
{
}
});
if (newNotice)
{
@@ -587,13 +714,7 @@ namespace Ink_Canvas
{
try
{
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
if (delayMinutes > 0)
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
}
await Helpers.DlassNoteUploader.UploadNoteFileAsync(zipFileName);
await Helpers.UploadHelper.UploadFileAsync(zipFileName);
}
catch (Exception)
{
@@ -696,13 +817,7 @@ namespace Ink_Canvas
{
try
{
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
if (delayMinutes > 0)
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
}
await Helpers.DlassNoteUploader.UploadNoteFileAsync(imagePathWithName);
await Helpers.UploadHelper.UploadFileAsync(imagePathWithName);
}
catch (Exception)
{
+5 -41
View File
@@ -1364,7 +1364,9 @@ namespace Ink_Canvas
{
if (isUpdatingSelectAll) return;
isUpdatingSelectAll = true;
selectAllCheckBox.IsChecked = categoryCheckBoxes.Values.All(cb => cb.IsChecked == true);
// 检查所有分类复选框是否都被勾选
bool allChecked = categoryCheckBoxes.Values.All(cb => cb.IsChecked == true);
selectAllCheckBox.IsChecked = allChecked;
isUpdatingSelectAll = false;
};
checkBox.Unchecked += (s, args) =>
@@ -3388,44 +3390,6 @@ namespace Ink_Canvas
HideSubPanels();
try
{
// 检查是否是第一次打开(检查用户是否已设置Token)
bool hasToken = !string.IsNullOrEmpty(Settings?.Dlass?.UserToken?.Trim());
bool isFirstTime = !hasToken;
if (isFirstTime)
{
// 第一次打开,询问用户是否已注册
var result = MessageBox.Show(
"您是否已经注册了Dlass账号?\n\n" +
"• 如果已注册:将直接打开设置管理页面\n" +
"• 如果未注册:将打开浏览器跳转到注册页面",
"Dlass账号注册",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result == MessageBoxResult.No)
{
// 用户未注册,打开浏览器
try
{
Process.Start(new ProcessStartInfo
{
FileName = "https://dlass.tech/dashboard",
UseShellExecute = true
});
LogHelper.WriteLogToFile("已打开浏览器跳转到Dlass注册页面", LogHelper.LogType.Event);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"打开浏览器时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"无法打开浏览器。请手动访问: https://dlass.tech/dashboard",
"提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
return; // 不打开设置窗口
}
// 如果用户选择"是",继续打开设置窗口
}
// 打开设置管理窗口
var dlassSettingsWindow = new Windows.DlassSettingsWindow();
dlassSettingsWindow.Owner = this;
@@ -3433,8 +3397,8 @@ namespace Ink_Canvas
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"打开Dlass设置管理窗口时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"打开Dlass设置管理窗口时发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
LogHelper.WriteLogToFile($"打开云存储管理窗口时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"打开云存储管理窗口时发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}