From 57cfd16e7b7957bd9d5ecd53b40c376534f3c2a7 Mon Sep 17 00:00:00 2001 From: ShaoHua <345265198@qqcom> Date: Tue, 30 Dec 2025 10:29:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=A1=B9=E7=9B=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PRD.md | 43 + Setup/Setup.vdproj | 956 ++++++++++++++++++ TodoList.slnx | 10 + TodoList/App.xaml | 9 + TodoList/App.xaml.cs | 308 ++++++ TodoList/AssemblyInfo.cs | 10 + .../Converters/EnumDescriptionConverter.cs | 31 + TodoList/Models/TodoItem.cs | 45 + TodoList/Services/FileDataService.cs | 70 ++ TodoList/Services/GlobalShortcutService.cs | 127 +++ TodoList/Services/IDataService.cs | 14 + TodoList/Services/SettingsService.cs | 56 + TodoList/TodoList.csproj | 21 + TodoList/ViewModels/MainViewModel.cs | 163 +++ TodoList/ViewModels/QuickEntryViewModel.cs | 58 ++ TodoList/Views/MainWindow.xaml | 256 +++++ TodoList/Views/MainWindow.xaml.cs | 62 ++ TodoList/Views/QuickEntryWindow.xaml | 84 ++ TodoList/Views/QuickEntryWindow.xaml.cs | 23 + TodoList/icon.ico | Bin 0 -> 197166 bytes publish.ps1 | 2 + 21 files changed, 2348 insertions(+) create mode 100644 PRD.md create mode 100644 Setup/Setup.vdproj create mode 100644 TodoList.slnx create mode 100644 TodoList/App.xaml create mode 100644 TodoList/App.xaml.cs create mode 100644 TodoList/AssemblyInfo.cs create mode 100644 TodoList/Converters/EnumDescriptionConverter.cs create mode 100644 TodoList/Models/TodoItem.cs create mode 100644 TodoList/Services/FileDataService.cs create mode 100644 TodoList/Services/GlobalShortcutService.cs create mode 100644 TodoList/Services/IDataService.cs create mode 100644 TodoList/Services/SettingsService.cs create mode 100644 TodoList/TodoList.csproj create mode 100644 TodoList/ViewModels/MainViewModel.cs create mode 100644 TodoList/ViewModels/QuickEntryViewModel.cs create mode 100644 TodoList/Views/MainWindow.xaml create mode 100644 TodoList/Views/MainWindow.xaml.cs create mode 100644 TodoList/Views/QuickEntryWindow.xaml create mode 100644 TodoList/Views/QuickEntryWindow.xaml.cs create mode 100644 TodoList/icon.ico create mode 100644 publish.ps1 diff --git a/PRD.md b/PRD.md new file mode 100644 index 0000000..dcbbad1 --- /dev/null +++ b/PRD.md @@ -0,0 +1,43 @@ +# TodoList 产品需求文档 (PRD) + +## 1. 项目概述 +本项目是一个基于 C# WPF (.NET 10) 开发的桌面待办事项管理应用 (TodoList)。旨在提供轻量、高效的任务管理体验,特别是通过快捷键快速唤起记录功能,最大化用户的操作效率。 + +## 2. 技术架构 +- **开发语言**: C# +- **UI 框架**: WPF (Windows Presentation Foundation) +- **目标框架**: .NET 10 +- **操作系统**: Windows + +## 3. 功能需求 + +### 3.1 核心功能:快速记录 (Quick Entry) +- **全局快捷键**: + - 允许用户注册/使用系统级全局快捷键(例如 `Ctrl + Alt + A` 或其他不冲突的组合)。 + - 支持在应用后台运行时响应快捷键。 +- **快速唤起**: + - 按下快捷键时,若应用最小化或隐藏,应立即弹出“新建任务”窗口或主界面。 + - 窗口弹出后,输入框应自动获取焦点,用户可直接打字。 + +### 3.2 任务模型 (Task Model) +每个任务需包含以下核心字段: +1. **任务名称 (Title/Content)**: 任务的具体描述。 +2. **紧急程度 (Priority/Urgency)**: + - 用于区分任务优先级(如:高、中、低)。 + - 需在界面上有直观的视觉区分(如颜色标记)。 +3. **完成状态 (IsCompleted)**: + - 标记任务是否已完成。 + +### 3.3 任务列表与视图 (Task List & View) +- **列表展示**: 展示当前所有未完成的任务。 +- **默认过滤**: + - 应用启动或刷新时,**默认隐藏已完成的任务**。 + - (可选) 提供“显示已完成任务”的切换开关以便查看历史记录。 + +### 3.4 离线与同步 (Offline & Sync) +- **离线记录**: 支持完全离线使用,数据优先保存于本地。 +- **数据同步**: 在网络可用时(或特定时机),自动将本地数据同步到服务端(预留同步机制)。 + +## 4. 非功能需求 +- **性能**: 启动速度快,快捷键响应低延迟。 +- **持久化**: 任务数据需保存到本地(如 SQLite, JSON, 或 XML),保证关闭应用后数据不丢失。 diff --git a/Setup/Setup.vdproj b/Setup/Setup.vdproj new file mode 100644 index 0000000..6edd7b8 --- /dev/null +++ b/Setup/Setup.vdproj @@ -0,0 +1,956 @@ +"DeployProject" +{ +"VSVersion" = "3:800" +"ProjectType" = "8:{978C614F-708E-4E1A-B201-565925725DBA}" +"IsWebType" = "8:FALSE" +"ProjectName" = "8:Setup" +"LanguageId" = "3:2052" +"CodePage" = "3:936" +"UILanguageId" = "3:2052" +"SccProjectName" = "8:" +"SccLocalPath" = "8:" +"SccAuxPath" = "8:" +"SccProvider" = "8:" + "Hierarchy" + { + "Entry" + { + "MsmKey" = "8:_17BA67E2733E421586A690779AE10839" + "OwnerKey" = "8:_UNDEFINED" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_1E301C03308E4C0AADE54375ACCA27E9" + "OwnerKey" = "8:_UNDEFINED" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_34EE79020CA44431B5F09CF100F2B372" + "OwnerKey" = "8:_UNDEFINED" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_42AD4BACEFC94D43B5DE5DDF9177EFAD" + "OwnerKey" = "8:_UNDEFINED" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_4C87263B310A4D3F8CC1BD3265F5B929" + "OwnerKey" = "8:_UNDEFINED" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_65835E291AAA6A7D2371E2E621AF3DC9" + "OwnerKey" = "8:_17BA67E2733E421586A690779AE10839" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_6792D68F0F43409CADA40277C6FE4B43" + "OwnerKey" = "8:_UNDEFINED" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_D50F9EB4AAE7436FBAD775A446017958" + "OwnerKey" = "8:_UNDEFINED" + "MsmSig" = "8:_UNDEFINED" + } + "Entry" + { + "MsmKey" = "8:_UNDEFINED" + "OwnerKey" = "8:_17BA67E2733E421586A690779AE10839" + "MsmSig" = "8:_UNDEFINED" + } + } + "Configurations" + { + "Debug" + { + "DisplayName" = "8:Debug" + "IsDebugOnly" = "11:TRUE" + "IsReleaseOnly" = "11:FALSE" + "OutputFilename" = "8:Debug\\Setup.msi" + "PackageFilesAs" = "3:2" + "PackageFileSize" = "3:-2147483648" + "CabType" = "3:1" + "Compression" = "3:2" + "SignOutput" = "11:FALSE" + "CertificateFile" = "8:" + "PrivateKeyFile" = "8:" + "TimeStampServer" = "8:" + "InstallerBootstrapper" = "3:2" + } + "Release" + { + "DisplayName" = "8:Release" + "IsDebugOnly" = "11:FALSE" + "IsReleaseOnly" = "11:TRUE" + "OutputFilename" = "8:Release\\Setup.msi" + "PackageFilesAs" = "3:2" + "PackageFileSize" = "3:-2147483648" + "CabType" = "3:1" + "Compression" = "3:2" + "SignOutput" = "11:FALSE" + "CertificateFile" = "8:" + "PrivateKeyFile" = "8:" + "TimeStampServer" = "8:" + "InstallerBootstrapper" = "3:2" + "BootstrapperCfg:{63ACBE69-63AA-4F98-B2B6-99F9E24495F2}" + { + "Enabled" = "11:FALSE" + "PromptEnabled" = "11:TRUE" + "PrerequisitesLocation" = "2:1" + "Url" = "8:" + "ComponentsUrl" = "8:" + "Items" + { + "{EDC2488A-8267-493A-A98E-7D9C3B36CDF3}:.NETFramework,Version=v4.7.2" + { + "Name" = "8:Microsoft .NET Framework 4.7.2 (x86 和 x64)" + "ProductCode" = "8:.NETFramework,Version=v4.7.2" + } + } + } + } + } + "Deployable" + { + "CustomAction" + { + } + "DefaultFeature" + { + "Name" = "8:DefaultFeature" + "Title" = "8:" + "Description" = "8:" + } + "ExternalPersistence" + { + "LaunchCondition" + { + "{A06ECF26-33A3-4562-8140-9B0E340D4F24}:_3DB760AE720B4C9DB0261DBC0A13DD96" + { + "Name" = "8:.NET Framework" + "Message" = "8:[VSDNETMSG]" + "FrameworkVersion" = "8:.NETFramework,Version=v4.7.2" + "AllowLaterVersions" = "11:FALSE" + "InstallUrl" = "8:http://go.microsoft.com/fwlink/?LinkId=863262" + } + } + } + "File" + { + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_17BA67E2733E421586A690779AE10839" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:TodoList, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_17BA67E2733E421586A690779AE10839" + { + "Name" = "8:TodoList.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:..\\TodoList\\bin\\Debug\\net10.0-windows\\TodoList.dll" + "TargetName" = "8:" + "Tag" = "8:" + "Folder" = "8:_8A20A7DDF24B44E8AC7BBB584966EEF1" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Vital" = "11:TRUE" + "ReadOnly" = "11:FALSE" + "Hidden" = "11:FALSE" + "System" = "11:FALSE" + "Permanent" = "11:FALSE" + "SharedLegacy" = "11:FALSE" + "PackageAs" = "3:1" + "Register" = "3:1" + "Exclude" = "11:FALSE" + "IsDependency" = "11:FALSE" + "IsolateTo" = "8:" + } + "{1FB2D0AE-D3B9-43D4-B9DD-F88EC61E35DE}:_1E301C03308E4C0AADE54375ACCA27E9" + { + "SourcePath" = "8:..\\TodoList\\bin\\Debug\\net10.0-windows\\TodoList.exe" + "TargetName" = "8:TodoList.exe" + "Tag" = "8:" + "Folder" = "8:_8A20A7DDF24B44E8AC7BBB584966EEF1" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Vital" = "11:TRUE" + "ReadOnly" = "11:FALSE" + "Hidden" = "11:FALSE" + "System" = "11:FALSE" + "Permanent" = "11:FALSE" + "SharedLegacy" = "11:FALSE" + "PackageAs" = "3:1" + "Register" = "3:1" + "Exclude" = "11:FALSE" + "IsDependency" = "11:FALSE" + "IsolateTo" = "8:" + } + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_34EE79020CA44431B5F09CF100F2B372" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:CommunityToolkit.Mvvm, Version=8.4.0.0, Culture=neutral, PublicKeyToken=4aff67a105548ee2, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_34EE79020CA44431B5F09CF100F2B372" + { + "Name" = "8:CommunityToolkit.Mvvm.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:..\\TodoList\\bin\\Debug\\net10.0-windows\\CommunityToolkit.Mvvm.dll" + "TargetName" = "8:" + "Tag" = "8:" + "Folder" = "8:_8A20A7DDF24B44E8AC7BBB584966EEF1" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Vital" = "11:TRUE" + "ReadOnly" = "11:FALSE" + "Hidden" = "11:FALSE" + "System" = "11:FALSE" + "Permanent" = "11:FALSE" + "SharedLegacy" = "11:FALSE" + "PackageAs" = "3:1" + "Register" = "3:1" + "Exclude" = "11:FALSE" + "IsDependency" = "11:FALSE" + "IsolateTo" = "8:" + } + "{1FB2D0AE-D3B9-43D4-B9DD-F88EC61E35DE}:_42AD4BACEFC94D43B5DE5DDF9177EFAD" + { + "SourcePath" = "8:..\\TodoList\\bin\\Debug\\net10.0-windows\\TodoList.runtimeconfig.json" + "TargetName" = "8:TodoList.runtimeconfig.json" + "Tag" = "8:" + "Folder" = "8:_8A20A7DDF24B44E8AC7BBB584966EEF1" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Vital" = "11:TRUE" + "ReadOnly" = "11:FALSE" + "Hidden" = "11:FALSE" + "System" = "11:FALSE" + "Permanent" = "11:FALSE" + "SharedLegacy" = "11:FALSE" + "PackageAs" = "3:1" + "Register" = "3:1" + "Exclude" = "11:FALSE" + "IsDependency" = "11:FALSE" + "IsolateTo" = "8:" + } + "{1FB2D0AE-D3B9-43D4-B9DD-F88EC61E35DE}:_4C87263B310A4D3F8CC1BD3265F5B929" + { + "SourcePath" = "8:..\\TodoList\\bin\\Debug\\net10.0-windows\\TodoList.deps.json" + "TargetName" = "8:TodoList.deps.json" + "Tag" = "8:" + "Folder" = "8:_8A20A7DDF24B44E8AC7BBB584966EEF1" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Vital" = "11:TRUE" + "ReadOnly" = "11:FALSE" + "Hidden" = "11:FALSE" + "System" = "11:FALSE" + "Permanent" = "11:FALSE" + "SharedLegacy" = "11:FALSE" + "PackageAs" = "3:1" + "Register" = "3:1" + "Exclude" = "11:FALSE" + "IsDependency" = "11:FALSE" + "IsolateTo" = "8:" + } + "{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_65835E291AAA6A7D2371E2E621AF3DC9" + { + "AssemblyRegister" = "3:1" + "AssemblyIsInGAC" = "11:FALSE" + "AssemblyAsmDisplayName" = "8:CommunityToolkit.Mvvm, Version=8.4.0.0, Culture=neutral, PublicKeyToken=4aff67a105548ee2, processorArchitecture=MSIL" + "ScatterAssemblies" + { + "_65835E291AAA6A7D2371E2E621AF3DC9" + { + "Name" = "8:CommunityToolkit.Mvvm.dll" + "Attributes" = "3:512" + } + } + "SourcePath" = "8:CommunityToolkit.Mvvm.dll" + "TargetName" = "8:" + "Tag" = "8:" + "Folder" = "8:_8A20A7DDF24B44E8AC7BBB584966EEF1" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Vital" = "11:TRUE" + "ReadOnly" = "11:FALSE" + "Hidden" = "11:FALSE" + "System" = "11:FALSE" + "Permanent" = "11:FALSE" + "SharedLegacy" = "11:FALSE" + "PackageAs" = "3:1" + "Register" = "3:1" + "Exclude" = "11:FALSE" + "IsDependency" = "11:TRUE" + "IsolateTo" = "8:" + } + "{1FB2D0AE-D3B9-43D4-B9DD-F88EC61E35DE}:_D50F9EB4AAE7436FBAD775A446017958" + { + "SourcePath" = "8:..\\TodoList\\bin\\Debug\\net10.0-windows\\icon.ico" + "TargetName" = "8:icon.ico" + "Tag" = "8:" + "Folder" = "8:_8A20A7DDF24B44E8AC7BBB584966EEF1" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Vital" = "11:TRUE" + "ReadOnly" = "11:FALSE" + "Hidden" = "11:FALSE" + "System" = "11:FALSE" + "Permanent" = "11:FALSE" + "SharedLegacy" = "11:FALSE" + "PackageAs" = "3:1" + "Register" = "3:1" + "Exclude" = "11:FALSE" + "IsDependency" = "11:FALSE" + "IsolateTo" = "8:" + } + } + "FileType" + { + } + "Folder" + { + "{1525181F-901A-416C-8A58-119130FE478E}:_4CF5CB48101445E3B47CD2485BDDA511" + { + "Name" = "8:#1916" + "AlwaysCreate" = "11:FALSE" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Property" = "8:DesktopFolder" + "Folders" + { + } + } + "{3C67513D-01DD-4637-8A68-80971EB9504F}:_8A20A7DDF24B44E8AC7BBB584966EEF1" + { + "DefaultLocation" = "8:[ProgramFilesFolder][Manufacturer]\\[ProductName]" + "Name" = "8:#1925" + "AlwaysCreate" = "11:FALSE" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Property" = "8:TARGETDIR" + "Folders" + { + } + } + "{1525181F-901A-416C-8A58-119130FE478E}:_BF5ADB1EDBF04FA0A47DA23313C521B9" + { + "Name" = "8:#1919" + "AlwaysCreate" = "11:FALSE" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Property" = "8:ProgramMenuFolder" + "Folders" + { + } + } + } + "LaunchCondition" + { + } + "Locator" + { + } + "MsiBootstrapper" + { + "LangId" = "3:2052" + "RequiresElevation" = "11:FALSE" + } + "Product" + { + "Name" = "8:Microsoft Visual Studio" + "ProductName" = "8:TodoList" + "ProductCode" = "8:{146611B1-AB9F-46C3-8FD4-EE7BCEC54C33}" + "PackageCode" = "8:{151EF683-7149-4D7A-9134-3092572B04CD}" + "UpgradeCode" = "8:{82F2F6EB-C24C-477B-81E6-43EDB0EE36B0}" + "AspNetVersion" = "8:4.0.30319.0" + "RestartWWWService" = "11:FALSE" + "RemovePreviousVersions" = "11:TRUE" + "DetectNewerInstalledVersion" = "11:TRUE" + "InstallAllUsers" = "11:TRUE" + "ProductVersion" = "8:1.0.0" + "Manufacturer" = "8:ShaoHua" + "ARPHELPTELEPHONE" = "8:" + "ARPHELPLINK" = "8:" + "Title" = "8:TodoList" + "Subject" = "8:" + "ARPCONTACT" = "8:ShaoHua" + "Keywords" = "8:" + "ARPCOMMENTS" = "8:" + "ARPURLINFOABOUT" = "8:" + "ARPPRODUCTICON" = "8:" + "ARPIconIndex" = "3:0" + "SearchPath" = "8:" + "UseSystemSearchPath" = "11:TRUE" + "TargetPlatform" = "3:0" + "PreBuildEvent" = "8:" + "PostBuildEvent" = "8:" + "RunPostBuildEvent" = "3:0" + } + "Registry" + { + "HKLM" + { + "Keys" + { + "{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_8522CD2219284FA48C170669A2A3D9B3" + { + "Name" = "8:Software" + "Condition" = "8:" + "AlwaysCreate" = "11:FALSE" + "DeleteAtUninstall" = "11:FALSE" + "Transitive" = "11:FALSE" + "Keys" + { + "{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_2553544E11294AD0ACF21B6F2D65909C" + { + "Name" = "8:[Manufacturer]" + "Condition" = "8:" + "AlwaysCreate" = "11:FALSE" + "DeleteAtUninstall" = "11:FALSE" + "Transitive" = "11:FALSE" + "Keys" + { + } + "Values" + { + } + } + } + "Values" + { + } + } + } + } + "HKCU" + { + "Keys" + { + "{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_BD961664BECD4769871995ED2576A507" + { + "Name" = "8:Software" + "Condition" = "8:" + "AlwaysCreate" = "11:FALSE" + "DeleteAtUninstall" = "11:FALSE" + "Transitive" = "11:FALSE" + "Keys" + { + "{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_885FDEC4E0C3484CA36F1A96BEAD1D41" + { + "Name" = "8:[Manufacturer]" + "Condition" = "8:" + "AlwaysCreate" = "11:FALSE" + "DeleteAtUninstall" = "11:FALSE" + "Transitive" = "11:FALSE" + "Keys" + { + } + "Values" + { + } + } + } + "Values" + { + } + } + } + } + "HKCR" + { + "Keys" + { + } + } + "HKU" + { + "Keys" + { + } + } + "HKPU" + { + "Keys" + { + } + } + } + "Sequences" + { + } + "Shortcut" + { + "{970C0BB2-C7D0-45D7-ABFA-7EC378858BC0}:_0D031B70CF7542448AC7303C2ED6DC6A" + { + "Name" = "8:TodoList" + "Arguments" = "8:" + "Description" = "8:" + "ShowCmd" = "3:1" + "IconIndex" = "3:0" + "Transitive" = "11:FALSE" + "Target" = "8:_1E301C03308E4C0AADE54375ACCA27E9" + "Folder" = "8:_BF5ADB1EDBF04FA0A47DA23313C521B9" + "WorkingFolder" = "8:_8A20A7DDF24B44E8AC7BBB584966EEF1" + "Icon" = "8:_D50F9EB4AAE7436FBAD775A446017958" + "Feature" = "8:" + } + "{970C0BB2-C7D0-45D7-ABFA-7EC378858BC0}:_AE6A93A941DD4EEDA219EAF034A157CC" + { + "Name" = "8:todoList主输出" + "Arguments" = "8:" + "Description" = "8:" + "ShowCmd" = "3:1" + "IconIndex" = "3:0" + "Transitive" = "11:FALSE" + "Target" = "8:_6792D68F0F43409CADA40277C6FE4B43" + "Folder" = "8:_BF5ADB1EDBF04FA0A47DA23313C521B9" + "WorkingFolder" = "8:_8A20A7DDF24B44E8AC7BBB584966EEF1" + "Icon" = "8:_D50F9EB4AAE7436FBAD775A446017958" + "Feature" = "8:" + } + } + "UserInterface" + { + "{2479F3F5-0309-486D-8047-8187E2CE5BA0}:_1A5922191B3444D2ADBFAAAEE654AFBE" + { + "UseDynamicProperties" = "11:FALSE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdBasicDialogs.wim" + } + "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_1A7F6DF07CA34F1290B53C77E9A24DCD" + { + "Name" = "8:#1901" + "Sequence" = "3:1" + "Attributes" = "3:2" + "Dialogs" + { + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_12C9A09546FF47E89C2F76E5857982C5" + { + "Sequence" = "3:100" + "DisplayName" = "8:进度" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdProgressDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + "ShowProgress" + { + "Name" = "8:ShowProgress" + "DisplayName" = "8:#1009" + "Description" = "8:#1109" + "Type" = "3:5" + "ContextData" = "8:1;True=1;False=0" + "Attributes" = "3:0" + "Setting" = "3:0" + "Value" = "3:1" + "DefaultValue" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + } + } + "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_1BED33199298468C95ACE087FD867323" + { + "Name" = "8:#1902" + "Sequence" = "3:1" + "Attributes" = "3:3" + "Dialogs" + { + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_3F862D9E99F34183BC0479F9A6500399" + { + "Sequence" = "3:100" + "DisplayName" = "8:已完成" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdFinishedDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + "UpdateText" + { + "Name" = "8:UpdateText" + "DisplayName" = "8:#1058" + "Description" = "8:#1158" + "Type" = "3:15" + "ContextData" = "8:" + "Attributes" = "3:0" + "Setting" = "3:1" + "Value" = "8:#1258" + "DefaultValue" = "8:#1258" + "UsePlugInResources" = "11:TRUE" + } + } + } + } + } + "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_2579AFB8C446463A9A8BCBDE6897AF55" + { + "Name" = "8:#1901" + "Sequence" = "3:2" + "Attributes" = "3:2" + "Dialogs" + { + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_1F08E1AAFAC14C4BA560722C839F686C" + { + "Sequence" = "3:100" + "DisplayName" = "8:进度" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdAdminProgressDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + "ShowProgress" + { + "Name" = "8:ShowProgress" + "DisplayName" = "8:#1009" + "Description" = "8:#1109" + "Type" = "3:5" + "ContextData" = "8:1;True=1;False=0" + "Attributes" = "3:0" + "Setting" = "3:0" + "Value" = "3:1" + "DefaultValue" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + } + } + "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_669B474BCA014E7C9871D52674DC793E" + { + "Name" = "8:#1900" + "Sequence" = "3:1" + "Attributes" = "3:1" + "Dialogs" + { + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_0EFA62CCD2C843EAA044829F13217B57" + { + "Sequence" = "3:300" + "DisplayName" = "8:确认安装" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdConfirmDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_860D684F21184955A2394B2FAE3B85B3" + { + "Sequence" = "3:200" + "DisplayName" = "8:安装文件夹" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdFolderDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + "InstallAllUsersVisible" + { + "Name" = "8:InstallAllUsersVisible" + "DisplayName" = "8:#1059" + "Description" = "8:#1159" + "Type" = "3:5" + "ContextData" = "8:1;True=1;False=0" + "Attributes" = "3:0" + "Setting" = "3:0" + "Value" = "3:1" + "DefaultValue" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_F62A5F384AC4466DAA5560FD53102C9B" + { + "Sequence" = "3:100" + "DisplayName" = "8:欢迎使用" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdWelcomeDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + "CopyrightWarning" + { + "Name" = "8:CopyrightWarning" + "DisplayName" = "8:#1002" + "Description" = "8:#1102" + "Type" = "3:3" + "ContextData" = "8:" + "Attributes" = "3:0" + "Setting" = "3:1" + "Value" = "8:#1202" + "DefaultValue" = "8:#1202" + "UsePlugInResources" = "11:TRUE" + } + "Welcome" + { + "Name" = "8:Welcome" + "DisplayName" = "8:#1003" + "Description" = "8:#1103" + "Type" = "3:3" + "ContextData" = "8:" + "Attributes" = "3:0" + "Setting" = "3:1" + "Value" = "8:#1203" + "DefaultValue" = "8:#1203" + "UsePlugInResources" = "11:TRUE" + } + } + } + } + } + "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_B97CCC8EE8F848C2A4276A2E58150A45" + { + "Name" = "8:#1902" + "Sequence" = "3:2" + "Attributes" = "3:3" + "Dialogs" + { + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_1CDE38F50B454FFA8F5C722E23D7165D" + { + "Sequence" = "3:100" + "DisplayName" = "8:已完成" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdAdminFinishedDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + } + } + "{2479F3F5-0309-486D-8047-8187E2CE5BA0}:_F85F3CEDA84E4C21AA1D2BEB630006E7" + { + "UseDynamicProperties" = "11:FALSE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdUserInterface.wim" + } + "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_F879EDB3FFCC474E9059D6DC3BED5CBE" + { + "Name" = "8:#1900" + "Sequence" = "3:2" + "Attributes" = "3:1" + "Dialogs" + { + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_4384C55A823E4238A33E43C392ADB6C2" + { + "Sequence" = "3:200" + "DisplayName" = "8:安装文件夹" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdAdminFolderDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_4D730CD1E9A541A9A0357FD14F274504" + { + "Sequence" = "3:100" + "DisplayName" = "8:欢迎使用" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdAdminWelcomeDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + "CopyrightWarning" + { + "Name" = "8:CopyrightWarning" + "DisplayName" = "8:#1002" + "Description" = "8:#1102" + "Type" = "3:3" + "ContextData" = "8:" + "Attributes" = "3:0" + "Setting" = "3:1" + "Value" = "8:#1202" + "DefaultValue" = "8:#1202" + "UsePlugInResources" = "11:TRUE" + } + "Welcome" + { + "Name" = "8:Welcome" + "DisplayName" = "8:#1003" + "Description" = "8:#1103" + "Type" = "3:3" + "ContextData" = "8:" + "Attributes" = "3:0" + "Setting" = "3:1" + "Value" = "8:#1203" + "DefaultValue" = "8:#1203" + "UsePlugInResources" = "11:TRUE" + } + } + } + "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_88BC734F7F1D4494AEE5F11E9797CD6B" + { + "Sequence" = "3:300" + "DisplayName" = "8:确认安装" + "UseDynamicProperties" = "11:TRUE" + "IsDependency" = "11:FALSE" + "SourcePath" = "8:\\VsdAdminConfirmDlg.wid" + "Properties" + { + "BannerBitmap" + { + "Name" = "8:BannerBitmap" + "DisplayName" = "8:#1001" + "Description" = "8:#1101" + "Type" = "3:8" + "ContextData" = "8:Bitmap" + "Attributes" = "3:4" + "Setting" = "3:1" + "UsePlugInResources" = "11:TRUE" + } + } + } + } + } + } + "MergeModule" + { + } + "ProjectOutput" + { + "{5259A561-127C-4D43-A0A1-72F10C7B3BF8}:_6792D68F0F43409CADA40277C6FE4B43" + { + "SourcePath" = "8:..\\TodoList\\obj\\Debug\\net10.0-windows\\TodoList.dll" + "TargetName" = "8:" + "Tag" = "8:" + "Folder" = "8:_8A20A7DDF24B44E8AC7BBB584966EEF1" + "Condition" = "8:" + "Transitive" = "11:FALSE" + "Vital" = "11:TRUE" + "ReadOnly" = "11:FALSE" + "Hidden" = "11:FALSE" + "System" = "11:FALSE" + "Permanent" = "11:FALSE" + "SharedLegacy" = "11:FALSE" + "PackageAs" = "3:1" + "Register" = "3:1" + "Exclude" = "11:FALSE" + "IsDependency" = "11:FALSE" + "IsolateTo" = "8:" + "ProjectOutputGroupRegister" = "3:1" + "OutputConfiguration" = "8:" + "OutputGroupCanonicalName" = "8:Built" + "OutputProjectGuid" = "8:{6CD40A2D-545E-C111-D437-1BE9E9C2C98C}" + "ShowKeyOutput" = "11:TRUE" + "ExcludeFilters" + { + } + } + } + } +} diff --git a/TodoList.slnx b/TodoList.slnx new file mode 100644 index 0000000..858e741 --- /dev/null +++ b/TodoList.slnx @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/TodoList/App.xaml b/TodoList/App.xaml new file mode 100644 index 0000000..3f556f9 --- /dev/null +++ b/TodoList/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/TodoList/App.xaml.cs b/TodoList/App.xaml.cs new file mode 100644 index 0000000..0ed47db --- /dev/null +++ b/TodoList/App.xaml.cs @@ -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(); + } + } +} diff --git a/TodoList/AssemblyInfo.cs b/TodoList/AssemblyInfo.cs new file mode 100644 index 0000000..cc29e7f --- /dev/null +++ b/TodoList/AssemblyInfo.cs @@ -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) +)] diff --git a/TodoList/Converters/EnumDescriptionConverter.cs b/TodoList/Converters/EnumDescriptionConverter.cs new file mode 100644 index 0000000..9d7d0f8 --- /dev/null +++ b/TodoList/Converters/EnumDescriptionConverter.cs @@ -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; + } + } +} diff --git a/TodoList/Models/TodoItem.cs b/TodoList/Models/TodoItem.cs new file mode 100644 index 0000000..3ec4228 --- /dev/null +++ b/TodoList/Models/TodoItem.cs @@ -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; + } +} diff --git a/TodoList/Services/FileDataService.cs b/TodoList/Services/FileDataService.cs new file mode 100644 index 0000000..7c0b10d --- /dev/null +++ b/TodoList/Services/FileDataService.cs @@ -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> LoadTasksAsync() + { + if (!File.Exists(_filePath)) + { + return new List(); + } + + try + { + using var stream = File.OpenRead(_filePath); + var items = await JsonSerializer.DeserializeAsync>(stream); + return items ?? new List(); + } + catch + { + return new List(); + } + } + + 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 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); + } + } + } +} diff --git a/TodoList/Services/GlobalShortcutService.cs b/TodoList/Services/GlobalShortcutService.cs new file mode 100644 index 0000000..b8a5c46 --- /dev/null +++ b/TodoList/Services/GlobalShortcutService.cs @@ -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, 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' + } + } +} diff --git a/TodoList/Services/IDataService.cs b/TodoList/Services/IDataService.cs new file mode 100644 index 0000000..470378f --- /dev/null +++ b/TodoList/Services/IDataService.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoList.Models; + +namespace TodoList.Services +{ + public interface IDataService + { + Task> LoadTasksAsync(); + Task SaveTaskAsync(TodoItem task); + Task SaveAllAsync(List tasks); + Task DeleteTaskAsync(string id); + } +} diff --git a/TodoList/Services/SettingsService.cs b/TodoList/Services/SettingsService.cs new file mode 100644 index 0000000..cc2763b --- /dev/null +++ b/TodoList/Services/SettingsService.cs @@ -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(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 { } + } + } +} diff --git a/TodoList/TodoList.csproj b/TodoList/TodoList.csproj new file mode 100644 index 0000000..b399328 --- /dev/null +++ b/TodoList/TodoList.csproj @@ -0,0 +1,21 @@ + + + + WinExe + net10.0-windows + enable + enable + true + true + icon.ico + + + + + + + + + + + diff --git a/TodoList/ViewModels/MainViewModel.cs b/TodoList/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..c68a825 --- /dev/null +++ b/TodoList/ViewModels/MainViewModel.cs @@ -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 + { + private readonly IDataService _dataService; + private readonly SettingsService _settingsService; + + [ObservableProperty] + private ObservableCollection 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 + { + } +} diff --git a/TodoList/ViewModels/QuickEntryViewModel.cs b/TodoList/ViewModels/QuickEntryViewModel.cs new file mode 100644 index 0000000..065af8e --- /dev/null +++ b/TodoList/ViewModels/QuickEntryViewModel.cs @@ -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(); + } + } +} diff --git a/TodoList/Views/MainWindow.xaml b/TodoList/Views/MainWindow.xaml new file mode 100644 index 0000000..29a8ab8 --- /dev/null +++ b/TodoList/Views/MainWindow.xaml @@ -0,0 +1,256 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +