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,270 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Data.Core.Plugins;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
using AvaloniaWebView;
|
||||
using Hua.Todo.Avalonia.Models;
|
||||
using Hua.Todo.Avalonia.Services;
|
||||
using Hua.Todo.Avalonia.Services.Platforms;
|
||||
using Hua.Todo.Avalonia.ViewModels;
|
||||
using Hua.Todo.Avalonia.Views;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Hua.Todo.Avalonia;
|
||||
|
||||
/// <summary>
|
||||
/// Avalonia 应用入口。
|
||||
/// 负责启动嵌入式 WebServer、创建主窗口并初始化 WebView 能力。
|
||||
/// </summary>
|
||||
public partial class App : global::Avalonia.Application
|
||||
{
|
||||
private AppSettings? _appSettings;
|
||||
private IEmbeddedWebServerService? _webServer;
|
||||
private IHotKeySettingsService? _hotKeySettingsService;
|
||||
private IGlobalHotKeyService? _hotKeyService;
|
||||
private HotKeyConfig? _hotKeyConfig;
|
||||
private WindowsKeyboardHandler? _keyboardHandler;
|
||||
private MainWindow? _mainWindow;
|
||||
private bool _isExitRequested;
|
||||
private bool _isHotkeyRegistered;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化应用资源并加载配置。
|
||||
/// </summary>
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
LoadSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册应用级服务。
|
||||
/// </summary>
|
||||
public override void RegisterServices()
|
||||
{
|
||||
base.RegisterServices();
|
||||
AvaloniaWebViewBuilder.Initialize(default);
|
||||
}
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
var settingsPath = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
|
||||
if (File.Exists(settingsPath))
|
||||
{
|
||||
var json = File.ReadAllText(settingsPath);
|
||||
_appSettings = JsonSerializer.Deserialize<AppSettings>(json);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[App] Failed to load settings: {ex.Message}");
|
||||
}
|
||||
|
||||
_appSettings ??= new AppSettings();
|
||||
|
||||
EnsureDefaultConnectionString(_appSettings);
|
||||
}
|
||||
|
||||
private static void EnsureDefaultConnectionString(AppSettings appSettings)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(appSettings.WebServer.ConnectionString))
|
||||
{
|
||||
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
var dbDir = Path.Combine(localAppData, "Hua.Todo");
|
||||
if (!Directory.Exists(dbDir))
|
||||
{
|
||||
Directory.CreateDirectory(dbDir);
|
||||
}
|
||||
|
||||
var dbPath = Path.Combine(dbDir, "Hua.Todo.db");
|
||||
appSettings.WebServer.ConnectionString = $"Data Source={dbPath};Cache=Shared";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Avalonia 框架初始化完成回调。
|
||||
/// 启动嵌入式 WebServer(后台启动并捕获异常,避免阻塞/崩溃),并创建主窗口。
|
||||
/// </summary>
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
DisableAvaloniaDataAnnotationValidation();
|
||||
|
||||
_webServer = new EmbeddedWebServerService(_appSettings!);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _webServer.StartAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[App] Web server start failed: {ex}");
|
||||
}
|
||||
});
|
||||
|
||||
_hotKeySettingsService = new HotKeySettingsService(_appSettings!);
|
||||
_hotKeyConfig = _hotKeySettingsService.GetConfig();
|
||||
_hotKeyService = GlobalHotKeyServiceFactory.Create();
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
_keyboardHandler = new WindowsKeyboardHandler();
|
||||
_keyboardHandler.EscKeyPressed += (_, _) =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (_mainWindow is { IsVisible: true })
|
||||
{
|
||||
_mainWindow.WindowState = WindowState.Minimized;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
DataContext = new AppTrayViewModel(
|
||||
AppMetadata.GetTrayTooltipText(),
|
||||
ShowMainWindow,
|
||||
() => ExitApplication(desktop));
|
||||
|
||||
_mainWindow = new MainWindow(_appSettings!, _webServer)
|
||||
{
|
||||
Width = 450,
|
||||
Height = 640,
|
||||
Title = AppMetadata.GetWindowTitle(),
|
||||
};
|
||||
|
||||
_mainWindow.Opened += (_, _) =>
|
||||
{
|
||||
RegisterHotkey();
|
||||
_keyboardHandler?.Start();
|
||||
};
|
||||
|
||||
_mainWindow.Closing += (_, e) =>
|
||||
{
|
||||
if (_isExitRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
e.Cancel = true;
|
||||
_mainWindow.Hide();
|
||||
};
|
||||
|
||||
desktop.MainWindow = _mainWindow;
|
||||
|
||||
desktop.Exit += async (s, e) =>
|
||||
{
|
||||
_keyboardHandler?.Dispose();
|
||||
_keyboardHandler = null;
|
||||
|
||||
if (_isHotkeyRegistered)
|
||||
{
|
||||
_hotKeyService?.UnregisterHotKey();
|
||||
_isHotkeyRegistered = false;
|
||||
}
|
||||
|
||||
if (_hotKeyService is IDisposable disposableHotKeyService)
|
||||
{
|
||||
disposableHotKeyService.Dispose();
|
||||
}
|
||||
|
||||
if (_webServer != null)
|
||||
{
|
||||
await _webServer.StopAsync();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
|
||||
private void ExitApplication(IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
_isExitRequested = true;
|
||||
desktop.Shutdown();
|
||||
}
|
||||
|
||||
private void ShowMainWindow()
|
||||
{
|
||||
if (_mainWindow == null) return;
|
||||
|
||||
if (!_mainWindow.IsVisible)
|
||||
{
|
||||
_mainWindow.Show();
|
||||
}
|
||||
|
||||
if (_mainWindow.WindowState == WindowState.Minimized)
|
||||
{
|
||||
_mainWindow.WindowState = WindowState.Normal;
|
||||
}
|
||||
|
||||
_mainWindow.Activate();
|
||||
}
|
||||
|
||||
private void RegisterHotkey()
|
||||
{
|
||||
if (_hotKeyService == null || _hotKeySettingsService == null) return;
|
||||
|
||||
var config = _hotKeyConfig ?? _hotKeySettingsService.GetConfig();
|
||||
_hotKeyConfig = config;
|
||||
|
||||
if (_hotKeyService.IsSupported && config.IsEnabled)
|
||||
{
|
||||
_hotKeyService.RegisterHotKey(config.Modifiers, config.Key, () =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(ShowMainWindow);
|
||||
});
|
||||
_isHotkeyRegistered = true;
|
||||
}
|
||||
|
||||
config.PropertyChanged -= OnHotKeyConfigPropertyChanged;
|
||||
config.PropertyChanged += OnHotKeyConfigPropertyChanged;
|
||||
}
|
||||
|
||||
private void OnHotKeyConfigPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (_hotKeyService == null || _hotKeySettingsService == null || _hotKeyConfig == null) return;
|
||||
|
||||
if (e.PropertyName != nameof(HotKeyConfig.Modifiers) &&
|
||||
e.PropertyName != nameof(HotKeyConfig.Key) &&
|
||||
e.PropertyName != nameof(HotKeyConfig.IsEnabled))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_hotKeySettingsService.SaveConfig(_hotKeyConfig);
|
||||
|
||||
if (_hotKeyConfig.IsEnabled && _hotKeyService.IsSupported)
|
||||
{
|
||||
_hotKeyService.UpdateHotKey(_hotKeyConfig.Modifiers, _hotKeyConfig.Key);
|
||||
_isHotkeyRegistered = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_hotKeyService.UnregisterHotKey();
|
||||
_isHotkeyRegistered = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void DisableAvaloniaDataAnnotationValidation()
|
||||
{
|
||||
var dataValidationPluginsToRemove =
|
||||
BindingPlugins.DataValidators.OfType<DataAnnotationsValidationPlugin>().ToArray();
|
||||
|
||||
foreach (var plugin in dataValidationPluginsToRemove)
|
||||
{
|
||||
BindingPlugins.DataValidators.Remove(plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user