Files
Hua.Todo/src/Hua.Todo.Avalonia/App.axaml.cs
T
ShaoHua d53828c150 feat: v1.2.0 开发进度更新
### 新增功能
- **Linux 官方支持**:新增 Hua.Todo.Avalonia 项目,正式适配 Linux 平台,同时支持 Windows 和 macOS
- **Avalonia 桌面交互**:增加托盘菜单(显示/退出)、关闭隐藏到托盘、Windows 全局热键唤起主窗口、热键配置本地持久化
- **SQLite DateTime 兼容修复**:新增 LenientUtcDateTimeStringConverter,解决历史遗留的 DateTime 脏数据解析问题
- **用户文档完善**:新增 docs/manual/新手指南.md 和 docs/manual/用户指南.md
- **部署文档**:新增 docs/manual/部署文档.md,详细说明多平台发布流程

### 优化与修复
- **发布脚本整理**:拆分/对齐各平台发布入口,新增 publish.ps1 作为统一入口
- **Windows WebView2 优化**:数据目录调整到 %LocalAppData%\Hua.Todo\WebView2,修复 Runtime 误判问题
- **MAUI 多平台构建**:在 Windows 开发机上默认仅构建 Android + Windows 目标
- **SPA 路由回落**:修复 Release 模式下 /swagger 路径的 404 问题
- **Swagger 输出**:补齐 Dynamic API 端点,避免接口缺失

### 文档更新
- **版本记录**:更新 v1.2.0 开发进度和功能列表
- **技术设计文档**:添加 Avalonia 项目架构和模块设计
- **项目结构**:更新 README.md 中的项目结构说明

### 其他变更
- 新增 Directory.Build.props 和更新 Directory.Build.targets
- 调整 src/Hua.Todo.Avalonia 项目配置和资源文件
- 更新 src/Hua.Todo.Web 前端资源文件
- 修复 src/Hua.Todo.Maui 相关配置和打包脚本
2026-04-09 21:39:07 +08:00

309 lines
9.3 KiB
C#

using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Platform;
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.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)
{
_webServer = EmbeddedWebServerServiceFactory.Create(_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;
}
});
};
}
// 只在支持的平台上初始化系统托盘
if (OperatingSystem.IsWindows() || OperatingSystem.IsLinux())
{
var trayViewModel = new AppTrayViewModel(
AppMetadata.GetTrayTooltipText(),
ShowMainWindow,
() => ExitApplication(desktop));
InitializeTrayIcon(trayViewModel);
}
_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();
}
};
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
{
_webServer = EmbeddedWebServerServiceFactory.Create(_appSettings!);
singleView.MainView = new MainView(_appSettings!, _webServer);
}
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 InitializeTrayIcon(AppTrayViewModel trayViewModel)
{
try
{
var trayIcons = new TrayIcons();
var iconUri = new Uri("avares://Hua.Todo.Avalonia/Assets/avalonia-logo.ico");
var icon = new WindowIcon(AssetLoader.Open(iconUri));
var trayIcon = new TrayIcon
{
Icon = icon,
ToolTipText = trayViewModel.TrayTooltipText,
Command = trayViewModel.ShowMainWindowCommand
};
var menu = new NativeMenu();
menu.Items.Add(new NativeMenuItem
{
Header = "显示主窗口",
Command = trayViewModel.ShowMainWindowCommand
});
menu.Items.Add(new NativeMenuItemSeparator());
menu.Items.Add(new NativeMenuItem
{
Header = "退出",
Command = trayViewModel.ExitApplicationCommand
});
trayIcon.Menu = menu;
trayIcons.Add(trayIcon);
TrayIcon.SetIcons(this, trayIcons);
}
catch (Exception ex)
{
Console.WriteLine($"[App] Tray icon initialization failed: {ex}");
}
}
}