添加项目文件。
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<Application x:Class="TodoList.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:TodoList"
|
||||
ShutdownMode="OnExplicitShutdown">
|
||||
<Application.Resources>
|
||||
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,308 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly:ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace TodoList.Converters
|
||||
{
|
||||
public class EnumDescriptionConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value == null) return string.Empty;
|
||||
|
||||
var type = value.GetType();
|
||||
var name = Enum.GetName(type, value);
|
||||
if (name == null) return value.ToString();
|
||||
|
||||
var field = type.GetField(name);
|
||||
if (field == null) return value.ToString();
|
||||
|
||||
var attr = Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) as DescriptionAttribute;
|
||||
return attr?.Description ?? value.ToString();
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json.Serialization;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace TodoList.Models
|
||||
{
|
||||
public enum TodoPriority
|
||||
{
|
||||
[Description("低")]
|
||||
Low,
|
||||
[Description("中")]
|
||||
Medium,
|
||||
[Description("高")]
|
||||
High
|
||||
}
|
||||
|
||||
public enum SyncStatus
|
||||
{
|
||||
Synced,
|
||||
Pending,
|
||||
Failed
|
||||
}
|
||||
|
||||
public partial class TodoItem : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private string id = Guid.NewGuid().ToString();
|
||||
|
||||
[ObservableProperty]
|
||||
private string content = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isCompleted;
|
||||
|
||||
[ObservableProperty]
|
||||
private TodoPriority priority = TodoPriority.Medium;
|
||||
|
||||
[ObservableProperty]
|
||||
private DateTime createdAt = DateTime.Now;
|
||||
|
||||
[ObservableProperty]
|
||||
private SyncStatus syncStatus = SyncStatus.Pending;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using TodoList.Models;
|
||||
|
||||
namespace TodoList.Services
|
||||
{
|
||||
public class FileDataService : IDataService
|
||||
{
|
||||
private readonly string _filePath;
|
||||
|
||||
public FileDataService()
|
||||
{
|
||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
var folder = Path.Combine(appData, "TodoListApp");
|
||||
Directory.CreateDirectory(folder);
|
||||
_filePath = Path.Combine(folder, "tasks.json");
|
||||
}
|
||||
|
||||
public async Task<List<TodoItem>> LoadTasksAsync()
|
||||
{
|
||||
if (!File.Exists(_filePath))
|
||||
{
|
||||
return new List<TodoItem>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(_filePath);
|
||||
var items = await JsonSerializer.DeserializeAsync<List<TodoItem>>(stream);
|
||||
return items ?? new List<TodoItem>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<TodoItem>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SaveTaskAsync(TodoItem task)
|
||||
{
|
||||
var tasks = await LoadTasksAsync();
|
||||
var existing = tasks.Find(t => t.Id == task.Id);
|
||||
if (existing != null)
|
||||
{
|
||||
tasks.Remove(existing);
|
||||
}
|
||||
tasks.Add(task);
|
||||
await SaveAllAsync(tasks);
|
||||
}
|
||||
|
||||
public async Task SaveAllAsync(List<TodoItem> tasks)
|
||||
{
|
||||
using var stream = File.Create(_filePath);
|
||||
await JsonSerializer.SerializeAsync(stream, tasks, new JsonSerializerOptions { WriteIndented = true });
|
||||
}
|
||||
|
||||
public async Task DeleteTaskAsync(string id)
|
||||
{
|
||||
var tasks = await LoadTasksAsync();
|
||||
var existing = tasks.Find(t => t.Id == id);
|
||||
if (existing != null)
|
||||
{
|
||||
tasks.Remove(existing);
|
||||
await SaveAllAsync(tasks);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
|
||||
namespace TodoList.Services
|
||||
{
|
||||
public class GlobalShortcutService : IDisposable
|
||||
{
|
||||
private const int HOTKEY_ID = 9000;
|
||||
|
||||
public const uint MOD_ALT = 0x0001;
|
||||
public const uint MOD_CONTROL = 0x0002;
|
||||
public const uint MOD_SHIFT = 0x0004;
|
||||
public const uint MOD_WIN = 0x0008;
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
|
||||
|
||||
private IntPtr _windowHandle;
|
||||
private HwndSource _source;
|
||||
private Action _onHotKeyPressed;
|
||||
private bool _isRegistered;
|
||||
|
||||
public void Register(IntPtr windowHandle, Action onHotKeyPressed, uint modifiers, uint key)
|
||||
{
|
||||
// If already registered, unregister first (to support updating)
|
||||
if (_isRegistered)
|
||||
{
|
||||
UnregisterHotKey(_windowHandle, HOTKEY_ID);
|
||||
_source?.RemoveHook(HwndHook);
|
||||
_isRegistered = false;
|
||||
}
|
||||
|
||||
_windowHandle = windowHandle;
|
||||
_onHotKeyPressed = onHotKeyPressed;
|
||||
|
||||
_source = HwndSource.FromHwnd(_windowHandle);
|
||||
if (_source == null) return; // Should not happen if handle is valid
|
||||
|
||||
_source.AddHook(HwndHook);
|
||||
|
||||
if (RegisterHotKey(_windowHandle, HOTKEY_ID, modifiers, key))
|
||||
{
|
||||
_isRegistered = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("Failed to register hotkey.");
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateShortcut(uint modifiers, uint key)
|
||||
{
|
||||
if (_windowHandle != IntPtr.Zero && _onHotKeyPressed != null)
|
||||
{
|
||||
// Re-register with new keys
|
||||
Register(_windowHandle, _onHotKeyPressed, modifiers, key);
|
||||
}
|
||||
}
|
||||
|
||||
private IntPtr HwndHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||
{
|
||||
const int WM_HOTKEY = 0x0312;
|
||||
if (msg == WM_HOTKEY)
|
||||
{
|
||||
if (wParam.ToInt32() == HOTKEY_ID)
|
||||
{
|
||||
_onHotKeyPressed?.Invoke();
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
public void Unregister()
|
||||
{
|
||||
if (_isRegistered)
|
||||
{
|
||||
_source?.RemoveHook(HwndHook);
|
||||
UnregisterHotKey(_windowHandle, HOTKEY_ID);
|
||||
_isRegistered = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Unregister();
|
||||
}
|
||||
|
||||
public static uint GetModifier(string modifiers)
|
||||
{
|
||||
uint mod = 0;
|
||||
if (string.IsNullOrEmpty(modifiers)) return mod;
|
||||
|
||||
var parts = modifiers.Split(',');
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var p = part.Trim();
|
||||
if (p.Equals("Control", StringComparison.OrdinalIgnoreCase)) mod |= MOD_CONTROL;
|
||||
if (p.Equals("Alt", StringComparison.OrdinalIgnoreCase)) mod |= MOD_ALT;
|
||||
if (p.Equals("Shift", StringComparison.OrdinalIgnoreCase)) mod |= MOD_SHIFT;
|
||||
if (p.Equals("Windows", StringComparison.OrdinalIgnoreCase)) mod |= MOD_WIN;
|
||||
}
|
||||
return mod;
|
||||
}
|
||||
|
||||
public static uint GetKey(string key)
|
||||
{
|
||||
if (Enum.TryParse<Key>(key, out var k))
|
||||
{
|
||||
return (uint)KeyInterop.VirtualKeyFromKey(k);
|
||||
}
|
||||
// Fallback for simple letters if Key enum doesn't match directly (though it should for A-Z)
|
||||
if (key.Length == 1)
|
||||
{
|
||||
char c = char.ToUpper(key[0]);
|
||||
if (c >= 'A' && c <= 'Z') return (uint)c;
|
||||
if (c >= '0' && c <= '9') return (uint)c;
|
||||
}
|
||||
return 0x41; // Default 'A'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using TodoList.Models;
|
||||
|
||||
namespace TodoList.Services
|
||||
{
|
||||
public interface IDataService
|
||||
{
|
||||
Task<List<TodoItem>> LoadTasksAsync();
|
||||
Task SaveTaskAsync(TodoItem task);
|
||||
Task SaveAllAsync(List<TodoItem> tasks);
|
||||
Task DeleteTaskAsync(string id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace TodoList.Services
|
||||
{
|
||||
public partial class AppSettings : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private string shortcutModifiers = "Control,Alt"; // Comma separated
|
||||
|
||||
[ObservableProperty]
|
||||
private string shortcutKey = "A";
|
||||
}
|
||||
|
||||
public class SettingsService
|
||||
{
|
||||
private readonly string _filePath;
|
||||
public AppSettings Settings { get; private set; }
|
||||
|
||||
public SettingsService()
|
||||
{
|
||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
var folder = Path.Combine(appData, "TodoListApp");
|
||||
Directory.CreateDirectory(folder);
|
||||
_filePath = Path.Combine(folder, "settings.json");
|
||||
Settings = LoadSettings();
|
||||
}
|
||||
|
||||
private AppSettings LoadSettings()
|
||||
{
|
||||
if (!File.Exists(_filePath)) return new AppSettings();
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(_filePath);
|
||||
return JsonSerializer.Deserialize<AppSettings>(json) ?? new AppSettings();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new AppSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(Settings, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(_filePath, json);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon>icon.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="icon.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,163 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using TodoList.Models;
|
||||
using TodoList.Services;
|
||||
|
||||
namespace TodoList.ViewModels
|
||||
{
|
||||
public partial class MainViewModel : ObservableObject, IRecipient<TaskAddedMessage>
|
||||
{
|
||||
private readonly IDataService _dataService;
|
||||
private readonly SettingsService _settingsService;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<TodoItem> tasks = new();
|
||||
|
||||
[ObservableProperty]
|
||||
private bool showCompleted = false;
|
||||
|
||||
[ObservableProperty]
|
||||
private string newContent;
|
||||
|
||||
[ObservableProperty]
|
||||
private TodoPriority newPriority = TodoPriority.Medium;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isSettingsOpen;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(FullShortcut))]
|
||||
private string shortcutKey;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(FullShortcut))]
|
||||
private string shortcutModifiers;
|
||||
|
||||
public string FullShortcut
|
||||
{
|
||||
get
|
||||
{
|
||||
var mods = ShortcutModifiers?.Replace(",", " + ");
|
||||
return string.IsNullOrEmpty(mods) ? ShortcutKey : $"{mods} + {ShortcutKey}";
|
||||
}
|
||||
}
|
||||
|
||||
public MainViewModel(IDataService dataService, SettingsService settingsService)
|
||||
{
|
||||
_dataService = dataService;
|
||||
_settingsService = settingsService;
|
||||
ShortcutKey = _settingsService.Settings.ShortcutKey;
|
||||
ShortcutModifiers = _settingsService.Settings.ShortcutModifiers;
|
||||
|
||||
WeakReferenceMessenger.Default.Register(this);
|
||||
LoadTasksCommand.Execute(null);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenSettings()
|
||||
{
|
||||
IsSettingsOpen = true;
|
||||
ShortcutKey = _settingsService.Settings.ShortcutKey;
|
||||
ShortcutModifiers = _settingsService.Settings.ShortcutModifiers;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CloseSettings()
|
||||
{
|
||||
IsSettingsOpen = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SaveSettings()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(ShortcutKey))
|
||||
{
|
||||
_settingsService.Settings.ShortcutKey = ShortcutKey.ToUpper();
|
||||
_settingsService.Settings.ShortcutModifiers = ShortcutModifiers;
|
||||
_settingsService.SaveSettings();
|
||||
}
|
||||
IsSettingsOpen = false;
|
||||
}
|
||||
|
||||
async partial void OnShowCompletedChanged(bool value)
|
||||
{
|
||||
await LoadTasksAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddTaskAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(NewContent)) return;
|
||||
|
||||
var newTask = new TodoItem
|
||||
{
|
||||
Content = NewContent,
|
||||
Priority = NewPriority,
|
||||
IsCompleted = false,
|
||||
SyncStatus = SyncStatus.Pending
|
||||
};
|
||||
|
||||
await _dataService.SaveTaskAsync(newTask);
|
||||
NewContent = string.Empty;
|
||||
NewPriority = TodoPriority.Medium;
|
||||
await LoadTasksAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task LoadTasksAsync()
|
||||
{
|
||||
var allTasks = await _dataService.LoadTasksAsync();
|
||||
|
||||
var filtered = ShowCompleted
|
||||
? allTasks
|
||||
: allTasks.Where(t => !t.IsCompleted).ToList();
|
||||
|
||||
// Sort: Uncompleted first, then by priority (High -> Low), then date
|
||||
var sorted = filtered
|
||||
.OrderBy(t => t.IsCompleted)
|
||||
.ThenByDescending(t => t.Priority)
|
||||
.ThenByDescending(t => t.CreatedAt)
|
||||
.ToList();
|
||||
|
||||
Tasks.Clear();
|
||||
foreach (var t in sorted)
|
||||
{
|
||||
Tasks.Add(t);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ToggleCompleteAsync(TodoItem item)
|
||||
{
|
||||
if (item == null) return;
|
||||
// item.IsCompleted is already toggled by UI binding before this command if TwoWay binding
|
||||
// But usually CheckBox command parameter is the item.
|
||||
// Let's assume the binding updates the property.
|
||||
|
||||
item.SyncStatus = SyncStatus.Pending; // Mark as pending sync
|
||||
await _dataService.SaveTaskAsync(item);
|
||||
await LoadTasksAsync(); // Refresh list to apply filter
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task DeleteAsync(TodoItem item)
|
||||
{
|
||||
if (item == null) return;
|
||||
await _dataService.DeleteTaskAsync(item.Id);
|
||||
Tasks.Remove(item);
|
||||
}
|
||||
|
||||
public async void Receive(TaskAddedMessage message)
|
||||
{
|
||||
await LoadTasksAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class TaskAddedMessage
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using TodoList.Models;
|
||||
using TodoList.Services;
|
||||
|
||||
namespace TodoList.ViewModels
|
||||
{
|
||||
public partial class QuickEntryViewModel : ObservableObject
|
||||
{
|
||||
private readonly IDataService _dataService;
|
||||
private Action _closeAction;
|
||||
|
||||
[ObservableProperty]
|
||||
private string content;
|
||||
|
||||
[ObservableProperty]
|
||||
private TodoPriority priority = TodoPriority.Medium;
|
||||
|
||||
public QuickEntryViewModel(IDataService dataService, Action closeAction)
|
||||
{
|
||||
_dataService = dataService;
|
||||
_closeAction = closeAction;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Content)) return;
|
||||
|
||||
var newTask = new TodoItem
|
||||
{
|
||||
Content = Content,
|
||||
Priority = Priority,
|
||||
IsCompleted = false,
|
||||
SyncStatus = SyncStatus.Pending
|
||||
};
|
||||
|
||||
await _dataService.SaveTaskAsync(newTask);
|
||||
|
||||
// Notify MainViewModel
|
||||
WeakReferenceMessenger.Default.Send(new TaskAddedMessage());
|
||||
|
||||
// Reset and close
|
||||
Content = string.Empty;
|
||||
Priority = TodoPriority.Medium;
|
||||
_closeAction?.Invoke();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Cancel()
|
||||
{
|
||||
_closeAction?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
<Window x:Class="TodoList.Views.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:TodoList.Views"
|
||||
xmlns:models="clr-namespace:TodoList.Models"
|
||||
xmlns:converters="clr-namespace:TodoList.Converters"
|
||||
mc:Ignorable="d"
|
||||
Title="待办事项" Height="600" Width="450"
|
||||
Background="#F5F5F7"
|
||||
Icon="/icon.ico"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
|
||||
<Window.Resources>
|
||||
<ObjectDataProvider x:Key="PriorityEnum" MethodName="GetValues"
|
||||
ObjectType="{x:Type sys:Enum}"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||
<ObjectDataProvider.MethodParameters>
|
||||
<x:Type TypeName="models:TodoPriority"/>
|
||||
</ObjectDataProvider.MethodParameters>
|
||||
</ObjectDataProvider>
|
||||
|
||||
<Style x:Key="ModernButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="#007AFF"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="10,5"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="5">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#0062CC"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||
<converters:EnumDescriptionConverter x:Key="EnumDescConverter"/>
|
||||
</Window.Resources>
|
||||
|
||||
<!-- Force Rebuild Trigger -->
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/> <!-- Toolbar -->
|
||||
<RowDefinition Height="Auto"/> <!-- Input -->
|
||||
<RowDefinition Height="*"/> <!-- List -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Header / Toolbar -->
|
||||
<Border Grid.Row="0" Background="White" Padding="15" Effect="{DynamicResource {x:Static DropShadowEffect.ShadowDepthProperty}}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Text="我的待办" FontSize="24" FontWeight="Bold" Foreground="#333"/>
|
||||
|
||||
<Button Grid.Column="1" Content="设置快捷键"
|
||||
Command="{Binding OpenSettingsCommand}"
|
||||
Background="Transparent" Foreground="#007AFF" Margin="0,0,15,0">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
|
||||
<CheckBox Grid.Column="2" Content="显示已完成"
|
||||
IsChecked="{Binding ShowCompleted}"
|
||||
VerticalAlignment="Center" Foreground="#555"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Input Area -->
|
||||
<Border Grid.Row="1" Margin="15" Background="White" CornerRadius="8" Padding="10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBox Text="{Binding NewContent, UpdateSourceTrigger=PropertyChanged}"
|
||||
FontSize="14" Padding="5" Margin="0,0,10,0" BorderThickness="0,0,0,1"
|
||||
VerticalContentAlignment="Center"
|
||||
Tag="添加新任务...">
|
||||
<TextBox.Style>
|
||||
<Style TargetType="TextBox">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="Text" Value="">
|
||||
<!-- Placeholder could be done with a visual brush or adornment, keeping it simple for now -->
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBox.Style>
|
||||
<TextBox.InputBindings>
|
||||
<KeyBinding Key="Enter" Command="{Binding AddTaskCommand}"/>
|
||||
</TextBox.InputBindings>
|
||||
</TextBox>
|
||||
|
||||
<ComboBox Grid.Column="1" ItemsSource="{Binding Source={StaticResource PriorityEnum}}"
|
||||
SelectedItem="{Binding NewPriority}"
|
||||
Width="80" Margin="0,0,10,0" VerticalContentAlignment="Center">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Converter={StaticResource EnumDescConverter}}"/>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<Button Grid.Column="2" Content="添加" Command="{Binding AddTaskCommand}" Style="{StaticResource ModernButton}"
|
||||
VerticalAlignment="Center" Height="32" Width="80"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Task List -->
|
||||
<ListBox Grid.Row="2" ItemsSource="{Binding Tasks}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
Margin="15,0,15,15"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
||||
<ListBox.ItemContainerStyle>
|
||||
<Style TargetType="ListBoxItem">
|
||||
<Setter Property="Background" Value="White"/>
|
||||
<Setter Property="Margin" Value="0,0,0,10"/>
|
||||
<Setter Property="Padding" Value="10"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="8" Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ListBox.ItemContainerStyle>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<CheckBox IsChecked="{Binding IsCompleted}"
|
||||
Command="{Binding DataContext.ToggleCompleteCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="{Binding}"
|
||||
VerticalAlignment="Center">
|
||||
<CheckBox.LayoutTransform>
|
||||
<ScaleTransform ScaleX="1.2" ScaleY="1.2"/>
|
||||
</CheckBox.LayoutTransform>
|
||||
</CheckBox>
|
||||
|
||||
<StackPanel Grid.Column="1" Margin="15,0">
|
||||
<TextBlock Text="{Binding Content}" FontSize="16" VerticalAlignment="Center">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsCompleted}" Value="True">
|
||||
<Setter Property="TextDecorations" Value="Strikethrough"/>
|
||||
<Setter Property="Foreground" Value="#999"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding IsCompleted}" Value="False">
|
||||
<Setter Property="Foreground" Value="#333"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding Priority, Converter={StaticResource EnumDescConverter}}" FontSize="12" Foreground="#888" Margin="0,2,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="3" Content="✕"
|
||||
Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="{Binding}"
|
||||
Width="24" Height="24"
|
||||
Background="Transparent" Foreground="#FF3B30"
|
||||
BorderThickness="0" FontSize="12" FontWeight="Bold"
|
||||
Cursor="Hand">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="border" Background="{TemplateBinding Background}" CornerRadius="12">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="border" Property="Background" Value="#1AFF3B30"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!-- Settings Dialog Overlay -->
|
||||
<Grid Grid.RowSpan="3" Background="#80000000" Visibility="{Binding IsSettingsOpen, Converter={StaticResource BoolToVis}}">
|
||||
<Border Background="White" Width="300" Height="200" CornerRadius="10" VerticalAlignment="Center" HorizontalAlignment="Center" Padding="20">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Text="快捷键设置" FontSize="18" FontWeight="Bold" HorizontalAlignment="Center"/>
|
||||
|
||||
<StackPanel Grid.Row="1" VerticalAlignment="Center">
|
||||
<TextBlock Text="当前支持组合键 (如 Alt+X, Ctrl+Alt+A)" Foreground="#666" Margin="0,0,0,5" FontSize="12" HorizontalAlignment="Center"/>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<TextBox x:Name="ShortcutBox"
|
||||
Text="{Binding FullShortcut, Mode=OneWay}"
|
||||
Width="200" FontSize="16"
|
||||
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
|
||||
PreviewKeyDown="ShortcutBox_PreviewKeyDown"
|
||||
CaretBrush="Transparent"
|
||||
IsReadOnly="True"
|
||||
Cursor="Hand"
|
||||
Padding="5"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="点击上方框并按下快捷键" Foreground="#999" FontSize="12" HorizontalAlignment="Center" Margin="0,5,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Button Content="取消" Command="{Binding CloseSettingsCommand}" Width="80" Margin="0,0,10,0"
|
||||
Background="#EEE" Foreground="#333" Height="30" BorderThickness="0">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="5">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
<Button Content="保存" Command="{Binding SaveSettingsCommand}" Width="80" Height="30" Style="{StaticResource ModernButton}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using TodoList.ViewModels;
|
||||
|
||||
namespace TodoList.Views
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow(MainViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = viewModel;
|
||||
}
|
||||
|
||||
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
e.Cancel = true;
|
||||
this.Hide();
|
||||
// Verify if app shuts down? No, ShutdownMode is Explicit.
|
||||
}
|
||||
|
||||
private void ShortcutBox_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
|
||||
// Ignore modifier keys alone being the "main" key
|
||||
if (e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl ||
|
||||
e.Key == Key.LeftAlt || e.Key == Key.RightAlt ||
|
||||
e.Key == Key.LeftShift || e.Key == Key.RightShift ||
|
||||
e.Key == Key.LWin || e.Key == Key.RWin)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = e.Key;
|
||||
if (key == Key.System) key = e.SystemKey; // Handle Alt+Key
|
||||
|
||||
// Build modifier string
|
||||
var modifiers = new System.Collections.Generic.List<string>();
|
||||
if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) modifiers.Add("Control");
|
||||
if ((Keyboard.Modifiers & ModifierKeys.Alt) == ModifierKeys.Alt) modifiers.Add("Alt");
|
||||
if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) modifiers.Add("Shift");
|
||||
if ((Keyboard.Modifiers & ModifierKeys.Windows) == ModifierKeys.Windows) modifiers.Add("Windows");
|
||||
|
||||
// Map key to string
|
||||
string keyStr = key.ToString();
|
||||
|
||||
// Simple mapping for letters/digits (A-Z, 0-9)
|
||||
if (keyStr.Length == 2 && keyStr.StartsWith("D") && char.IsDigit(keyStr[1]))
|
||||
{
|
||||
keyStr = keyStr.Substring(1);
|
||||
}
|
||||
|
||||
// Update ViewModel
|
||||
if (DataContext is MainViewModel vm)
|
||||
{
|
||||
vm.ShortcutModifiers = string.Join(",", modifiers);
|
||||
vm.ShortcutKey = keyStr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<Window x:Class="TodoList.Views.QuickEntryWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:TodoList.Views"
|
||||
xmlns:models="clr-namespace:TodoList.Models"
|
||||
xmlns:converters="clr-namespace:TodoList.Converters"
|
||||
mc:Ignorable="d"
|
||||
Title="新建待办" Height="220" Width="400"
|
||||
WindowStyle="None" ResizeMode="NoResize"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Topmost="True"
|
||||
Background="White"
|
||||
BorderBrush="#007AFF" BorderThickness="1">
|
||||
|
||||
<Window.Effect>
|
||||
<DropShadowEffect BlurRadius="20" ShadowDepth="5" Opacity="0.3"/>
|
||||
</Window.Effect>
|
||||
|
||||
<Window.Resources>
|
||||
<ObjectDataProvider x:Key="PriorityEnum" MethodName="GetValues"
|
||||
ObjectType="{x:Type sys:Enum}"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||
<ObjectDataProvider.MethodParameters>
|
||||
<x:Type TypeName="models:TodoPriority"/>
|
||||
</ObjectDataProvider.MethodParameters>
|
||||
</ObjectDataProvider>
|
||||
<converters:EnumDescriptionConverter x:Key="EnumDescConverter"/>
|
||||
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="5" BorderThickness="0">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Text="新建待办" FontSize="18" FontWeight="Bold" Foreground="#333"/>
|
||||
|
||||
<TextBox Grid.Row="1" Margin="0,15,0,15"
|
||||
Text="{Binding Content, UpdateSourceTrigger=PropertyChanged}"
|
||||
FontSize="14" Padding="8" BorderThickness="0,0,0,1"
|
||||
x:Name="InputBox">
|
||||
<TextBox.InputBindings>
|
||||
<KeyBinding Key="Enter" Command="{Binding SaveCommand}"/>
|
||||
<KeyBinding Key="Esc" Command="{Binding CancelCommand}"/>
|
||||
</TextBox.InputBindings>
|
||||
</TextBox>
|
||||
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" VerticalAlignment="Top">
|
||||
<TextBlock Text="优先级:" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="#666"/>
|
||||
<ComboBox ItemsSource="{Binding Source={StaticResource PriorityEnum}}"
|
||||
SelectedItem="{Binding Priority}"
|
||||
Width="100">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Converter={StaticResource EnumDescConverter}}"/>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Content="取消" Command="{Binding CancelCommand}" Margin="0,0,10,0" Width="80" Height="32"
|
||||
Background="#F0F0F0" Foreground="#333"/>
|
||||
<Button Content="保存" Command="{Binding SaveCommand}" Width="80" Height="32" IsDefault="True"
|
||||
Background="#007AFF" Foreground="White"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using TodoList.Services;
|
||||
using TodoList.ViewModels;
|
||||
|
||||
namespace TodoList.Views
|
||||
{
|
||||
public partial class QuickEntryWindow : Window
|
||||
{
|
||||
public QuickEntryWindow(IDataService dataService)
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new QuickEntryViewModel(dataService, () => this.Hide());
|
||||
}
|
||||
|
||||
protected override void OnActivated(EventArgs e)
|
||||
{
|
||||
base.OnActivated(e);
|
||||
InputBox.Focus();
|
||||
InputBox.SelectAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 192 KiB |
Reference in New Issue
Block a user