Merge pull request #279 from InkCanvasForClass/beta

ICC CE 1.7.17.2
This commit is contained in:
CJK_mkp
2025-11-08 20:46:01 +08:00
committed by GitHub
24 changed files with 2938 additions and 154 deletions
+14
View File
@@ -706,6 +706,20 @@ namespace Ink_Canvas
{
LogHelper.WriteLogToFile($"App | 清理更新标记文件失败: {ex.Message}", LogHelper.LogType.Warning);
}
Task.Run(async () =>
{
try
{
await Task.Delay(3000);
LogHelper.WriteLogToFile("App | 最终应用启动,删除AutoUpdate文件夹");
AutoUpdateHelper.DeleteUpdatesFolder();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"App | 删除AutoUpdate文件夹失败: {ex.Message}", LogHelper.LogType.Warning);
}
});
}
// 如果不是最终应用启动,才检查更新标记文件
+2 -2
View File
@@ -49,5 +49,5 @@ using System.Windows;
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.7.17.0")]
[assembly: AssemblyFileVersion("1.7.17.0")]
[assembly: AssemblyVersion("1.7.17.2")]
[assembly: AssemblyFileVersion("1.7.17.2")]
+456
View File
@@ -0,0 +1,456 @@
using Ink_Canvas.Helpers;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// Dlass API 客户端,用于与服务端通信
/// </summary>
public class DlassApiClient : IDisposable
{
private const string DEFAULT_BASE_URL = "https://dlass.tech";
private readonly string _appId;
private readonly string _appSecret;
private readonly string _baseUrl;
private HttpClient _httpClient;
private string _accessToken;
private DateTime _tokenExpiresAt;
private string _userToken;
/// <summary>
/// 初始化 Dlass API 客户端
/// </summary>
/// <param name="appId">应用ID</param>
/// <param name="appSecret">应用密钥</param>
/// <param name="baseUrl">API基础URL,如果为空则使用默认URL</param>
/// <param name="userToken">用户Token,如果提供则优先使用用户token而不是App Secret</param>
public DlassApiClient(string appId, string appSecret, string baseUrl = null, string userToken = null)
{
_appId = appId ?? throw new ArgumentNullException(nameof(appId));
_appSecret = appSecret ?? throw new ArgumentNullException(nameof(appSecret));
_userToken = userToken;
_baseUrl = baseUrl ?? DEFAULT_BASE_URL;
_baseUrl = _baseUrl.TrimEnd('/');
if (!_baseUrl.StartsWith("http://") && !_baseUrl.StartsWith("https://"))
{
_baseUrl = "https://" + _baseUrl;
}
_httpClient = new HttpClient
{
BaseAddress = new Uri(_baseUrl),
Timeout = TimeSpan.FromSeconds(30)
};
_httpClient.DefaultRequestHeaders.Add("User-Agent", "InkCanvas/1.0");
}
/// <summary>
/// 获取访问令牌(Access Token
/// </summary>
public async Task<string> GetAccessTokenAsync()
{
if (!string.IsNullOrEmpty(_userToken))
{
return _userToken;
}
if (!string.IsNullOrEmpty(_accessToken) && DateTime.Now < _tokenExpiresAt.AddMinutes(-5))
{
return _accessToken;
}
try
{
var requestData = new
{
app_id = _appId,
app_secret = _appSecret,
grant_type = "client_credentials"
};
var json = JsonConvert.SerializeObject(requestData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("/oauth/token", content);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(responseContent);
_accessToken = tokenResponse.AccessToken;
_tokenExpiresAt = DateTime.Now.AddSeconds(tokenResponse.ExpiresIn ?? 3600);
return _accessToken;
}
else
{
throw new Exception($"获取Access Token失败: {response.StatusCode}");
}
}
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);
}
}
/// <summary>
/// 发送GET请求
/// </summary>
public async Task<T> GetAsync<T>(string endpoint, bool requireAuth = true)
{
try
{
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync();
}
var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
var response = await _httpClient.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(content))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(content);
}
else
{
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
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);
}
}
/// <summary>
/// 发送POST请求
/// </summary>
public async Task<T> PostAsync<T>(string endpoint, object data = null, bool requireAuth = true)
{
try
{
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync();
}
var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
if (data != null)
{
var json = JsonConvert.SerializeObject(data);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(content))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(content);
}
else
{
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
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);
}
}
/// <summary>
/// 发送PUT请求
/// </summary>
public async Task<T> PutAsync<T>(string endpoint, object data = null, bool requireAuth = true)
{
try
{
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync();
}
var request = new HttpRequestMessage(HttpMethod.Put, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
// 如果是用户token,使用X-User-Token header
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
if (data != null)
{
var json = JsonConvert.SerializeObject(data);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(content))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(content);
}
else
{
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
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);
}
}
/// <summary>
/// 发送DELETE请求
/// </summary>
public async Task<bool> DeleteAsync(string endpoint, bool requireAuth = true)
{
try
{
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync();
}
var request = new HttpRequestMessage(HttpMethod.Delete, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
// 如果是用户token,使用X-User-Token header
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
var response = await _httpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
{
return true;
}
else
{
return false;
}
}
catch (HttpRequestException httpEx)
{
return false;
}
catch (TaskCanceledException timeoutEx)
{
return false;
}
catch (Exception ex)
{
return false;
}
}
/// <summary>
/// 上传笔记文件
/// </summary>
/// <param name="endpoint">上传端点</param>
/// <param name="filePath">文件路径</param>
/// <param name="boardId">白板ID</param>
/// <param name="secretKey">白板密钥</param>
/// <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)
{
try
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"文件不存在: {filePath}");
}
var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
// 设置白板认证头
request.Headers.Add("X-Board-ID", boardId);
request.Headers.Add("X-Secret-Key", secretKey);
// 创建multipart/form-data内容
var content = new MultipartFormDataContent();
// 添加文件
var fileContent = new ByteArrayContent(File.ReadAllBytes(filePath));
var fileName = Path.GetFileName(filePath);
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
content.Add(fileContent, "file", fileName);
// 添加可选参数
if (!string.IsNullOrEmpty(title))
{
content.Add(new StringContent(title), "title");
}
if (!string.IsNullOrEmpty(description))
{
content.Add(new StringContent(description), "description");
}
if (!string.IsNullOrEmpty(tags))
{
content.Add(new StringContent(tags), "tags");
}
request.Content = content;
var response = await _httpClient.SendAsync(request);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(responseContent))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(responseContent);
}
else
{
throw new Exception($"上传文件失败: {response.StatusCode} - {responseContent}");
}
}
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);
}
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
_httpClient?.Dispose();
}
#region
/// <summary>
/// Token响应模型
/// </summary>
private class TokenResponse
{
[JsonProperty("access_token")]
public string AccessToken { get; set; }
[JsonProperty("expires_in")]
public int? ExpiresIn { get; set; }
[JsonProperty("token_type")]
public string TokenType { get; set; }
}
#endregion
}
}
+493
View File
@@ -0,0 +1,493 @@
using Ink_Canvas.Helpers;
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; // 最大重试次数
/// <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>
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格式)
/// </summary>
/// <param name="filePath">文件路径(支持PNG和ICSTK</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")
{
return false;
}
var fileInfo = new FileInfo(filePath);
if (fileInfo.Length > 10 * 1024 * 1024)
{
LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过10MB限制", LogHelper.LogType.Error);
return false;
}
// 获取上传延迟时间(分钟)
var delayMinutes = MainWindow.Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
// 如果设置了延迟时间,在后台任务中等待后再加入队列
if (delayMinutes > 0)
{
_ = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
EnqueueFile(filePath);
});
}
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
});
// 如果队列达到批量大小,触发批量上传
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);
return;
}
userToken = MainWindow.Settings?.Dlass?.UserToken;
if (string.IsNullOrEmpty(userToken))
{
LogHelper.WriteLogToFile("上传失败:未设置用户Token", LogHelper.LogType.Error);
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);
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);
return;
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"批量上传获取白板信息时出错: {ex.Message}", LogHelper.LogType.Error);
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);
// 如果队列达到批量大小,继续处理
if (_uploadQueue.Count >= BATCH_SIZE)
{
_ = ProcessUploadQueueAsync();
}
}
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")
{
return false;
}
// 检查文件大小(最大10MB
var fileInfo = new FileInfo(filePath);
if (fileInfo.Length > 10 * 1024 * 1024)
{
LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过10MB限制", 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;
var fileType = fileExtension == ".icstk" ? "墨迹文件" : "笔记";
var description = $"自动上传的{fileType} - {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
var tags = fileExtension == ".icstk" ? "自动上传,墨迹,icstk" : "自动上传,笔记,png";
// 创建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")
{
return false; // 文件格式错误,不可重试
}
// 检查文件大小
try
{
var fileInfo = new FileInfo(filePath);
if (fileInfo.Length > 10 * 1024 * 1024)
{
return false; // 文件过大,不可重试
}
}
catch
{
return false; // 无法读取文件信息,不可重试
}
// 其他错误(超时、网络错误等)可以重试
return true;
}
}
}
+11
View File
@@ -3185,6 +3185,17 @@
FontSize="14" Margin="8,0,0,0" />
</ui:SimpleStackPanel>
<Line HorizontalAlignment="Center" X1="0" Y1="0" X2="400" Y2="0" Stroke="#3f3f46"
StrokeThickness="1" Margin="0,8,0,8" />
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,8,0,0">
<Button Name="BtnDlassSettingsManage" Content="Dlass设置管理"
HorizontalAlignment="Left"
Click="BtnDlassSettingsManage_Click"
Padding="15,5"
Margin="0,0,0,0"/>
</ui:SimpleStackPanel>
<Line HorizontalAlignment="Center" X1="0" Y1="0" X2="400" Y2="0" Stroke="#3f3f46"
StrokeThickness="1" Margin="0,8,0,8" />
+56 -46
View File
@@ -667,7 +667,23 @@ namespace Ink_Canvas
await Application.Current.Dispatcher.InvokeAsync(() =>
{
var activePresentation = _pptManager?.GetCurrentActivePresentation();
Presentation activePresentation = null;
int currentSlide = 0;
int totalSlides = 0;
if (wn?.View != null && wn.Presentation != null)
{
activePresentation = wn.Presentation;
currentSlide = wn.View.CurrentShowPosition;
totalSlides = activePresentation.Slides.Count;
}
else
{
activePresentation = _pptManager?.GetCurrentActivePresentation();
currentSlide = _pptManager?.GetCurrentSlideNumber() ?? 0;
totalSlides = _pptManager?.SlidesCount ?? 0;
}
if (activePresentation != null)
{
if (Settings.PowerPointSettings.IsSupportWPS)
@@ -691,8 +707,6 @@ namespace Ink_Canvas
}
// 更新UI状态
var currentSlide = _pptManager?.GetCurrentSlideNumber() ?? 0;
var totalSlides = _pptManager?.SlidesCount ?? 0;
_pptUIManager?.UpdateSlideShowStatus(true, currentSlide, totalSlides);
// 设置浮动栏透明度和边距
@@ -744,37 +758,34 @@ namespace Ink_Canvas
PenIcon_Click(null, null);
// 然后设置颜色
BtnColorRed_Click(null, null);
Dispatcher.BeginInvoke(new Action(() =>
try
{
try
if (inkCanvas.EditingMode == InkCanvasEditingMode.Ink)
{
if (inkCanvas.EditingMode == InkCanvasEditingMode.Ink)
UpdateCurrentToolMode("pen");
SetFloatingBarHighlightPosition("pen");
if (Settings.Appearance.IsShowQuickColorPalette && QuickColorPalettePanel != null && QuickColorPaletteSingleRowPanel != null)
{
UpdateCurrentToolMode("pen");
SetFloatingBarHighlightPosition("pen");
if (Settings.Appearance.IsShowQuickColorPalette && QuickColorPalettePanel != null && QuickColorPaletteSingleRowPanel != null)
// 根据显示模式选择显示哪个面板
if (Settings.Appearance.QuickColorPaletteDisplayMode == 0)
{
// 根据显示模式选择显示哪个面板
if (Settings.Appearance.QuickColorPaletteDisplayMode == 0)
{
// 单行显示模式
QuickColorPalettePanel.Visibility = Visibility.Collapsed;
QuickColorPaletteSingleRowPanel.Visibility = Visibility.Visible;
}
else
{
// 双行显示模式
QuickColorPalettePanel.Visibility = Visibility.Visible;
QuickColorPaletteSingleRowPanel.Visibility = Visibility.Collapsed;
}
// 单行显示模式
QuickColorPalettePanel.Visibility = Visibility.Collapsed;
QuickColorPaletteSingleRowPanel.Visibility = Visibility.Visible;
}
else
{
// 双行显示模式
QuickColorPalettePanel.Visibility = Visibility.Visible;
QuickColorPaletteSingleRowPanel.Visibility = Visibility.Collapsed;
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"PPT进入批注模式后同步浮动栏高光状态失败: {ex.Message}", LogHelper.LogType.Error);
}
}), DispatcherPriority.Loaded);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"PPT进入批注模式后同步浮动栏高光状态失败: {ex.Message}", LogHelper.LogType.Error);
}
}
isEnteredSlideShowEndEvent = false;
@@ -807,20 +818,19 @@ namespace Ink_Canvas
{
Application.Current.Dispatcher.InvokeAsync(() =>
{
var activePresentation = _pptManager?.GetCurrentActivePresentation();
if (activePresentation != null)
if (wn?.View == null || wn.Presentation == null)
{
if (Settings.PowerPointSettings.IsSupportWPS)
{
}
else
{
_multiPPTInkManager?.SwitchToPresentation(activePresentation);
}
return;
}
var currentSlide = _pptManager?.GetCurrentSlideNumber() ?? 0;
var totalSlides = _pptManager?.SlidesCount ?? 0;
var currentSlide = wn.View.CurrentShowPosition;
var activePresentation = wn.Presentation;
var totalSlides = activePresentation.Slides.Count;
if (!Settings.PowerPointSettings.IsSupportWPS)
{
_multiPPTInkManager?.SwitchToPresentation(activePresentation);
}
// 使用防抖机制处理页面切换
HandleSlideSwitchWithDebounce(currentSlide, totalSlides);
@@ -1135,6 +1145,9 @@ namespace Ink_Canvas
{
try
{
ClearStrokes(true);
timeMachine.ClearStrokeHistory();
StrokeCollection strokes = null;
if (Settings.PowerPointSettings.IsSupportWPS)
{
@@ -1145,9 +1158,8 @@ namespace Ink_Canvas
strokes = _multiPPTInkManager?.LoadSlideStrokes(slideIndex);
}
if (strokes != null)
if (strokes != null && strokes.Count > 0)
{
inkCanvas.Strokes.Clear();
inkCanvas.Strokes.Add(strokes);
}
}
@@ -1278,7 +1290,6 @@ namespace Ink_Canvas
// 获取当前页面索引
var currentSlideIndex = _pptManager?.GetCurrentSlideNumber() ?? 0;
// 验证页面索引的有效性
if (newSlideIndex <= 0)
{
@@ -1311,10 +1322,9 @@ namespace Ink_Canvas
}
}
}
else if (inkCanvas.Strokes.Count > 0 && currentSlideIndex <= 0)
{
}
ClearStrokes(true);
timeMachine.ClearStrokeHistory();
StrokeCollection newStrokes = null;
if (Settings.PowerPointSettings.IsSupportWPS)
{
@@ -1324,9 +1334,9 @@ namespace Ink_Canvas
{
newStrokes = _multiPPTInkManager?.SwitchToSlide(newSlideIndex, null);
}
if (newStrokes != null)
if (newStrokes != null && newStrokes.Count > 0)
{
inkCanvas.Strokes.Clear();
inkCanvas.Strokes.Add(newStrokes);
}
@@ -7,6 +7,7 @@ using System.Drawing.Imaging;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms;
@@ -132,6 +133,23 @@ namespace Ink_Canvas
var fs = new FileStream(savePathWithName, FileMode.Create);
inkCanvas.Strokes.Save(fs);
fs.Close();
_ = 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)
@@ -310,6 +328,23 @@ namespace Ink_Canvas
var fs = new FileStream(savePathWithName, FileMode.Create);
inkCanvas.Strokes.Save(fs);
fs.Close();
_ = Task.Run(async () =>
{
try
{
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
if (delayMinutes > 0)
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
}
await Helpers.DlassNoteUploader.UploadNoteFileAsync(imagePathWithName);
}
catch (Exception)
{
}
});
}
}
+17
View File
@@ -3,6 +3,7 @@ using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Forms;
@@ -65,6 +66,22 @@ namespace Ink_Canvas
{
ShowNotification($"截图成功保存至 {savePath}");
}
_ = Task.Run(async () =>
{
try
{
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
if (delayMinutes > 0)
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
}
await Helpers.DlassNoteUploader.UploadNoteFileAsync(savePath);
}
catch (Exception)
{
}
});
}
// 获取日期文件夹路径
+58 -2
View File
@@ -1846,6 +1846,62 @@ namespace Ink_Canvas
SaveSettingsToFile();
}
private void BtnDlassSettingsManage_Click(object sender, RoutedEventArgs e)
{
if (isOpeningOrHidingSettingsPane) return;
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;
dlassSettingsWindow.ShowDialog();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"打开Dlass设置管理窗口时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"打开Dlass设置管理窗口时发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void ToggleSwitchAutoDelSavedFiles_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
@@ -2672,8 +2728,8 @@ namespace Ink_Canvas
if (ToggleSwitchEnableOvertimeRedText.IsOn && !ToggleSwitchEnableOvertimeCountUp.IsOn)
{
ToggleSwitchEnableOvertimeRedText.IsOn = false;
return;
ToggleSwitchEnableOvertimeCountUp.IsOn = true;
Settings.RandSettings.EnableOvertimeCountUp = true;
}
Settings.RandSettings.EnableOvertimeRedText = ToggleSwitchEnableOvertimeRedText.IsOn;
@@ -592,7 +592,7 @@ namespace Ink_Canvas
ToggleSwitchEnableTwoFingerTranslate.IsOn = false;
BoardToggleSwitchEnableTwoFingerTranslate.IsOn = false;
Settings.Gesture.IsEnableTwoFingerTranslate = false;
if (!isInMultiTouchMode) ToggleSwitchEnableMultiTouchMode.IsOn = true;
// if (!isInMultiTouchMode) ToggleSwitchEnableMultiTouchMode.IsOn = true;
}
else
{
+2 -2
View File
@@ -49,5 +49,5 @@ using System.Windows;
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.7.17.0")]
[assembly: AssemblyFileVersion("1.7.17.0")]
[assembly: AssemblyVersion("1.7.17.2")]
[assembly: AssemblyFileVersion("1.7.17.2")]
+24 -1
View File
@@ -29,6 +29,8 @@ namespace Ink_Canvas
public ModeSettings ModeSettings { get; set; } = new ModeSettings();
[JsonProperty("camera")]
public CameraSettings Camera { get; set; } = new CameraSettings();
[JsonProperty("dlass")]
public DlassSettings Dlass { get; set; } = new DlassSettings();
}
public class Canvas
@@ -121,7 +123,7 @@ namespace Ink_Canvas
[JsonIgnore]
public bool IsEnableTwoFingerGestureTranslateOrRotation => IsEnableTwoFingerTranslate || IsEnableTwoFingerRotation;
[JsonProperty("isEnableMultiTouchMode")]
public bool IsEnableMultiTouchMode { get; set; } = true;
public bool IsEnableMultiTouchMode { get; set; } = false;
[JsonProperty("isEnableTwoFingerZoom")]
public bool IsEnableTwoFingerZoom { get; set; } = true;
[JsonProperty("isEnableTwoFingerTranslate")]
@@ -724,4 +726,25 @@ namespace Ink_Canvas
[JsonProperty("selectedCameraIndex")]
public int SelectedCameraIndex { get; set; } = 0;
}
public class DlassSettings
{
[JsonProperty("userToken")]
public string UserToken { get; set; } = string.Empty;
[JsonProperty("savedTokens")]
public List<string> SavedTokens { get; set; } = new List<string>();
[JsonProperty("selectedClassName")]
public string SelectedClassName { get; set; } = string.Empty;
[JsonProperty("apiBaseUrl")]
public string ApiBaseUrl { get; set; } = "https://dlass.tech";
[JsonProperty("isAutoUploadNotes")]
public bool IsAutoUploadNotes { get; set; } = false;
[JsonProperty("autoUploadDelayMinutes")]
public int AutoUploadDelayMinutes { get; set; } = 0;
}
}
+494
View File
@@ -0,0 +1,494 @@
<Window x:Class="Ink_Canvas.Windows.DlassSettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
xmlns:controls="clr-namespace:Ink_Canvas.Windows.Controls"
mc:Ignorable="d"
WindowStyle="None"
Title="Dlass设置管理" Height="600" Width="900"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResize"
AllowsTransparency="True"
Background="Transparent">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Controls/WinUI3CloseButton.xaml" />
</ResourceDictionary.MergedDictionaries>
<SolidColorBrush x:Key="WindowBackground" Color="#1e1e1e"/>
<SolidColorBrush x:Key="BorderBrush" Color="#3f3f46"/>
<SolidColorBrush x:Key="TextForeground" Color="#fafafa"/>
<SolidColorBrush x:Key="TextSecondary" Color="#a1a1aa"/>
<SolidColorBrush x:Key="AccentColor" Color="#3b82f6"/>
<SolidColorBrush x:Key="TitleForeground" Color="#fafafa"/>
<SolidColorBrush x:Key="NewTimerWindowButtonForeground" Color="White"/>
</ResourceDictionary>
</Window.Resources>
<Border Background="{StaticResource WindowBackground}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1"
CornerRadius="15"
Margin="10"
x:Name="MainBorder"
MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
<Grid>
<controls:WinUI3CloseButton x:Name="BtnClose"
HorizontalAlignment="Right" VerticalAlignment="Top"
Margin="0,0,0,0" Cursor="Hand" Click="BtnClose_Click"
Content="✕"/>
<!-- 主要内容区域 -->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 标题栏 -->
<Grid Grid.Row="0"
Height="50"
Background="{StaticResource WindowBackground}"
x:Name="TitleBar"
VerticalAlignment="Top"
MouseLeftButtonDown="TitleBar_MouseLeftButtonDown"
Margin="0,0,46,0">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Margin="22,0,0,0">
<!-- 设置图标 -->
<Path Data="M12 15.5A3.5 3.5 0 0 1 8.5 12A3.5 3.5 0 0 1 12 8.5A3.5 3.5 0 0 1 15.5 12A3.5 3.5 0 0 1 12 15.5M19.43 12.97C19.47 12.65 19.5 12.33 19.5 12C19.5 11.67 19.47 11.34 19.43 11.03L21.54 9.37C21.73 9.22 21.78 8.95 21.66 8.73L19.66 5.27C19.54 5.05 19.27 4.96 19.05 5.05L16.56 6.05C16.04 5.65 15.5 5.32 14.87 5.07L14.5 2.42C14.46 2.18 14.25 2 14 2H10C9.75 2 9.54 2.18 9.5 2.42L9.13 5.07C8.5 5.32 7.96 5.66 7.44 6.05L4.95 5.05C4.73 4.96 4.46 5.05 4.35 5.27L2.35 8.73C2.23 8.95 2.27 9.22 2.46 9.37L4.57 11.03C4.53 11.34 4.5 11.67 4.5 12C4.5 12.33 4.53 12.65 4.57 12.97L2.46 14.63C2.27 14.78 2.23 15.05 2.35 15.27L4.35 18.73C4.46 18.95 4.73 19.03 4.95 18.95L7.44 17.95C7.96 18.34 8.5 18.68 9.13 18.93L9.5 21.58C9.54 21.82 9.75 22 10 22H14C14.25 22 14.46 21.82 14.5 21.58L14.87 18.93C15.5 18.67 16.04 18.34 16.56 17.95L19.05 18.95C19.27 19.03 19.54 18.95 19.66 18.73L21.66 15.27C21.78 15.05 21.73 14.78 21.54 14.63L19.43 12.97Z"
Stroke="{StaticResource TitleForeground}"
StrokeThickness="1.5"
StrokeLineJoin="Round"
Fill="Transparent"
Width="24" Height="24"
Stretch="Uniform"
Margin="0,0,8,0"/>
<!-- 标题文字 -->
<TextBlock Text="Dlass设置管理"
FontSize="28"
FontWeight="Bold"
Foreground="{StaticResource TitleForeground}"
x:Name="TitleText"/>
</StackPanel>
</Grid>
<!-- 主内容区 -->
<Border Grid.Row="1"
Background="{StaticResource WindowBackground}"
Padding="20,10,20,20">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<ui:SimpleStackPanel Spacing="16">
<!-- 内容区域 -->
<Border BorderThickness="1"
BorderBrush="{StaticResource BorderBrush}"
CornerRadius="8"
Padding="20"
Background="#27272a">
<ui:SimpleStackPanel Spacing="16">
<TextBlock Text="Dlass设置管理"
FontSize="18"
FontWeight="SemiBold"
Foreground="{StaticResource TextForeground}"/>
<TextBlock Text="管理您的Dlass服务端连接和设置。"
FontSize="14"
Foreground="{StaticResource TextSecondary}"
TextWrapping="Wrap"
Margin="0,0,0,8"/>
<Line HorizontalAlignment="Stretch"
X1="0" Y1="0" X2="1" Y2="0"
Stroke="{StaticResource BorderBrush}"
StrokeThickness="1"
Margin="0,2,0,2"/>
<!-- 用户Token设置 -->
<TextBlock Text="用户Token"
FontSize="16"
FontWeight="SemiBold"
Foreground="{StaticResource TextForeground}"
Margin="0,0,0,1"/>
<TextBlock Text="设置您的用户Token以访问Dlass服务端功能。您可以从Dlass平台获取您的用户Token。"
FontSize="12"
Foreground="{StaticResource TextSecondary}"
TextWrapping="Wrap"
Margin="0,0,0,2"/>
<ui:SimpleStackPanel Orientation="Vertical" Spacing="4">
<ComboBox x:Name="CmbSavedTokens"
FontSize="14"
Padding="12,8"
Background="#18181b"
Foreground="{StaticResource TextForeground}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1"
MinHeight="36"
IsEditable="False"
IsReadOnly="True"
SelectionChanged="CmbSavedTokens_SelectionChanged">
</ComboBox>
<TextBox x:Name="TxtNewToken"
FontSize="14"
Padding="12,4"
Background="#18181b"
Foreground="{StaticResource TextForeground}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1"
TextWrapping="Wrap"
AcceptsReturn="False"
MaxLength="500"
MinHeight="36"
Tag="输入新的Token">
<TextBox.Style>
<Style TargetType="TextBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ScrollViewer x:Name="PART_ContentHost" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</TextBox.Style>
</TextBox>
<ui:SimpleStackPanel Orientation="Horizontal" Spacing="8">
<Button x:Name="BtnSaveToken"
Content="保存Token"
Padding="12,6"
FontSize="13"
Background="{StaticResource AccentColor}"
Foreground="White"
BorderThickness="0"
Cursor="Hand"
Click="BtnSaveToken_Click">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#2563eb"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#1d4ed8"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button x:Name="BtnClearToken"
Content="清除Token"
Padding="12,6"
FontSize="13"
Background="Transparent"
Foreground="{StaticResource TextForeground}"
BorderThickness="1"
BorderBrush="{StaticResource BorderBrush}"
Cursor="Hand"
Click="BtnClearToken_Click">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#27272a"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button x:Name="BtnTestToken"
Content="测试连接"
Padding="12,6"
FontSize="13"
Background="Transparent"
Foreground="{StaticResource TextForeground}"
BorderThickness="1"
BorderBrush="{StaticResource BorderBrush}"
Cursor="Hand"
Click="BtnTestToken_Click">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#27272a"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</ui:SimpleStackPanel>
<TextBlock x:Name="TxtTokenStatus"
Text=""
FontSize="12"
Foreground="{StaticResource TextSecondary}"
Margin="0,4,0,0"/>
</ui:SimpleStackPanel>
<!-- 连接状态 -->
<ui:SimpleStackPanel Orientation="Horizontal" Spacing="12" Margin="0,4,0,0">
<TextBlock Text="连接状态:"
FontSize="14"
Foreground="{StaticResource TextForeground}"
VerticalAlignment="Center"
Width="100"/>
<TextBlock x:Name="TxtConnectionStatus"
Text="未连接"
FontSize="14"
Foreground="{StaticResource TextSecondary}"
VerticalAlignment="Center"/>
</ui:SimpleStackPanel>
<Line HorizontalAlignment="Stretch"
X1="0" Y1="0" X2="1" Y2="0"
Stroke="{StaticResource BorderBrush}"
StrokeThickness="1"
Margin="0,2,0,1"/>
<!-- 班级选择 -->
<TextBlock Text="班级选择"
FontSize="16"
FontWeight="SemiBold"
Foreground="{StaticResource TextForeground}"
Margin="0,0,0,1"/>
<TextBlock Text="连接成功后,将自动加载可用班级列表。"
FontSize="12"
Foreground="{StaticResource TextSecondary}"
TextWrapping="Wrap"
Margin="0,0,0,2"/>
<ComboBox x:Name="CmbClassSelection"
FontSize="14"
Padding="12,8"
Background="#18181b"
Foreground="{StaticResource TextForeground}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1"
MinHeight="36"
IsEditable="False"
IsReadOnly="True"
SelectionChanged="CmbClassSelection_SelectionChanged">
</ComboBox>
<Line HorizontalAlignment="Stretch"
X1="0" Y1="0" X2="1" Y2="0"
Stroke="{StaticResource BorderBrush}"
StrokeThickness="1"
Margin="0,2,0,1"/>
<!-- 自动上传设置 -->
<TextBlock Text="自动上传设置"
FontSize="16"
FontWeight="SemiBold"
Foreground="{StaticResource TextForeground}"
Margin="0,0,0,1"/>
<ui:SimpleStackPanel Orientation="Vertical" Spacing="4">
<ui:ToggleSwitch x:Name="ToggleSwitchAutoUploadNotes"
Header="自动上传笔记"
FontSize="14"
Foreground="White"
Toggled="ToggleSwitchAutoUploadNotes_Toggled">
</ui:ToggleSwitch>
<TextBlock Text="启用后,保存的PNG截图和ICSTK墨迹文件将自动上传到所选班级的白板。"
FontSize="12"
Foreground="{StaticResource TextSecondary}"
TextWrapping="Wrap"
Margin="0,0,0,2"/>
<ui:SimpleStackPanel Orientation="Horizontal" Spacing="12"
IsEnabled="{Binding ElementName=ToggleSwitchAutoUploadNotes, Path=IsOn}">
<TextBlock Text="上传延迟时间:"
FontSize="14"
Foreground="{StaticResource TextForeground}"
VerticalAlignment="Center"
Width="120"/>
<TextBox x:Name="TxtUploadDelayMinutes"
FontSize="14"
Background="#18181b"
Foreground="{StaticResource TextForeground}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1"
MinWidth="100"
Height="32"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
HorizontalContentAlignment="Left"
TextChanged="TxtUploadDelayMinutes_TextChanged"
PreviewTextInput="TxtUploadDelayMinutes_PreviewTextInput">
<TextBox.Style>
<Style TargetType="TextBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6">
<ScrollViewer x:Name="PART_ContentHost"
Margin="12,0"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</TextBox.Style>
</TextBox>
<TextBlock Text="分钟"
FontSize="14"
Foreground="{StaticResource TextSecondary}"
VerticalAlignment="Center"
Margin="0,0,0,0"/>
</ui:SimpleStackPanel>
<TextBlock Text="设置上传延迟时间(0-60分钟),可以在保存后等待一段时间再上传。"
FontSize="12"
Foreground="{StaticResource TextSecondary}"
TextWrapping="Wrap"
Margin="0,2,0,0"/>
</ui:SimpleStackPanel>
<Line HorizontalAlignment="Stretch"
X1="0" Y1="0" X2="1" Y2="0"
Stroke="{StaticResource BorderBrush}"
StrokeThickness="1"
Margin="0,2,0,1"/>
<!-- 操作按钮 -->
<ui:SimpleStackPanel Orientation="Horizontal" Spacing="12" Margin="0,8,0,0">
<Button Content="保存"
Padding="16,8"
FontSize="14"
Background="{StaticResource AccentColor}"
Foreground="White"
BorderThickness="0"
Cursor="Hand"
Click="BtnSave_Click">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#2563eb"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#1d4ed8"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button Content="取消"
Padding="16,8"
FontSize="14"
Background="Transparent"
Foreground="{StaticResource TextForeground}"
BorderThickness="1"
BorderBrush="{StaticResource BorderBrush}"
Cursor="Hand"
Click="BtnCancel_Click">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#27272a"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</ui:SimpleStackPanel>
</ui:SimpleStackPanel>
</Border>
</ui:SimpleStackPanel>
</ScrollViewer>
</Border>
</Grid>
</Grid>
</Border>
</Window>
@@ -0,0 +1,709 @@
using Ink_Canvas.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using MessageBox = iNKORE.UI.WPF.Modern.Controls.MessageBox;
namespace Ink_Canvas.Windows
{
/// <summary>
/// DlassSettingsWindow.xaml 的交互逻辑
/// </summary>
public partial class DlassSettingsWindow : Window
{
private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA";
private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA";
private DlassApiClient _apiClient;
private List<WhiteboardInfo> _currentWhiteboards = new List<WhiteboardInfo>();
private UserInfo _currentUser;
public DlassSettingsWindow(MainWindow mainWindow = null)
{
InitializeComponent();
// 初始化班级下拉框
CmbClassSelection.Items.Clear();
CmbClassSelection.Items.Add("(等待连接)");
CmbClassSelection.SelectedIndex = 0;
CmbClassSelection.IsEnabled = false;
// 加载保存的token
LoadUserToken();
// 加载自动上传设置
LoadAutoUploadSettings();
// 初始化API客户端(优先使用用户token)
InitializeApiClient();
// 窗口关闭时释放资源
Closed += (s, e) => _apiClient?.Dispose();
// 测试连接
_ = TestConnectionAsync();
}
/// <summary>
/// 初始化API客户端
/// </summary>
private void InitializeApiClient()
{
var userToken = GetUserToken();
var apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl;
if (string.IsNullOrEmpty(apiBaseUrl) || apiBaseUrl.Contains("api.dlass.tech"))
{
apiBaseUrl = "https://dlass.tech";
if (MainWindow.Settings?.Dlass != null)
{
MainWindow.Settings.Dlass.ApiBaseUrl = apiBaseUrl;
MainWindow.SaveSettingsToFile();
}
}
if (!string.IsNullOrEmpty(userToken))
{
_apiClient = new DlassApiClient(APP_ID, APP_SECRET, baseUrl: apiBaseUrl, userToken: userToken);
}
else
{
_apiClient = new DlassApiClient(APP_ID, APP_SECRET, baseUrl: apiBaseUrl);
}
}
/// <summary>
/// 获取用户token
/// </summary>
private string GetUserToken()
{
if (MainWindow.Settings?.Dlass != null)
{
return MainWindow.Settings.Dlass.UserToken ?? string.Empty;
}
return string.Empty;
}
/// <summary>
/// 获取保存的Token列表
/// </summary>
private List<string> GetSavedTokens()
{
if (MainWindow.Settings?.Dlass != null)
{
return MainWindow.Settings.Dlass.SavedTokens ?? new List<string>();
}
return new List<string>();
}
/// <summary>
/// 加载用户token到UI
/// </summary>
private void LoadUserToken()
{
var savedTokens = GetSavedTokens();
var currentToken = GetUserToken();
CmbSavedTokens.Items.Clear();
if (savedTokens.Count > 0)
{
foreach (var token in savedTokens)
{
CmbSavedTokens.Items.Add(token);
}
if (!string.IsNullOrEmpty(currentToken))
{
var index = savedTokens.IndexOf(currentToken);
if (index >= 0)
{
CmbSavedTokens.SelectedIndex = index;
}
else
{
CmbSavedTokens.SelectedIndex = 0;
}
}
else if (CmbSavedTokens.Items.Count > 0)
{
CmbSavedTokens.SelectedIndex = 0;
}
}
else
{
CmbSavedTokens.Items.Add("(无保存的Token");
CmbSavedTokens.SelectedIndex = 0;
CmbSavedTokens.IsEnabled = false;
}
TxtNewToken.Text = string.Empty;
if (!string.IsNullOrEmpty(currentToken))
{
TxtTokenStatus.Text = "已选择Token";
TxtTokenStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(34, 197, 94));
}
else
{
TxtTokenStatus.Text = "未设置Token";
TxtTokenStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(161, 161, 170));
}
}
/// <summary>
/// 保存用户token
/// </summary>
private void SaveUserToken(string token)
{
if (MainWindow.Settings?.Dlass != null)
{
MainWindow.Settings.Dlass.UserToken = token ?? string.Empty;
MainWindow.SaveSettingsToFile();
}
}
/// <summary>
/// 添加Token到保存列表
/// </summary>
private void AddTokenToList(string token)
{
if (MainWindow.Settings?.Dlass != null)
{
if (MainWindow.Settings.Dlass.SavedTokens == null)
{
MainWindow.Settings.Dlass.SavedTokens = new List<string>();
}
if (!string.IsNullOrEmpty(token) && !MainWindow.Settings.Dlass.SavedTokens.Contains(token))
{
MainWindow.Settings.Dlass.SavedTokens.Add(token);
MainWindow.SaveSettingsToFile();
}
}
}
/// <summary>
/// 从列表删除Token
/// </summary>
private void RemoveTokenFromList(string token)
{
if (MainWindow.Settings?.Dlass != null && MainWindow.Settings.Dlass.SavedTokens != null)
{
MainWindow.Settings.Dlass.SavedTokens.Remove(token);
MainWindow.SaveSettingsToFile();
}
}
/// <summary>
/// 加载班级列表到下拉框
/// </summary>
private void LoadClasses(List<WhiteboardInfo> whiteboards, UserInfo user = null)
{
CmbClassSelection.Items.Clear();
if (whiteboards != null && whiteboards.Count > 0)
{
var teacherName = user?.Username ?? "未知教师";
var classGroups = whiteboards
.Where(w => !string.IsNullOrEmpty(w.ClassName))
.GroupBy(w => w.ClassName)
.OrderBy(g => g.Key)
.ToList();
foreach (var group in classGroups)
{
var className = group.Key;
var displayText = $"{teacherName} - {className}";
CmbClassSelection.Items.Add(new ClassSelectionItem
{
DisplayText = displayText,
ClassName = className,
TeacherName = teacherName
});
}
var savedClassName = MainWindow.Settings?.Dlass?.SelectedClassName ?? string.Empty;
if (!string.IsNullOrEmpty(savedClassName))
{
var savedItem = CmbClassSelection.Items.Cast<ClassSelectionItem>()
.FirstOrDefault(item => item.ClassName == savedClassName);
if (savedItem != null)
{
CmbClassSelection.SelectedItem = savedItem;
}
else if (CmbClassSelection.Items.Count > 0)
{
CmbClassSelection.SelectedIndex = 0;
}
}
else if (CmbClassSelection.Items.Count > 0)
{
CmbClassSelection.SelectedIndex = 0;
}
CmbClassSelection.IsEnabled = true;
}
else
{
CmbClassSelection.Items.Add("(无可用班级)");
CmbClassSelection.SelectedIndex = 0;
CmbClassSelection.IsEnabled = false;
}
}
/// <summary>
/// 班级选择改变事件
/// </summary>
private void CmbClassSelection_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
try
{
if (CmbClassSelection.SelectedItem is ClassSelectionItem selectedItem)
{
if (MainWindow.Settings?.Dlass != null)
{
MainWindow.Settings.Dlass.SelectedClassName = selectedItem.ClassName;
MainWindow.SaveSettingsToFile();
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"选择班级时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 加载自动上传设置
/// </summary>
private void LoadAutoUploadSettings()
{
try
{
if (MainWindow.Settings?.Dlass != null)
{
ToggleSwitchAutoUploadNotes.IsOn = MainWindow.Settings.Dlass.IsAutoUploadNotes;
var delayMinutes = MainWindow.Settings.Dlass.AutoUploadDelayMinutes;
if (delayMinutes < 0 || delayMinutes > 60)
{
delayMinutes = 0;
}
TxtUploadDelayMinutes.Text = delayMinutes.ToString();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加载自动上传设置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 自动上传开关切换事件
/// </summary>
private void ToggleSwitchAutoUploadNotes_Toggled(object sender, RoutedEventArgs e)
{
try
{
if (MainWindow.Settings?.Dlass != null)
{
MainWindow.Settings.Dlass.IsAutoUploadNotes = ToggleSwitchAutoUploadNotes.IsOn;
MainWindow.SaveSettingsToFile();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存自动上传设置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 上传延迟时间输入框文本改变事件
/// </summary>
private void TxtUploadDelayMinutes_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
try
{
if (MainWindow.Settings?.Dlass != null && int.TryParse(TxtUploadDelayMinutes.Text, out int delayMinutes))
{
// 限制范围在0-60分钟
if (delayMinutes < 0)
{
delayMinutes = 0;
TxtUploadDelayMinutes.Text = "0";
}
else if (delayMinutes > 60)
{
delayMinutes = 60;
TxtUploadDelayMinutes.Text = "60";
}
MainWindow.Settings.Dlass.AutoUploadDelayMinutes = delayMinutes;
MainWindow.SaveSettingsToFile();
}
else if (string.IsNullOrWhiteSpace(TxtUploadDelayMinutes.Text))
{
// 空文本时设置为0
if (MainWindow.Settings?.Dlass != null)
{
MainWindow.Settings.Dlass.AutoUploadDelayMinutes = 0;
MainWindow.SaveSettingsToFile();
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存上传延迟时间时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 上传延迟时间输入框预览文本输入事件(只允许数字)
/// </summary>
private void TxtUploadDelayMinutes_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
Regex regex = new Regex("[^0-9]+");
e.Handled = regex.IsMatch(e.Text);
}
/// <summary>
/// 标题栏拖动事件
/// </summary>
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ButtonState == MouseButtonState.Pressed)
{
DragMove();
}
}
/// <summary>
/// 关闭按钮点击事件
/// </summary>
private void BtnClose_Click(object sender, RoutedEventArgs e)
{
Close();
}
/// <summary>
/// 下拉框选择改变事件
/// </summary>
private void CmbSavedTokens_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
try
{
if (CmbSavedTokens.SelectedItem != null && CmbSavedTokens.SelectedItem.ToString() != "(无保存的Token")
{
var selectedToken = CmbSavedTokens.SelectedItem.ToString();
SaveUserToken(selectedToken);
_apiClient?.Dispose();
InitializeApiClient();
TxtTokenStatus.Text = "已选择Token";
TxtTokenStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(34, 197, 94));
_ = TestConnectionAsync();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"选择Token时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 保存Token按钮点击事件
/// </summary>
private void BtnSaveToken_Click(object sender, RoutedEventArgs e)
{
try
{
var token = TxtNewToken.Text?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(token))
{
MessageBox.Show("请输入新的用户Token", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
AddTokenToList(token);
SaveUserToken(token);
_apiClient?.Dispose();
InitializeApiClient();
LoadUserToken();
MessageBox.Show("Token已成功保存并已选择", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
_ = TestConnectionAsync();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存Token时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"保存Token时发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 清除Token按钮点击事件
/// </summary>
private void BtnClearToken_Click(object sender, RoutedEventArgs e)
{
try
{
if (CmbSavedTokens.SelectedItem == null || CmbSavedTokens.SelectedItem.ToString() == "(无保存的Token")
{
MessageBox.Show("请先选择一个Token", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var selectedToken = CmbSavedTokens.SelectedItem.ToString();
var result = MessageBox.Show($"确定要删除已选中的Token吗?", "确认", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
RemoveTokenFromList(selectedToken);
if (GetUserToken() == selectedToken)
{
SaveUserToken(string.Empty);
}
_apiClient?.Dispose();
InitializeApiClient();
LoadUserToken();
CmbClassSelection.Items.Clear();
CmbClassSelection.Items.Add("(等待连接)");
CmbClassSelection.SelectedIndex = 0;
CmbClassSelection.IsEnabled = false;
_currentWhiteboards.Clear();
_currentUser = null;
TxtConnectionStatus.Text = "未连接";
TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(161, 161, 170));
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"删除Token时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"删除Token时发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 测试Token连接按钮点击事件
/// </summary>
private async void BtnTestToken_Click(object sender, RoutedEventArgs e)
{
await TestConnectionAsync();
}
/// <summary>
/// 保存按钮点击事件
/// </summary>
private async void BtnSave_Click(object sender, RoutedEventArgs e)
{
try
{
// TODO: 根据实际API文档实现保存逻辑
// 示例:保存设置到服务器
// var settings = new { ... };
// await _apiClient.PostAsync<ApiResponse>("/api/settings", settings);
MessageBox.Show("设置已保存", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
Close();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存设置时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"保存设置时发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 取消按钮点击事件
/// </summary>
private void BtnCancel_Click(object sender, RoutedEventArgs e)
{
Close();
}
/// <summary>
/// 测试API连接
/// </summary>
private async Task TestConnectionAsync()
{
Dispatcher.Invoke(() =>
{
TxtConnectionStatus.Text = "测试中...";
TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(161, 161, 170)); // 灰色
});
try
{
var userToken = GetUserToken();
if (string.IsNullOrEmpty(userToken))
{
Dispatcher.Invoke(() =>
{
TxtConnectionStatus.Text = "未设置Token";
TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(239, 68, 68)); // 红色
});
return;
}
// 根据文档,使用 auth-with-token 接口验证token
// 此接口需要POST请求,包含app_id, app_secret和user_token
try
{
var authData = new
{
app_id = APP_ID,
app_secret = APP_SECRET,
user_token = userToken
};
var result = await _apiClient.PostAsync<AuthWithTokenResponse>("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false);
if (result != null && result.Success)
{
var whiteboards = result.Whiteboards ?? new List<WhiteboardInfo>();
_currentWhiteboards = whiteboards;
_currentUser = result.User;
var whiteboardCount = whiteboards.Count;
Dispatcher.Invoke(() =>
{
TxtConnectionStatus.Text = $"已连接 (找到 {whiteboardCount} 个白板)";
TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(34, 197, 94));
// 加载班级列表
LoadClasses(whiteboards, result.User);
});
}
else
{
throw new Exception("认证响应失败");
}
}
catch (Exception ex)
{
if (userToken.Length < 10)
{
throw new Exception("Token格式可能不正确(长度过短,至少需要10个字符)");
}
LogHelper.WriteLogToFile($"Token验证失败: {ex.Message}", LogHelper.LogType.Error);
throw;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"Dlass API连接测试失败: {ex.Message}", LogHelper.LogType.Error);
Dispatcher.Invoke(() =>
{
TxtConnectionStatus.Text = "连接失败";
TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(239, 68, 68));
// 清空班级列表
CmbClassSelection.Items.Clear();
CmbClassSelection.Items.Add("(无可用班级)");
CmbClassSelection.SelectedIndex = 0;
CmbClassSelection.IsEnabled = false;
_currentWhiteboards.Clear();
});
}
}
}
#region API响应模型
/// <summary>
/// auth-with-token接口响应模型
/// </summary>
public class AuthWithTokenResponse
{
[Newtonsoft.Json.JsonProperty("success")]
public bool Success { get; set; }
[Newtonsoft.Json.JsonProperty("whiteboards")]
public List<WhiteboardInfo> Whiteboards { get; set; }
[Newtonsoft.Json.JsonProperty("count")]
public int Count { get; set; }
[Newtonsoft.Json.JsonProperty("user")]
public UserInfo User { get; set; }
}
/// <summary>
/// 白板信息模型
/// </summary>
public class WhiteboardInfo
{
[Newtonsoft.Json.JsonProperty("id")]
public int Id { get; set; }
[Newtonsoft.Json.JsonProperty("name")]
public string Name { get; set; }
[Newtonsoft.Json.JsonProperty("board_id")]
public string BoardId { get; set; }
[Newtonsoft.Json.JsonProperty("secret_key")]
public string SecretKey { get; set; }
[Newtonsoft.Json.JsonProperty("class_name")]
public string ClassName { get; set; }
[Newtonsoft.Json.JsonProperty("class_id")]
public int ClassId { get; set; }
[Newtonsoft.Json.JsonProperty("is_online")]
public bool IsOnline { get; set; }
[Newtonsoft.Json.JsonProperty("last_heartbeat")]
public string LastHeartbeat { get; set; }
[Newtonsoft.Json.JsonProperty("created_at")]
public string CreatedAt { get; set; }
}
/// <summary>
/// 用户信息模型
/// </summary>
public class UserInfo
{
[Newtonsoft.Json.JsonProperty("id")]
public int Id { get; set; }
[Newtonsoft.Json.JsonProperty("username")]
public string Username { get; set; }
[Newtonsoft.Json.JsonProperty("email")]
public string Email { get; set; }
}
/// <summary>
/// 班级选择项
/// </summary>
public class ClassSelectionItem
{
public string DisplayText { get; set; }
public string ClassName { get; set; }
public string TeacherName { get; set; }
public override string ToString()
{
return DisplayText;
}
}
#endregion
}
@@ -125,9 +125,8 @@ namespace Ink_Canvas
seconds = timeSpan.Seconds;
}
// 更新小时显示
SetDigitDisplay("FullHour1Display", hours / 10, shouldShowRed);
SetDigitDisplay("FullHour2Display", hours % 10, shouldShowRed);
SetDigitDisplay("FullHour1Display", Math.Abs(hours / 10) % 10, shouldShowRed);
SetDigitDisplay("FullHour2Display", (hours % 10 + 10) % 10, shouldShowRed);
// 更新分钟显示
SetDigitDisplay("FullMinute1Display", minutes / 10, shouldShowRed);
@@ -28,6 +28,9 @@ namespace Ink_Canvas
this.Left = parent.Left;
this.Top = parent.Top;
// 根据分辨率和DPI缩放窗口
ScaleWindowForResolution();
// 启动更新定时器
updateTimer = new System.Timers.Timer(100); // 100ms更新一次
updateTimer.Elapsed += UpdateTimer_Elapsed;
@@ -39,6 +42,43 @@ namespace Ink_Canvas
ApplyTheme();
}
/// <summary>
/// 根据屏幕分辨率和 DPI 缩放窗口大小(保持原始尺寸,使用Transform缩放)
/// </summary>
private void ScaleWindowForResolution()
{
try
{
// 获取屏幕尺寸(考虑 DPI 缩放)
double screenWidth = SystemParameters.PrimaryScreenWidth;
double screenHeight = SystemParameters.PrimaryScreenHeight;
// 基准分辨率(1920x1080
const double baseWidth = 1920.0;
const double baseHeight = 1080.0;
// 计算缩放比例(使用较小的比例以保持比例)
double scaleX = screenWidth / baseWidth;
double scaleY = screenHeight / baseHeight;
double scale = Math.Min(scaleX, scaleY);
// 限制最小和最大缩放,避免过小或过大
scale = Math.Max(0.5, Math.Min(2.0, scale));
// 应用缩放变换到整个窗口内容
var scaleTransform = this.FindName("WindowScaleTransform") as ScaleTransform;
if (scaleTransform != null)
{
scaleTransform.ScaleX = scale;
scaleTransform.ScaleY = scale;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"缩放窗口大小时出错: {ex.Message}");
}
}
private void UpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
if (parentWindow != null)
@@ -129,9 +169,8 @@ namespace Ink_Canvas
seconds = timeSpan.Seconds;
}
// 更新小时显示
SetDigitDisplay("MinHour1Display", hours / 10, shouldShowRed);
SetDigitDisplay("MinHour2Display", hours % 10, shouldShowRed);
SetDigitDisplay("MinHour1Display", Math.Abs(hours / 10) % 10, shouldShowRed);
SetDigitDisplay("MinHour2Display", (hours % 10 + 10) % 10, shouldShowRed);
// 更新分钟显示
SetDigitDisplay("MinMinute1Display", minutes / 10, shouldShowRed);
+71 -33
View File
@@ -126,9 +126,10 @@ namespace Ink_Canvas
private DateTime lastActivityTime = DateTime.Now;
// 机器学习相关
private RollCallHistoryData historyData = new RollCallHistoryData();
private int maxRecentHistory = 20;
private double avoidanceWeight = 0.8; // 避免重复的权重
private static RollCallHistoryData historyData = null;
private static readonly object historyLock = new object();
private static int maxRecentHistory = 20;
private static double avoidanceWeight = 0.8; // 避免重复的权重
private const double FREQUENCY_WEIGHT = 0.2; // 频率平衡的权重
// 单次抽相关
@@ -394,13 +395,13 @@ namespace Ink_Canvas
switch (selectedRollCallMode)
{
case "Random":
return SelectNamesWithML(availableNames, count);
return SelectNamesWithML(availableNames, count, random);
case "Sequential":
return SelectNamesSequentially(availableNames, count);
case "Group":
return SelectNamesInGroups(availableNames, count);
default:
return SelectNamesWithML(availableNames, count);
return SelectNamesWithML(availableNames, count, random);
}
}
@@ -471,18 +472,25 @@ namespace Ink_Canvas
/// </summary>
/// <param name="availableNames">可用名单</param>
/// <param name="count">需要选择的人数</param>
/// <param name="random">随机数生成器</param>
/// <returns>选择的人员名单</returns>
private List<string> SelectNamesWithML(List<string> availableNames, int count)
public static List<string> SelectNamesWithML(List<string> availableNames, int count, Random random)
{
if (availableNames == null || availableNames.Count == 0)
return new List<string>();
// 确保历史数据已初始化
if (historyData == null)
{
LoadRollCallHistory();
}
// 检查是否启用机器学习避免重复
bool enableML = MainWindow.Settings?.RandSettings?.EnableMLAvoidance ?? true;
if (!enableML)
{
// 如果禁用机器学习,使用简单随机选择
return SelectNamesRandomly(availableNames, count);
return SelectNamesRandomly(availableNames, count, random);
}
var selectedNames = new List<string>();
@@ -490,7 +498,7 @@ namespace Ink_Canvas
for (int i = 0; i < count && remainingNames.Count > 0; i++)
{
string selectedName = SelectSingleNameWithML(remainingNames, selectedNames);
string selectedName = SelectSingleNameWithML(remainingNames, selectedNames, random);
if (!string.IsNullOrEmpty(selectedName))
{
selectedNames.Add(selectedName);
@@ -504,7 +512,7 @@ namespace Ink_Canvas
/// <summary>
/// 简单随机选择点名人员
/// </summary>
private List<string> SelectNamesRandomly(List<string> availableNames, int count)
private static List<string> SelectNamesRandomly(List<string> availableNames, int count, Random random)
{
if (availableNames == null || availableNames.Count == 0)
return new List<string>();
@@ -525,7 +533,7 @@ namespace Ink_Canvas
/// <summary>
/// 使用机器学习算法选择单个人员
/// </summary>
private string SelectSingleNameWithML(List<string> availableNames, List<string> alreadySelected)
private static string SelectSingleNameWithML(List<string> availableNames, List<string> alreadySelected, Random random)
{
if (availableNames.Count == 0) return null;
if (availableNames.Count == 1) return availableNames[0];
@@ -554,15 +562,15 @@ namespace Ink_Canvas
}
// 使用加权随机选择
return WeightedRandomSelection(nameWeights);
return WeightedRandomSelection(nameWeights, random);
}
/// <summary>
/// 计算避免最近重复的权重
/// </summary>
private double CalculateRecentAvoidanceWeight(string name)
private static double CalculateRecentAvoidanceWeight(string name)
{
if (historyData.History == null || historyData.History.Count == 0)
if (historyData == null || historyData.History == null || historyData.History.Count == 0)
return 0.0;
// 获取最近记录
@@ -576,9 +584,9 @@ namespace Ink_Canvas
/// <summary>
/// 计算频率平衡权重
/// </summary>
private double CalculateFrequencyWeight(string name)
private static double CalculateFrequencyWeight(string name)
{
if (historyData.NameFrequency == null || !historyData.NameFrequency.ContainsKey(name))
if (historyData == null || historyData.NameFrequency == null || !historyData.NameFrequency.ContainsKey(name))
return 0.5; // 如果从未被选中,给予中等权重
int totalSelections = historyData.NameFrequency.Values.Sum();
@@ -594,7 +602,7 @@ namespace Ink_Canvas
/// <summary>
/// 加权随机选择
/// </summary>
private string WeightedRandomSelection(Dictionary<string, double> nameWeights)
private static string WeightedRandomSelection(Dictionary<string, double> nameWeights, Random random)
{
if (nameWeights.Count == 0) return null;
@@ -619,15 +627,23 @@ namespace Ink_Canvas
/// <summary>
/// 更新点名历史记录
/// </summary>
private void UpdateRollCallHistory(List<string> selectedNames)
public static void UpdateRollCallHistory(List<string> selectedNames)
{
if (selectedNames == null || selectedNames.Count == 0) return;
// 更新历史记录
if (historyData.History == null)
historyData.History = new List<string>();
// 确保历史数据已初始化
if (historyData == null)
{
LoadRollCallHistory();
}
historyData.History.AddRange(selectedNames);
lock (historyLock)
{
// 更新历史记录
if (historyData.History == null)
historyData.History = new List<string>();
historyData.History.AddRange(selectedNames);
// 保持历史记录不超过100条
if (historyData.History.Count > 100)
@@ -647,18 +663,20 @@ namespace Ink_Canvas
historyData.NameFrequency[name] = 1;
}
historyData.LastUpdate = DateTime.Now;
historyData.LastUpdate = DateTime.Now;
// 保存到文件
SaveRollCallHistory();
// 保存到文件
SaveRollCallHistory();
}
}
#endregion
#region
/// <summary>
/// 加载点名历史记录
/// </summary>
private void LoadRollCallHistory()
private static void LoadRollCallHistory()
{
try
{
@@ -695,7 +713,7 @@ namespace Ink_Canvas
/// <summary>
/// 保存点名历史记录
/// </summary>
private void SaveRollCallHistory()
private static void SaveRollCallHistory()
{
try
{
@@ -838,6 +856,21 @@ namespace Ink_Canvas
}
}
private void ViewHistory_Click(object sender, RoutedEventArgs e)
{
try
{
// 打开历史记录查看窗口
var historyWindow = new RollCallHistoryWindow();
historyWindow.ShowDialog();
}
catch (Exception ex)
{
MessageBox.Show($"打开历史记录失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
LogHelper.WriteLogToFile($"打开历史记录失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private void LoadNamesFromFile()
{
try
@@ -1292,8 +1325,9 @@ namespace Ink_Canvas
// 动画结束,显示最终结果
Application.Current.Dispatcher.Invoke(() =>
{
// 使用60个数字进行抽选
var selectedNumbers = SelectMultipleNumbers(currentCount);
// 使用降重抽选方法选择数字
var numberList = Enumerable.Range(1, 60).Select(n => n.ToString()).ToList();
var selectedNumbers = SelectNamesWithML(numberList, currentCount, random);
// 更新历史记录
UpdateRollCallHistory(selectedNumbers);
@@ -1384,8 +1418,8 @@ namespace Ink_Canvas
// 动画结束,显示最终结果
Application.Current.Dispatcher.Invoke(() =>
{
// 根据选择的模式进行不同的点名逻辑
var selectedNames = SelectNamesByMode(nameList, currentCount);
// 使用降重抽选方法
var selectedNames = SelectNamesWithML(nameList, currentCount, random);
// 更新历史记录
UpdateRollCallHistory(selectedNames);
@@ -1442,8 +1476,12 @@ namespace Ink_Canvas
// 动画结束,显示最终结果
Application.Current.Dispatcher.Invoke(() =>
{
// 根据选择的数量进行抽选
var selectedNumbers = SelectMultipleNumbers(currentCount);
// 使用降重抽选方法选择数字
var numberList = Enumerable.Range(1, 60).Select(n => n.ToString()).ToList();
var selectedNumbers = SelectNamesWithML(numberList, currentCount, random);
// 更新历史记录
UpdateRollCallHistory(selectedNumbers);
if (selectedNumbers.Count == 1)
{
@@ -1482,7 +1520,7 @@ namespace Ink_Canvas
}
/// <summary>
/// 选择多个数字(不重复)
/// 选择多个数字
/// </summary>
private List<string> SelectMultipleNumbers(int count)
{
+13 -1
View File
@@ -418,7 +418,7 @@
Foreground="{DynamicResource NewRollCallWindowButtonForeground}"/>
</Button>
<Button x:Name="ClearListBtn" Width="90" Height="40" Background="{DynamicResource NewRollCallWindowButtonBackground}"
BorderThickness="0" Click="ClearList_Click" Cursor="Hand">
BorderThickness="0" Click="ClearList_Click" Cursor="Hand" Margin="0,0,10,0">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="8">
@@ -429,6 +429,18 @@
<TextBlock Text="清空名单" FontSize="14"
Foreground="{DynamicResource NewRollCallWindowButtonForeground}"/>
</Button>
<Button x:Name="ViewHistoryBtn" Width="90" Height="40" Background="{DynamicResource NewRollCallWindowButtonBackground}"
BorderThickness="0" Click="ViewHistory_Click" Cursor="Hand">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="8">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<TextBlock Text="查看历史" FontSize="14"
Foreground="{DynamicResource NewRollCallWindowButtonForeground}"/>
</Button>
</StackPanel>
</StackPanel>
</Grid>
+14 -21
View File
@@ -130,8 +130,8 @@ namespace Ink_Canvas
bool shouldShowRed = MainWindow.Settings.RandSettings?.EnableOvertimeRedText == true;
SetDigitDisplay("Digit1Display", displayHours / 10, shouldShowRed);
SetDigitDisplay("Digit2Display", displayHours % 10, shouldShowRed);
SetDigitDisplay("Digit1Display", Math.Abs(displayHours / 10) % 10, shouldShowRed);
SetDigitDisplay("Digit2Display", (displayHours % 10 + 10) % 10, shouldShowRed);
SetDigitDisplay("Digit3Display", overtimeSpan.Minutes / 10, shouldShowRed);
SetDigitDisplay("Digit4Display", overtimeSpan.Minutes % 10, shouldShowRed);
SetDigitDisplay("Digit5Display", overtimeSpan.Seconds / 10, shouldShowRed);
@@ -430,10 +430,12 @@ namespace Ink_Canvas
if (isPaused) return null;
var elapsed = DateTime.Now - startTime;
var totalSeconds = hour * 3600 + minute * 60 + second;
var remaining = totalSeconds - elapsed.TotalSeconds;
var totalTimeSpan = new TimeSpan(hour, minute, second);
var leftTimeSpan = totalTimeSpan - elapsed;
return TimeSpan.FromSeconds(remaining);
if (leftTimeSpan.Milliseconds > 0) leftTimeSpan += new TimeSpan(0, 0, 1);
return leftTimeSpan;
}
public void StopTimer()
@@ -796,25 +798,16 @@ namespace Ink_Canvas
private void Reset_Click(object sender, RoutedEventArgs e)
{
if (!isTimerRunning)
if (isTimerRunning)
{
UpdateDigitDisplays();
isOvertimeMode = false;
}
else if (isTimerRunning && isPaused)
{
UpdateDigitDisplays();
StartPauseIcon.Data = Geometry.Parse(PlayIconData);
isTimerRunning = false;
timer.Stop();
isPaused = false;
isOvertimeMode = false;
}
else
{
startTime = DateTime.Now;
Timer_Elapsed(timer, null);
isTimerRunning = false;
}
isPaused = false;
isOvertimeMode = false;
UpdateDigitDisplays();
StartPauseIcon.Data = Geometry.Parse(PlayIconData);
}
private void PlayTimerSound()
+109 -8
View File
@@ -14,6 +14,10 @@ namespace Ink_Canvas
/// </summary>
public partial class QuickDrawFloatingButton : Window
{
private bool isDragging = false;
private Point dragStartPoint;
private Point windowStartPoint;
public QuickDrawFloatingButton()
{
InitializeComponent();
@@ -21,6 +25,14 @@ namespace Ink_Canvas
// 设置无焦点状态
this.Focusable = false;
this.ShowInTaskbar = false;
// 窗口句柄创建后应用无焦点模式
this.SourceInitialized += QuickDrawFloatingButton_SourceInitialized;
}
private void QuickDrawFloatingButton_SourceInitialized(object sender, EventArgs e)
{
ApplyNoFocusMode();
}
@@ -29,12 +41,13 @@ namespace Ink_Canvas
// 设置位置到屏幕右下角稍微靠近中部
SetPositionToBottomRight();
// 应用无焦点模式
ApplyNoFocusMode();
// 应用置顶
ApplyFloatingButtonTopmost();
// 如果主窗口在无焦点模式下,启动置顶维护
if (MainWindow.Settings?.Advanced?.IsNoFocusMode == true &&
MainWindow.Settings?.Advanced?.EnableUIAccessTopMost != true)
if (MainWindow.Settings?.Advanced?.EnableUIAccessTopMost != true)
{
StartTopmostMaintenance();
}
@@ -62,6 +75,9 @@ namespace Ink_Canvas
{
try
{
// 如果正在拖动,不触发点击事件
if (isDragging) return;
// 打开快抽窗口
var quickDrawWindow = new QuickDrawWindow();
quickDrawWindow.ShowDialog();
@@ -72,6 +88,68 @@ namespace Ink_Canvas
}
}
private void DragArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
isDragging = false;
// 记录鼠标在屏幕上的初始位置
dragStartPoint = this.PointToScreen(e.GetPosition(this));
// 记录窗口的初始位置
windowStartPoint = new Point(this.Left, this.Top);
((UIElement)sender).CaptureMouse();
e.Handled = true;
}
private void DragArea_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed && ((UIElement)sender).IsMouseCaptured)
{
// 获取鼠标在屏幕上的当前位置
Point currentScreenPoint = this.PointToScreen(e.GetPosition(this));
Vector diff = currentScreenPoint - dragStartPoint;
if (!isDragging && (Math.Abs(diff.X) > 3 || Math.Abs(diff.Y) > 3))
{
isDragging = true;
}
if (isDragging)
{
// 使用窗口初始位置加上鼠标移动的距离
double newLeft = windowStartPoint.X + diff.X;
double newTop = windowStartPoint.Y + diff.Y;
// 限制在屏幕范围内
var workingArea = SystemParameters.WorkArea;
newLeft = Math.Max(workingArea.Left, Math.Min(newLeft, workingArea.Right - this.Width));
newTop = Math.Max(workingArea.Top, Math.Min(newTop, workingArea.Bottom - this.Height));
this.Left = newLeft;
this.Top = newTop;
}
}
}
private void DragArea_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (((UIElement)sender).IsMouseCaptured)
{
((UIElement)sender).ReleaseMouseCapture();
}
// 延迟重置拖动状态,避免触发点击事件
if (isDragging)
{
Dispatcher.BeginInvoke(new Action(() => { isDragging = false; }),
DispatcherPriority.Background);
}
else
{
isDragging = false;
}
e.Handled = true;
}
@@ -105,6 +183,7 @@ namespace Ink_Canvas
private static extern uint GetCurrentProcessId();
private const int GWL_EXSTYLE = -20;
private const int WS_EX_NOACTIVATE = 0x08000000;
private const int WS_EX_TOPMOST = 0x00000008;
private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
private static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
@@ -118,6 +197,26 @@ namespace Ink_Canvas
private DispatcherTimer topmostMaintenanceTimer;
private bool isTopmostMaintenanceEnabled;
/// <summary>
/// 应用无焦点模式
/// </summary>
private void ApplyNoFocusMode()
{
try
{
var hwnd = new WindowInteropHelper(this).Handle;
if (hwnd == IntPtr.Zero) return;
int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
// 悬浮快抽窗口始终启用无焦点模式
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_NOACTIVATE);
}
catch (Exception)
{
}
}
/// <summary>
/// 应用悬浮按钮置顶
/// </summary>
@@ -194,11 +293,7 @@ namespace Ink_Canvas
return;
}
if (MainWindow.Settings?.Advanced?.IsNoFocusMode != true)
{
StopTopmostMaintenance();
return;
}
// 悬浮快抽窗口始终启用无焦点模式,不需要检查主窗口设置
var hwnd = new WindowInteropHelper(this).Handle;
if (hwnd == IntPtr.Zero) return;
@@ -233,6 +328,12 @@ namespace Ink_Canvas
{
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TOPMOST);
}
// 确保无焦点模式样式正确
if ((exStyle & WS_EX_NOACTIVATE) == 0)
{
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_NOACTIVATE);
}
}
}
catch (Exception ex)
+58 -13
View File
@@ -7,7 +7,7 @@
mc:Ignorable="d" WindowStyle="None" AllowsTransparency="True"
Loaded="FloatingButton_Loaded" WindowStartupLocation="Manual"
ShowInTaskbar="False" Focusable="False"
Title="快抽悬浮按钮" Height="45" Width="45">
Title="快抽悬浮按钮" Height="45" Width="65">
<Window.Resources>
<ResourceDictionary>
@@ -21,23 +21,68 @@
<Border Background="{DynamicResource QuickDrawFloatingButtonBackground}"
CornerRadius="8"
BorderThickness="1"
BorderBrush="{DynamicResource QuickDrawFloatingButtonBorderBrush}"
MouseLeftButtonDown="FloatingButton_Click"
Cursor="Hand">
BorderBrush="{DynamicResource QuickDrawFloatingButtonBorderBrush}">
<Border.Effect>
<DropShadowEffect Color="Black" Direction="315" ShadowDepth="3" Opacity="0.3" BlurRadius="5"/>
</Border.Effect>
<Grid>
<Path Data="M5 7C5 8.06087 5.42143 9.07828 6.17157 9.82843C6.92172 10.5786 7.93913 11 9 11C10.0609 11 11.0783 10.5786 11.8284 9.82843C12.5786 9.07828 13 8.06087 13 7C13 5.93913 12.5786 4.92172 11.8284 4.17157C11.0783 3.42143 10.0609 3 9 3C7.93913 3 6.92172 3.42143 6.17157 4.17157C5.42143 4.92172 5 5.93913 5 7Z M3 21V19C3 17.9391 3.42143 16.9217 4.17157 16.1716C4.92172 15.4214 5.93913 15 7 15H11C12.0609 15 13.0783 15.4214 13.8284 16.1716C14.5786 16.9217 15 17.9391 15 19V21 M16 3.13C16.8604 3.35031 17.623 3.85071 18.1676 4.55232C18.7122 5.25392 19.0078 6.11683 19.0078 7.005C19.0078 7.89318 18.7122 8.75608 18.1676 9.45769C17.623 10.1593 16.8604 10.6597 16 10.88 M21 21V19C20.9949 18.1172 20.6979 17.2608 20.1553 16.5644C19.6126 15.868 18.8548 15.3707 18 15.15"
Stroke="{DynamicResource QuickDrawFloatingButtonIconForeground}"
StrokeThickness="2"
StrokeLineJoin="Round"
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="22"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 拖动区域 -->
<Border Grid.Column="0"
MouseLeftButtonDown="DragArea_MouseLeftButtonDown"
MouseMove="DragArea_MouseMove"
MouseLeftButtonUp="DragArea_MouseLeftButtonUp"
Cursor="SizeAll"
Background="Transparent">
<Grid VerticalAlignment="Center" Height="14" IsHitTestVisible="False">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="4"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="4"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 三个白色横线 -->
<Border Grid.Row="0" Background="{DynamicResource QuickDrawFloatingButtonIconForeground}" Height="2" Width="10"
HorizontalAlignment="Center"
CornerRadius="1" Opacity="0.8" IsHitTestVisible="False"/>
<Border Grid.Row="2" Background="{DynamicResource QuickDrawFloatingButtonIconForeground}" Height="2" Width="10"
HorizontalAlignment="Center"
CornerRadius="1" Opacity="0.8" IsHitTestVisible="False"/>
<Border Grid.Row="4" Background="{DynamicResource QuickDrawFloatingButtonIconForeground}" Height="2" Width="10"
HorizontalAlignment="Center"
CornerRadius="1" Opacity="0.8" IsHitTestVisible="False"/>
</Grid>
</Border>
<!-- 半透明分割线 -->
<Rectangle Grid.Column="1" Width="1" Fill="#20FFFFFF" Margin="0,8,0,8"/>
<!-- 按钮区域 -->
<Border Grid.Column="2"
MouseLeftButtonDown="FloatingButton_Click"
Cursor="Hand"
Background="Transparent">
<Grid IsHitTestVisible="False">
<Path Data="M5 7C5 8.06087 5.42143 9.07828 6.17157 9.82843C6.92172 10.5786 7.93913 11 9 11C10.0609 11 11.0783 10.5786 11.8284 9.82843C12.5786 9.07828 13 8.06087 13 7C13 5.93913 12.5786 4.92172 11.8284 4.17157C11.0783 3.42143 10.0609 3 9 3C7.93913 3 6.92172 3.42143 6.17157 4.17157C5.42143 4.92172 5 5.93913 5 7Z M3 21V19C3 17.9391 3.42143 16.9217 4.17157 16.1716C4.92172 15.4214 5.93913 15 7 15H11C12.0609 15 13.0783 15.4214 13.8284 16.1716C14.5786 16.9217 15 17.9391 15 19V21 M16 3.13C16.8604 3.35031 17.623 3.85071 18.1676 4.55232C18.7122 5.25392 19.0078 6.11683 19.0078 7.005C19.0078 7.89318 18.7122 8.75608 18.1676 9.45769C17.623 10.1593 16.8604 10.6597 16 10.88 M21 21V19C20.9949 18.1172 20.6979 17.2608 20.1553 16.5644C19.6126 15.868 18.8548 15.3707 18 15.15"
Stroke="{DynamicResource QuickDrawFloatingButtonIconForeground}"
StrokeThickness="2"
StrokeLineJoin="Round"
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsHitTestVisible="False"/>
</Grid>
</Border>
</Grid>
</Border>
</Window>
+27 -18
View File
@@ -99,7 +99,7 @@ namespace Ink_Canvas
{
const int animationTimes = 100; // 动画次数
const int sleepTime = 5; // 每次动画间隔(毫秒)
new System.Threading.Thread(() =>
{
if (nameList.Count > 0)
@@ -121,7 +121,7 @@ namespace Ink_Canvas
private void StartNameDrawAnimation(int animationTimes, int sleepTime)
{
List<string> usedNames = new List<string>();
for (int i = 0; i < animationTimes; i++)
{
// 随机选择一个名字进行动画显示,避免立即重复
@@ -130,25 +130,29 @@ namespace Ink_Canvas
{
randomName = nameList[random.Next(0, nameList.Count)];
} while (usedNames.Count > 0 && usedNames[usedNames.Count - 1] == randomName);
usedNames.Add(randomName);
Application.Current.Dispatcher.Invoke(() =>
{
MainResultDisplay.Text = randomName;
});
System.Threading.Thread.Sleep(sleepTime);
}
// 动画结束,显示最终结果
Application.Current.Dispatcher.Invoke(() =>
{
// 随机选择一个最终名字
string finalName = nameList[random.Next(0, nameList.Count)];
// 使用降重抽选方法选择最终名字
var selectedNames = NewStyleRollCallWindow.SelectNamesWithML(nameList, 1, random);
string finalName = selectedNames.Count > 0 ? selectedNames[0] : nameList[random.Next(0, nameList.Count)];
MainResultDisplay.Text = finalName;
// 更新历史记录
NewStyleRollCallWindow.UpdateRollCallHistory(new List<string> { finalName });
});
// 显示结果后,等待一段时间让用户看到结果,然后关闭窗口
new System.Threading.Thread(() =>
{
@@ -166,7 +170,7 @@ namespace Ink_Canvas
private void StartNumberDrawAnimation(int animationTimes, int sleepTime)
{
List<int> usedNumbers = new List<int>();
for (int i = 0; i < animationTimes; i++)
{
// 随机选择一个数字进行动画显示,避免立即重复
@@ -175,25 +179,30 @@ namespace Ink_Canvas
{
randomNumber = random.Next(1, 61); // 1-60
} while (usedNumbers.Count > 0 && usedNumbers[usedNumbers.Count - 1] == randomNumber);
usedNumbers.Add(randomNumber);
Application.Current.Dispatcher.Invoke(() =>
{
MainResultDisplay.Text = randomNumber.ToString();
});
System.Threading.Thread.Sleep(sleepTime);
}
// 动画结束,显示最终结果
Application.Current.Dispatcher.Invoke(() =>
{
// 随机选择一个最终数字
int finalNumber = random.Next(1, 61);
MainResultDisplay.Text = finalNumber.ToString();
// 使用降重抽选方法选择最终数字
var numberList = Enumerable.Range(1, 60).Select(n => n.ToString()).ToList();
var selectedNumbers = NewStyleRollCallWindow.SelectNamesWithML(numberList, 1, random);
string finalNumber = selectedNumbers.Count > 0 ? selectedNumbers[0] : random.Next(1, 61).ToString();
MainResultDisplay.Text = finalNumber;
// 更新历史记录
NewStyleRollCallWindow.UpdateRollCallHistory(new List<string> { finalNumber });
});
// 显示结果后,等待一段时间让用户看到结果,然后关闭窗口
new System.Threading.Thread(() =>
{
@@ -0,0 +1,46 @@
<Window x:Class="Ink_Canvas.RollCallHistoryWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Ink_Canvas"
mc:Ignorable="d" FontFamily="Microsoft YaHei UI" ui:WindowHelper.UseModernWindowStyle="True"
WindowStartupLocation="CenterScreen"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern" Topmost="True"
Title="Ink Canvas 抽奖 - 点名历史记录" Height="500" Width="400"
Loaded="Window_Loaded">
<Window.Resources>
<!-- 主题资源 -->
<SolidColorBrush x:Key="RollCallHistoryWindowBackground" Color="White"/>
<SolidColorBrush x:Key="RollCallHistoryWindowForeground" Color="Black"/>
<SolidColorBrush x:Key="RollCallHistoryWindowButtonBackground" Color="#F4F4F5"/>
<SolidColorBrush x:Key="RollCallHistoryWindowButtonForeground" Color="Black"/>
<SolidColorBrush x:Key="RollCallHistoryWindowBorderBrush" Color="#E4E4E7"/>
</Window.Resources>
<Grid Background="{DynamicResource RollCallHistoryWindowBackground}">
<Label Content="点名历史记录"
Margin="10"
Foreground="{DynamicResource RollCallHistoryWindowForeground}"
FontFamily="Microsoft YaHei UI"/>
<TextBox Name="TextBoxHistory"
FontFamily="Microsoft YaHei UI"
VerticalScrollBarVisibility="Auto"
AcceptsReturn="True"
IsReadOnly="True"
Margin="10,40,10,50"
Background="{DynamicResource RollCallHistoryWindowBackground}"
Foreground="{DynamicResource RollCallHistoryWindowForeground}"
BorderBrush="{DynamicResource RollCallHistoryWindowBorderBrush}"/>
<Button Margin="10"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
Content="关闭"
FontFamily="Microsoft YaHei UI"
Width="100"
Click="Button_Click"
Background="{DynamicResource RollCallHistoryWindowButtonBackground}"
Foreground="{DynamicResource RollCallHistoryWindowButtonForeground}"
BorderBrush="{DynamicResource RollCallHistoryWindowBorderBrush}"/>
</Grid>
</Window>
@@ -0,0 +1,184 @@
using Ink_Canvas.Helpers;
using System;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using Newtonsoft.Json;
using MessageBox = iNKORE.UI.WPF.Modern.Controls.MessageBox;
namespace Ink_Canvas
{
/// <summary>
/// Interaction logic for RollCallHistoryWindow.xaml
/// </summary>
public partial class RollCallHistoryWindow : Window
{
public RollCallHistoryWindow()
{
InitializeComponent();
AnimationsHelper.ShowWithSlideFromBottomAndFade(this, 0.25);
ApplyTheme();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
LoadHistory();
}
private void LoadHistory()
{
try
{
string configsFolder = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configs");
string historyJsonPath = System.IO.Path.Combine(configsFolder, "RollCallHistory.json");
if (!File.Exists(historyJsonPath))
{
TextBoxHistory.Text = "暂无历史记录";
return;
}
string jsonContent = File.ReadAllText(historyJsonPath);
var historyData = JsonConvert.DeserializeObject<RollCallHistoryData>(jsonContent);
if (historyData == null || historyData.History == null || historyData.History.Count == 0)
{
TextBoxHistory.Text = "暂无历史记录";
return;
}
// 按时间倒序显示(最新的在上方)
// 由于历史记录是按时间顺序添加的,所以直接反转即可
var reversedHistory = historyData.History.ToList();
reversedHistory.Reverse();
// 显示历史记录,每行一个
TextBoxHistory.Text = string.Join(Environment.NewLine, reversedHistory);
// 显示统计信息
int totalCount = historyData.History.Count;
string lastUpdate = historyData.LastUpdate.ToString("yyyy-MM-dd HH:mm:ss");
string header = $"共 {totalCount} 条记录,最后更新:{lastUpdate}\n\n";
TextBoxHistory.Text = header + TextBoxHistory.Text;
}
catch (Exception ex)
{
TextBoxHistory.Text = $"加载历史记录失败: {ex.Message}";
LogHelper.WriteLogToFile($"加载点名历史记录失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Close();
}
private void ApplyTheme()
{
try
{
if (MainWindow.Settings != null)
{
ApplyTheme(MainWindow.Settings);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用历史记录窗口主题出错: {ex.Message}", LogHelper.LogType.Error);
}
}
private void ApplyTheme(Settings settings)
{
try
{
if (settings.Appearance.Theme == 0) // 浅色主题
{
iNKORE.UI.WPF.Modern.ThemeManager.SetRequestedTheme(this, iNKORE.UI.WPF.Modern.ElementTheme.Light);
ApplyThemeResources("Light");
}
else if (settings.Appearance.Theme == 1) // 深色主题
{
iNKORE.UI.WPF.Modern.ThemeManager.SetRequestedTheme(this, iNKORE.UI.WPF.Modern.ElementTheme.Dark);
ApplyThemeResources("Dark");
}
else // 跟随系统主题
{
bool isSystemLight = IsSystemThemeLight();
if (isSystemLight)
{
iNKORE.UI.WPF.Modern.ThemeManager.SetRequestedTheme(this, iNKORE.UI.WPF.Modern.ElementTheme.Light);
ApplyThemeResources("Light");
}
else
{
iNKORE.UI.WPF.Modern.ThemeManager.SetRequestedTheme(this, iNKORE.UI.WPF.Modern.ElementTheme.Dark);
ApplyThemeResources("Dark");
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用历史记录窗口主题出错: {ex.Message}", LogHelper.LogType.Error);
}
}
private void ApplyThemeResources(string theme)
{
try
{
var resources = this.Resources;
if (theme == "Light")
{
// 应用浅色主题资源
resources["RollCallHistoryWindowBackground"] = new SolidColorBrush(Color.FromRgb(255, 255, 255));
resources["RollCallHistoryWindowForeground"] = new SolidColorBrush(Color.FromRgb(24, 24, 27));
resources["RollCallHistoryWindowButtonBackground"] = new SolidColorBrush(Color.FromRgb(244, 244, 245));
resources["RollCallHistoryWindowButtonForeground"] = new SolidColorBrush(Color.FromRgb(24, 24, 27));
resources["RollCallHistoryWindowBorderBrush"] = new SolidColorBrush(Color.FromRgb(228, 228, 231));
}
else
{
// 应用深色主题资源
resources["RollCallHistoryWindowBackground"] = new SolidColorBrush(Color.FromRgb(31, 31, 31)); // #1f1f1f
resources["RollCallHistoryWindowForeground"] = new SolidColorBrush(Colors.White);
resources["RollCallHistoryWindowButtonBackground"] = new SolidColorBrush(Color.FromRgb(42, 42, 42)); // #2a2a2a
resources["RollCallHistoryWindowButtonForeground"] = new SolidColorBrush(Colors.White);
resources["RollCallHistoryWindowBorderBrush"] = new SolidColorBrush(Color.FromRgb(224, 224, 224)); // #E0E0E0
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用历史记录窗口主题资源出错: {ex.Message}", LogHelper.LogType.Error);
}
}
private bool IsSystemThemeLight()
{
var light = false;
try
{
var registryKey = Microsoft.Win32.Registry.CurrentUser;
var themeKey = registryKey.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
if (themeKey != null)
{
var value = themeKey.GetValue("AppsUseLightTheme");
if (value != null)
{
light = (int)value == 1;
}
themeKey.Close();
}
}
catch
{
// 如果无法读取注册表,默认使用浅色主题
light = true;
}
return light;
}
}
}