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
+604
View File
@@ -0,0 +1,604 @@
using Newtonsoft.Json;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 上传队列项数据(用于序列化)
/// </summary>
public class UploadQueueItemData
{
[JsonProperty("file_path")]
public string FilePath { get; set; }
[JsonProperty("retry_count")]
public int RetryCount { get; set; }
[JsonProperty("added_time")]
public DateTime AddedTime { get; set; }
}
/// <summary>
/// 上传队列项
/// </summary>
public class UploadQueueItem
{
public string FilePath { get; set; }
public int RetryCount { get; set; }
}
/// <summary>
/// 通用上传队列基类
/// </summary>
public abstract class BaseUploadQueue : IDisposable
{
protected const int BATCH_SIZE = 10; // 批量上传大小
protected const int MAX_RETRY_COUNT = 3; // 最大重试次数
/// <summary>
/// 上传队列
/// </summary>
protected readonly ConcurrentQueue<UploadQueueItem> _uploadQueue = new ConcurrentQueue<UploadQueueItem>();
/// <summary>
/// 队列处理锁,防止并发处理
/// </summary>
protected readonly SemaphoreSlim _queueProcessingLock = new SemaphoreSlim(1, 1);
/// <summary>
/// 队列保存锁,防止并发保存
/// </summary>
protected readonly SemaphoreSlim _queueSaveLock = new SemaphoreSlim(1, 1);
/// <summary>
/// 是否已初始化队列
/// </summary>
protected bool _isQueueInitialized = false;
/// <summary>
/// 是否已释放资源
/// </summary>
private bool _disposed = false;
/// <summary>
/// 队列文件名
/// </summary>
protected abstract string QueueFileName { get; }
/// <summary>
/// 允许的文件扩展名
/// </summary>
protected virtual HashSet<string> AllowedExtensions => new HashSet<string> { ".png", ".icstk", ".xml", ".zip" };
/// <summary>
/// 获取队列文件路径
/// </summary>
protected string GetQueueFilePath()
{
var configsDir = Path.Combine(App.RootPath, "Configs");
if (!Directory.Exists(configsDir))
{
Directory.CreateDirectory(configsDir);
}
return Path.Combine(configsDir, QueueFileName);
}
/// <summary>
/// 获取最大文件大小
/// </summary>
/// <param name="extension">文件扩展名</param>
/// <returns>最大文件大小(字节)</returns>
protected virtual long GetMaxFileSize(string extension)
{
return extension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
}
/// <summary>
/// 初始化上传队列
/// </summary>
public void InitializeQueue()
{
if (_isQueueInitialized)
{
return;
}
try
{
var queueFilePath = GetQueueFilePath();
if (!File.Exists(queueFilePath))
{
_isQueueInitialized = true;
return;
}
var jsonContent = File.ReadAllText(queueFilePath);
if (string.IsNullOrWhiteSpace(jsonContent))
{
_isQueueInitialized = true;
return;
}
var queueData = JsonConvert.DeserializeObject<List<UploadQueueItemData>>(jsonContent);
if (queueData == null || queueData.Count == 0)
{
_isQueueInitialized = true;
return;
}
int restoredCount = 0;
int skippedCount = 0;
foreach (var item in queueData)
{
// 验证文件是否存在
if (!File.Exists(item.FilePath))
{
skippedCount++;
continue;
}
// 验证文件格式和大小
if (!IsValidFile(item.FilePath))
{
skippedCount++;
continue;
}
// 恢复队列项
_uploadQueue.Enqueue(new UploadQueueItem
{
FilePath = item.FilePath,
RetryCount = item.RetryCount
});
restoredCount++;
}
_isQueueInitialized = true;
if (restoredCount > 0)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 已恢复上传队列:{restoredCount}个文件,跳过{skippedCount}个无效文件", LogHelper.LogType.Event);
// 如果恢复了队列,触发处理
_ = Task.Run(async () =>
{
try
{
await ProcessUploadQueueAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 恢复上传队列后处理时出错: {ex}", LogHelper.LogType.Error);
}
});
}
else if (skippedCount > 0)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 队列恢复完成:跳过{skippedCount}个无效文件", LogHelper.LogType.Event);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 恢复上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
_isQueueInitialized = true; // 即使出错也标记为已初始化,避免重复尝试
}
}
/// <summary>
/// 保存队列到文件
/// </summary>
protected async Task SaveQueueToFileAsync(CancellationToken cancellationToken = default)
{
if (!await _queueSaveLock.WaitAsync(1000, cancellationToken)) // 最多等待1秒
{
return; // 如果无法获取锁,跳过保存(避免阻塞)
}
try
{
cancellationToken.ThrowIfCancellationRequested();
var queueData = new List<UploadQueueItemData>();
// 将队列转换为可序列化的格式
foreach (var item in _uploadQueue)
{
cancellationToken.ThrowIfCancellationRequested();
queueData.Add(new UploadQueueItemData
{
FilePath = item.FilePath,
RetryCount = item.RetryCount,
AddedTime = DateTime.Now
});
}
var queueFilePath = GetQueueFilePath();
// 如果队列为空,清空文件
if (queueData.Count == 0)
{
ClearQueueFile();
return;
}
var jsonContent = JsonConvert.SerializeObject(queueData, Formatting.Indented);
// 使用进程保护的写入门控,避免安全面板中"进程文件保护"占用导致无法写入
var tempFilePath = queueFilePath + ".tmp";
ProcessProtectionManager.WithWriteAccess(queueFilePath, () =>
{
File.WriteAllText(tempFilePath, jsonContent);
if (File.Exists(queueFilePath))
File.Delete(queueFilePath);
File.Move(tempFilePath, queueFilePath);
});
}
catch (OperationCanceledException)
{
// 取消操作,静默处理
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 保存上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
}
finally
{
_queueSaveLock.Release();
}
}
/// <summary>
/// 清空队列文件
/// </summary>
protected void ClearQueueFile()
{
try
{
var queueFilePath = GetQueueFilePath();
ProcessProtectionManager.WithWriteAccess(queueFilePath, () =>
{
if (File.Exists(queueFilePath))
File.WriteAllText(queueFilePath, "[]");
});
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 清空队列文件时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 将文件加入上传队列
/// </summary>
protected void EnqueueFile(string filePath, int retryCount = 0, CancellationToken cancellationToken = default)
{
_uploadQueue.Enqueue(new UploadQueueItem
{
FilePath = filePath,
RetryCount = retryCount
});
// 异步保存队列到文件
_ = Task.Run(async () =>
{
try
{
cancellationToken.ThrowIfCancellationRequested();
await SaveQueueToFileAsync(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// 取消操作,静默处理
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 保存上传队列时出错(后台任务): {ex}", LogHelper.LogType.Error);
}
}, cancellationToken);
// 触发队列处理
_ = ProcessUploadQueueAsync(cancellationToken);
}
/// <summary>
/// 处理上传队列,批量上传文件
/// </summary>
protected async Task ProcessUploadQueueAsync(CancellationToken cancellationToken = default)
{
// 使用信号量防止并发处理
if (!await _queueProcessingLock.WaitAsync(0, cancellationToken))
{
return; // 已有处理任务在运行
}
try
{
cancellationToken.ThrowIfCancellationRequested();
var filesToUpload = new List<UploadQueueItem>();
// 从队列中取出最多BATCH_SIZE个文件
int count = 0;
while (count < BATCH_SIZE && _uploadQueue.TryDequeue(out UploadQueueItem item))
{
cancellationToken.ThrowIfCancellationRequested();
// 再次检查文件是否存在
if (File.Exists(item.FilePath) && IsValidFile(item.FilePath))
{
filesToUpload.Add(item);
count++;
}
}
if (filesToUpload.Count == 0)
{
return;
}
// 检查是否启用
if (!IsUploadEnabled())
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败:上传未启用", LogHelper.LogType.Error);
// 将文件重新加入队列
foreach (var item in filesToUpload)
{
EnqueueFile(item.FilePath, item.RetryCount, cancellationToken);
}
return;
}
// 并发上传所有文件,并处理失败重试
var uploadTasks = filesToUpload.Select(async item =>
{
try
{
cancellationToken.ThrowIfCancellationRequested();
var success = await UploadFileInternalAsync(item.FilePath, cancellationToken);
if (!success)
{
// 检查是否是可重试的错误
if (IsRetryableError(item.FilePath))
{
// 检查重试次数
if (item.RetryCount < MAX_RETRY_COUNT)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败,将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event);
EnqueueFile(item.FilePath, item.RetryCount + 1, cancellationToken);
}
else
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
}
}
}
return success;
}
catch (OperationCanceledException)
{
// 取消操作,将文件重新加入队列
EnqueueFile(item.FilePath, item.RetryCount, cancellationToken);
throw;
}
catch (Exception ex)
{
// 检查是否是可重试的错误(超时、网络错误等)
var errorMessage = ex.Message.ToLower();
bool isRetryable = errorMessage.Contains("超时") ||
errorMessage.Contains("timeout") ||
errorMessage.Contains("网络错误") ||
errorMessage.Contains("network") ||
errorMessage.Contains("408") || // 请求超时
errorMessage.Contains("423") || // 资源锁定
errorMessage.Contains("429") || // 请求过多
errorMessage.Contains("500") || // 服务器错误
errorMessage.Contains("502") || // 网关错误
errorMessage.Contains("503") || // 服务不可用
errorMessage.Contains("504"); // 网关超时
if (isRetryable && IsRetryableError(item.FilePath))
{
// 检查重试次数
if (item.RetryCount < MAX_RETRY_COUNT)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败({ex.Message}),将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
EnqueueFile(item.FilePath, item.RetryCount + 1, cancellationToken);
}
else
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
}
}
else
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败(不可重试): {Path.GetFileName(item.FilePath)} - {ex.Message}", LogHelper.LogType.Error);
}
return false;
}
});
await Task.WhenAll(uploadTasks).ConfigureAwait(false);
// 上传完成后保存队列状态
await SaveQueueToFileAsync(cancellationToken).ConfigureAwait(false);
// 检查队列中是否还有文件,如果有就继续处理
if (_uploadQueue.Count > 0)
{
_ = Task.Run(async () =>
{
try
{
await ProcessUploadQueueAsync(cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 继续处理上传队列时出错: {ex}", LogHelper.LogType.Error);
}
}, cancellationToken);
}
}
finally
{
_queueProcessingLock.Release();
}
}
/// <summary>
/// 验证文件是否有效
/// </summary>
protected virtual bool IsValidFile(string filePath)
{
try
{
var fileExtension = Path.GetExtension(filePath).ToLower();
if (!AllowedExtensions.Contains(fileExtension))
{
return false;
}
var fileInfo = new FileInfo(filePath);
long maxSize = GetMaxFileSize(fileExtension);
if (fileInfo.Length > maxSize)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败:文件过大({fileInfo.Length / 1024 / 1024:F2}MB),超过{maxSize / 1024 / 1024}MB限制", LogHelper.LogType.Error);
return false;
}
return true;
}
catch
{
return false;
}
}
/// <summary>
/// 判断错误是否可重试
/// </summary>
protected bool IsRetryableError(string filePath)
{
// 检查文件是否存在
if (!File.Exists(filePath))
{
return false; // 文件不存在,不可重试
}
// 检查文件是否有效
if (!IsValidFile(filePath))
{
return false; // 文件无效,不可重试
}
// 检查是否启用
if (!IsUploadEnabled())
{
return false; // 上传未启用,不可重试
}
// 其他错误(超时、网络错误等)可以重试
return true;
}
/// <summary>
/// 检查上传是否启用
/// </summary>
protected abstract bool IsUploadEnabled();
/// <summary>
/// 内部上传方法,执行实际上传操作
/// </summary>
protected abstract Task<bool> UploadFileInternalAsync(string filePath, CancellationToken cancellationToken);
/// <summary>
/// 异步上传文件
/// </summary>
public async Task<bool> UploadFileAsync(string filePath, CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
// 检查是否启用
if (!IsUploadEnabled())
{
return false;
}
// 基本验证
if (!File.Exists(filePath))
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error);
return false;
}
if (!IsValidFile(filePath))
{
return false;
}
// 确保队列已初始化
if (!_isQueueInitialized)
{
InitializeQueue();
}
// 加入队列
EnqueueFile(filePath, 0, cancellationToken);
return true;
}
catch (OperationCanceledException)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传被取消: {Path.GetFileName(filePath)}", LogHelper.LogType.Event);
throw;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 加入上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// 释放资源
/// </summary>
/// <param name="disposing">是否手动释放</param>
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_queueProcessingLock?.Dispose();
_queueSaveLock?.Dispose();
}
_disposed = true;
}
}
/// <summary>
/// 析构函数
/// </summary>
~BaseUploadQueue()
{
Dispose(false);
}
}
}
+68 -39
View File
@@ -4,6 +4,7 @@ using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers
@@ -54,7 +55,8 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 获取访问令牌(Access Token
/// </summary>
public async Task<string> GetAccessTokenAsync()
/// <param name="cancellationToken">取消令牌</param>
public async Task<string> GetAccessTokenAsync(CancellationToken cancellationToken = default)
{
if (!string.IsNullOrEmpty(_userToken))
{
@@ -68,6 +70,8 @@ namespace Ink_Canvas.Helpers
try
{
cancellationToken.ThrowIfCancellationRequested();
var requestData = new
{
app_id = _appId,
@@ -78,7 +82,7 @@ namespace Ink_Canvas.Helpers
var json = JsonConvert.SerializeObject(requestData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("/oauth/token", content);
var response = await _httpClient.PostAsync("/oauth/token", content, cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
@@ -93,14 +97,14 @@ namespace Ink_Canvas.Helpers
throw new Exception($"获取Access Token失败: {response.StatusCode}");
}
}
catch (OperationCanceledException)
{
throw;
}
catch (HttpRequestException httpEx)
{
throw new Exception($"获取Access Token时网络错误: {httpEx.Message}", httpEx);
}
catch (TaskCanceledException timeoutEx)
{
throw new Exception("获取Access Token时请求超时", timeoutEx);
}
catch (Exception ex)
{
throw new Exception($"获取Access Token时出错: {ex.Message}", ex);
@@ -110,14 +114,19 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 发送GET请求
/// </summary>
public async Task<T> GetAsync<T>(string endpoint, bool requireAuth = true)
/// <param name="endpoint">API端点</param>
/// <param name="requireAuth">是否需要认证</param>
/// <param name="cancellationToken">取消令牌</param>
public async Task<T> GetAsync<T>(string endpoint, bool requireAuth = true, CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync();
token = await GetAccessTokenAsync(cancellationToken);
}
var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
@@ -134,7 +143,7 @@ namespace Ink_Canvas.Helpers
}
}
var response = await _httpClient.SendAsync(request);
var response = await _httpClient.SendAsync(request, cancellationToken);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
@@ -150,14 +159,14 @@ namespace Ink_Canvas.Helpers
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
catch (OperationCanceledException)
{
throw;
}
catch (HttpRequestException httpEx)
{
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
}
catch (TaskCanceledException timeoutEx)
{
throw new Exception($"请求超时: {endpoint}", timeoutEx);
}
catch (Exception ex)
{
throw new Exception($"发送请求时出错: {ex.Message}", ex);
@@ -167,14 +176,20 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 发送POST请求
/// </summary>
public async Task<T> PostAsync<T>(string endpoint, object data = null, bool requireAuth = true)
/// <param name="endpoint">API端点</param>
/// <param name="data">请求数据</param>
/// <param name="requireAuth">是否需要认证</param>
/// <param name="cancellationToken">取消令牌</param>
public async Task<T> PostAsync<T>(string endpoint, object data = null, bool requireAuth = true, CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync();
token = await GetAccessTokenAsync(cancellationToken);
}
var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
@@ -197,7 +212,7 @@ namespace Ink_Canvas.Helpers
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.SendAsync(request);
var response = await _httpClient.SendAsync(request, cancellationToken);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
@@ -213,14 +228,14 @@ namespace Ink_Canvas.Helpers
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
catch (OperationCanceledException)
{
throw;
}
catch (HttpRequestException httpEx)
{
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
}
catch (TaskCanceledException timeoutEx)
{
throw new Exception($"请求超时: {endpoint}", timeoutEx);
}
catch (Exception ex)
{
throw new Exception($"发送请求时出错: {ex.Message}", ex);
@@ -230,14 +245,20 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 发送PUT请求
/// </summary>
public async Task<T> PutAsync<T>(string endpoint, object data = null, bool requireAuth = true)
/// <param name="endpoint">API端点</param>
/// <param name="data">请求数据</param>
/// <param name="requireAuth">是否需要认证</param>
/// <param name="cancellationToken">取消令牌</param>
public async Task<T> PutAsync<T>(string endpoint, object data = null, bool requireAuth = true, CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync();
token = await GetAccessTokenAsync(cancellationToken);
}
var request = new HttpRequestMessage(HttpMethod.Put, endpoint);
@@ -261,7 +282,7 @@ namespace Ink_Canvas.Helpers
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.SendAsync(request);
var response = await _httpClient.SendAsync(request, cancellationToken);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
@@ -277,14 +298,14 @@ namespace Ink_Canvas.Helpers
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
catch (OperationCanceledException)
{
throw;
}
catch (HttpRequestException httpEx)
{
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
}
catch (TaskCanceledException timeoutEx)
{
throw new Exception($"请求超时: {endpoint}", timeoutEx);
}
catch (Exception ex)
{
throw new Exception($"发送请求时出错: {ex.Message}", ex);
@@ -294,14 +315,19 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 发送DELETE请求
/// </summary>
public async Task<bool> DeleteAsync(string endpoint, bool requireAuth = true)
/// <param name="endpoint">API端点</param>
/// <param name="requireAuth">是否需要认证</param>
/// <param name="cancellationToken">取消令牌</param>
public async Task<bool> DeleteAsync(string endpoint, bool requireAuth = true, CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync();
token = await GetAccessTokenAsync(cancellationToken);
}
var request = new HttpRequestMessage(HttpMethod.Delete, endpoint);
@@ -319,7 +345,7 @@ namespace Ink_Canvas.Helpers
}
}
var response = await _httpClient.SendAsync(request);
var response = await _httpClient.SendAsync(request, cancellationToken);
if (response.IsSuccessStatusCode)
{
@@ -330,11 +356,11 @@ namespace Ink_Canvas.Helpers
return false;
}
}
catch (HttpRequestException)
catch (OperationCanceledException)
{
return false;
throw;
}
catch (TaskCanceledException)
catch (HttpRequestException)
{
return false;
}
@@ -354,10 +380,13 @@ namespace Ink_Canvas.Helpers
/// <param name="title">笔记标题(可选)</param>
/// <param name="description">笔记描述(可选)</param>
/// <param name="tags">笔记标签(可选)</param>
public async Task<T> UploadNoteAsync<T>(string endpoint, string filePath, string boardId, string secretKey, string title = null, string description = null, string tags = null)
/// <param name="cancellationToken">取消令牌</param>
public async Task<T> UploadNoteAsync<T>(string endpoint, string filePath, string boardId, string secretKey, string title = null, string description = null, string tags = null, CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"文件不存在: {filePath}");
@@ -394,7 +423,7 @@ namespace Ink_Canvas.Helpers
request.Content = content;
var response = await _httpClient.SendAsync(request);
var response = await _httpClient.SendAsync(request, cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
@@ -410,14 +439,14 @@ namespace Ink_Canvas.Helpers
throw new Exception($"上传文件失败: {response.StatusCode} - {responseContent}");
}
}
catch (OperationCanceledException)
{
throw;
}
catch (HttpRequestException httpEx)
{
throw new Exception($"上传文件时网络错误: {httpEx.Message}", httpEx);
}
catch (TaskCanceledException timeoutEx)
{
throw new Exception($"上传文件超时: {endpoint}", timeoutEx);
}
catch (Exception ex)
{
throw new Exception($"上传文件时出错: {ex.Message}", ex);
-807
View File
@@ -1,807 +0,0 @@
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// Dlass笔记自动上传辅助类
/// </summary>
public class DlassNoteUploader
{
private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA";
private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA";
private const int BATCH_SIZE = 10; // 批量上传大小
private const int MAX_RETRY_COUNT = 3; // 最大重试次数
private const string QUEUE_FILE_NAME = "DlassUploadQueue.json";
/// <summary>
/// 上传队列项
/// </summary>
private class UploadQueueItemData
{
[JsonProperty("file_path")]
public string FilePath { get; set; }
[JsonProperty("retry_count")]
public int RetryCount { get; set; }
[JsonProperty("added_time")]
public DateTime AddedTime { get; set; }
}
/// <summary>
/// 上传队列项
/// </summary>
private class UploadQueueItem
{
public string FilePath { get; set; }
public int RetryCount { get; set; }
}
/// <summary>
/// 上传队列
/// </summary>
private static readonly ConcurrentQueue<UploadQueueItem> _uploadQueue = new ConcurrentQueue<UploadQueueItem>();
/// <summary>
/// 队列处理锁,防止并发处理
/// </summary>
private static readonly SemaphoreSlim _queueProcessingLock = new SemaphoreSlim(1, 1);
/// <summary>
/// 队列保存锁,防止并发保存
/// </summary>
private static readonly SemaphoreSlim _queueSaveLock = new SemaphoreSlim(1, 1);
/// <summary>
/// 是否已初始化队列
/// </summary>
private static bool _isQueueInitialized = false;
/// <summary>
/// 获取队列文件路径
/// </summary>
private static string GetQueueFilePath()
{
var configsDir = Path.Combine(App.RootPath, "Configs");
if (!Directory.Exists(configsDir))
{
Directory.CreateDirectory(configsDir);
}
return Path.Combine(configsDir, QUEUE_FILE_NAME);
}
/// <summary>
/// 初始化上传队列
/// </summary>
public static void InitializeQueue()
{
if (_isQueueInitialized)
{
return;
}
try
{
var queueFilePath = GetQueueFilePath();
if (!File.Exists(queueFilePath))
{
_isQueueInitialized = true;
return;
}
var jsonContent = File.ReadAllText(queueFilePath);
if (string.IsNullOrWhiteSpace(jsonContent))
{
_isQueueInitialized = true;
return;
}
var queueData = JsonConvert.DeserializeObject<List<UploadQueueItemData>>(jsonContent);
if (queueData == null || queueData.Count == 0)
{
_isQueueInitialized = true;
return;
}
int restoredCount = 0;
int skippedCount = 0;
foreach (var item in queueData)
{
// 验证文件是否存在
if (!File.Exists(item.FilePath))
{
skippedCount++;
continue;
}
// 验证文件格式和大小
var fileExtension = Path.GetExtension(item.FilePath).ToLower();
if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".xml" && fileExtension != ".zip")
{
skippedCount++;
continue;
}
try
{
var fileInfo = new FileInfo(item.FilePath);
long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
if (fileInfo.Length > maxSize)
{
skippedCount++;
continue;
}
}
catch
{
skippedCount++;
continue;
}
// 恢复队列项
_uploadQueue.Enqueue(new UploadQueueItem
{
FilePath = item.FilePath,
RetryCount = item.RetryCount
});
restoredCount++;
}
_isQueueInitialized = true;
if (restoredCount > 0)
{
LogHelper.WriteLogToFile($"已恢复上传队列:{restoredCount}个文件,跳过{skippedCount}个无效文件", LogHelper.LogType.Event);
// 如果恢复了队列,触发处理
_ = Task.Run(async () =>
{
try
{
await ProcessUploadQueueAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"恢复上传队列后处理时出错: {ex}", LogHelper.LogType.Error);
}
});
}
else if (skippedCount > 0)
{
LogHelper.WriteLogToFile($"队列恢复完成:跳过{skippedCount}个无效文件", LogHelper.LogType.Event);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"恢复上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
_isQueueInitialized = true; // 即使出错也标记为已初始化,避免重复尝试
}
}
/// <summary>
/// 保存队列到文件
/// </summary>
private static async Task SaveQueueToFileAsync()
{
if (!await _queueSaveLock.WaitAsync(1000)) // 最多等待1秒
{
return; // 如果无法获取锁,跳过保存(避免阻塞)
}
try
{
var queueData = new List<UploadQueueItemData>();
// 将队列转换为可序列化的格式
foreach (var item in _uploadQueue)
{
queueData.Add(new UploadQueueItemData
{
FilePath = item.FilePath,
RetryCount = item.RetryCount,
AddedTime = DateTime.Now
});
}
var queueFilePath = GetQueueFilePath();
// 如果队列为空,清空文件
if (queueData.Count == 0)
{
ClearQueueFile();
return;
}
var jsonContent = JsonConvert.SerializeObject(queueData, Formatting.Indented);
// 使用进程保护的写入门控,避免安全面板中“进程文件保护”占用导致无法写入
var tempFilePath = queueFilePath + ".tmp";
ProcessProtectionManager.WithWriteAccess(queueFilePath, () =>
{
File.WriteAllText(tempFilePath, jsonContent);
if (File.Exists(queueFilePath))
File.Delete(queueFilePath);
File.Move(tempFilePath, queueFilePath);
});
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
}
finally
{
_queueSaveLock.Release();
}
}
/// <summary>
/// 清空队列文件
/// </summary>
private static void ClearQueueFile()
{
try
{
var queueFilePath = GetQueueFilePath();
ProcessProtectionManager.WithWriteAccess(queueFilePath, () =>
{
if (File.Exists(queueFilePath))
File.WriteAllText(queueFilePath, "[]");
});
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清空队列文件时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 上传笔记响应模型
/// </summary>
public class UploadNoteResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
[JsonProperty("note_id")]
public int? NoteId { get; set; }
[JsonProperty("filename")]
public string Filename { get; set; }
[JsonProperty("file_path")]
public string FilePath { get; set; }
[JsonProperty("file_url")]
public string FileUrl { get; set; }
}
/// <summary>
/// 白板信息模型(用于查找白板)
/// </summary>
private class WhiteboardInfo
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("board_id")]
public string BoardId { get; set; }
[JsonProperty("secret_key")]
public string SecretKey { get; set; }
[JsonProperty("class_name")]
public string ClassName { get; set; }
[JsonProperty("class_id")]
public int ClassId { get; set; }
}
/// <summary>
/// 认证响应模型
/// </summary>
private class AuthWithTokenResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("whiteboards")]
public List<WhiteboardInfo> Whiteboards { get; set; }
}
/// <summary>
/// 异步上传笔记文件到Dlass(支持PNG、ICSTK、XML和ZIP格式)
/// </summary>
/// <param name="filePath">文件路径(支持PNG、ICSTK、XML和ZIP</param>
/// <returns>是否成功加入队列(不等待实际上传完成)</returns>
public static async Task<bool> UploadNoteFileAsync(string filePath)
{
try
{
// 检查是否启用自动上传
if (MainWindow.Settings?.Dlass?.IsAutoUploadNotes != true)
{
return false;
}
// 基本验证
if (!File.Exists(filePath))
{
LogHelper.WriteLogToFile($"上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error);
return false;
}
var fileExtension = Path.GetExtension(filePath).ToLower();
if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".xml" && fileExtension != ".zip")
{
return false;
}
var fileInfo = new FileInfo(filePath);
long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
if (fileInfo.Length > maxSize)
{
LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过{maxSize / 1024 / 1024}MB限制", LogHelper.LogType.Error);
return false;
}
// 获取上传延迟时间(分钟)
var delayMinutes = MainWindow.Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
// 如果设置了延迟时间,在后台任务中等待后再加入队列
if (delayMinutes > 0)
{
_ = Task.Run(async () =>
{
try
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes)).ConfigureAwait(false);
if (MainWindow.Settings?.Dlass?.IsAutoUploadNotes != true)
{
LogHelper.WriteLogToFile($"延迟结束后自动上传已关闭,跳过入队: {filePath}", LogHelper.LogType.Event);
return;
}
if (!File.Exists(filePath))
{
LogHelper.WriteLogToFile($"延迟结束后文件已不存在,跳过入队: {filePath}", LogHelper.LogType.Event);
return;
}
EnqueueFile(filePath);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"延迟加入上传队列时出错: {ex}", LogHelper.LogType.Error);
}
});
}
else
{
EnqueueFile(filePath);
}
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加入上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 将文件加入上传队列
/// </summary>
private static void EnqueueFile(string filePath, int retryCount = 0)
{
_uploadQueue.Enqueue(new UploadQueueItem
{
FilePath = filePath,
RetryCount = retryCount
});
// 异步保存队列到文件
_ = Task.Run(async () =>
{
try
{
await SaveQueueToFileAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存上传队列时出错(后台任务): {ex}", LogHelper.LogType.Error);
}
});
// 如果队列达到批量大小,触发批量上传
if (_uploadQueue.Count >= BATCH_SIZE)
{
_ = ProcessUploadQueueAsync();
}
}
/// <summary>
/// 处理上传队列,批量上传文件
/// </summary>
private static async Task ProcessUploadQueueAsync()
{
// 使用信号量防止并发处理
if (!await _queueProcessingLock.WaitAsync(0))
{
return; // 已有处理任务在运行
}
try
{
var filesToUpload = new List<UploadQueueItem>();
// 从队列中取出最多BATCH_SIZE个文件
while (filesToUpload.Count < BATCH_SIZE && _uploadQueue.TryDequeue(out UploadQueueItem item))
{
// 再次检查文件是否存在
if (File.Exists(item.FilePath))
{
filesToUpload.Add(item);
}
}
if (filesToUpload.Count == 0)
{
return;
}
// 获取共享的白板信息(同一批次的所有文件共享认证信息)
WhiteboardInfo sharedWhiteboard = null;
string apiBaseUrl = null;
string userToken = null;
try
{
var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName;
if (string.IsNullOrEmpty(selectedClassName))
{
LogHelper.WriteLogToFile("上传失败:未选择班级", LogHelper.LogType.Error);
// 将文件重新加入队列
foreach (var item in filesToUpload)
{
EnqueueFile(item.FilePath, item.RetryCount);
}
return;
}
userToken = MainWindow.Settings?.Dlass?.UserToken;
if (string.IsNullOrEmpty(userToken))
{
LogHelper.WriteLogToFile("上传失败:未设置用户Token", LogHelper.LogType.Error);
// 将文件重新加入队列
foreach (var item in filesToUpload)
{
EnqueueFile(item.FilePath, item.RetryCount);
}
return;
}
apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
// 获取白板信息(只获取一次,所有文件共享)
using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken))
{
var authData = new
{
app_id = APP_ID,
app_secret = APP_SECRET,
user_token = userToken
};
var authResult = await apiClient.PostAsync<AuthWithTokenResponse>("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false);
if (authResult == null || !authResult.Success || authResult.Whiteboards == null)
{
LogHelper.WriteLogToFile("上传失败:无法获取白板信息", LogHelper.LogType.Error);
// 将文件重新加入队列
foreach (var item in filesToUpload)
{
EnqueueFile(item.FilePath, item.RetryCount);
}
return;
}
sharedWhiteboard = authResult.Whiteboards
.FirstOrDefault(w => !string.IsNullOrEmpty(w.ClassName) && w.ClassName == selectedClassName);
if (sharedWhiteboard == null || string.IsNullOrEmpty(sharedWhiteboard.BoardId) || string.IsNullOrEmpty(sharedWhiteboard.SecretKey))
{
LogHelper.WriteLogToFile($"上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error);
// 将文件重新加入队列
foreach (var item in filesToUpload)
{
EnqueueFile(item.FilePath, item.RetryCount);
}
return;
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"批量上传获取白板信息时出错: {ex.Message}", LogHelper.LogType.Error);
// 将文件重新加入队列
foreach (var item in filesToUpload)
{
EnqueueFile(item.FilePath, item.RetryCount);
}
return;
}
// 并发上传所有文件(共享白板信息),并处理失败重试
var uploadTasks = filesToUpload.Select(async item =>
{
try
{
var success = await UploadFileInternalAsync(item.FilePath, sharedWhiteboard, apiBaseUrl, userToken);
if (!success)
{
// 检查是否是可重试的错误
if (IsRetryableError(item.FilePath))
{
// 检查重试次数
if (item.RetryCount < MAX_RETRY_COUNT)
{
LogHelper.WriteLogToFile($"上传失败,将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event);
EnqueueFile(item.FilePath, item.RetryCount + 1);
}
else
{
LogHelper.WriteLogToFile($"上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
}
}
}
return success;
}
catch (Exception ex)
{
// 检查是否是可重试的错误(超时、网络错误等)
var errorMessage = ex.Message.ToLower();
bool isRetryable = errorMessage.Contains("超时") ||
errorMessage.Contains("timeout") ||
errorMessage.Contains("网络错误") ||
errorMessage.Contains("network");
if (isRetryable && IsRetryableError(item.FilePath))
{
// 检查重试次数
if (item.RetryCount < MAX_RETRY_COUNT)
{
LogHelper.WriteLogToFile($"上传失败({ex.Message}),将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event);
EnqueueFile(item.FilePath, item.RetryCount + 1);
}
else
{
LogHelper.WriteLogToFile($"上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
}
}
return false;
}
});
await Task.WhenAll(uploadTasks);
// 上传完成后保存队列状态
await SaveQueueToFileAsync();
// 如果队列达到批量大小,继续处理
if (_uploadQueue.Count >= BATCH_SIZE)
{
_ = Task.Run(async () =>
{
try
{
await ProcessUploadQueueAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"继续批量处理上传队列时出错: {ex}", LogHelper.LogType.Error);
}
});
}
}
finally
{
_queueProcessingLock.Release();
}
}
/// <summary>
/// 内部上传方法,执行实际上传操作
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="whiteboard">白板信息(如果为null则重新获取)</param>
/// <param name="apiBaseUrl">API基础URL(如果为null则从设置获取)</param>
/// <param name="userToken">用户Token(如果为null则从设置获取)</param>
private static async Task<bool> UploadFileInternalAsync(string filePath, WhiteboardInfo whiteboard = null, string apiBaseUrl = null, string userToken = null)
{
try
{
// 再次检查文件是否存在(可能在队列等待时被删除)
if (!File.Exists(filePath))
{
return false;
}
// 检查文件扩展名
var fileExtension = Path.GetExtension(filePath).ToLower();
if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".xml" && fileExtension != ".zip")
{
return false;
}
// 检查文件大小(最大10MB,ZIP文件可能更大,允许50MB)
var fileInfo = new FileInfo(filePath);
long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
if (fileInfo.Length > maxSize)
{
LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过{maxSize / 1024 / 1024}MB限制", LogHelper.LogType.Error);
return false;
}
// 如果白板信息未提供,则重新获取
if (whiteboard == null)
{
var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName;
if (string.IsNullOrEmpty(selectedClassName))
{
LogHelper.WriteLogToFile("上传失败:未选择班级", LogHelper.LogType.Error);
return false;
}
userToken = userToken ?? MainWindow.Settings?.Dlass?.UserToken;
if (string.IsNullOrEmpty(userToken))
{
LogHelper.WriteLogToFile("上传失败:未设置用户Token", LogHelper.LogType.Error);
return false;
}
apiBaseUrl = apiBaseUrl ?? MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
// 创建API客户端并获取白板信息
using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken))
{
var authData = new
{
app_id = APP_ID,
app_secret = APP_SECRET,
user_token = userToken
};
var authResult = await apiClient.PostAsync<AuthWithTokenResponse>("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false);
if (authResult == null || !authResult.Success || authResult.Whiteboards == null)
{
LogHelper.WriteLogToFile("上传失败:无法获取白板信息", LogHelper.LogType.Error);
return false;
}
// 查找匹配班级的白板
whiteboard = authResult.Whiteboards
.FirstOrDefault(w => !string.IsNullOrEmpty(w.ClassName) && w.ClassName == selectedClassName);
if (whiteboard == null || string.IsNullOrEmpty(whiteboard.BoardId) || string.IsNullOrEmpty(whiteboard.SecretKey))
{
LogHelper.WriteLogToFile($"上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error);
return false;
}
}
}
// 获取API基础URL和用户Token(如果未提供)
apiBaseUrl = apiBaseUrl ?? MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
userToken = userToken ?? MainWindow.Settings?.Dlass?.UserToken;
// 准备上传参数
var fileName = Path.GetFileNameWithoutExtension(filePath);
var title = fileName;
string fileType;
string tags;
if (fileExtension == ".zip")
{
fileType = "多页面墨迹压缩包";
tags = "自动上传,多页面,zip,压缩包";
}
else if (fileExtension == ".icstk")
{
fileType = "墨迹文件";
tags = "自动上传,墨迹,icstk";
}
else if (fileExtension == ".xml")
{
fileType = "XML文件";
tags = "自动上传,xml";
}
else
{
fileType = "笔记";
tags = "自动上传,笔记,png";
}
var description = $"自动上传的{fileType} - {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
// 创建API客户端并上传文件
using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken))
{
var uploadResult = await apiClient.UploadNoteAsync<UploadNoteResponse>(
"/api/whiteboard/upload_note",
filePath,
whiteboard.BoardId,
whiteboard.SecretKey,
title,
description,
tags);
if (uploadResult != null && uploadResult.Success)
{
LogHelper.WriteLogToFile($"笔记上传成功:{fileName} -> {uploadResult.FileUrl}", LogHelper.LogType.Event);
return true;
}
else
{
LogHelper.WriteLogToFile($"上传失败:服务器响应失败 - {uploadResult?.Message ?? ""}", LogHelper.LogType.Error);
return false;
}
}
}
catch (Exception ex)
{
// 记录错误信息,抛出异常以便调用方判断是否可重试
LogHelper.WriteLogToFile($"上传笔记时出错: {ex.Message}", LogHelper.LogType.Error);
throw;
}
}
/// <summary>
/// 判断错误是否可重试(超时、网络错误等)
/// </summary>
private static bool IsRetryableError(string filePath)
{
// 检查文件是否存在
if (!File.Exists(filePath))
{
return false; // 文件不存在,不可重试
}
// 检查文件扩展名
var fileExtension = Path.GetExtension(filePath).ToLower();
if (fileExtension != ".png" && fileExtension != ".icstk" && fileExtension != ".xml" && fileExtension != ".zip")
{
return false; // 文件格式错误,不可重试
}
// 检查文件大小
try
{
var fileInfo = new FileInfo(filePath);
long maxSize = fileExtension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
if (fileInfo.Length > maxSize)
{
return false; // 文件过大,不可重试
}
}
catch
{
return false; // 无法读取文件信息,不可重试
}
// 其他错误(超时、网络错误等)可以重试
return true;
}
}
}
+257
View File
@@ -0,0 +1,257 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// Dlass上传队列
/// </summary>
public class DlassUploadQueue : BaseUploadQueue
{
private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA";
private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA";
/// <summary>
/// 队列文件名
/// </summary>
protected override string QueueFileName => "DlassUploadQueue.json";
/// <summary>
/// 上传笔记响应模型
/// </summary>
public class UploadNoteResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
[JsonProperty("note_id")]
public int? NoteId { get; set; }
[JsonProperty("filename")]
public string Filename { get; set; }
[JsonProperty("file_path")]
public string FilePath { get; set; }
[JsonProperty("file_url")]
public string FileUrl { get; set; }
}
/// <summary>
/// 白板信息模型(用于查找白板)
/// </summary>
private class WhiteboardInfo
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("board_id")]
public string BoardId { get; set; }
[JsonProperty("secret_key")]
public string SecretKey { get; set; }
[JsonProperty("class_name")]
public string ClassName { get; set; }
[JsonProperty("class_id")]
public int ClassId { get; set; }
}
/// <summary>
/// 认证响应模型
/// </summary>
private class AuthWithTokenResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("whiteboards")]
public List<WhiteboardInfo> Whiteboards { get; set; }
}
/// <summary>
/// 检查上传是否启用
/// </summary>
protected override bool IsUploadEnabled()
{
return MainWindow.Settings?.Dlass?.IsAutoUploadNotes == true;
}
/// <summary>
/// 内部上传方法,执行实际上传操作
/// </summary>
protected override async Task<bool> UploadFileInternalAsync(string filePath, CancellationToken cancellationToken)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
// 再次检查文件是否存在(可能在队列等待时被删除)
if (!File.Exists(filePath))
{
return false;
}
// 获取白板信息
var whiteboard = await GetWhiteboardInfo(cancellationToken);
if (whiteboard == null)
{
return false;
}
// 获取API基础URL和用户Token
var apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
var userToken = MainWindow.Settings?.Dlass?.UserToken;
// 准备上传参数
var fileName = Path.GetFileNameWithoutExtension(filePath);
var fileExtension = Path.GetExtension(filePath).ToLower();
var title = fileName;
string fileType;
string tags;
if (fileExtension == ".zip")
{
fileType = "多页面墨迹压缩包";
tags = "自动上传,多页面,zip,压缩包";
}
else if (fileExtension == ".icstk")
{
fileType = "墨迹文件";
tags = "自动上传,墨迹,icstk";
}
else if (fileExtension == ".xml")
{
fileType = "XML文件";
tags = "自动上传,xml";
}
else
{
fileType = "笔记";
tags = "自动上传,笔记,png";
}
var description = $"自动上传的{fileType} - {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
// 创建API客户端并上传文件
var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken);
try
{
cancellationToken.ThrowIfCancellationRequested();
var uploadResult = await apiClient.UploadNoteAsync<UploadNoteResponse>(
"/api/whiteboard/upload_note",
filePath,
whiteboard.BoardId,
whiteboard.SecretKey,
title,
description,
tags,
cancellationToken);
if (uploadResult != null && uploadResult.Success)
{
return true;
}
else
{
return false;
}
}
finally
{
apiClient.Dispose();
}
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception)
{
throw;
}
}
/// <summary>
/// 获取白板信息
/// </summary>
private async Task<WhiteboardInfo> GetWhiteboardInfo(CancellationToken cancellationToken)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName;
if (string.IsNullOrEmpty(selectedClassName))
{
LogHelper.WriteLogToFile("[DlassUploadQueue] 上传失败:未选择班级", LogHelper.LogType.Error);
return null;
}
var userToken = MainWindow.Settings?.Dlass?.UserToken;
if (string.IsNullOrEmpty(userToken))
{
LogHelper.WriteLogToFile("[DlassUploadQueue] 上传失败:未设置用户Token", LogHelper.LogType.Error);
return null;
}
var apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
// 创建API客户端并获取白板信息
var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken);
try
{
var authData = new
{
app_id = APP_ID,
app_secret = APP_SECRET,
user_token = userToken
};
var authResult = await apiClient.PostAsync<AuthWithTokenResponse>("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false, cancellationToken: cancellationToken);
if (authResult == null || !authResult.Success || authResult.Whiteboards == null)
{
LogHelper.WriteLogToFile("[DlassUploadQueue] 上传失败:无法获取白板信息", LogHelper.LogType.Error);
return null;
}
// 查找匹配班级的白板
var whiteboard = authResult.Whiteboards
.FirstOrDefault(w => !string.IsNullOrEmpty(w.ClassName) && w.ClassName == selectedClassName);
if (whiteboard == null || string.IsNullOrEmpty(whiteboard.BoardId) || string.IsNullOrEmpty(whiteboard.SecretKey))
{
LogHelper.WriteLogToFile($"[DlassUploadQueue] 上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error);
return null;
}
return whiteboard;
}
finally
{
apiClient.Dispose();
}
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception)
{
return null;
}
}
}
}
+87 -6
View File
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
namespace Ink_Canvas.Helpers
{
@@ -24,8 +26,9 @@ namespace Ink_Canvas.Helpers
/// 上传文件
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>是否上传成功</returns>
Task<bool> UploadAsync(string filePath);
Task<bool> UploadAsync(string filePath, CancellationToken cancellationToken = default);
}
/// <summary>
@@ -33,6 +36,8 @@ namespace Ink_Canvas.Helpers
/// </summary>
public class DlassUploadProvider : IUploadProvider
{
public static readonly DlassUploadQueue Queue = new DlassUploadQueue();
/// <summary>
/// 提供者名称
/// </summary>
@@ -41,16 +46,46 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 是否启用
/// </summary>
public bool IsEnabled => MainWindow.Settings?.Dlass?.IsAutoUploadNotes ?? false;
public bool IsEnabled => MainWindow.Settings?.Upload?.EnabledProviders?.Contains(Name) ?? false;
/// <summary>
/// 上传文件
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>是否上传成功</returns>
public async Task<bool> UploadAsync(string filePath)
public async Task<bool> UploadAsync(string filePath, CancellationToken cancellationToken = default)
{
return await DlassNoteUploader.UploadNoteFileAsync(filePath);
return await Queue.UploadFileAsync(filePath, cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// WebDav上传提供者
/// </summary>
public class WebDavUploadProvider : IUploadProvider
{
public static readonly WebDavUploadQueue Queue = new WebDavUploadQueue();
/// <summary>
/// 提供者名称
/// </summary>
public string Name => "WebDav";
/// <summary>
/// 是否启用
/// </summary>
public bool IsEnabled => MainWindow.Settings?.Upload?.EnabledProviders?.Contains(Name) ?? false;
/// <summary>
/// 上传文件
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>是否上传成功</returns>
public async Task<bool> UploadAsync(string filePath, CancellationToken cancellationToken = default)
{
return await Queue.UploadFileAsync(filePath, cancellationToken).ConfigureAwait(false);
}
}
@@ -77,6 +112,14 @@ namespace Ink_Canvas.Helpers
// 注册默认上传提供者
RegisterProviderInternal(new DlassUploadProvider());
RegisterProviderInternal(new WebDavUploadProvider());
// 注册上传队列
UploadQueueHelper.RegisterQueue(DlassUploadProvider.Queue);
UploadQueueHelper.RegisterQueue(WebDavUploadProvider.Queue);
// 初始化所有上传队列
UploadQueueHelper.InitializeAllQueues();
_initialized = true;
}
@@ -113,8 +156,9 @@ namespace Ink_Canvas.Helpers
/// 上传文件到所有启用的提供者
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>是否至少有一个提供者上传成功</returns>
public static async Task<bool> UploadFileAsync(string filePath)
public static async Task<bool> UploadFileAsync(string filePath, CancellationToken cancellationToken = default)
{
if (!_initialized)
{
@@ -129,19 +173,56 @@ namespace Ink_Canvas.Helpers
bool anySuccess = false;
// 获取上传延迟时间
int delayMinutes = MainWindow.Settings?.Upload?.UploadDelayMinutes ?? 0;
// 应用上传延迟
if (delayMinutes > 0)
{
LogHelper.WriteLogToFile($"上传延迟 {delayMinutes} 分钟", LogHelper.LogType.Event);
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(TimeSpan.FromMinutes(delayMinutes), cancellationToken).ConfigureAwait(false);
}
// 上传前验证文件是否存在且可访问
if (!File.Exists(filePath))
{
LogHelper.WriteLogToFile($"上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error);
return false;
}
try
{
// 检查文件是否可访问
using (var fileStream = File.OpenRead(filePath))
{
// 文件可访问
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"上传失败:文件不可访问 - {filePath}, 原因: {ex.Message}", LogHelper.LogType.Error);
return false;
}
foreach (var provider in providersSnapshot)
{
try
{
if (provider.IsEnabled)
{
bool success = await provider.UploadAsync(filePath);
bool success = await provider.UploadAsync(filePath, cancellationToken).ConfigureAwait(false);
if (success)
{
anySuccess = true;
}
}
}
catch (OperationCanceledException)
{
LogHelper.WriteLogToFile($"上传被取消: {provider.Name}", LogHelper.LogType.Event);
throw;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"使用 {provider.Name} 上传失败: {ex}", LogHelper.LogType.Error);
+94
View File
@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 上传队列帮助类,提供统一的队列管理功能
/// </summary>
public static class UploadQueueHelper
{
private static readonly List<BaseUploadQueue> _queues = new List<BaseUploadQueue>();
private static readonly object _syncLock = new object();
private static volatile bool _initialized = false;
/// <summary>
/// 初始化所有上传队列
/// </summary>
public static void InitializeAllQueues()
{
lock (_syncLock)
{
if (_initialized)
return;
// 初始化所有注册的队列
foreach (var queue in _queues)
{
try
{
queue.InitializeQueue();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[UploadQueueHelper] 初始化队列时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
_initialized = true;
}
}
/// <summary>
/// 注册上传队列
/// </summary>
/// <param name="queue">上传队列实例</param>
public static void RegisterQueue(BaseUploadQueue queue)
{
if (queue == null)
return;
lock (_syncLock)
{
if (!_queues.Contains(queue))
{
try
{
// 先初始化队列,再添加到列表
queue.InitializeQueue();
_queues.Add(queue);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[UploadQueueHelper] 注册队列时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
}
}
/// <summary>
/// 获取所有注册的上传队列
/// </summary>
/// <returns>上传队列列表</returns>
public static IReadOnlyList<BaseUploadQueue> GetAllQueues()
{
lock (_syncLock)
{
return new List<BaseUploadQueue>(_queues).AsReadOnly();
}
}
/// <summary>
/// 确保所有队列都已初始化
/// </summary>
public static void EnsureQueuesInitialized()
{
if (!_initialized)
{
InitializeAllQueues();
}
}
}
}
+61
View File
@@ -0,0 +1,61 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// WebDAV上传队列
/// </summary>
public class WebDavUploadQueue : BaseUploadQueue
{
/// <summary>
/// 队列文件名
/// </summary>
protected override string QueueFileName => "WebDavUploadQueue.json";
/// <summary>
/// 检查上传是否启用
/// </summary>
protected override bool IsUploadEnabled()
{
return WebDavUploader.IsWebDavEnabled();
}
/// <summary>
/// 内部上传方法,执行实际上传操作
/// </summary>
protected override async Task<bool> UploadFileInternalAsync(string filePath, CancellationToken cancellationToken)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
// 再次检查文件是否存在(可能在队列等待时被删除)
if (!File.Exists(filePath))
{
return false;
}
// 检查WebDAV是否仍然启用
if (!WebDavUploader.IsWebDavEnabled())
{
return false;
}
// 调用WebDavUploader进行实际上传
var success = await WebDavUploader.UploadFileAsync(filePath, cancellationToken);
return success;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception)
{
throw;
}
}
}
}
+171
View File
@@ -0,0 +1,171 @@
using System;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using WebDav;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// WebDav上传工具类
/// </summary>
public static class WebDavUploader
{
/// <summary>
/// 上传文件到WebDav服务器
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>是否上传成功</returns>
public static async Task<bool> UploadFileAsync(string filePath, CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
// 检查文件是否存在
if (!File.Exists(filePath))
{
return false;
}
// 获取WebDav设置
var webDavUrl = MainWindow.Settings?.Dlass?.WebDavUrl;
var username = MainWindow.Settings?.Dlass?.WebDavUsername;
var password = MainWindow.Settings?.Dlass?.WebDavPassword;
var rootDirectory = MainWindow.Settings?.Dlass?.WebDavRootDirectory;
// 验证设置
if (string.IsNullOrEmpty(webDavUrl))
{
return false;
}
// 构建完整的目标路径
var fileName = Path.GetFileName(filePath);
var targetPath = Path.Combine(rootDirectory ?? string.Empty, fileName).Replace("\\", "/");
if (targetPath.StartsWith("/"))
{
targetPath = targetPath.Substring(1);
}
// 创建WebDav客户端
var clientParams = new WebDavClientParams
{
BaseAddress = new Uri(webDavUrl),
Credentials = new NetworkCredential(username ?? string.Empty, password ?? string.Empty)
};
using (var client = new WebDavClient(clientParams))
{
cancellationToken.ThrowIfCancellationRequested();
// 先直接尝试上传文件
using (var fileStream = File.OpenRead(filePath))
{
// 检查取消令牌
cancellationToken.ThrowIfCancellationRequested();
var result = await client.PutFile(targetPath, fileStream);
if (result.IsSuccessful)
{
return true;
}
else
{
// 上传失败,尝试创建目录
var directoryPath = Path.GetDirectoryName(targetPath);
if (!string.IsNullOrEmpty(directoryPath))
{
await EnsureDirectoryExistsAsync(client, directoryPath, cancellationToken);
// 再次尝试上传文件
cancellationToken.ThrowIfCancellationRequested();
using (var retryStream = File.OpenRead(filePath))
{
var retryResult = await client.PutFile(targetPath, retryStream);
return retryResult.IsSuccessful;
}
}
else
{
// 没有目录路径,直接返回失败
return false;
}
}
}
}
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// 确保WebDav目录存在
/// </summary>
/// <param name="client">WebDav客户端</param>
/// <param name="directoryPath">目录路径</param>
/// <param name="cancellationToken">取消令牌</param>
private static async Task EnsureDirectoryExistsAsync(IWebDavClient client, string directoryPath, CancellationToken cancellationToken)
{
try
{
// 分割路径并逐级创建目录
var pathParts = directoryPath.Split('/');
var currentPath = string.Empty;
foreach (var part in pathParts)
{
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrEmpty(part))
continue;
currentPath = Path.Combine(currentPath, part).Replace("\\", "/");
// 检查取消令牌
cancellationToken.ThrowIfCancellationRequested();
// 尝试创建目录
await client.Mkcol(currentPath);
}
}
catch (Exception)
{
// 静默处理目录创建错误
}
}
/// <summary>
/// 检查WebDAV是否已启用
/// </summary>
/// <returns>是否启用</returns>
public static bool IsWebDavEnabled()
{
// 检查WebDav设置是否有效
var webDavUrl = MainWindow.Settings?.Dlass?.WebDavUrl;
if (string.IsNullOrEmpty(webDavUrl))
{
return false;
}
// 尝试解析URL
try
{
new Uri(webDavUrl);
return true;
}
catch
{
return false;
}
}
}
}
+1
View File
@@ -166,6 +166,7 @@
<PackageReference Include="AForge.Video.DirectShow" Version="2.2.5" />
<PackageReference Include="AForge.Imaging" Version="2.2.5" />
<PackageReference Include="AForge.Math" Version="2.2.5" />
<PackageReference Include="WebDav.Client" Version="2.9.0" />
</ItemGroup>
<ItemGroup>
<COMReference Include="IWshRuntimeLibrary">
+1 -1
View File
@@ -3655,7 +3655,7 @@
StrokeThickness="1" Margin="0,8,0,8" />
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,8,0,0">
<Button Name="BtnDlassSettingsManage" Content="Dlass设置管理"
<Button Name="BtnDlassSettingsManage" Content="云存储管理"
HorizontalAlignment="Left"
Click="BtnDlassSettingsManage_Click"
Padding="15,5"
+15 -8
View File
@@ -1,6 +1,5 @@
using Ink_Canvas.Helpers;
using Ink_Canvas.Helpers.Plugins;
using Ink_Canvas.Properties;
using Ink_Canvas.Windows;
using iNKORE.UI.WPF.Modern;
using iNKORE.UI.WPF.Modern.Controls;
@@ -1159,8 +1158,16 @@ namespace Ink_Canvas
AutoBackupManager.Initialize(Settings);
CheckUpdateChannelAndTelemetryConsistency();
// 初始化Dlass上传队列(恢复上次的上传队列)
DlassNoteUploader.InitializeQueue();
// 初始化上传队列(恢复上次的上传队列)
try
{
UploadQueueHelper.InitializeAllQueues();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[MainWindow] 初始化上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
// 继续执行其他初始化操作,不中断整个加载过程
}
_ = TelemetryUploader.UploadTelemetryIfNeededAsync();
@@ -1995,7 +2002,7 @@ namespace Ink_Canvas
else
{
// 下载失败
MessageBox.Show(Strings.GetString("Msg_UpdateDownloadFailed"), Strings.GetString("Msg_DownloadFailedTitle"), MessageBoxButton.OK, MessageBoxImage.Error);
MessageBox.Show("更新下载失败,请检查网络连接后重试。", "下载失败", MessageBoxButton.OK, MessageBoxImage.Error);
}
break;
@@ -2018,12 +2025,12 @@ namespace Ink_Canvas
timerCheckAutoUpdateWithSilence.Start();
// 通知用户
MessageBox.Show(Strings.GetString("Msg_UpdateReady"), Strings.GetString("Msg_UpdateReadyTitle"), MessageBoxButton.OK, MessageBoxImage.Information);
MessageBox.Show("更新已下载完成,将在软件关闭时自动安装。", "更新已准备就绪", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
LogHelper.WriteLogToFile("AutoUpdate | Update download failed", LogHelper.LogType.Error);
MessageBox.Show(Strings.GetString("Msg_UpdateDownloadFailed"), Strings.GetString("Msg_DownloadFailedTitle"), MessageBoxButton.OK, MessageBoxImage.Error);
MessageBox.Show("更新下载失败,请检查网络连接后重试。", "下载失败", MessageBoxButton.OK, MessageBoxImage.Error);
}
break;
@@ -2038,8 +2045,8 @@ namespace Ink_Canvas
SaveSettingsToFile();
// 通知用户
MessageBox.Show(string.Format(Strings.GetString("Msg_SkipVersion"), AvailableLatestVersion),
Strings.GetString("Msg_SkipVersionTitle"),
MessageBox.Show($"已设置跳过版本 {AvailableLatestVersion},在下次发布新版本之前不会再提示更新。",
"已跳过此版本",
MessageBoxButton.OK,
MessageBoxImage.Information);
break;
+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);
}
}
+35 -5
View File
@@ -32,6 +32,9 @@ namespace Ink_Canvas
[JsonProperty("dlass")]
public DlassSettings Dlass { get; set; } = new DlassSettings();
[JsonProperty("upload")]
public UploadSettings Upload { get; set; } = new UploadSettings();
[JsonProperty("security")]
public Security Security { get; set; } = new Security();
}
@@ -102,13 +105,10 @@ namespace Ink_Canvas
public bool LineEndpointSnapping { get; set; } = true; // 是否启用直线端点吸附
[JsonProperty("lineEndpointSnappingThreshold")]
public int LineEndpointSnappingThreshold { get; set; } = 15; // 直线端点吸附的距离阈值(像素)
[JsonProperty("usingWhiteboard")]
public bool UsingWhiteboard { get; set; }
[JsonProperty("customBackgroundColor")]
public string CustomBackgroundColor { get; set; } = "#162924";
[JsonProperty("hyperbolaAsymptoteOption")]
public OptionalOperation HyperbolaAsymptoteOption { get; set; } = OptionalOperation.Ask;
[JsonProperty("isCompressPicturesUploaded")]
@@ -121,8 +121,6 @@ namespace Ink_Canvas
public bool ClearCanvasAlsoClearImages { get; set; } = true;
[JsonProperty("showCircleCenter")]
public bool ShowCircleCenter { get; set; }
// 墨迹渐隐功能设置
[JsonProperty("enableInkFade")]
public bool EnableInkFade { get; set; } = false;
[JsonProperty("inkFadeTime")]
@@ -448,6 +446,7 @@ namespace Ink_Canvas
[JsonProperty("isAutoFoldInEasiNote3")]
public bool IsAutoFoldInEasiNote3 { get; set; }
[JsonProperty("isAutoFoldInEasiNote3C")]
public bool IsAutoFoldInEasiNote3C { get; set; }
@@ -859,6 +858,37 @@ namespace Ink_Canvas
get { return _autoUploadDelayMinutes; }
set { _autoUploadDelayMinutes = Math.Max(0, value); }
}
[JsonProperty("webDavUrl")]
public string WebDavUrl { get; set; } = string.Empty;
[JsonProperty("webDavUsername")]
public string WebDavUsername { get; set; } = string.Empty;
[JsonProperty("webDavPassword")]
public string WebDavPassword { get; set; } = string.Empty;
[JsonProperty("webDavRootDirectory")]
public string WebDavRootDirectory { get; set; } = string.Empty;
}
public class UploadSettings
{
[JsonProperty("uploadDelayMinutes")]
public int UploadDelayMinutes
{
get { return _uploadDelayMinutes; }
set { _uploadDelayMinutes = Math.Max(0, Math.Min(60, value)); }
}
private int _uploadDelayMinutes = 0;
[JsonProperty("enabledProviders")]
public List<string> EnabledProviders
{
get { return _enabledProviders; }
set { _enabledProviders = value ?? new List<string>(); }
}
private List<string> _enabledProviders = new List<string>();
}
File diff suppressed because it is too large Load Diff
+265 -16
View File
@@ -7,20 +7,31 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using MessageBox = iNKORE.UI.WPF.Modern.Controls.MessageBox;
using ui = iNKORE.UI.WPF.Modern.Controls;
namespace Ink_Canvas.Windows
{
/// <summary>
/// DlassSettingsWindow.xaml 的交互逻辑
/// 云储存管理窗口
/// </summary>
/// <remarks>
/// 该窗口包含三个标签页:
/// 1. 通用设置 - 管理所有上传提供者的通用设置,包括上传延迟时间和提供者启用/禁用
/// 2. Dlass - 管理Dlass服务端连接和设置,包括用户Token、班级选择和自动上传设置
/// 3. WebDav - 预留的WebDav连接设置页面
/// </remarks>
public partial class DlassSettingsWindow : Window
{
private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA";
private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA";
// 静态 Regex 实例,用于验证数字输入
private static readonly Regex _nonDigitRegex = new Regex("[^0-9]+", RegexOptions.Compiled | RegexOptions.CultureInvariant);
private DlassApiClient _apiClient;
private List<WhiteboardInfo> _currentWhiteboards = new List<WhiteboardInfo>();
private UserInfo _currentUser;
private bool _isFirstTimeDlassTab = true;
public DlassSettingsWindow(MainWindow mainWindow = null)
{
@@ -38,6 +49,12 @@ namespace Ink_Canvas.Windows
// 加载自动上传设置
LoadAutoUploadSettings();
// 加载通用设置
LoadUniversalUploadSettings();
// 加载WebDav设置
LoadWebDavSettings();
// 初始化API客户端(优先使用用户token)
InitializeApiClient();
@@ -291,7 +308,7 @@ namespace Ink_Canvas.Windows
{
delayMinutes = 0;
}
TxtUploadDelayMinutes.Text = delayMinutes.ToString();
}
}
catch (Exception ex)
@@ -310,6 +327,31 @@ namespace Ink_Canvas.Windows
if (MainWindow.Settings?.Dlass != null)
{
MainWindow.Settings.Dlass.IsAutoUploadNotes = ToggleSwitchAutoUploadNotes.IsOn;
// 同步更新到EnabledProviders列表
if (MainWindow.Settings.Upload != null)
{
if (MainWindow.Settings.Upload.EnabledProviders == null)
{
MainWindow.Settings.Upload.EnabledProviders = new List<string>();
}
if (ToggleSwitchAutoUploadNotes.IsOn)
{
if (!MainWindow.Settings.Upload.EnabledProviders.Contains("Dlass"))
{
MainWindow.Settings.Upload.EnabledProviders.Add("Dlass");
}
}
else
{
MainWindow.Settings.Upload.EnabledProviders.Remove("Dlass");
}
// 重新加载通用设置,更新UI
LoadUniversalUploadSettings();
}
MainWindow.SaveSettingsToFile();
}
}
@@ -319,53 +361,145 @@ namespace Ink_Canvas.Windows
}
}
/// <summary>
/// 上传延迟时间输入框文本改变事件
/// 加载通用设置
/// </summary>
private void TxtUploadDelayMinutes_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
private void LoadUniversalUploadSettings()
{
try
{
if (MainWindow.Settings?.Dlass != null && int.TryParse(TxtUploadDelayMinutes.Text, out int delayMinutes))
// 加载上传延迟时间
if (MainWindow.Settings?.Upload != null)
{
var delayMinutes = MainWindow.Settings.Upload.UploadDelayMinutes;
if (delayMinutes < 0 || delayMinutes > 60)
{
delayMinutes = 0;
}
TxtUniversalUploadDelayMinutes.Text = delayMinutes.ToString();
}
// 加载上传提供者列表
LoadUploadProvidersList();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加载通用设置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 加载上传提供者列表
/// </summary>
private void LoadUploadProvidersList()
{
try
{
var providers = UploadHelper.GetProviders();
LstUploadProviders.ItemsSource = providers;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加载上传提供者列表时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 通用设置延迟时间输入框文本改变事件
/// </summary>
private void TxtUniversalUploadDelayMinutes_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
try
{
if (MainWindow.Settings?.Upload != null && int.TryParse(TxtUniversalUploadDelayMinutes.Text, out int delayMinutes))
{
// 限制范围在0-60分钟
if (delayMinutes < 0)
{
delayMinutes = 0;
TxtUploadDelayMinutes.Text = "0";
TxtUniversalUploadDelayMinutes.Text = "0";
}
else if (delayMinutes > 60)
{
delayMinutes = 60;
TxtUploadDelayMinutes.Text = "60";
TxtUniversalUploadDelayMinutes.Text = "60";
}
MainWindow.Settings.Dlass.AutoUploadDelayMinutes = delayMinutes;
MainWindow.Settings.Upload.UploadDelayMinutes = delayMinutes;
MainWindow.SaveSettingsToFile();
}
else if (string.IsNullOrWhiteSpace(TxtUploadDelayMinutes.Text))
else if (string.IsNullOrWhiteSpace(TxtUniversalUploadDelayMinutes.Text))
{
// 空文本时设置为0
if (MainWindow.Settings?.Dlass != null)
if (MainWindow.Settings?.Upload != null)
{
MainWindow.Settings.Dlass.AutoUploadDelayMinutes = 0;
MainWindow.Settings.Upload.UploadDelayMinutes = 0;
MainWindow.SaveSettingsToFile();
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存上传延迟时间时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.WriteLogToFile($"保存通用设置延迟时间时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 上传延迟时间输入框预览文本输入事件(只允许数字)
/// 通用设置延迟时间输入框预览文本输入事件(只允许数字)
/// </summary>
private void TxtUploadDelayMinutes_PreviewTextInput(object sender, TextCompositionEventArgs e)
private void TxtUniversalUploadDelayMinutes_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
Regex regex = new Regex("[^0-9]+");
e.Handled = regex.IsMatch(e.Text);
e.Handled = _nonDigitRegex.IsMatch(e.Text);
}
/// <summary>
/// 上传提供者启用/禁用开关切换事件
/// </summary>
private void ToggleProviderEnabled_Toggled(object sender, RoutedEventArgs e)
{
try
{
if (sender is iNKORE.UI.WPF.Modern.Controls.ToggleSwitch toggleSwitch && toggleSwitch.DataContext is IUploadProvider provider)
{
if (MainWindow.Settings?.Upload != null)
{
if (MainWindow.Settings.Upload.EnabledProviders == null)
{
MainWindow.Settings.Upload.EnabledProviders = new List<string>();
}
if (toggleSwitch.IsOn)
{
if (!MainWindow.Settings.Upload.EnabledProviders.Contains(provider.Name))
{
MainWindow.Settings.Upload.EnabledProviders.Add(provider.Name);
}
}
else
{
MainWindow.Settings.Upload.EnabledProviders.Remove(provider.Name);
}
// 同步更新Dlass的IsAutoUploadNotes设置(如果是Dlass提供者)
if (provider.Name == "Dlass" && MainWindow.Settings.Dlass != null)
{
MainWindow.Settings.Dlass.IsAutoUploadNotes = toggleSwitch.IsOn;
// 同步更新Dlass标签页中的开关状态
ToggleSwitchAutoUploadNotes.IsOn = toggleSwitch.IsOn;
}
MainWindow.SaveSettingsToFile();
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存上传提供者启用状态时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
@@ -532,6 +666,121 @@ namespace Ink_Canvas.Windows
Close();
}
/// <summary>
/// TabControl选择改变事件
/// </summary>
private void TabControl_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
try
{
if (sender is System.Windows.Controls.TabControl tabControl && tabControl.SelectedItem is System.Windows.Controls.TabItem selectedTab)
{
// 检查是否切换到Dlass标签页
if (selectedTab.Tag?.ToString() == "DlassTab" && _isFirstTimeDlassTab)
{
// 检查是否是第一次打开(检查用户是否已设置Token)
bool hasToken = !string.IsNullOrEmpty(GetUserToken()?.Trim());
bool isFirstTime = !hasToken;
if (isFirstTime)
{
// 第一次打开,询问用户是否已注册
var result = MessageBox.Show(
"您是否已经注册了Dlass账号?\n\n" +
"• 如果已注册:将打开Dlass管理标签页\n" +
"• 如果未注册:将打开浏览器跳转到注册页面",
"Dlass账号注册",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result == MessageBoxResult.No)
{
// 用户未注册,打开浏览器
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.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);
}
}
// 如果用户选择"是",继续打开设置窗口
}
// 标记为已打开过Dlass标签页
_isFirstTimeDlassTab = false;
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"TabControl选择改变事件处理出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 加载WebDav设置
/// </summary>
private void LoadWebDavSettings()
{
try
{
if (MainWindow.Settings?.Dlass != null)
{
TxtWebDavUrl.Text = MainWindow.Settings.Dlass.WebDavUrl;
TxtWebDavUsername.Text = MainWindow.Settings.Dlass.WebDavUsername;
TxtWebDavPassword.Password = MainWindow.Settings.Dlass.WebDavPassword;
TxtWebDavRootDirectory.Text = MainWindow.Settings.Dlass.WebDavRootDirectory;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加载WebDav设置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 保存WebDav设置按钮点击事件
/// </summary>
private void BtnSaveWebDav_Click(object sender, RoutedEventArgs e)
{
try
{
if (MainWindow.Settings?.Dlass != null)
{
MainWindow.Settings.Dlass.WebDavUrl = TxtWebDavUrl.Text;
MainWindow.Settings.Dlass.WebDavUsername = TxtWebDavUsername.Text;
MainWindow.Settings.Dlass.WebDavPassword = TxtWebDavPassword.Password;
MainWindow.Settings.Dlass.WebDavRootDirectory = TxtWebDavRootDirectory.Text;
MainWindow.SaveSettingsToFile();
MessageBox.Show("WebDav设置已保存", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存WebDav设置时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"保存WebDav设置时发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 取消WebDav设置按钮点击事件
/// </summary>
private void BtnCancelWebDav_Click(object sender, RoutedEventArgs e)
{
// 重新加载设置,恢复原值
LoadWebDavSettings();
}
/// <summary>
/// 测试API连接
/// </summary>
@@ -657,11 +657,11 @@
</StackPanel>
</Border>
<!-- Dlass设置管理 -->
<!-- 云存储管理 -->
<Border Margin="0,25,0,0" BorderBrush="#e6e6e6" BorderThickness="1.25,1.25,1.25,4" CornerRadius="8">
<StackPanel Orientation="Vertical" Margin="18,18,18,18">
<TextBlock Text="Dlass设置管理" FontWeight="Bold" Foreground="#2e3436" FontSize="18" Margin="0,0,0,12"/>
<Button x:Name="BtnDlassSettingsManage" Tag="dlass_settings" Content="Dlass设置管理" Padding="15,5" HorizontalAlignment="Left" Background="#2563eb" Foreground="White" Click="Button_Click"/>
<TextBlock Text="云存储管理" FontWeight="Bold" Foreground="#2e3436" FontSize="18" Margin="0,0,0,12"/>
<Button x:Name="BtnDlassSettingsManage" Tag="dlass_settings" Content="云存储管理" Padding="15,5" HorizontalAlignment="Left" Background="#2563eb" Foreground="White" Click="Button_Click"/>
</StackPanel>
</Border>
</StackPanel>
+6
View File
@@ -137,6 +137,12 @@
"System.Text.Json": "8.0.5"
}
},
"WebDav.Client": {
"type": "Direct",
"requested": "[2.9.0, )",
"resolved": "2.9.0",
"contentHash": "GLhd1tQAJeuVO1sj3Wm/dkg0GEVWxk+XGl6rdegMSMHenZuOaWQw4PifWDsjNEC1dtV1/C8JJfK0qfdkM+VIgA=="
},
"AForge": {
"type": "Transitive",
"resolved": "2.2.5",