移除1.0.0版本
This commit is contained in:
@@ -1,9 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,364 +0,0 @@
|
|||||||
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;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace TodoList
|
|
||||||
{
|
|
||||||
public partial class App : System.Windows.Application
|
|
||||||
{
|
|
||||||
private const string MainWindowTitle = "待办事项";
|
|
||||||
private IDataService _dataService;
|
|
||||||
private GlobalShortcutService _shortcutService;
|
|
||||||
private MainWindow _mainWindow;
|
|
||||||
private QuickEntryWindow? _quickEntryWindow;
|
|
||||||
private SettingsService _settingsService;
|
|
||||||
private 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, MainWindowTitle);
|
|
||||||
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.Title = MainWindowTitle;
|
|
||||||
_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 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 = GetNotifyIconText();
|
|
||||||
_notifyIcon.DoubleClick += (s, e) => ShowMainWindow();
|
|
||||||
|
|
||||||
var contextMenu = new ContextMenuStrip();
|
|
||||||
contextMenu.Items.Add("打开主界面", null, (s, e) => ShowMainWindow());
|
|
||||||
contextMenu.Items.Add("退出", null, (s, e) => ExitApplication());
|
|
||||||
_notifyIcon.ContextMenuStrip = contextMenu;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetNotifyIconText()
|
|
||||||
{
|
|
||||||
var version = GetDisplayVersion();
|
|
||||||
var text = string.IsNullOrWhiteSpace(version) ? MainWindowTitle : $"{MainWindowTitle} v{version}";
|
|
||||||
|
|
||||||
return text.Length > 63 ? text[..63] : text;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? GetDisplayVersion()
|
|
||||||
{
|
|
||||||
var asm = Assembly.GetExecutingAssembly();
|
|
||||||
|
|
||||||
var info = asm.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion?.Trim();
|
|
||||||
if (!string.IsNullOrWhiteSpace(info))
|
|
||||||
{
|
|
||||||
var plus = info.IndexOf('+');
|
|
||||||
return plus >= 0 ? info[..plus] : info;
|
|
||||||
}
|
|
||||||
|
|
||||||
var file = asm.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version?.Trim();
|
|
||||||
if (!string.IsNullOrWhiteSpace(file))
|
|
||||||
{
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
return asm.GetName().Version?.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
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.");
|
|
||||||
ShowQuickEntryWindow();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowQuickEntryWindow()
|
|
||||||
{
|
|
||||||
if (_quickEntryWindow == null)
|
|
||||||
{
|
|
||||||
_quickEntryWindow = new QuickEntryWindow(_dataService);
|
|
||||||
_quickEntryWindow.Title = "新建待办";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_quickEntryWindow.WindowState == WindowState.Minimized)
|
|
||||||
{
|
|
||||||
_quickEntryWindow.WindowState = WindowState.Normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
_quickEntryWindow.Show();
|
|
||||||
_quickEntryWindow.Activate();
|
|
||||||
|
|
||||||
var helper = new WindowInteropHelper(_quickEntryWindow);
|
|
||||||
var handle = helper.Handle;
|
|
||||||
if (handle != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
SetForegroundWindow(handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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)
|
|
||||||
)]
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
# Basic configuration
|
|
||||||
$ScriptPath = $PSScriptRoot
|
|
||||||
$ProjectFile = (Get-ChildItem -Path $ScriptPath -Filter "*.csproj" -File)[0].FullName
|
|
||||||
$SetupScript = Join-Path $ScriptPath "setup.iss"
|
|
||||||
|
|
||||||
# Read version from project file
|
|
||||||
$currentVersion = "1.0.0"
|
|
||||||
[xml]$csproj = Get-Content $ProjectFile
|
|
||||||
if ($csproj.Project.PropertyGroup.Version) {
|
|
||||||
$currentVersion = $csproj.Project.PropertyGroup.Version
|
|
||||||
}
|
|
||||||
|
|
||||||
# Increment version
|
|
||||||
$versionParts = $currentVersion.Split(".")
|
|
||||||
$patch = [int]$versionParts[2] + 1
|
|
||||||
$newVersion = $versionParts[0] + "." + $versionParts[1] + "." + $patch
|
|
||||||
|
|
||||||
# Update project version
|
|
||||||
$content = Get-Content $ProjectFile -Raw
|
|
||||||
$content = $content -replace "<Version>.*</Version>", "<Version>$newVersion</Version>"
|
|
||||||
Set-Content $ProjectFile -Value $content
|
|
||||||
|
|
||||||
# Update setup script version
|
|
||||||
if (Test-Path $SetupScript) {
|
|
||||||
$issContent = Get-Content $SetupScript
|
|
||||||
for ($i = 0; $i -lt $issContent.Count; $i++) {
|
|
||||||
if ($issContent[$i] -like '#define MyAppVersion *') {
|
|
||||||
$issContent[$i] = '#define MyAppVersion "' + $newVersion + '"'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Set-Content $SetupScript -Value $issContent
|
|
||||||
}
|
|
||||||
|
|
||||||
# Build project
|
|
||||||
dotnet publish $ProjectFile -c Release -r win-x64 --self-contained false -p:PublishSingleFile=true
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
Write-Error "Build failed"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Package
|
|
||||||
$ISCC = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe"
|
|
||||||
if (Test-Path $ISCC) {
|
|
||||||
& $ISCC $SetupScript
|
|
||||||
if ($LASTEXITCODE -eq 0) {
|
|
||||||
Write-Host "Setup package created successfully!" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Error "Packaging failed"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Error "Inno Setup compiler not found"
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
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 enum SortBy
|
|
||||||
{
|
|
||||||
[Description("创建时间")]
|
|
||||||
CreatedAt,
|
|
||||||
[Description("完成时间")]
|
|
||||||
CompletedAt,
|
|
||||||
[Description("优先级")]
|
|
||||||
Priority
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum SortOrder
|
|
||||||
{
|
|
||||||
[Description("升序")]
|
|
||||||
Ascending,
|
|
||||||
[Description("降序")]
|
|
||||||
Descending
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class TodoItem : ObservableObject
|
|
||||||
{
|
|
||||||
[ObservableProperty]
|
|
||||||
[property: SQLite.PrimaryKey]
|
|
||||||
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 DateTime? completedAt;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private SyncStatus syncStatus = SyncStatus.Pending;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
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(bool? completed = null)
|
|
||||||
{
|
|
||||||
if (!File.Exists(_filePath))
|
|
||||||
{
|
|
||||||
return new List<TodoItem>();
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var stream = File.OpenRead(_filePath);
|
|
||||||
var items = await JsonSerializer.DeserializeAsync<List<TodoItem>>(stream);
|
|
||||||
var tasks = items ?? new List<TodoItem>();
|
|
||||||
|
|
||||||
if (completed.HasValue)
|
|
||||||
{
|
|
||||||
return tasks.Where(t => t.IsCompleted == completed.Value).ToList();
|
|
||||||
}
|
|
||||||
return tasks;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return new List<TodoItem>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<TodoItem> 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);
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<TodoItem> UpdateTaskAsync(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);
|
|
||||||
}
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<TodoItem> ToggleCompleteAsync(string id)
|
|
||||||
{
|
|
||||||
var tasks = await LoadTasksAsync();
|
|
||||||
var task = tasks.Find(t => t.Id == id);
|
|
||||||
if (task != null)
|
|
||||||
{
|
|
||||||
task.IsCompleted = !task.IsCompleted;
|
|
||||||
task.CompletedAt = task.IsCompleted ? DateTime.Now : null;
|
|
||||||
task.SyncStatus = SyncStatus.Pending;
|
|
||||||
await SaveAllAsync(tasks);
|
|
||||||
}
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using TodoList.Models;
|
|
||||||
|
|
||||||
namespace TodoList.Services
|
|
||||||
{
|
|
||||||
public interface IDataService
|
|
||||||
{
|
|
||||||
Task<List<TodoItem>> LoadTasksAsync(bool? completed = null);
|
|
||||||
Task<TodoItem> SaveTaskAsync(TodoItem task);
|
|
||||||
Task<TodoItem> UpdateTaskAsync(TodoItem task);
|
|
||||||
Task<TodoItem> ToggleCompleteAsync(string id);
|
|
||||||
Task DeleteTaskAsync(string id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
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 { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using SQLite;
|
|
||||||
using TodoList.Models;
|
|
||||||
|
|
||||||
namespace TodoList.Services
|
|
||||||
{
|
|
||||||
public class SqliteDataService : IDataService
|
|
||||||
{
|
|
||||||
private readonly SQLiteAsyncConnection _database;
|
|
||||||
|
|
||||||
public SqliteDataService()
|
|
||||||
{
|
|
||||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
|
||||||
var folder = Path.Combine(appData, "TodoListApp");
|
|
||||||
Directory.CreateDirectory(folder);
|
|
||||||
var databasePath = Path.Combine(folder, "TodoList.sqlite");
|
|
||||||
|
|
||||||
_database = new SQLiteAsyncConnection(databasePath);
|
|
||||||
_database.CreateTableAsync<TodoItem>().Wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<TodoItem>> LoadTasksAsync(bool? completed = null)
|
|
||||||
{
|
|
||||||
var query = _database.Table<TodoItem>();
|
|
||||||
if (completed.HasValue)
|
|
||||||
{
|
|
||||||
query = query.Where(t => t.IsCompleted == completed.Value);
|
|
||||||
}
|
|
||||||
return await query.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<TodoItem> SaveTaskAsync(TodoItem task)
|
|
||||||
{
|
|
||||||
await _database.InsertOrReplaceAsync(task);
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<TodoItem> UpdateTaskAsync(TodoItem task)
|
|
||||||
{
|
|
||||||
await _database.UpdateAsync(task);
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<TodoItem> ToggleCompleteAsync(string id)
|
|
||||||
{
|
|
||||||
var task = await _database.FindAsync<TodoItem>(id);
|
|
||||||
if (task != null)
|
|
||||||
{
|
|
||||||
task.IsCompleted = !task.IsCompleted;
|
|
||||||
task.CompletedAt = task.IsCompleted ? DateTime.Now : null;
|
|
||||||
task.SyncStatus = SyncStatus.Pending;
|
|
||||||
await _database.UpdateAsync(task);
|
|
||||||
}
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SaveAllAsync(List<TodoItem> tasks)
|
|
||||||
{
|
|
||||||
await _database.RunInTransactionAsync(tran =>
|
|
||||||
{
|
|
||||||
foreach (var task in tasks)
|
|
||||||
{
|
|
||||||
tran.InsertOrReplace(task);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteTaskAsync(string id)
|
|
||||||
{
|
|
||||||
await _database.DeleteAsync<TodoItem>(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>WinExe</OutputType>
|
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<CodePage>65001</CodePage>
|
|
||||||
<UseWPF>true</UseWPF>
|
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
|
||||||
<ApplicationIcon>icon.ico</ApplicationIcon>
|
|
||||||
<Version>1.0.21</Version>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
|
||||||
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
|
|
||||||
<PackageReference Include="System.Net.Http.Json" Version="10.0.5" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Resource Include="icon.ico" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
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 isEditDialogOpen;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private TodoItem editingTask;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private string editContent;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private TodoPriority editPriority = TodoPriority.Medium;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private bool isSettingsOpen;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
[NotifyPropertyChangedFor(nameof(FullShortcut))]
|
|
||||||
private string shortcutKey;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
[NotifyPropertyChangedFor(nameof(FullShortcut))]
|
|
||||||
private string shortcutModifiers;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private SortBy sortBy = SortBy.Priority;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private Models.SortOrder sortOrder = Models.SortOrder.Descending;
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
async partial void OnSortByChanged(SortBy value)
|
|
||||||
{
|
|
||||||
await LoadTasksAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
async partial void OnSortOrderChanged(Models.SortOrder 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();
|
|
||||||
|
|
||||||
IOrderedEnumerable<TodoItem> sorted;
|
|
||||||
|
|
||||||
if (SortBy == SortBy.Priority)
|
|
||||||
{
|
|
||||||
sorted = SortOrder == Models.SortOrder.Ascending
|
|
||||||
? filtered.OrderBy(t => t.Priority).ThenBy(t => t.CreatedAt)
|
|
||||||
: filtered.OrderByDescending(t => t.Priority).ThenBy(t => t.CreatedAt);
|
|
||||||
}
|
|
||||||
else if (SortBy == SortBy.CreatedAt)
|
|
||||||
{
|
|
||||||
sorted = SortOrder == Models.SortOrder.Ascending
|
|
||||||
? filtered.OrderBy(t => t.CreatedAt)
|
|
||||||
: filtered.OrderByDescending(t => t.CreatedAt);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
sorted = SortOrder == Models.SortOrder.Ascending
|
|
||||||
? filtered.OrderBy(t => t.CompletedAt).ThenBy(t => t.CreatedAt)
|
|
||||||
: filtered.OrderByDescending(t => t.CompletedAt).ThenBy(t => t.CreatedAt);
|
|
||||||
}
|
|
||||||
|
|
||||||
Tasks.Clear();
|
|
||||||
foreach (var t in sorted)
|
|
||||||
{
|
|
||||||
Tasks.Add(t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task ToggleCompleteAsync(TodoItem item)
|
|
||||||
{
|
|
||||||
if (item == null) return;
|
|
||||||
|
|
||||||
if (item.IsCompleted)
|
|
||||||
{
|
|
||||||
item.CompletedAt = DateTime.Now;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
item.CompletedAt = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.SyncStatus = SyncStatus.Pending;
|
|
||||||
await _dataService.SaveTaskAsync(item);
|
|
||||||
await LoadTasksAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task DeleteAsync(TodoItem item)
|
|
||||||
{
|
|
||||||
if (item == null) return;
|
|
||||||
await _dataService.DeleteTaskAsync(item.Id);
|
|
||||||
Tasks.Remove(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void OpenEditDialog(TodoItem item)
|
|
||||||
{
|
|
||||||
if (item == null) return;
|
|
||||||
EditingTask = item;
|
|
||||||
EditContent = item.Content;
|
|
||||||
EditPriority = item.Priority;
|
|
||||||
IsEditDialogOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void CloseEditDialog()
|
|
||||||
{
|
|
||||||
IsEditDialogOpen = false;
|
|
||||||
EditingTask = null;
|
|
||||||
EditContent = string.Empty;
|
|
||||||
EditPriority = TodoPriority.Medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task SaveEditAsync()
|
|
||||||
{
|
|
||||||
if (EditingTask == null || string.IsNullOrWhiteSpace(EditContent)) return;
|
|
||||||
|
|
||||||
EditingTask.Content = EditContent;
|
|
||||||
EditingTask.Priority = EditPriority;
|
|
||||||
EditingTask.SyncStatus = SyncStatus.Pending;
|
|
||||||
|
|
||||||
await _dataService.SaveTaskAsync(EditingTask);
|
|
||||||
await LoadTasksAsync();
|
|
||||||
CloseEditDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void Receive(TaskAddedMessage message)
|
|
||||||
{
|
|
||||||
await LoadTasksAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TaskAddedMessage
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
<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="{Binding AppVersion, StringFormat='待办事项 v{0}'}" Height="450" Width="350"
|
|
||||||
Background="#F5F5F7"
|
|
||||||
Icon="/icon.ico"
|
|
||||||
WindowStartupLocation="CenterScreen"
|
|
||||||
PreviewKeyDown="Window_PreviewKeyDown">
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<ObjectDataProvider x:Key="SortByEnum" MethodName="GetValues"
|
|
||||||
ObjectType="{x:Type sys:Enum}"
|
|
||||||
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
|
||||||
<ObjectDataProvider.MethodParameters>
|
|
||||||
<x:Type TypeName="models:SortBy"/>
|
|
||||||
</ObjectDataProvider.MethodParameters>
|
|
||||||
</ObjectDataProvider>
|
|
||||||
|
|
||||||
<ObjectDataProvider x:Key="SortOrderEnum" MethodName="GetValues"
|
|
||||||
ObjectType="{x:Type sys:Enum}"
|
|
||||||
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
|
||||||
<ObjectDataProvider.MethodParameters>
|
|
||||||
<x:Type TypeName="models:SortOrder"/>
|
|
||||||
</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 -->
|
|
||||||
<RowDefinition Height="Auto"/> <!-- Footer -->
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<!-- Header / Toolbar -->
|
|
||||||
<Border Grid.Row="0" Background="White" Padding="10" Effect="{DynamicResource {x:Static DropShadowEffect.ShadowDepthProperty}}">
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Margin="5,0,10,0" VerticalAlignment="Center">
|
|
||||||
<ComboBox ItemsSource="{Binding Source={StaticResource SortByEnum}}"
|
|
||||||
SelectedItem="{Binding SortBy}"
|
|
||||||
Width="70" VerticalContentAlignment="Center" Margin="0,0,3,0">
|
|
||||||
<ComboBox.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<TextBlock Text="{Binding Converter={StaticResource EnumDescConverter}}"/>
|
|
||||||
</DataTemplate>
|
|
||||||
</ComboBox.ItemTemplate>
|
|
||||||
</ComboBox>
|
|
||||||
<ComboBox ItemsSource="{Binding Source={StaticResource SortOrderEnum}}"
|
|
||||||
SelectedItem="{Binding SortOrder}"
|
|
||||||
Width="50" VerticalContentAlignment="Center">
|
|
||||||
<ComboBox.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<TextBlock Text="{Binding Converter={StaticResource EnumDescConverter}}"/>
|
|
||||||
</DataTemplate>
|
|
||||||
</ComboBox.ItemTemplate>
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<Button Grid.Column="2" Content="设置快捷键"
|
|
||||||
Command="{Binding OpenSettingsCommand}"
|
|
||||||
Background="Transparent" Foreground="#007AFF" Margin="0,0,10,0" FontSize="11">
|
|
||||||
<Button.Template>
|
|
||||||
<ControlTemplate TargetType="Button">
|
|
||||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Button.Template>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<CheckBox Grid.Column="3" Content="已完成"
|
|
||||||
IsChecked="{Binding ShowCompleted}"
|
|
||||||
VerticalAlignment="Center" Foreground="#555" FontSize="11"/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Input Area -->
|
|
||||||
<Border Grid.Row="1" Margin="10" Background="White" CornerRadius="6" Padding="8">
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<TextBox Text="{Binding NewContent, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
FontSize="12" Padding="4" Margin="0,0,8,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="65" Margin="0,0,8,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="28" Width="65" FontSize="12"/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Task List -->
|
|
||||||
<ListBox Grid.Row="2" ItemsSource="{Binding Tasks}"
|
|
||||||
HorizontalContentAlignment="Stretch"
|
|
||||||
Background="Transparent" BorderThickness="0"
|
|
||||||
Margin="10,0,10,10"
|
|
||||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
|
||||||
<ListBox.ItemContainerStyle>
|
|
||||||
<Style TargetType="ListBoxItem">
|
|
||||||
<Setter Property="Background" Value="White"/>
|
|
||||||
<Setter Property="Margin" Value="0,0,0,6"/>
|
|
||||||
<Setter Property="Padding" Value="8"/>
|
|
||||||
<EventSetter Event="MouseDoubleClick" Handler="ListBoxItem_MouseDoubleClick"/>
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="ListBoxItem">
|
|
||||||
<Border Background="{TemplateBinding Background}" CornerRadius="8" Padding="{TemplateBinding Padding}">
|
|
||||||
<ContentPresenter/>
|
|
||||||
</Border>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
<Style.Triggers>
|
|
||||||
<DataTrigger Binding="{Binding Priority}" Value="High">
|
|
||||||
<Setter Property="Background" Value="#FFCDD2"/>
|
|
||||||
</DataTrigger>
|
|
||||||
<DataTrigger Binding="{Binding Priority}" Value="Medium">
|
|
||||||
<Setter Property="Background" Value="#FFE0B2"/>
|
|
||||||
</DataTrigger>
|
|
||||||
<DataTrigger Binding="{Binding Priority}" Value="Low">
|
|
||||||
<Setter Property="Background" Value="#C8E6C9"/>
|
|
||||||
</DataTrigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</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.0" ScaleY="1.0"/>
|
|
||||||
</CheckBox.LayoutTransform>
|
|
||||||
</CheckBox>
|
|
||||||
|
|
||||||
<StackPanel Grid.Column="1" Margin="10,0">
|
|
||||||
<TextBlock Text="{Binding Content}" FontSize="13" 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="10" Foreground="#888" Margin="0,1,0,0"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<Button Grid.Column="2" Content="✎"
|
|
||||||
Command="{Binding DataContext.OpenEditDialogCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
|
||||||
CommandParameter="{Binding}"
|
|
||||||
Width="20" Height="20"
|
|
||||||
Background="Transparent" Foreground="#007AFF"
|
|
||||||
BorderThickness="0" FontSize="10" FontWeight="Bold"
|
|
||||||
Cursor="Hand" Margin="0,0,4,0">
|
|
||||||
<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="#1A007AFF"/>
|
|
||||||
</Trigger>
|
|
||||||
</ControlTemplate.Triggers>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Button.Template>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button Grid.Column="3" Content="✕"
|
|
||||||
Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
|
||||||
CommandParameter="{Binding}"
|
|
||||||
Width="20" Height="20"
|
|
||||||
Background="Transparent" Foreground="#FF3B30"
|
|
||||||
BorderThickness="0" FontSize="10" 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>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Edit Dialog Overlay -->
|
|
||||||
<Grid Grid.RowSpan="3" Background="#80000000" Visibility="{Binding IsEditDialogOpen, Converter={StaticResource BoolToVis}}">
|
|
||||||
<Border Background="White" Width="300" Height="220" CornerRadius="8" VerticalAlignment="Center" HorizontalAlignment="Center" Padding="15">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<TextBlock Text="编辑任务" FontSize="15" FontWeight="Bold" HorizontalAlignment="Center" Margin="0,0,0,12"/>
|
|
||||||
|
|
||||||
<StackPanel Grid.Row="1" VerticalAlignment="Center">
|
|
||||||
<TextBlock Text="任务内容" Foreground="#666" Margin="0,0,0,4" FontSize="10"/>
|
|
||||||
<TextBox Text="{Binding EditContent, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
FontSize="12" Padding="6" Margin="0,0,0,12" BorderThickness="1" BorderBrush="#DDD"
|
|
||||||
VerticalContentAlignment="Center"/>
|
|
||||||
|
|
||||||
<TextBlock Text="优先级" Foreground="#666" Margin="0,0,0,4" FontSize="10"/>
|
|
||||||
<ComboBox ItemsSource="{Binding Source={StaticResource PriorityEnum}}"
|
|
||||||
SelectedItem="{Binding EditPriority}"
|
|
||||||
Width="260" VerticalContentAlignment="Center" BorderThickness="1" BorderBrush="#DDD" FontSize="11">
|
|
||||||
<ComboBox.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<TextBlock Text="{Binding Converter={StaticResource EnumDescConverter}}"/>
|
|
||||||
</DataTemplate>
|
|
||||||
</ComboBox.ItemTemplate>
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,12,0,0">
|
|
||||||
<Button Content="取消" Command="{Binding CloseEditDialogCommand}" Width="70" Margin="0,0,8,0"
|
|
||||||
Background="#EEE" Foreground="#333" Height="26" BorderThickness="0" FontSize="11">
|
|
||||||
<Button.Template>
|
|
||||||
<ControlTemplate TargetType="Button">
|
|
||||||
<Border Background="{TemplateBinding Background}" CornerRadius="4">
|
|
||||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
|
||||||
</Border>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Button.Template>
|
|
||||||
</Button>
|
|
||||||
<Button Content="保存" Command="{Binding SaveEditCommand}" Width="70" Height="26" Style="{StaticResource ModernButton}" FontSize="11"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<Border Grid.Row="3" Background="#F5F5F7" Padding="10,8" HorizontalAlignment="Center">
|
|
||||||
<TextBlock FontSize="10">
|
|
||||||
<Hyperlink NavigateUri="https://github.com/xinshoushangdao/TodoList" RequestNavigate="Hyperlink_RequestNavigate">
|
|
||||||
<Run Text="GitHub Repository"/>
|
|
||||||
</Hyperlink>
|
|
||||||
</TextBlock>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</Window>
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
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 Window_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.Key == Key.Escape)
|
|
||||||
{
|
|
||||||
// If settings are open, close settings?
|
|
||||||
// But user requirement is "Equals pressing X button", which usually means Close/Hide window.
|
|
||||||
// However, if we want better UX:
|
|
||||||
if (DataContext is MainViewModel { IsSettingsOpen: true } vm)
|
|
||||||
{
|
|
||||||
vm.IsSettingsOpen = false;
|
|
||||||
e.Handled = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default behavior: Close (Hide) Window
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ListBoxItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is ListBoxItem item && item.DataContext is Models.TodoItem todoItem && DataContext is MainViewModel vm)
|
|
||||||
{
|
|
||||||
vm.OpenEditDialogCommand.Execute(todoItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
|
|
||||||
{
|
|
||||||
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri) { UseShellExecute = true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
<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="180" Width="320"
|
|
||||||
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="15">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<TextBlock Text="新建待办" FontSize="15" FontWeight="Bold" Foreground="#333"/>
|
|
||||||
|
|
||||||
<TextBox Grid.Row="1" Margin="0,12,0,12"
|
|
||||||
Text="{Binding Content, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
FontSize="12" Padding="6" 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,8,0" Foreground="#666" FontSize="11"/>
|
|
||||||
<ComboBox ItemsSource="{Binding Source={StaticResource PriorityEnum}}"
|
|
||||||
SelectedItem="{Binding Priority}"
|
|
||||||
Width="80" FontSize="11">
|
|
||||||
<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,8,0" Width="65" Height="26"
|
|
||||||
Background="#F0F0F0" Foreground="#333" FontSize="11"/>
|
|
||||||
<Button Content="保存" Command="{Binding SaveCommand}" Width="65" Height="26" IsDefault="True"
|
|
||||||
Background="#007AFF" Foreground="#White" FontSize="11"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</Window>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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.
|
Before Width: | Height: | Size: 192 KiB |
@@ -1,56 +0,0 @@
|
|||||||
#define MyAppName "TodoList"
|
|
||||||
#define MyAppVersion "1.0.21"
|
|
||||||
#define MyAppPublisher "ShaoHua"
|
|
||||||
#define MyAppURL "https://git.we965.cn/Tools/TodoList"
|
|
||||||
#define MyAppExeName "TodoList.exe"
|
|
||||||
|
|
||||||
[Setup]
|
|
||||||
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
|
|
||||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
|
||||||
AppId={{8B8A6E3F-1234-5678-9ABC-DEF012345678}
|
|
||||||
AppName={#MyAppName}
|
|
||||||
AppVersion={#MyAppVersion}
|
|
||||||
;AppVerName={#MyAppName} {#MyAppVersion}
|
|
||||||
AppPublisher={#MyAppPublisher}
|
|
||||||
AppPublisherURL={#MyAppURL}
|
|
||||||
AppSupportURL={#MyAppURL}
|
|
||||||
AppUpdatesURL={#MyAppURL}
|
|
||||||
DefaultDirName={autopf}\{#MyAppName}
|
|
||||||
DisableProgramGroupPage=yes
|
|
||||||
; Remove the following line to run in administrative install mode (install for all users.)
|
|
||||||
PrivilegesRequired=lowest
|
|
||||||
OutputDir=Output
|
|
||||||
OutputBaseFilename={#MyAppName}_Setup_v{#MyAppVersion}
|
|
||||||
SetupIconFile=icon.ico
|
|
||||||
Compression=lzma
|
|
||||||
SolidCompression=yes
|
|
||||||
WizardStyle=modern
|
|
||||||
|
|
||||||
[Languages]
|
|
||||||
Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"
|
|
||||||
|
|
||||||
[Tasks]
|
|
||||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
|
||||||
|
|
||||||
[Files]
|
|
||||||
Source: "bin\Release\net8.0-windows\win-x64\publish\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
|
||||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
|
||||||
|
|
||||||
[Icons]
|
|
||||||
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
|
||||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
|
||||||
|
|
||||||
[Run]
|
|
||||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user