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 FileDataService(); _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(); } } }