feat: 引入 CloudSync 核心能力并新增 Avalonia 桌面端与发布脚本
- 后端:新增 CloudSync 认证/权限/端点/服务与 DTO - 数据:新增用户/会话/安全策略实体与 EF Core migrations - 前端:新增云同步设置 UI、客户端与本地存储;Vite 支持 maui 构建输出到 wwwroot - 桌面端:新增 Avalonia 项目、内置 WebServer、托盘与 Windows 全局热键 - 发布/构建:新增 Windows/Linux 发布脚本与统一入口;调整 MAUI 资源与安装包配置 - 文档:同步更新 README/docs 与协作规则
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace Hua.Todo.Avalonia.Services.Platforms;
|
||||
|
||||
/// <summary>
|
||||
/// Windows 平台全局热键服务实现。
|
||||
/// 通过 <c>RegisterHotKey</c> 将热键注册到专用线程的消息队列中,避免依赖窗口句柄与 WndProc Hook。
|
||||
/// </summary>
|
||||
public sealed class WindowsGlobalHotKeyService : IGlobalHotKeyService, IDisposable
|
||||
{
|
||||
private const int HotKeyId = 9000;
|
||||
private const uint WmHotKey = 0x0312;
|
||||
private const uint WmQuit = 0x0012;
|
||||
private const uint WmAppExecute = 0x8001;
|
||||
|
||||
public const uint ModAlt = 0x0001;
|
||||
public const uint ModControl = 0x0002;
|
||||
public const uint ModShift = 0x0004;
|
||||
public const uint ModWin = 0x0008;
|
||||
|
||||
private readonly ConcurrentDictionary<int, Action> _actions = new();
|
||||
private int _actionId;
|
||||
private Thread? _thread;
|
||||
private uint _threadId;
|
||||
private ManualResetEventSlim? _threadStarted;
|
||||
|
||||
private Action? _callback;
|
||||
private bool _isRegistered;
|
||||
private uint _currentModifiers;
|
||||
private uint _currentKey;
|
||||
private bool _isDisposed;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsSupported => OperatingSystem.IsWindows();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RegisterHotKey(string modifiers, string key, Action callback)
|
||||
{
|
||||
if (!IsSupported) return;
|
||||
|
||||
_callback = callback;
|
||||
_currentModifiers = ParseModifiers(modifiers);
|
||||
_currentKey = ParseKey(key);
|
||||
|
||||
StartThreadIfNeeded();
|
||||
InvokeOnHotKeyThread(() =>
|
||||
{
|
||||
UnregisterHotKeyInternal();
|
||||
|
||||
if (_currentKey == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (RegisterHotKey(IntPtr.Zero, HotKeyId, _currentModifiers, _currentKey))
|
||||
{
|
||||
_isRegistered = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("[WindowsGlobalHotKeyService] Failed to register hotkey.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UnregisterHotKey()
|
||||
{
|
||||
if (!IsSupported) return;
|
||||
|
||||
StartThreadIfNeeded();
|
||||
InvokeOnHotKeyThread(UnregisterHotKeyInternal);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UpdateHotKey(string modifiers, string key)
|
||||
{
|
||||
if (_callback == null) return;
|
||||
RegisterHotKey(modifiers, key, _callback);
|
||||
}
|
||||
|
||||
private void UnregisterHotKeyInternal()
|
||||
{
|
||||
if (_isRegistered)
|
||||
{
|
||||
UnregisterHotKey(IntPtr.Zero, HotKeyId);
|
||||
_isRegistered = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void StartThreadIfNeeded()
|
||||
{
|
||||
if (_thread != null) return;
|
||||
|
||||
_threadStarted = new ManualResetEventSlim(false);
|
||||
_thread = new Thread(MessageLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "Hua.Todo HotKey Thread"
|
||||
};
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
_thread.SetApartmentState(ApartmentState.STA);
|
||||
}
|
||||
_thread.Start();
|
||||
_threadStarted.Wait();
|
||||
}
|
||||
|
||||
private void MessageLoop()
|
||||
{
|
||||
_threadId = GetCurrentThreadId();
|
||||
_threadStarted?.Set();
|
||||
|
||||
while (GetMessage(out var msg, IntPtr.Zero, 0, 0) != 0)
|
||||
{
|
||||
if (msg.message == WmHotKey && msg.wParam == (UIntPtr)HotKeyId)
|
||||
{
|
||||
try
|
||||
{
|
||||
_callback?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[WindowsGlobalHotKeyService] Hotkey callback failed: {ex}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.message == WmAppExecute)
|
||||
{
|
||||
var id = unchecked((int)msg.wParam);
|
||||
if (_actions.TryRemove(id, out var action))
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[WindowsGlobalHotKeyService] Action execution failed: {ex}");
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.message == WmQuit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
TranslateMessage(ref msg);
|
||||
DispatchMessage(ref msg);
|
||||
}
|
||||
|
||||
_threadStarted?.Dispose();
|
||||
_threadStarted = null;
|
||||
}
|
||||
|
||||
private void InvokeOnHotKeyThread(Action action)
|
||||
{
|
||||
var id = Interlocked.Increment(ref _actionId);
|
||||
_actions[id] = action;
|
||||
|
||||
if (!PostThreadMessage(_threadId, WmAppExecute, (UIntPtr)id, IntPtr.Zero))
|
||||
{
|
||||
_actions.TryRemove(id, out _);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint ParseModifiers(string modifiers)
|
||||
{
|
||||
uint mod = 0;
|
||||
if (string.IsNullOrWhiteSpace(modifiers)) return mod;
|
||||
|
||||
var parts = modifiers.Split(',');
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var p = part.Trim();
|
||||
if (p.Equals("Control", StringComparison.OrdinalIgnoreCase)) mod |= ModControl;
|
||||
if (p.Equals("Alt", StringComparison.OrdinalIgnoreCase)) mod |= ModAlt;
|
||||
if (p.Equals("Shift", StringComparison.OrdinalIgnoreCase)) mod |= ModShift;
|
||||
if (p.Equals("Windows", StringComparison.OrdinalIgnoreCase)) mod |= ModWin;
|
||||
}
|
||||
|
||||
return mod;
|
||||
}
|
||||
|
||||
private static uint ParseKey(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key)) return 0;
|
||||
|
||||
if (key.Length == 1)
|
||||
{
|
||||
var c = char.ToUpperInvariant(key[0]);
|
||||
if (c is >= 'A' and <= 'Z') return c;
|
||||
if (c is >= '0' and <= '9') return c;
|
||||
}
|
||||
|
||||
return 0x58; // Default 'X'
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
_isDisposed = true;
|
||||
|
||||
try
|
||||
{
|
||||
UnregisterHotKey();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
if (_threadId != 0)
|
||||
{
|
||||
PostThreadMessage(_threadId, WmQuit, UIntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct Msg
|
||||
{
|
||||
public IntPtr hwnd;
|
||||
public uint message;
|
||||
public UIntPtr wParam;
|
||||
public IntPtr lParam;
|
||||
public uint time;
|
||||
public POINT pt;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct POINT
|
||||
{
|
||||
public int x;
|
||||
public int y;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern sbyte GetMessage(out Msg lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool TranslateMessage(ref Msg lpMsg);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr DispatchMessage(ref Msg lpMsg);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool PostThreadMessage(uint idThread, uint msg, UIntPtr wParam, IntPtr lParam);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern uint GetCurrentThreadId();
|
||||
}
|
||||
Reference in New Issue
Block a user