Files
Hua.Todo/TodoList/App.xaml.cs
T
2025-12-31 04:06:10 +08:00

309 lines
11 KiB
C#

using System;
using System.Threading;
using System.Windows;
using System.Windows.Interop;
using Microsoft.Win32;
using TodoList.Services;
using TodoList.ViewModels;
using TodoList.Views;
using System.Linq;
namespace TodoList
{
public partial class App : System.Windows.Application
{
private IDataService _dataService;
private GlobalShortcutService _shortcutService;
private MainWindow _mainWindow;
private SettingsService _settingsService;
private System.Windows.Forms.NotifyIcon _notifyIcon;
private Mutex _mutex;
private EventWaitHandle _eventWaitHandle;
private const string UniqueEventName = "Global\\TodoListApp_Event_v1";
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
public App()
{
// Disable hardware acceleration to prevent black screen issues
// Must be set before any UI is created
System.Windows.Media.RenderOptions.ProcessRenderMode = System.Windows.Interop.RenderMode.SoftwareOnly;
}
protected override void OnStartup(StartupEventArgs e)
{
// Ensure app doesn't shutdown when main window closes (we hide it)
this.ShutdownMode = ShutdownMode.OnExplicitShutdown;
const string appName = "Global\\TodoListApp_Unique_Mutex_v1";
bool createdNew;
_mutex = new Mutex(true, appName, out createdNew);
if (!createdNew)
{
// Signal the existing instance
try
{
using (var evt = EventWaitHandle.OpenExisting(UniqueEventName))
{
evt.Set();
}
}
catch
{
// Fallback to old method if event open fails
var hWnd = FindWindow(null, "待办事项");
if (hWnd != IntPtr.Zero)
{
ShowWindow(hWnd, 9); // SW_RESTORE
SetForegroundWindow(hWnd);
}
}
// Force exit to prevent second instance from running
Environment.Exit(0);
return;
}
// Create the event handle for this instance
_eventWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, UniqueEventName);
// Start a thread to listen for signals
Thread thread = new Thread(() =>
{
while (true)
{
_eventWaitHandle.WaitOne();
this.Dispatcher.Invoke(() => ShowMainWindow());
}
});
thread.IsBackground = true;
thread.Start();
base.OnStartup(e);
// Configure Auto Start
ConfigureAutoStart();
// Create Desktop/StartMenu Shortcut if needed (optional feature)
// CreateShortcut();
this.DispatcherUnhandledException += App_DispatcherUnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
try
{
_settingsService = new SettingsService();
_dataService = new SqliteDataService();
_shortcutService = new GlobalShortcutService();
var mainViewModel = new MainViewModel(_dataService, _settingsService);
_mainWindow = new MainWindow(mainViewModel);
_mainWindow.Loaded += MainWindow_Loaded;
// Initialize Tray Icon
InitializeTrayIcon();
// Check for silent mode
bool silent = e.Args.Contains("--silent") || e.Args.Contains("-s");
if (!silent)
{
_mainWindow.Show();
}
}
catch (Exception ex)
{
Log("Startup Error: " + ex.ToString());
System.Windows.MessageBox.Show("Startup Error: " + ex.Message);
}
}
private void ConfigureAutoStart()
{
try
{
var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
string cmd = $"\"{exePath}\" --silent";
// If running as dotnet tool, try to find the shim or stable entry point
// Usually %USERPROFILE%\.dotnet\tools\todo.exe
var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var toolShim = System.IO.Path.Combine(userProfile, ".dotnet", "tools", "todo.exe");
if (System.IO.File.Exists(toolShim))
{
// If the shim exists and we are likely running it (or just installed it), prefer the shim
// This handles updates better as the shim path stays constant.
// But we should verify if the current process IS related to it?
// Actually, if installed as tool, we definitely want to use the shim.
cmd = $"\"{toolShim}\" --silent";
}
var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true);
if (key != null)
{
var existing = key.GetValue("TodoListApp");
if (existing == null || existing.ToString() != cmd)
{
key.SetValue("TodoListApp", cmd);
}
key.Close();
}
}
catch (Exception ex)
{
Log("AutoStart Error: " + ex.Message);
}
}
private void InitializeTrayIcon()
{
_notifyIcon = new System.Windows.Forms.NotifyIcon();
// Try load icon from resource or file
try
{
// Load from embedded resource
var resourceUri = new Uri("pack://application:,,,/icon.ico");
var streamInfo = System.Windows.Application.GetResourceStream(resourceUri);
if (streamInfo != null)
{
using (var stream = streamInfo.Stream)
{
_notifyIcon.Icon = new System.Drawing.Icon(stream);
}
}
else
{
_notifyIcon.Icon = System.Drawing.SystemIcons.Application;
}
}
catch
{
_notifyIcon.Icon = System.Drawing.SystemIcons.Application;
}
_notifyIcon.Visible = true;
_notifyIcon.Text = "TodoList";
_notifyIcon.DoubleClick += (s, e) => ShowMainWindow();
var contextMenu = new System.Windows.Forms.ContextMenuStrip();
contextMenu.Items.Add("打开主界面", null, (s, e) => ShowMainWindow());
contextMenu.Items.Add("退出", null, (s, e) => ExitApplication());
_notifyIcon.ContextMenuStrip = contextMenu;
}
private void ShowMainWindow()
{
Log("ShowMainWindow called");
if (_mainWindow != null)
{
if (_mainWindow.WindowState == WindowState.Minimized)
{
_mainWindow.WindowState = WindowState.Normal;
}
_mainWindow.Show();
_mainWindow.Activate();
// Force foreground
var helper = new WindowInteropHelper(_mainWindow);
var handle = helper.Handle;
if (handle != IntPtr.Zero)
{
SetForegroundWindow(handle);
}
}
}
private void ExitApplication()
{
_notifyIcon?.Dispose();
_notifyIcon = null;
Shutdown();
}
private void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
Log("Dispatcher Error: " + e.Exception.ToString());
System.Windows.MessageBox.Show("Dispatcher Error: " + e.Exception.Message);
e.Handled = true;
}
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Log("Domain Error: " + e.ExceptionObject.ToString());
System.Windows.MessageBox.Show("Critical Error: " + e.ExceptionObject.ToString());
}
private void Log(string message)
{
try
{
System.IO.File.AppendAllText("error.log", DateTime.Now + ": " + message + Environment.NewLine);
}
catch { }
}
private bool _isHotkeyRegistered;
private void RegisterHotkey()
{
if (_isHotkeyRegistered || _shortcutService == null || _settingsService == null) return;
var helper = new WindowInteropHelper(_mainWindow);
var handle = helper.Handle;
if (handle != IntPtr.Zero)
{
var settings = _settingsService.Settings;
var mods = GlobalShortcutService.GetModifier(settings.ShortcutModifiers);
var key = GlobalShortcutService.GetKey(settings.ShortcutKey);
_shortcutService.Register(handle, OnHotKeyPressed, mods, key);
_isHotkeyRegistered = true;
// Subscribe to settings changes to update hotkey
_settingsService.Settings.PropertyChanged += (s, args) =>
{
if (args.PropertyName == nameof(AppSettings.ShortcutModifiers) ||
args.PropertyName == nameof(AppSettings.ShortcutKey))
{
var newMods = GlobalShortcutService.GetModifier(_settingsService.Settings.ShortcutModifiers);
var newKey = GlobalShortcutService.GetKey(_settingsService.Settings.ShortcutKey);
_shortcutService.UpdateShortcut(newMods, newKey);
}
};
}
}
private void OnHotKeyPressed()
{
Log("Hotkey pressed.");
ShowMainWindow();
}
protected override void OnExit(ExitEventArgs e)
{
_notifyIcon?.Dispose();
_shortcutService?.Dispose();
base.OnExit(e);
}
// We can hook into MainWindow's Loaded event to register hotkey
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
RegisterHotkey();
}
}
}