├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── TagsTree.sln └── TagsTree ├── Algorithm ├── BidirectionalDictionary.cs ├── DoubleKeysDictionary.cs └── TableDictionary.cs ├── App.xaml ├── App.xaml.cs ├── AppConfig.cs ├── AppContext.cs ├── Assets ├── LockScreenLogo.scale-200.png ├── SplashScreen.scale-200.png ├── Square150x150Logo.scale-200.png ├── Square44x44Logo.scale-200.png ├── Square44x44Logo.targetsize-24_altform-unplated.png ├── StoreLogo.png └── Wide310x150Logo.scale-200.png ├── Controls ├── InputContentDialog.xaml ├── InputContentDialog.xaml.cs ├── Items │ ├── DataGridIconColumn.xaml │ ├── DataGridIconColumn.xaml.cs │ ├── DataGridNameColumn.xaml │ ├── DataGridNameColumn.xaml.cs │ ├── DataGridPartialPathColumn.xaml │ ├── DataGridPartialPathColumn.xaml.cs │ ├── DataGridTagsColumn.xaml │ └── DataGridTagsColumn.xaml.cs ├── TagCompleteBox.xaml ├── TagCompleteBox.xaml.cs ├── TagSearchBox.xaml └── TagSearchBox.xaml.cs ├── Delegates └── ResultChanged.cs ├── Interfaces ├── IFileModel.cs ├── IFullName.cs └── ITypeGetter.cs ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── Models ├── FileBase.cs ├── FileChanged.cs ├── FileChangedMerger.cs ├── FileModel.cs ├── RelationsDataTable.cs ├── TagModel.cs └── TagsTreeDictionary.cs ├── Package.appxmanifest ├── Properties ├── Resources.Designer.cs ├── Resources.resx └── launchSettings.json ├── Resources ├── ConstantStrings.cs ├── Folder.png ├── Link.png ├── Loading.gif ├── NotFound.png └── e8bb.png ├── Services ├── C.cs ├── ExtensionMethods │ ├── FileModelHelper.cs │ ├── FileSystemHelper.cs │ ├── FileViewModelHelper.cs │ ├── FullNameHelper.cs │ └── TagViewModelHelper.cs ├── FilesObserver.cs ├── IconsHelper.cs ├── Serialization.cs └── ShowContentDialog.cs ├── TagsTree.csproj ├── Views ├── FileEditTagsPage.xaml ├── FileEditTagsPage.xaml.cs ├── FileImporterPage.xaml ├── FileImporterPage.xaml.cs ├── FilePropertiesPage.xaml ├── FilePropertiesPage.xaml.cs ├── FilesObserverPage.xaml ├── FilesObserverPage.xaml.cs ├── IndexPage.xaml ├── IndexPage.xaml.cs ├── SelectTagToEditPage.xaml ├── SelectTagToEditPage.xaml.cs ├── SettingsPage.xaml ├── SettingsPage.xaml.cs ├── TagEditFilesPage.xaml ├── TagEditFilesPage.xaml.cs ├── TagSearchFilesPage.xaml ├── TagSearchFilesPage.xaml.cs ├── TagsManagerPage.xaml ├── TagsManagerPage.xaml.cs └── ViewModels │ ├── Controls │ ├── InputContentDialogViewModels.cs │ └── TagCompleteBoxViewModel.cs │ ├── FileEditTagsViewModel.cs │ ├── FileImporterViewModel.cs │ ├── FilePropertiesPageViewModel.cs │ ├── FileViewModel.cs │ ├── FilesObserverViewModel.cs │ ├── SelectTagToEditPageViewModel.cs │ ├── SettingsViewModel.cs │ ├── TagEditFilesViewModel.cs │ ├── TagSearchFilesViewModel.cs │ ├── TagViewModel.cs │ └── TagsManagerViewModel.cs └── app.manifest /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # Tye 66 | .tye/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.vspscc 97 | *.vssscc 98 | .builds 99 | *.pidb 100 | *.svclog 101 | *.scc 102 | 103 | # Chutzpah Test files 104 | _Chutzpah* 105 | 106 | # Visual C++ cache files 107 | ipch/ 108 | *.aps 109 | *.ncb 110 | *.opendb 111 | *.opensdf 112 | *.sdf 113 | *.cachefile 114 | *.VC.db 115 | *.VC.VC.opendb 116 | 117 | # Visual Studio profiler 118 | *.psess 119 | *.vsp 120 | *.vspx 121 | *.sap 122 | 123 | # Visual Studio Trace Files 124 | *.e2e 125 | 126 | # TFS 2012 Local Workspace 127 | $tf/ 128 | 129 | # Guidance Automation Toolkit 130 | *.gpState 131 | 132 | # ReSharper is a .NET coding add-in 133 | _ReSharper*/ 134 | *.[Rr]e[Ss]harper 135 | *.DotSettings.user 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Coverlet is a free, cross platform Code Coverage Tool 148 | coverage*.json 149 | coverage*.xml 150 | coverage*.info 151 | 152 | # Visual Studio code coverage results 153 | *.coverage 154 | *.coveragexml 155 | 156 | # NCrunch 157 | _NCrunch_* 158 | .*crunch*.local.xml 159 | nCrunchTemp_* 160 | 161 | # MightyMoose 162 | *.mm.* 163 | AutoTest.Net/ 164 | 165 | # Web workbench (sass) 166 | .sass-cache/ 167 | 168 | # Installshield output folder 169 | [Ee]xpress/ 170 | 171 | # DocProject is a documentation generator add-in 172 | DocProject/buildhelp/ 173 | DocProject/Help/*.HxT 174 | DocProject/Help/*.HxC 175 | DocProject/Help/*.hhc 176 | DocProject/Help/*.hhk 177 | DocProject/Help/*.hhp 178 | DocProject/Help/Html2 179 | DocProject/Help/html 180 | 181 | # Click-Once directory 182 | publish/ 183 | 184 | # Publish Web Output 185 | *.[Pp]ublish.xml 186 | *.azurePubxml 187 | # Note: Comment the next line if you want to checkin your web deploy settings, 188 | # but database connection strings (with potential passwords) will be unencrypted 189 | *.pubxml 190 | *.publishproj 191 | 192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 193 | # checkin your Azure Web App publish settings, but sensitive information contained 194 | # in these scripts will be unencrypted 195 | PublishScripts/ 196 | 197 | # NuGet Packages 198 | *.nupkg 199 | # NuGet Symbol Packages 200 | *.snupkg 201 | # The packages folder can be ignored because of Package Restore 202 | **/[Pp]ackages/* 203 | # except build/, which is used as an MSBuild target. 204 | !**/[Pp]ackages/build/ 205 | # Uncomment if necessary however generally it will be regenerated when needed 206 | #!**/[Pp]ackages/repositories.config 207 | # NuGet v3's project.json files produces more ignorable files 208 | *.nuget.props 209 | *.nuget.targets 210 | 211 | # Microsoft Azure Build Output 212 | csx/ 213 | *.build.csdef 214 | 215 | # Microsoft Azure Emulator 216 | ecf/ 217 | rcf/ 218 | 219 | # Windows Store app package directories and files 220 | AppPackages/ 221 | BundleArtifacts/ 222 | Package.StoreAssociation.xml 223 | _pkginfo.txt 224 | *.appx 225 | *.appxbundle 226 | *.appxupload 227 | 228 | # Visual Studio cache files 229 | # files ending in .cache can be ignored 230 | *.[Cc]ache 231 | # but keep track of directories ending in .cache 232 | !?*.[Cc]ache/ 233 | 234 | # Others 235 | ClientBin/ 236 | ~$* 237 | *~ 238 | *.dbmdl 239 | *.dbproj.schemaview 240 | *.jfm 241 | *.pfx 242 | *.publishsettings 243 | orleans.codegen.cs 244 | 245 | # Including strong name files can present a security risk 246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 247 | #*.snk 248 | 249 | # Since there are multiple workflows, uncomment next line to ignore bower_components 250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 251 | #bower_components/ 252 | 253 | # RIA/Silverlight projects 254 | Generated_Code/ 255 | 256 | # Backup & report files from converting an old project file 257 | # to a newer Visual Studio version. Backup files are not needed, 258 | # because we have git ;-) 259 | _UpgradeReport_Files/ 260 | Backup*/ 261 | UpgradeLog*.XML 262 | UpgradeLog*.htm 263 | ServiceFabricBackup/ 264 | *.rptproj.bak 265 | 266 | # SQL Server files 267 | *.mdf 268 | *.ldf 269 | *.ndf 270 | 271 | # Business Intelligence projects 272 | *.rdl.data 273 | *.bim.layout 274 | *.bim_*.settings 275 | *.rptproj.rsuser 276 | *- [Bb]ackup.rdl 277 | *- [Bb]ackup ([0-9]).rdl 278 | *- [Bb]ackup ([0-9][0-9]).rdl 279 | 280 | # Microsoft Fakes 281 | FakesAssemblies/ 282 | 283 | # GhostDoc plugin setting file 284 | *.GhostDoc.xml 285 | 286 | # Node.js Tools for Visual Studio 287 | .ntvs_analysis.dat 288 | node_modules/ 289 | 290 | # Visual Studio 6 build log 291 | *.plg 292 | 293 | # Visual Studio 6 workspace options file 294 | *.opt 295 | 296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 297 | *.vbw 298 | 299 | # Visual Studio LightSwitch build output 300 | **/*.HTMLClient/GeneratedArtifacts 301 | **/*.DesktopClient/GeneratedArtifacts 302 | **/*.DesktopClient/ModelManifest.xml 303 | **/*.Server/GeneratedArtifacts 304 | **/*.Server/ModelManifest.xml 305 | _Pvt_Extensions 306 | 307 | # Paket dependency manager 308 | .paket/paket.exe 309 | paket-files/ 310 | 311 | # FAKE - F# Make 312 | .fake/ 313 | 314 | # CodeRush personal settings 315 | .cr/personal 316 | 317 | # Python Tools for Visual Studio (PTVS) 318 | __pycache__/ 319 | *.pyc 320 | 321 | # Cake - Uncomment if you are using it 322 | # tools/** 323 | # !tools/packages.config 324 | 325 | # Tabs Studio 326 | *.tss 327 | 328 | # Telerik's JustMock configuration file 329 | *.jmconfig 330 | 331 | # BizTalk build output 332 | *.btp.cs 333 | *.btm.cs 334 | *.odx.cs 335 | *.xsd.cs 336 | 337 | # OpenCover UI analysis results 338 | OpenCover/ 339 | 340 | # Azure Stream Analytics local run output 341 | ASALocalRun/ 342 | 343 | # MSBuild Binary and Structured Log 344 | *.binlog 345 | 346 | # NVidia Nsight GPU debugger configuration file 347 | *.nvuser 348 | 349 | # MFractors (Xamarin productivity tool) working folder 350 | .mfractor/ 351 | 352 | # Local History for Visual Studio 353 | .localhistory/ 354 | 355 | # BeatPulse healthcheck temp database 356 | healthchecksdb 357 | 358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 359 | MigrationBackup/ 360 | 361 | # Ionide (cross platform F# VS Code tools) working folder 362 | .ionide/ 363 | 364 | # Fody - auto-generated XML schema 365 | FodyWeavers.xsd 366 | 367 | ## 368 | ## Visual studio for Mac 369 | ## 370 | 371 | 372 | # globs 373 | Makefile.in 374 | *.userprefs 375 | *.usertasks 376 | config.make 377 | config.status 378 | aclocal.m4 379 | install-sh 380 | autom4te.cache/ 381 | *.tar.gz 382 | tarballs/ 383 | test-results/ 384 | 385 | # Mac bundle stuff 386 | *.dmg 387 | *.app 388 | 389 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 390 | # General 391 | .DS_Store 392 | .AppleDouble 393 | .LSOverride 394 | 395 | # Icon must end with two \r 396 | Icon 397 | 398 | 399 | # Thumbnails 400 | ._* 401 | 402 | # Files that might appear in the root of a volume 403 | .DocumentRevisions-V100 404 | .fseventsd 405 | .Spotlight-V100 406 | .TemporaryItems 407 | .Trashes 408 | .VolumeIcon.icns 409 | .com.apple.timemachine.donotpresent 410 | 411 | # Directories potentially created on remote AFP share 412 | .AppleDB 413 | .AppleDesktop 414 | Network Trash Folder 415 | Temporary Items 416 | .apdisk 417 | 418 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 419 | # Windows thumbnail cache files 420 | Thumbs.db 421 | ehthumbs.db 422 | ehthumbs_vista.db 423 | 424 | # Dump file 425 | *.stackdump 426 | 427 | # Folder config file 428 | [Dd]esktop.ini 429 | 430 | # Recycle Bin used on file shares 431 | $RECYCLE.BIN/ 432 | 433 | # Windows Installer files 434 | *.cab 435 | *.msi 436 | *.msix 437 | *.msm 438 | *.msp 439 | 440 | # Windows shortcuts 441 | *.lnk 442 | 443 | # JetBrains Rider 444 | .idea/ 445 | *.sln.iml 446 | 447 | ## 448 | ## Visual Studio Code 449 | ## 450 | .vscode/* 451 | !.vscode/settings.json 452 | !.vscode/tasks.json 453 | !.vscode/launch.json 454 | !.vscode/extensions.json 455 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TagsTree 2 | 3 | ## 介绍 4 | 5 | TagsTree 是一个用树状结构标签管理文件的软件 6 | 7 | TagsTree 能在不干涉文件本身的前提下,为文件添加标签 8 | 9 | ## 使用方法 10 | 11 | [GitHub Wiki](https://github.com/Poker-sang/TagsTree/wiki) 12 | 13 | ## 关于项目 14 | 15 | 项目名称:TagTree 16 | 17 | 项目地址:[GitHub](https://github.com/Poker-sang/TagsTree) 18 | 19 | 版本:1.30 20 | 21 | ## 联系方式 22 | 23 | 作者:[扑克](https://github.com/Poker-sang) 24 | 25 | 邮箱:poker_sang@outlook.com 26 | 27 | QQ:2639914082 28 | 29 | 2022.11.16 30 | -------------------------------------------------------------------------------- /TagsTree.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # 17 4 | VisualStudioVersion = 17.0.32002.185 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TagsTree", "TagsTree\TagsTree.csproj", "{2C2F11B8-E743-40D9-ACD3-C8FB65DB85C3}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFolder", "SolutionFolder", "{C68548A6-9772-4B61-A979-580EA1AB01B4}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | .gitignore = .gitignore 12 | LICENSE = LICENSE 13 | README.md = README.md 14 | EndProjectSection 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|arm64 = Debug|arm64 19 | Debug|x64 = Debug|x64 20 | Debug|x86 = Debug|x86 21 | Release|arm64 = Release|arm64 22 | Release|x64 = Release|x64 23 | Release|x86 = Release|x86 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {2C2F11B8-E743-40D9-ACD3-C8FB65DB85C3}.Debug|arm64.ActiveCfg = Debug|arm64 27 | {2C2F11B8-E743-40D9-ACD3-C8FB65DB85C3}.Debug|arm64.Build.0 = Debug|arm64 28 | {2C2F11B8-E743-40D9-ACD3-C8FB65DB85C3}.Debug|arm64.Deploy.0 = Debug|arm64 29 | {2C2F11B8-E743-40D9-ACD3-C8FB65DB85C3}.Debug|x64.ActiveCfg = Debug|x64 30 | {2C2F11B8-E743-40D9-ACD3-C8FB65DB85C3}.Debug|x64.Build.0 = Debug|x64 31 | {2C2F11B8-E743-40D9-ACD3-C8FB65DB85C3}.Debug|x64.Deploy.0 = Debug|x64 32 | {2C2F11B8-E743-40D9-ACD3-C8FB65DB85C3}.Debug|x86.ActiveCfg = Debug|x86 33 | {2C2F11B8-E743-40D9-ACD3-C8FB65DB85C3}.Debug|x86.Build.0 = Debug|x86 34 | {2C2F11B8-E743-40D9-ACD3-C8FB65DB85C3}.Release|arm64.ActiveCfg = Release|arm64 35 | {2C2F11B8-E743-40D9-ACD3-C8FB65DB85C3}.Release|arm64.Build.0 = Release|arm64 36 | {2C2F11B8-E743-40D9-ACD3-C8FB65DB85C3}.Release|arm64.Deploy.0 = Release|arm64 37 | {2C2F11B8-E743-40D9-ACD3-C8FB65DB85C3}.Release|x64.ActiveCfg = Release|x64 38 | {2C2F11B8-E743-40D9-ACD3-C8FB65DB85C3}.Release|x64.Build.0 = Release|x64 39 | {2C2F11B8-E743-40D9-ACD3-C8FB65DB85C3}.Release|x64.Deploy.0 = Release|x64 40 | {2C2F11B8-E743-40D9-ACD3-C8FB65DB85C3}.Release|x86.ActiveCfg = Release|x86 41 | {2C2F11B8-E743-40D9-ACD3-C8FB65DB85C3}.Release|x86.Build.0 = Release|x86 42 | {2C2F11B8-E743-40D9-ACD3-C8FB65DB85C3}.Release|x86.Deploy.0 = Release|x86 43 | EndGlobalSection 44 | GlobalSection(SolutionProperties) = preSolution 45 | HideSolutionNode = FALSE 46 | EndGlobalSection 47 | GlobalSection(ExtensibilityGlobals) = postSolution 48 | SolutionGuid = {0123A772-38BD-43A4-B4ED-45752433B39F} 49 | EndGlobalSection 50 | EndGlobal 51 | -------------------------------------------------------------------------------- /TagsTree/Algorithm/BidirectionalDictionary.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using TagsTree.Services; 3 | 4 | namespace TagsTree.Algorithm; 5 | 6 | /// 7 | /// 包含两个不同类型的双向字典 8 | /// 9 | public class BidirectionalDictionary where TKey : notnull where TValue : notnull 10 | { 11 | private readonly Dictionary _dict1 = []; 12 | 13 | private readonly Dictionary _dict2 = []; 14 | 15 | public int Count => _dict1.Count; 16 | 17 | public Dictionary.KeyCollection Keys => _dict1.Keys; 18 | 19 | public Dictionary.KeyCollection Values => _dict2.Keys; 20 | 21 | public TValue this[TKey key] 22 | { 23 | get => _dict1[key]; 24 | set 25 | { 26 | if (!_dict1.TryAdd(key, value)) 27 | return; 28 | _dict2[value] = key; 29 | } 30 | } 31 | 32 | public TKey this[TValue key] 33 | { 34 | get => _dict2[key]; 35 | set 36 | { 37 | if (_dict2.ContainsKey(key)) 38 | return; 39 | _dict1[value] = key; 40 | _dict2[key] = value; 41 | } 42 | } 43 | 44 | public bool Contains(TKey key) => _dict1.ContainsKey(key); 45 | 46 | public bool Contains(TValue key) => _dict2.ContainsKey(key); 47 | 48 | public bool Remove(TKey key) 49 | { 50 | if (!_dict1.TryGetValue(key, out var value)) 51 | return false; 52 | _ = _dict2.Remove(value); 53 | _ = _dict1.Remove(key); 54 | return true; 55 | } 56 | 57 | public bool Remove(TValue key) 58 | { 59 | if (!_dict2.TryGetValue(key, out var value)) 60 | return false; 61 | _ = _dict1.Remove(value); 62 | _ = _dict2.Remove(key); 63 | return true; 64 | } 65 | 66 | public void Deserialize(string path) 67 | { 68 | _dict1.Clear(); 69 | _dict2.Clear(); 70 | foreach (var (key, value) in Serialization.Deserialize>(path)) 71 | { 72 | _dict1[key] = value; 73 | _dict2[value] = key; 74 | } 75 | } 76 | 77 | public void Serialize(string path) => Serialization.Serialize(path, _dict1); 78 | } 79 | -------------------------------------------------------------------------------- /TagsTree/Algorithm/DoubleKeysDictionary.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using WinUI3Utilities; 4 | 5 | namespace TagsTree.Algorithm; 6 | 7 | public class DoubleKeysDictionary where TKey1 : notnull where TKey2 : notnull where TValue : notnull 8 | { 9 | public DoubleKeysDictionary() 10 | { 11 | if (typeof(TKey1) == typeof(TKey2)) 12 | ThrowHelper.Argument(typeof(TKey1), $"DoubleKeysDictionary的两个键类型不能相同,此处类型都为{typeof(TKey1).Name}"); 13 | } 14 | 15 | private readonly Dictionary _dict1 = []; 16 | 17 | private readonly Dictionary _dict2 = []; 18 | 19 | public int Count => _dict1.Count; 20 | 21 | public Dictionary.KeyCollection Keys1 => _dict1.Keys; 22 | 23 | public Dictionary.KeyCollection Keys2 => _dict2.Keys; 24 | 25 | public Dictionary.ValueCollection Values => _dict2.Values; 26 | 27 | public TValue this[TKey1 key1, TKey2 key2] 28 | { 29 | set 30 | { 31 | if (_dict1.ContainsKey(key1) || _dict2.ContainsKey(key2)) 32 | return; 33 | _dict1[key1] = key2; 34 | _dict2[key2] = value; 35 | } 36 | } 37 | 38 | public void ChangeKey2(TKey2 oldKey2, TKey2 newKey2) 39 | { 40 | if (!_dict2.ContainsKey(oldKey2)) 41 | return; 42 | foreach (var pair in _dict1.Where(pair => Equals(pair.Value, oldKey2))) 43 | _dict1[pair.Key] = newKey2; 44 | _ = _dict2.Remove(oldKey2, out var value); 45 | _dict2[newKey2] = value!; 46 | } 47 | 48 | public TValue this[TKey1 key1] => _dict2[_dict1[key1]]; 49 | 50 | public TValue this[TKey2 key2] => _dict2[key2]; 51 | 52 | public TValue? GetValueOrDefault(TKey1 key1) => _dict1.TryGetValue(key1, out var value) ? _dict2[value] : default; 53 | 54 | public TValue? GetValueOrDefault(TKey2 key2) => _dict2.GetValueOrDefault(key2); 55 | 56 | public bool ContainsKey(TKey1 key1) => _dict1.ContainsKey(key1); 57 | 58 | public bool ContainsKey(TKey2 key2) => _dict2.ContainsKey(key2); 59 | 60 | public bool ContainsValue(TValue value) => _dict2.ContainsValue(value); 61 | 62 | public bool Remove(TKey1 key1) 63 | { 64 | if (!_dict1.TryGetValue(key1, out var value)) 65 | return false; 66 | _ = _dict2.Remove(value); 67 | _ = _dict1.Remove(key1); 68 | return true; 69 | } 70 | 71 | public void Clear() 72 | { 73 | _dict1.Clear(); 74 | _dict2.Clear(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /TagsTree/Algorithm/TableDictionary.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace TagsTree.Algorithm; 8 | 9 | public class TableDictionary where TColumn : IParsable where TRow : IParsable 10 | { 11 | public readonly Dictionary Columns = []; 12 | 13 | public readonly Dictionary Rows = []; 14 | 15 | public readonly List> Table = []; 16 | 17 | public List this[TColumn column] 18 | { 19 | get => Table[Columns[column]]; 20 | set => Table[Columns[column]] = value; 21 | } 22 | 23 | public bool this[TColumn column, TRow row] 24 | { 25 | get => this[column][Rows[row]]; 26 | set => this[column][Rows[row]] = value; 27 | } 28 | 29 | public void AddColumn(TColumn column) 30 | { 31 | Columns[column] = Columns.Count; 32 | var temp = new List(Rows.Count); 33 | for (var i = 0; i < Rows.Count; ++i) 34 | temp.Add(false); 35 | Table.Add(temp); 36 | } 37 | 38 | public void AddRow(TRow row) 39 | { 40 | Rows[row] = Rows.Count; 41 | foreach (var list in Table) 42 | list.Add(false); 43 | } 44 | 45 | public void RemoveColumn(TColumn column) 46 | { 47 | var index = Columns[column]; 48 | Table.RemoveAt(index); 49 | _ = Columns.Remove(column); 50 | foreach (var (key, value) in Columns) 51 | if (value > index) 52 | --Columns[key]; 53 | } 54 | 55 | public void RemoveRow(TRow row) 56 | { 57 | var index = Rows[row]; 58 | foreach (var list in Table) 59 | list.RemoveAt(index); 60 | _ = Rows.Remove(row); 61 | foreach (var (key, value) in Rows) 62 | if (value > index) 63 | --Rows[key]; 64 | } 65 | 66 | public void Deserialize(string path) 67 | { 68 | Table.Clear(); 69 | Columns.Clear(); 70 | Rows.Clear(); 71 | var buffer = File.ReadAllText(path); 72 | var lines = buffer.Split(';'); 73 | var rows = lines[0].Split(',').Select(row => TRow.Parse(row, null)).ToArray(); 74 | foreach (var row in rows) 75 | Rows[row] = Rows.Count; 76 | foreach (var line in lines.Skip(1)) 77 | { 78 | var columns = line.Split(','); 79 | var column = TColumn.Parse(columns[0], null); 80 | Columns[column] = Columns.Count; 81 | Table.Add([]); 82 | foreach (var c in columns[1]) 83 | this[column].Add(c is '1'); 84 | } 85 | } 86 | 87 | public void Serialize(string path) 88 | { 89 | if (Table.Count is 0 || Table[0].Count is 0) 90 | return; 91 | var buffer = Rows.Keys.Aggregate("", (current, key) => current + key + ","); 92 | buffer = buffer.Remove(buffer.Length - 1) + ";"; 93 | buffer = Columns.Aggregate(buffer, (current, pair) => this[pair.Key].Aggregate(current + pair.Key + ",", (currentX, value) => currentX + (value ? 1 : 0)) + ";"); 94 | buffer = buffer.Remove(buffer.Length - 1); 95 | File.WriteAllText(path, buffer, Encoding.UTF8); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /TagsTree/App.xaml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 0,48,0,0 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /TagsTree/App.xaml.cs: -------------------------------------------------------------------------------- 1 | // #define FIRST_TIME 2 | 3 | using FluentIcons.WinUI; 4 | using Microsoft.UI.Xaml; 5 | using WinUI3Utilities; 6 | 7 | namespace TagsTree; 8 | 9 | public partial class App : Application 10 | { 11 | public const string AppName = nameof(TagsTree); 12 | 13 | public App() 14 | { 15 | _ = this.UseSegoeMetrics(); 16 | InitializeComponent(); 17 | SettingsValueConverter.Context = ConfigSerializeContext.Default; 18 | AppContext.Initialize(); 19 | } 20 | 21 | public static MainWindow MainWindow { get; private set; } = null!; 22 | 23 | /// Details about the launch request and process. 24 | protected override void OnLaunched(LaunchActivatedEventArgs args) 25 | { 26 | MainWindow = new(); 27 | MainWindow.Initialize(new() { Size = WindowHelper.EstimatedWindowSize() }); 28 | MainWindow.Activate(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /TagsTree/AppConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using WinUI3Utilities.Attributes; 3 | 4 | namespace TagsTree; 5 | 6 | [GenerateConstructor] 7 | public partial record AppConfig 8 | { 9 | public int Theme { get; set; } 10 | 11 | public string LibraryPath { get; set; } = ""; 12 | 13 | public bool PathTagsEnabled { get; set; } = true; 14 | 15 | [AttributeIgnore(typeof(SettingsViewModelAttribute<>))] 16 | public bool FilesObserverEnabled { get; set; } 17 | 18 | public AppConfig() 19 | { 20 | 21 | } 22 | } 23 | 24 | [JsonSerializable(typeof(string))] 25 | public partial class ConfigSerializeContext : JsonSerializerContext; 26 | -------------------------------------------------------------------------------- /TagsTree/AppContext.cs: -------------------------------------------------------------------------------- 1 | #define FIRST_TIME 2 | using System.Collections.ObjectModel; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using Microsoft.UI.Xaml; 6 | using TagsTree.Algorithm; 7 | using TagsTree.Models; 8 | using TagsTree.Services; 9 | using TagsTree.Services.ExtensionMethods; 10 | using TagsTree.Views; 11 | using TagsTree.Views.ViewModels; 12 | using Windows.Storage; 13 | using WinUI3Utilities.Attributes; 14 | 15 | namespace TagsTree; 16 | 17 | [AppContext] 18 | public static partial class AppContext 19 | { 20 | public static AppConfig AppConfig { get; private set; } = null!; 21 | 22 | public static string AppLocalFolder { get; private set; } = null!; 23 | 24 | public static FilesObserver FilesObserver { get; private set; } = null!; 25 | 26 | public static ObservableCollection FilesChangedList => FilesObserverPage.Vm.FilesChangedList; 27 | 28 | public static SettingsViewModel SettingViewModel { get; } = new(); 29 | 30 | public static void Initialize() 31 | { 32 | AppLocalFolder = ApplicationData.Current.LocalFolder.Path; 33 | InitializeConfiguration(); 34 | AppConfig = 35 | #if FIRST_TIME 36 | LoadConfiguration() ?? 37 | #endif 38 | new AppConfig(); 39 | FilesObserver = new(); 40 | } 41 | 42 | public static void SetDefaultAppConfig() => AppConfig = new(); 43 | 44 | public static async Task FilesObserverChanged() => await FilesObserver.FilesObserverChanged(AppConfig.LibraryPath); 45 | 46 | public static async Task ExceptionHandler(string exception) 47 | { 48 | //switch (await ShowContentDialog.Warning( 49 | // new TextBlock 50 | // { 51 | // TextWrapping = TextWrapping.Wrap, 52 | // Inlines = 53 | // { 54 | // AppLocalFolder.GetHyperlink("软件设置"), 55 | // new Run { Text = $"里,{exception}和{RelationsName}存储数据数不同" } 56 | // } 57 | // })) 58 | if (await ShowContentDialog.Warning( 59 | $"路径「{AppLocalFolder}」下,{exception}和{RelationsName}存储数据数不同", 60 | $"删除关系文件{RelationsName}并重新生成", "关闭软件并打开目录")) 61 | { 62 | File.Delete(RelationsPath); 63 | Relations.Reload(); 64 | } 65 | else 66 | { 67 | AppLocalFolder.Open(); 68 | Application.Current.Exit(); 69 | } 70 | } 71 | 72 | public static string FilesChangedPath => AppLocalFolder + "\\" + FilesChangedName; 73 | 74 | public static string TagsPath => AppLocalFolder + "\\" + TagsName; 75 | 76 | private static string FilesPath => AppLocalFolder + "\\" + FilesName; 77 | 78 | private static string RelationsPath => AppLocalFolder + "\\" + RelationsName; 79 | 80 | public const string FilesChangedName = "FileChanged.json"; 81 | 82 | public const string TagsName = "TagsTree.json"; 83 | 84 | private const string FilesName = "Files.json"; 85 | 86 | private const string RelationsName = "Relations.csv"; 87 | 88 | /// 89 | /// 保存标签 90 | /// 91 | public static void SaveTags() => Tags.Serialize(TagsPath); 92 | 93 | /// 94 | /// 保存文件 95 | /// 96 | public static void SaveFiles() => IdFile.Serialize(FilesPath); 97 | 98 | /// 99 | /// 保存关系 100 | /// 101 | public static void SaveRelations() => Relations.Serialize(RelationsPath); 102 | 103 | /// 104 | /// 所有标签 105 | /// 106 | public static TagsTreeDictionary Tags { get; set; } = new(); 107 | 108 | /// 109 | /// 所有文件 110 | /// 111 | public static BidirectionalDictionary IdFile { get; } = new(); 112 | 113 | /// 114 | /// 所有关系 115 | /// 116 | public static RelationsDataTable Relations { get; } = new(); 117 | 118 | /// 119 | /// 重新加载新的配置文件 120 | /// 121 | public static string? LoadConfig() 122 | { 123 | // 文件监视 124 | FilesObserverPage.Vm = new(FileChanged.Deserialize(FilesChangedPath)); 125 | 126 | // 标签 127 | Tags.DeserializeTree(TagsPath); 128 | Tags.LoadDictionary(); 129 | 130 | // 文件 131 | IdFile.Deserialize(FilesPath); 132 | 133 | // 关系 134 | Relations.Deserialize(RelationsPath); 135 | 136 | // 如果本来是空,则按照标签和文件生成关系 137 | if (Relations.TagsCount is 0 && Relations.FilesCount is 0) 138 | Relations.Reload(); 139 | else 140 | { 141 | // 检查 142 | // 第一个是空标签减去 143 | if (Tags.TagsDictionary.Count != Relations.TagsCount + 1) 144 | return TagsName; 145 | if (IdFile.Count != Relations.FilesCount) 146 | return FilesName; 147 | } 148 | 149 | return null; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /TagsTree/Assets/LockScreenLogo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Poker-sang/TagsTree/c0bfee08f228c45b0163497164e0fb9e630a1d31/TagsTree/Assets/LockScreenLogo.scale-200.png -------------------------------------------------------------------------------- /TagsTree/Assets/SplashScreen.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Poker-sang/TagsTree/c0bfee08f228c45b0163497164e0fb9e630a1d31/TagsTree/Assets/SplashScreen.scale-200.png -------------------------------------------------------------------------------- /TagsTree/Assets/Square150x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Poker-sang/TagsTree/c0bfee08f228c45b0163497164e0fb9e630a1d31/TagsTree/Assets/Square150x150Logo.scale-200.png -------------------------------------------------------------------------------- /TagsTree/Assets/Square44x44Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Poker-sang/TagsTree/c0bfee08f228c45b0163497164e0fb9e630a1d31/TagsTree/Assets/Square44x44Logo.scale-200.png -------------------------------------------------------------------------------- /TagsTree/Assets/Square44x44Logo.targetsize-24_altform-unplated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Poker-sang/TagsTree/c0bfee08f228c45b0163497164e0fb9e630a1d31/TagsTree/Assets/Square44x44Logo.targetsize-24_altform-unplated.png -------------------------------------------------------------------------------- /TagsTree/Assets/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Poker-sang/TagsTree/c0bfee08f228c45b0163497164e0fb9e630a1d31/TagsTree/Assets/StoreLogo.png -------------------------------------------------------------------------------- /TagsTree/Assets/Wide310x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Poker-sang/TagsTree/c0bfee08f228c45b0163497164e0fb9e630a1d31/TagsTree/Assets/Wide310x150Logo.scale-200.png -------------------------------------------------------------------------------- /TagsTree/Controls/InputContentDialog.xaml: -------------------------------------------------------------------------------- 1 | 10 | 16 | 17 | 21 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /TagsTree/Controls/InputContentDialog.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using System.Threading.Tasks; 4 | using Microsoft.UI.Xaml.Controls; 5 | using TagsTree.Services.ExtensionMethods; 6 | using TagsTree.Views.ViewModels.Controls; 7 | using WinUI3Utilities; 8 | using WinUI3Utilities.Attributes; 9 | 10 | namespace TagsTree.Controls; 11 | 12 | [DependencyProperty("Text", "\"\"")] 13 | public partial class InputContentDialog : UserControl 14 | { 15 | private readonly InputContentDialogViewModels _vm = new(); 16 | 17 | public InputContentDialog() => InitializeComponent(); 18 | 19 | public void Load(string title, Func judge, FileSystemHelper.InvalidMode mode, string text = "") 20 | { 21 | Content.To().Title = title; 22 | _judge = judge; 23 | switch (mode) 24 | { 25 | case FileSystemHelper.InvalidMode.Name: 26 | _vm.WarningText = @"不能包含\/:*?""<>|和除空格外的空白字符"; 27 | _invalidRegex = FileSystemHelper.GetInvalidNameChars; 28 | break; 29 | case FileSystemHelper.InvalidMode.Path: 30 | _vm.WarningText = @"不能包含/:*?""<>|和除空格外的空白字符"; 31 | _invalidRegex = FileSystemHelper.GetInvalidPathChars; 32 | break; 33 | default: 34 | ThrowHelper.ArgumentOutOfRange(mode); 35 | return; 36 | } 37 | 38 | Text = text; 39 | } 40 | 41 | /// 42 | /// 是否取消这次输入 43 | /// 44 | private bool _canceled; 45 | 46 | private Func _judge = null!; 47 | 48 | private string _invalidRegex = ""; 49 | 50 | #region 事件处理 51 | 52 | public async Task ShowAsync() 53 | { 54 | _canceled = true; 55 | _ = await Content.To().ShowAsync(); 56 | return _canceled; 57 | } 58 | 59 | private void OnPrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs e) 60 | { 61 | e.Cancel = true; 62 | if (!new Regex($@"^[^{_invalidRegex}]+$").IsMatch(Text)) 63 | { 64 | _vm.Message = _vm.WarningText; 65 | _vm.IsOpen = true; 66 | return; 67 | } 68 | 69 | var result = _judge(this); 70 | if (result is not null) 71 | { 72 | _vm.Message = result; 73 | _vm.IsOpen = true; 74 | return; 75 | } 76 | 77 | e.Cancel = false; 78 | _canceled = false; 79 | } 80 | 81 | private void OnCloseButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs e) => _canceled = true; 82 | 83 | #endregion 84 | } 85 | -------------------------------------------------------------------------------- /TagsTree/Controls/Items/DataGridIconColumn.xaml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /TagsTree/Controls/Items/DataGridIconColumn.xaml.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.WinUI.UI.Controls; 2 | 3 | namespace TagsTree.Controls.Items; 4 | 5 | public sealed partial class DataGridIconColumn : DataGridTemplateColumn 6 | { 7 | public DataGridIconColumn() => InitializeComponent(); 8 | } 9 | -------------------------------------------------------------------------------- /TagsTree/Controls/Items/DataGridNameColumn.xaml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /TagsTree/Controls/Items/DataGridNameColumn.xaml.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.WinUI.UI.Controls; 2 | 3 | namespace TagsTree.Controls.Items; 4 | 5 | public sealed partial class DataGridNameColumn : DataGridTemplateColumn 6 | { 7 | public DataGridNameColumn() => InitializeComponent(); 8 | } -------------------------------------------------------------------------------- /TagsTree/Controls/Items/DataGridPartialPathColumn.xaml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /TagsTree/Controls/Items/DataGridPartialPathColumn.xaml.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.WinUI.UI.Controls; 2 | 3 | namespace TagsTree.Controls.Items; 4 | 5 | public sealed partial class DataGridPartialPathColumn : DataGridTemplateColumn 6 | { 7 | public DataGridPartialPathColumn() => InitializeComponent(); 8 | } -------------------------------------------------------------------------------- /TagsTree/Controls/Items/DataGridTagsColumn.xaml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /TagsTree/Controls/Items/DataGridTagsColumn.xaml.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.WinUI.UI.Controls; 2 | 3 | namespace TagsTree.Controls.Items; 4 | 5 | public sealed partial class DataGridTagsColumn : DataGridTemplateColumn 6 | { 7 | public DataGridTagsColumn() => InitializeComponent(); 8 | } -------------------------------------------------------------------------------- /TagsTree/Controls/TagCompleteBox.xaml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /TagsTree/Controls/TagCompleteBox.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using CommunityToolkit.Mvvm.ComponentModel; 3 | using Microsoft.UI.Xaml; 4 | using Microsoft.UI.Xaml.Controls; 5 | using TagsTree.Models; 6 | using TagsTree.Services.ExtensionMethods; 7 | using TagsTree.Views.ViewModels; 8 | using WinUI3Utilities; 9 | using WinUI3Utilities.Attributes; 10 | using TagCompleteBoxViewModel = TagsTree.Views.ViewModels.Controls.TagCompleteBoxViewModel; 11 | 12 | namespace TagsTree.Controls; 13 | 14 | [INotifyPropertyChanged] 15 | [DependencyProperty("TagsSource", DependencyPropertyDefaultValue.Default, IsNullable = true)] 16 | public partial class TagCompleteBox : UserControl 17 | { 18 | private readonly TagCompleteBoxViewModel _vm; 19 | 20 | public TagCompleteBox() 21 | { 22 | _vm = new(this); 23 | InitializeComponent(); 24 | _vm.State = true; 25 | } 26 | 27 | #region 依赖属性 28 | 29 | public static readonly DependencyProperty PathProperty = DependencyProperty.Register(nameof(Path), typeof(string), typeof(TagCompleteBox), new PropertyMetadata("")); 30 | 31 | public string Path 32 | { 33 | get => GetValue(PathProperty).To(); 34 | set 35 | { 36 | SetValue(PathProperty, value); 37 | _vm.Path = value; 38 | _vm.State = false; 39 | } 40 | } 41 | 42 | #endregion 43 | 44 | #region 事件处理 45 | 46 | private void PathComplement(object sender, RoutedEventArgs e) => _vm.Path = _vm.Path.GetTagViewModel(TagsSource)?.FullName ?? _vm.Path; 47 | 48 | private void PathChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs e) => _vm.Path = Regex.Replace(_vm.Path, $@"[{FileSystemHelper.GetInvalidPathChars}]+", ""); 49 | 50 | private void SuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs e) => _vm.Path = e.SelectedItem.To().FullName; 51 | 52 | private void TappedEnter(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs e) => _vm.State = false; 53 | 54 | private void OnItemClicked(BreadcrumbBar sender, BreadcrumbBarItemClickedEventArgs e) => _vm.State = true; 55 | 56 | private void AutoSuggestBoxIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) 57 | { 58 | var asb = sender.To(); 59 | if (asb.IsEnabled) 60 | _ = asb.Focus(FocusState.Programmatic); 61 | } 62 | 63 | #endregion 64 | } 65 | -------------------------------------------------------------------------------- /TagsTree/Controls/TagSearchBox.xaml: -------------------------------------------------------------------------------- 1 | 11 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /TagsTree/Controls/TagSearchBox.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Text.RegularExpressions; 3 | using CommunityToolkit.Mvvm.ComponentModel; 4 | using Microsoft.UI.Xaml.Controls; 5 | using TagsTree.Delegates; 6 | using TagsTree.Models; 7 | using TagsTree.Services.ExtensionMethods; 8 | using Windows.Foundation; 9 | using WinUI3Utilities.Attributes; 10 | 11 | namespace TagsTree.Controls; 12 | 13 | [INotifyPropertyChanged] 14 | [DependencyProperty("Text", "\"\"")] 15 | [DependencyProperty("TagsSource", DependencyPropertyDefaultValue.Default, IsNullable = true)] 16 | public partial class TagSearchBox : UserControl 17 | { 18 | public TagSearchBox() => InitializeComponent(); 19 | 20 | public event ResultChangedEventHandler ResultChanged = null!; 21 | 22 | #region 事件处理 23 | 24 | private void SuggestionChosen(AutoSuggestBox autoSuggestBox, AutoSuggestBoxSuggestionChosenEventArgs e) 25 | { 26 | // TODO: SuggestionChosen 27 | // var index = Text.LastIndexOf(' ') + 1; 28 | // if (index is 0) 29 | // Text = e.SelectedItem.ToString(); 30 | // else Text = Text[..index] + e.SelectedItem; 31 | } 32 | 33 | [GeneratedRegex(" +")] 34 | private static partial Regex MyRegex(); 35 | 36 | private void TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs e) 37 | { 38 | Text = Regex.Replace(Text, $@"[{FileSystemHelper.GetInvalidPathChars}]+", ""); 39 | Text = MyRegex().Replace(Text, " ").TrimStart(); 40 | sender.ItemsSource = Text.TagSuggest(' ', TagsSource); 41 | } 42 | 43 | private void QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs e) => SubmitQuery(e.QueryText); 44 | 45 | public void SubmitQuery(string queryText) 46 | { 47 | if (queryText is "") 48 | { 49 | ResultChanged.Invoke(AppContext.IdFile.Values); 50 | return; 51 | } 52 | 53 | var temp = queryText.Split(' ').Select(item => 54 | AppContext.Tags.TagsDictionary.GetValueOrDefault(item) ?? new PathTagModel(item)).ToArray(); 55 | ResultChanged.Invoke(RelationsDataTable.GetFileModels(temp)); 56 | } 57 | 58 | #endregion 59 | 60 | #region 操作 61 | 62 | public void ResetQuerySubmitted(TypedEventHandler eventHandler) 63 | { 64 | AutoSuggestBox.QuerySubmitted -= QuerySubmitted; 65 | AutoSuggestBox.QuerySubmitted += eventHandler; 66 | } 67 | 68 | #endregion 69 | } 70 | -------------------------------------------------------------------------------- /TagsTree/Delegates/ResultChanged.cs: -------------------------------------------------------------------------------- 1 | namespace TagsTree.Delegates; 2 | 3 | public delegate void ResultChangedEventHandler(System.Collections.Generic.IEnumerable newResult); -------------------------------------------------------------------------------- /TagsTree/Interfaces/IFileModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using TagsTree.Models; 3 | using TagsTree.Views.ViewModels; 4 | 5 | namespace TagsTree.Interfaces; 6 | 7 | public interface IFileModel : IFullName 8 | { 9 | public string PartialPath { get; } 10 | 11 | public bool IsFolder { get; } 12 | 13 | public IEnumerable GetAncestorTags(TagViewModel parentTag); 14 | 15 | public string Extension { get; } 16 | 17 | public bool Exists { get; } 18 | 19 | public string Tags { get; } 20 | 21 | public IEnumerable PathTags { get; } 22 | 23 | public bool PathContains(PathTagModel pathTag); 24 | } 25 | -------------------------------------------------------------------------------- /TagsTree/Interfaces/IFullName.cs: -------------------------------------------------------------------------------- 1 | namespace TagsTree.Interfaces; 2 | 3 | public interface IFullName 4 | { 5 | public string Name { get; } 6 | 7 | public string Path { get; } 8 | 9 | public string FullName { get; } 10 | } 11 | -------------------------------------------------------------------------------- /TagsTree/Interfaces/ITypeGetter.cs: -------------------------------------------------------------------------------- 1 | namespace TagsTree.Interfaces; 2 | 3 | public interface ITypeGetter 4 | { 5 | public static abstract System.Type TypeGetter { get; } 6 | } 7 | -------------------------------------------------------------------------------- /TagsTree/MainWindow.xaml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 19 | 23 | 27 | 32 | 33 | 34 | 44 | 45 | 50 | 55 | 60 | 65 | 66 | 67 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /TagsTree/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.UI.Xaml; 5 | using Microsoft.UI.Xaml.Controls; 6 | using TagsTree.Services; 7 | using TagsTree.Views; 8 | using WinUI3Utilities; 9 | 10 | namespace TagsTree; 11 | 12 | public sealed partial class MainWindow : Window 13 | { 14 | public MainWindow() => InitializeComponent(); 15 | 16 | private async void Loaded(object sender, RoutedEventArgs e) 17 | { 18 | NavigationView.SettingsItem.To().Tag = SettingsPage.TypeGetter; 19 | 20 | if (AppContext.SettingViewModel.ConfigSet) 21 | await ConfigIsSet(); 22 | else 23 | DisplaySettings(); 24 | } 25 | 26 | // private void OnSizeChanged(object sender, SizeChangedEventArgs e) 27 | // { 28 | // DragZoneHelper.SetDragZones(new() 29 | // { 30 | //#if DEBUG 31 | // ExcludeDebugToolbarArea = true, 32 | //#endif 33 | // DragZoneLeftIndent = (int)NavigationView.CompactPaneLength 34 | // }); 35 | // } 36 | 37 | public async Task ConfigIsSet() 38 | { 39 | if (AppContext.LoadConfig() is { } exception) 40 | { 41 | DisplaySettings(); 42 | await AppContext.ExceptionHandler(exception); 43 | } 44 | else 45 | { 46 | GotoPage(); 47 | NavigationView.SelectedItem = NavigationView.MenuItems[0]; 48 | } 49 | 50 | IconsHelper.LoadFilesIcons(); 51 | 52 | foreach (var menuItem in NavigationView.MenuItems.Cast()) 53 | menuItem.IsEnabled = true; 54 | 55 | await AppContext.FilesObserverChanged(); 56 | } 57 | 58 | public void GotoPage(object? parameter = null) where T : Page => Frame.Navigate(typeof(T), parameter); 59 | 60 | private void DisplaySettings() 61 | { 62 | GotoPage(); 63 | NavigationView.SelectedItem = NavigationView.SettingsItem; 64 | } 65 | 66 | private void BackRequested(NavigationView sender, NavigationViewBackRequestedEventArgs e) 67 | { 68 | Frame.GoBack(); 69 | NavigationView.SelectedItem = Frame.Content switch 70 | { 71 | IndexPage or 72 | TagSearchFilesPage or 73 | FilePropertiesPage or 74 | FileEditTagsPage => NavigationView.MenuItems[0], 75 | TagsManagerPage => NavigationView.MenuItems[1], 76 | FileImporterPage => NavigationView.MenuItems[2], 77 | SelectTagToEditPage or 78 | TagEditFilesPage => NavigationView.MenuItems[3], 79 | FilesObserverPage => NavigationView.FooterMenuItems[0], 80 | SettingsPage => NavigationView.SettingsItem, 81 | _ => NavigationView.SelectedItem 82 | }; 83 | NavigationView.IsBackEnabled = Frame.CanGoBack; 84 | } 85 | 86 | private void ItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs e) 87 | { 88 | if (e.InvokedItemContainer.Tag is Type item && item != Frame.Content.GetType()) 89 | _ = Frame.Navigate(item); 90 | } 91 | 92 | private void OnPaneChanging(NavigationView sender, object e) 93 | { 94 | sender.UpdateAppTitleMargin(TitleTextBlock); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /TagsTree/Models/FileBase.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text.Json.Serialization; 3 | using TagsTree.Interfaces; 4 | using TagsTree.Services.ExtensionMethods; 5 | 6 | namespace TagsTree.Models; 7 | 8 | public abstract class FileBase(string name, string path, int id) : IFullName 9 | { 10 | public int Id { get; } = id; 11 | 12 | public string Name { get; protected set; } = name; 13 | 14 | public string Path { get; protected set; } = path; 15 | 16 | /// 17 | /// Path必然包含文件路径 18 | /// 19 | [JsonIgnore] public string FullName => @$"{Path}\{Name}"; 20 | 21 | [JsonIgnore] public string PartialPath => Path.GetPartialPath(); 22 | 23 | [JsonIgnore] public bool IsFolder => Directory.Exists(FullName); 24 | } 25 | -------------------------------------------------------------------------------- /TagsTree/Models/FileChanged.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.Text.Json.Serialization; 4 | using TagsTree.Services; 5 | using TagsTree.Services.ExtensionMethods; 6 | 7 | namespace TagsTree.Models; 8 | 9 | public class FileChanged : FileBase 10 | { 11 | public static int Num { get; set; } = 1; 12 | 13 | public ChangedType Type { get; } 14 | 15 | public string Remark { get; } 16 | 17 | public string DisplayType => Type switch 18 | { 19 | ChangedType.Create => nameof(ChangedType.Create), 20 | ChangedType.Move => nameof(ChangedType.Move), 21 | ChangedType.Rename => nameof(ChangedType.Rename), 22 | ChangedType.Delete => nameof(ChangedType.Delete), 23 | _ => "" 24 | }; 25 | 26 | [JsonIgnore] 27 | public string DisplayRemark => Type switch 28 | { 29 | ChangedType.Move => "旧路径:" + Remark.GetPartialPath(), 30 | ChangedType.Rename => "旧名称:" + Remark, 31 | _ => Remark 32 | }; 33 | 34 | [JsonIgnore] 35 | public string OldFullName => Type switch 36 | { 37 | ChangedType.Move => $"{Remark}\\{Name}", 38 | ChangedType.Rename => $"{Path}\\{Remark}", 39 | _ => FullName 40 | }; 41 | 42 | public static ObservableCollection Deserialize(string path) => Serialization.Deserialize>(path); 43 | 44 | public static void Serialize(string path, ObservableCollection collection) => Serialization.Serialize(path, collection); 45 | 46 | public enum ChangedType 47 | { 48 | Create = 0, 49 | Move = 1, 50 | Rename = 2, 51 | Delete = 3 52 | } 53 | 54 | /// 55 | /// 56 | /// Move留旧路径,Rename留旧名称 57 | public FileChanged(string fullName, ChangedType type, string remark = "") : base(fullName.GetName(), fullName.GetPath(), Num) 58 | { 59 | Num++; 60 | Type = type; 61 | Remark = remark; 62 | } 63 | 64 | /// 65 | /// 反序列化专用,不要调用该构造器 66 | /// 67 | [JsonConstructor] 68 | public FileChanged(int id, string name, string path, ChangedType type, string remark = "") : base(name, path, id) 69 | { 70 | Num = Math.Max(Num, id + 1); 71 | Type = type; 72 | Remark = remark; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /TagsTree/Models/FileChangedMerger.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using WinUI3Utilities; 3 | 4 | namespace TagsTree.Models; 5 | 6 | public class FileChangedMerger 7 | { 8 | private List Create { get; } = []; 9 | 10 | private List Move { get; } = []; 11 | 12 | private List Rename { get; } = []; 13 | 14 | private List Delete { get; } = []; 15 | 16 | private bool IsExisted { get; set; } = true; 17 | 18 | public string CurrentName => 19 | // Rename在第一个,其他随意 20 | Rename.Count is not 0 ? Rename[^1].Name : 21 | Move.Count is not 0 ? Move[^1].Name : 22 | Create.Count is not 0 ? Create[^1].Name : 23 | Delete[^1].Name; 24 | 25 | public string OriginalName => 26 | // Rename在第一个,其他随意 27 | Rename.Count is not 0 ? Rename[0].Remark : 28 | Move.Count is not 0 ? Move[0].Name : 29 | Create.Count is not 0 ? Create[0].Name : 30 | Delete[0].Name; 31 | 32 | public string CurrentPath => 33 | // Move在第一个,其他随意 34 | Move.Count is not 0 ? Move[^1].Path : 35 | Rename.Count is not 0 ? Rename[^1].Path : 36 | Create.Count is not 0 ? Create[^1].Path : 37 | Delete[^1].Path; 38 | 39 | public string OriginalPath => 40 | // Move在第一个,其他随意 41 | Move.Count is not 0 ? Move[0].Remark : 42 | Rename.Count is not 0 ? Rename[0].Path : 43 | Create.Count is not 0 ? Create[0].Path : 44 | Delete[0].Path; 45 | 46 | public string CurrentFullName => $"{CurrentPath}\\{CurrentName}"; 47 | 48 | public string OriginalFullName => $"{OriginalPath}\\{OriginalName}"; 49 | 50 | public FileChangedMerger(FileChanged fileChanged) 51 | { 52 | switch (fileChanged.Type) 53 | { 54 | case FileChanged.ChangedType.Create: 55 | Create.Add(fileChanged); 56 | break; 57 | case FileChanged.ChangedType.Move: 58 | Move.Add(fileChanged); 59 | break; 60 | case FileChanged.ChangedType.Rename: 61 | Rename.Add(fileChanged); 62 | break; 63 | case FileChanged.ChangedType.Delete: 64 | Delete.Add(fileChanged); 65 | IsExisted = false; 66 | break; 67 | default: 68 | ThrowHelper.ArgumentOutOfRange(fileChanged); 69 | break; 70 | } 71 | } 72 | 73 | public bool CanMerge(FileChanged fileChanged) 74 | { 75 | if (fileChanged.OldFullName != CurrentFullName) 76 | return false; 77 | // 同或逻辑 78 | if (fileChanged is { Type: FileChanged.ChangedType.Create } == IsExisted) 79 | return ThrowHelper.Exception("逻辑出错,可能是遗漏监听文件所致"); 80 | switch (fileChanged.Type) 81 | { 82 | case FileChanged.ChangedType.Create: 83 | Create.Add(fileChanged); 84 | IsExisted = true; 85 | return true; 86 | case FileChanged.ChangedType.Move: 87 | Move.Add(fileChanged); 88 | return true; 89 | case FileChanged.ChangedType.Rename: 90 | Rename.Add(fileChanged); 91 | return true; 92 | case FileChanged.ChangedType.Delete: 93 | Delete.Add(fileChanged); 94 | IsExisted = false; 95 | return true; 96 | default: 97 | return ThrowHelper.ArgumentOutOfRange(fileChanged); 98 | } 99 | } 100 | 101 | public MergeResult GetMergeResult() => IsExisted 102 | ? Create.Count == Delete.Count 103 | ? OriginalFullName == CurrentFullName 104 | ? MergeResult.Nothing 105 | : OriginalName == CurrentName && OriginalPath != CurrentPath 106 | ? MergeResult.Move 107 | : OriginalName != CurrentName && OriginalPath == CurrentPath 108 | ? MergeResult.Rename 109 | : MergeResult.MoveRename 110 | : MergeResult.Create 111 | : Create.Count == Delete.Count 112 | ? MergeResult.Nothing 113 | : MergeResult.Delete; 114 | 115 | public enum MergeResult 116 | { 117 | /// 118 | /// 创建后删除或没有任何改变,没有留下记录 119 | /// 120 | Nothing, 121 | /// 122 | /// 只有路径改变 123 | /// 124 | Move, 125 | /// 126 | /// 只有名称改变 127 | /// 128 | Rename, 129 | /// 130 | /// 路径和名称改变 131 | /// 132 | MoveRename, 133 | /// 134 | /// 创建文件, 135 | /// 136 | Create, 137 | /// 138 | /// 删除文件, 139 | /// 140 | Delete 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /TagsTree/Models/FileModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text.Json.Serialization; 7 | using TagsTree.Interfaces; 8 | using TagsTree.Services.ExtensionMethods; 9 | using TagsTree.Views.ViewModels; 10 | 11 | namespace TagsTree.Models; 12 | 13 | public class FileModel : FileBase, IFileModel 14 | { 15 | private static int Num { get; set; } 16 | 17 | /// 18 | /// 用的复制构造 19 | /// 20 | /// 21 | protected FileModel(FileModel fileModel) : this(fileModel.Id, fileModel.Name, fileModel.Path) 22 | { 23 | } 24 | 25 | /// 26 | /// 用的虚拟构造 27 | /// 28 | /// 29 | protected FileModel(string fullName) : this(-1, fullName.GetName(), fullName.GetPath()) 30 | { 31 | } 32 | 33 | /// 34 | /// 反序列化专用,不要从外部调用该构造器 35 | /// 36 | [JsonConstructor] 37 | public FileModel(int id, string name, string path) : base(name, path, id) => Num = Math.Max(Num, id + 1); 38 | 39 | /// 40 | /// 从构造的,并生成 41 | /// 42 | public static FileModel FromFullName(string fullName) 43 | { 44 | var ret = new FileModel(Num, fullName.GetName(), fullName.GetPath()); 45 | ++Num; 46 | return ret; 47 | } 48 | 49 | public void Reload(string fullName) 50 | { 51 | FileSystemInfo info = IsFolder ? new DirectoryInfo(fullName) : new FileInfo(fullName); 52 | Name = info.Name; 53 | Path = fullName.GetPath(); 54 | } 55 | 56 | public static bool IsValidPath(string path) => path.Contains(AppContext.AppConfig.LibraryPath); 57 | 58 | /// 59 | /// 表示拥有的标签是的子标签 60 | /// 61 | /// 62 | /// 63 | public bool? HasTag(TagViewModel tag) 64 | { 65 | foreach (var tagPossessed in Tags.GetTagViewModels()) 66 | if (tag == tagPossessed) 67 | return true; 68 | else if (tag.HasChildTag(tagPossessed)) 69 | return null; 70 | return false; 71 | } 72 | 73 | public IEnumerable GetAncestorTags(TagViewModel parentTag) => Tags.GetTagViewModels().Where(parentTag.HasChildTag); 74 | 75 | [JsonIgnore] public string Extension => IsFolder ? "文件夹" : Name.Split('.', StringSplitOptions.RemoveEmptyEntries)[^1].ToUpper(CultureInfo.CurrentCulture); 76 | 77 | [JsonIgnore] public bool Exists => Directory.Exists(FullName) || File.Exists(FullName); 78 | 79 | [JsonIgnore] 80 | public string Tags 81 | { 82 | get 83 | { 84 | var tags = AppContext.Relations.GetTags(Id).Select(tag => tag.Name).Aggregate("", (current, tag) => current + " " + tag); 85 | return tags is "" ? "" : tags[1..]; 86 | } 87 | } 88 | 89 | [JsonIgnore] public IEnumerable PathTags => PartialPath is "..." ? Enumerable.Empty() : PartialPath[4..].Split('\\', StringSplitOptions.RemoveEmptyEntries); //PartialPath不会是空串 90 | 91 | public bool PathContains(PathTagModel pathTag) => PartialPath is not "..." && (PartialPath[3..] + "\\").Contains($"\\{pathTag.Name}\\"); 92 | } 93 | -------------------------------------------------------------------------------- /TagsTree/Models/RelationsDataTable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | using TagsTree.Algorithm; 7 | using TagsTree.Views.ViewModels; 8 | 9 | namespace TagsTree.Models; 10 | 11 | /// 12 | /// Column标签,Row是文件,键分别是文件和标签的Id 13 | /// 14 | /// 15 | /// 记录节省文件空间 16 | /// 17 | public partial class RelationsDataTable : TableDictionary 18 | { 19 | public bool this[TagViewModel tag, FileModel fileModel] 20 | { 21 | get => base[tag.Id, fileModel.Id]; 22 | set => base[tag.Id, fileModel.Id] = value; 23 | } 24 | 25 | private Dictionary Tags => Columns; 26 | 27 | private Dictionary Files => Rows; 28 | 29 | public int TagsCount => Tags.Count; 30 | 31 | public int FilesCount => Files.Count; 32 | 33 | public IEnumerable GetTags(int fileId) => Tags.Where(pair => base[pair.Key, fileId]).Select(pair => AppContext.Tags.TagsDictionary[pair.Key]); 34 | 35 | [GeneratedRegex("(.)", RegexOptions.IgnoreCase)] 36 | private static partial Regex FuzzyRegex(); 37 | 38 | public static ObservableCollection FuzzySearchName(string input, IEnumerable range) 39 | { // 大小写不敏感 40 | // 完整包含搜索内容 41 | var precise = new List(); 42 | // 有序并全部包含所有字符 43 | var fuzzy = new List(); 44 | // 包含任意一个字符,并按包含数排序 45 | var part = new List<(int Count, FileViewModel FileViewModel)>(); 46 | var fuzzyRegex = new Regex(FuzzyRegex().Replace(input, ".+$1")); 47 | var partRegex = new Regex($"[{input}]", RegexOptions.IgnoreCase); 48 | foreach (var fileViewModel in range) 49 | if (fileViewModel.Name.Contains(input, StringComparison.OrdinalIgnoreCase)) 50 | precise.Add(fileViewModel); 51 | else if (fuzzyRegex.IsMatch(fileViewModel.Name)) 52 | fuzzy.Add(fileViewModel); 53 | else if (partRegex.Matches(fileViewModel.Name) is { Count: not 0 } matches) 54 | part.Add((matches.Count, fileViewModel)); 55 | 56 | precise.AddRange(fuzzy); 57 | part.Sort((x, y) => x.Count.CompareTo(y.Count)); 58 | precise.AddRange(part.Select(item => item.FileViewModel)); 59 | return [..precise]; 60 | } 61 | 62 | public static IEnumerable GetFileModels(ICollection? tags = null) 63 | { 64 | IEnumerable filesRange = AppContext.IdFile.Values; 65 | if (tags is null or { Count: 0 }) 66 | return filesRange; 67 | var individualTags = tags.ToHashSet(); 68 | return individualTags.Aggregate(filesRange, (current, pathTagModel) => GetFileModels(pathTagModel, current)); 69 | } 70 | 71 | private static IEnumerable GetFileModels(PathTagModel pathTagModel, IEnumerable filesRange) 72 | { 73 | if (pathTagModel is TagViewModel tagViewModel) 74 | { 75 | return filesRange.Where(fileModel => fileModel.HasTag(tagViewModel) is not false); 76 | } 77 | 78 | // 唯一需要判断是否能使用路径作为标签的地方 79 | return AppContext.AppConfig.PathTagsEnabled ? filesRange.Where(fileModel => fileModel.PathContains(pathTagModel)) : []; 80 | } 81 | 82 | public void NewTag(TagViewModel tagViewModel) => AddColumn(tagViewModel.Id); 83 | 84 | public void NewFile(FileModel fileModel) => AddRow(fileModel.Id); 85 | 86 | public void DeleteTag(TagViewModel tagViewModel) => RemoveColumn(tagViewModel.Id); 87 | 88 | public void DeleteFile(FileModel fileModel) => RemoveRow(fileModel.Id); 89 | 90 | public void Reload() 91 | { 92 | Table.Clear(); 93 | Columns.Clear(); 94 | Rows.Clear(); 95 | foreach (var fileIds in AppContext.IdFile.Keys) 96 | Files[fileIds] = FilesCount; 97 | foreach (var tagIds in AppContext.Tags.TagsDictionary.Keys1.Skip(1)) 98 | { 99 | Tags[tagIds] = TagsCount; 100 | Table.Add([]); 101 | for (var i = 0; i < AppContext.IdFile.Keys.Count; ++i) 102 | this[tagIds].Add(false); 103 | } 104 | } 105 | 106 | public new void Deserialize(string path) 107 | { 108 | try 109 | { 110 | base.Deserialize(path); 111 | } 112 | catch (Exception) 113 | { 114 | Reload(); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /TagsTree/Models/TagModel.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using Microsoft.UI.Xaml.Controls; 3 | using TagsTree.Interfaces; 4 | 5 | namespace TagsTree.Models; 6 | 7 | public class PathTagModel(string name) 8 | { 9 | public string Name { get; protected set; } = name; 10 | 11 | /// 12 | /// 选择建议时会用到 13 | /// 14 | public override string ToString() => Name; 15 | } 16 | 17 | public class TagModel : PathTagModel, IFullName 18 | { 19 | [JsonIgnore] private static int Num { get; set; } = 1; 20 | 21 | public int Id { get; } 22 | 23 | [JsonIgnore] protected TagModel? BaseParent { get; set; } 24 | 25 | [JsonIgnore] public string Path => BaseParent is null ? "" : BaseParent.FullName; 26 | 27 | [JsonIgnore] public string FullName => (Path is "" ? "" : Path + '\\') + Name; 28 | 29 | protected TagModel(int id, string name) : base(name) 30 | { 31 | Num = System.Math.Max(Num, id + 1); 32 | Id = id; 33 | } 34 | 35 | protected TagModel(string name, TagModel? parent) : base(name) 36 | { 37 | Id = Num; 38 | ++Num; 39 | BaseParent = parent; 40 | } 41 | 42 | /// 43 | /// 判断提供的是否是自己的子标签(不包含自己) 44 | /// 45 | /// 46 | /// 47 | public bool HasChildTag(TagModel child) => $"\\\\{child.Path}\\".Contains($"\\{FullName}\\"); 48 | } 49 | -------------------------------------------------------------------------------- /TagsTree/Models/TagsTreeDictionary.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using TagsTree.Algorithm; 4 | using TagsTree.Services; 5 | using TagsTree.Views.ViewModels; 6 | 7 | namespace TagsTree.Models; 8 | 9 | public class TagsTreeDictionary 10 | { 11 | public DoubleKeysDictionary TagsDictionary { get; } = new(); 12 | 13 | /// 14 | /// 属于反序列化一部分 15 | /// 16 | public TagViewModel TagsTree { get; } = new(0, ""); 17 | 18 | public IEnumerable TagsDictionaryValues => TagsDictionary.Values.Skip(1); 19 | 20 | /// 21 | /// 或[""] 22 | /// 23 | public TagViewModel TagsDictionaryRoot => TagsDictionary[0]; 24 | 25 | public TagViewModel AddTag(TagViewModel path, string name) 26 | { 27 | var temp = new TagViewModel(name, path); 28 | path.SubTags.Add(temp); 29 | 30 | TagsDictionary[temp.Id, name] = temp; 31 | return temp; 32 | } 33 | 34 | public void MoveTag(TagViewModel tag, TagViewModel newPath) 35 | { 36 | _ = TagsDictionary[tag.Id].Parent!.SubTags.Remove(tag); 37 | newPath.SubTags.Add(tag); 38 | } 39 | 40 | public void RenameTag(TagViewModel tag, string newName) 41 | { 42 | TagsDictionary.ChangeKey2(tag.Name, newName); 43 | 44 | tag.Name = newName; 45 | } 46 | 47 | public void DeleteTag(TagViewModel tag) 48 | { 49 | _ = TagsDictionary[tag.Id].Parent!.SubTags.Remove(tag); 50 | 51 | _ = TagsDictionary.Remove(tag.Id); 52 | } 53 | 54 | /// 55 | /// 递归读取标签到 56 | /// 57 | /// 标签所在路径的标签 58 | private void RecursiveLoadTags(TagViewModel tag) 59 | { 60 | TagsDictionary[tag.Id, tag.Name] = tag; 61 | foreach (var subTag in tag.SubTags) 62 | RecursiveLoadTags(subTag); 63 | } 64 | 65 | public void DeserializeTree(string path) 66 | { 67 | // 为了触发TagsTree.SubTags.CollectionChanged 68 | foreach (var subTag in Serialization.Deserialize>(path)) 69 | TagsTree.SubTags.Add(subTag); 70 | } 71 | 72 | public void LoadDictionary() 73 | { 74 | TagsDictionary.Clear(); 75 | RecursiveLoadTags(TagsTree); 76 | } 77 | 78 | public void Serialize(string path) => Serialization.Serialize(path, TagsTree.SubTags); 79 | } 80 | -------------------------------------------------------------------------------- /TagsTree/Package.appxmanifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 8 | 9 | 13 | 14 | 15 | TagsTreeWinUI3 (Package) 16 | Poker 17 | Images\StoreLogo.png 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /TagsTree/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // 此代码由工具生成。 4 | // 运行时版本:4.0.30319.42000 5 | // 6 | // 对此文件的更改可能会导致不正确的行为,并且如果 7 | // 重新生成代码,这些更改将会丢失。 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace TagsTree.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// 一个强类型的资源类,用于查找本地化的字符串等。 17 | /// 18 | // 此类是由 StronglyTypedResourceBuilder 19 | // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 20 | // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen 21 | // (以 /str 作为命令选项),或重新生成 VS 项目。 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// 返回此类使用的缓存的 ResourceManager 实例。 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("TagsTree.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// 重写当前线程的 CurrentUICulture 属性,对 51 | /// 使用此强类型资源类的所有资源查找执行重写。 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// 查找 System.Byte[] 类型的本地化资源。 65 | /// 66 | internal static byte[] e8bb { 67 | get { 68 | object obj = ResourceManager.GetObject("e8bb", resourceCulture); 69 | return ((byte[])(obj)); 70 | } 71 | } 72 | 73 | /// 74 | /// 查找 System.Byte[] 类型的本地化资源。 75 | /// 76 | internal static byte[] Folder { 77 | get { 78 | object obj = ResourceManager.GetObject("Folder", resourceCulture); 79 | return ((byte[])(obj)); 80 | } 81 | } 82 | 83 | /// 84 | /// 查找 System.Byte[] 类型的本地化资源。 85 | /// 86 | internal static byte[] Link { 87 | get { 88 | object obj = ResourceManager.GetObject("Link", resourceCulture); 89 | return ((byte[])(obj)); 90 | } 91 | } 92 | 93 | /// 94 | /// 查找 System.Byte[] 类型的本地化资源。 95 | /// 96 | internal static byte[] Loading { 97 | get { 98 | object obj = ResourceManager.GetObject("Loading", resourceCulture); 99 | return ((byte[])(obj)); 100 | } 101 | } 102 | 103 | /// 104 | /// 查找 System.Byte[] 类型的本地化资源。 105 | /// 106 | internal static byte[] NotFound { 107 | get { 108 | object obj = ResourceManager.GetObject("NotFound", resourceCulture); 109 | return ((byte[])(obj)); 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /TagsTree/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | ..\Resources\e8bb.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 123 | 124 | 125 | ..\Resources\Folder.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 126 | 127 | 128 | ..\Resources\Link.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 129 | 130 | 131 | ..\Resources\Loading.gif;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 132 | 133 | 134 | ..\Resources\NotFound.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 135 | 136 | -------------------------------------------------------------------------------- /TagsTree/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "TagsTree": { 4 | "commandName": "MsixPackage" 5 | }, 6 | "TagsTree (Unpackaged)": { 7 | "commandName": "Project" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /TagsTree/Resources/ConstantStrings.cs: -------------------------------------------------------------------------------- 1 | namespace TagsTree.Resources; 2 | 3 | public class ConstantStrings 4 | { 5 | public const string AuthorUri = "https://github.com/Poker-sang/"; 6 | 7 | public const string RepositoryUri = AuthorUri + nameof(TagsTree); 8 | 9 | public const string LicenseUri = RepositoryUri + "/blob/main/LICENSE"; 10 | 11 | public const string MailUri = "mailto:poker_sang@outlook.com"; 12 | 13 | public const string QqUri = "http://wpa.qq.com/msgrd?v=3&uin=2639914082&site=qq&menu=yes"; 14 | 15 | public const string GitHubSvgPath = "M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"; 16 | 17 | public const string QqSvgPath = "M21.395 15.035a39.548 39.548 0 0 0-.803-2.264l-1.079-2.695c.001-.032.014-.562.014-.836C19.526 4.632 17.351 0 12 0S4.474 4.632 4.474 9.241c0 .274.013.804.014.836l-1.08 2.695a38.97 38.97 0 0 0-.802 2.264c-1.021 3.283-.69 4.643-.438 4.673.54.065 2.103-2.472 2.103-2.472 0 1.469.756 3.387 2.394 4.771-.612.188-1.363.479-1.845.835-.434.32-.379.646-.301.778.343.578 5.883.369 7.482.189 1.6.18 7.14.389 7.483-.189.078-.132.132-.458-.301-.778-.483-.356-1.233-.646-1.846-.836 1.637-1.384 2.393-3.302 2.393-4.771 0 0 1.563 2.537 2.103 2.472.251-.03.581-1.39-.438-4.673zM12.662 4.846c.039-1.052.659-1.878 1.385-1.846s1.281.912 1.242 1.964c-.039 1.051-.659 1.878-1.385 1.846s-1.282-.912-1.242-1.964zM9.954 3c.725-.033 1.345.794 1.384 1.846.04 1.052-.517 1.931-1.242 1.963-.726.033-1.346-.794-1.385-1.845C8.672 3.912 9.228 3.033 9.954 3zM7.421 8.294c.194-.43 2.147-.908 4.566-.908h.026c2.418 0 4.372.479 4.566.908a.14.14 0 0 1 .014.061c0 .031-.01.059-.026.083-.163.238-2.333 1.416-4.553 1.416h-.026c-2.221 0-4.39-1.178-4.553-1.416a.136.136 0 0 1-.014-.144zm10.422 8.622c-.22 3.676-2.403 5.987-5.774 6.021h-.137c-3.37-.033-5.554-2.345-5.773-6.021-.081-1.35.001-2.496.147-3.43.318.063.638.122.958.176v3.506s1.658.334 3.318.103v-3.225c.488.027.96.04 1.406.034h.025c1.678.021 3.714-.204 5.683-.594.146.934.227 2.08.147 3.43zM10.48 5.804c.313-.041.542-.409.508-.825-.033-.415-.314-.72-.629-.679-.313.04-.541.409-.508.824.034.417.315.72.629.68zM14.479 5.156c.078.037.221.042.289-.146.035-.095.025-.165-.009-.214-.023-.033-.133-.118-.371-.176-.904-.22-1.341.384-1.405.499-.04.072-.012.176.056.227.067.051.139.037.179-.006.58-.628 1.21-.208 1.261-.184z"; 18 | } 19 | -------------------------------------------------------------------------------- /TagsTree/Resources/Folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Poker-sang/TagsTree/c0bfee08f228c45b0163497164e0fb9e630a1d31/TagsTree/Resources/Folder.png -------------------------------------------------------------------------------- /TagsTree/Resources/Link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Poker-sang/TagsTree/c0bfee08f228c45b0163497164e0fb9e630a1d31/TagsTree/Resources/Link.png -------------------------------------------------------------------------------- /TagsTree/Resources/Loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Poker-sang/TagsTree/c0bfee08f228c45b0163497164e0fb9e630a1d31/TagsTree/Resources/Loading.gif -------------------------------------------------------------------------------- /TagsTree/Resources/NotFound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Poker-sang/TagsTree/c0bfee08f228c45b0163497164e0fb9e630a1d31/TagsTree/Resources/NotFound.png -------------------------------------------------------------------------------- /TagsTree/Resources/e8bb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Poker-sang/TagsTree/c0bfee08f228c45b0163497164e0fb9e630a1d31/TagsTree/Resources/e8bb.png -------------------------------------------------------------------------------- /TagsTree/Services/C.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Text; 2 | using Microsoft.UI.Xaml.Controls; 3 | using Microsoft.UI.Xaml.Media; 4 | using Microsoft.UI.Xaml; 5 | using System.Globalization; 6 | using System; 7 | using Windows.UI; 8 | using Windows.UI.Text; 9 | using WinUI3Utilities; 10 | 11 | namespace TagsTree.Services; 12 | 13 | /// 14 | /// Converters 15 | /// 16 | public static class C 17 | { 18 | public static bool Negation(bool value) => !value; 19 | 20 | public static bool IsNull(object? value) => value is null; 21 | 22 | public static bool IsNotNull(object? value) => value is not null; 23 | 24 | public static Visibility ToVisibility(bool value) => value ? Visibility.Visible : Visibility.Collapsed; 25 | 26 | public static Visibility ToVisibilityNegation(bool value) => value ? Visibility.Collapsed : Visibility.Visible; 27 | 28 | public static Visibility IsNullToVisibility(object? value) => value is null ? Visibility.Visible : Visibility.Collapsed; 29 | 30 | public static Visibility IsNotNullToVisibility(object? value) => value is null ? Visibility.Collapsed : Visibility.Visible; 31 | 32 | public static bool IsNotZero(int value) => value is not 0; 33 | 34 | public static bool IsNotZeroL(long value) => value is not 0; 35 | 36 | public static Visibility IsNotZeroToVisibility(int value) => value is not 0 ? Visibility.Visible : Visibility.Collapsed; 37 | 38 | public static Visibility IsNotZeroDToVisibility(double value) => value is not 0 ? Visibility.Visible : Visibility.Collapsed; 39 | 40 | public static unsafe Color ToAlphaColor(uint color) 41 | { 42 | var ptr = &color; 43 | var c = (byte*)ptr; 44 | return Color.FromArgb(c[3], c[2], c[1], c[0]); 45 | } 46 | 47 | public static SolidColorBrush ToSolidColorBrush(uint value) => new(ToAlphaColor(value)); 48 | 49 | public static unsafe uint ToAlphaUInt(Color color) 50 | { 51 | uint ret; 52 | var ptr = &ret; 53 | var c = (byte*)ptr; 54 | c[0] = color.B; 55 | c[1] = color.G; 56 | c[2] = color.R; 57 | c[3] = color.A; 58 | return ret; 59 | } 60 | 61 | public static double ToDouble(bool value) => value ? 1 : 0; 62 | 63 | public static double ToDoubleNegation(bool value) => value ? 0 : 1; 64 | 65 | public static string CultureDateTimeDateFormatter(DateTime value, CultureInfo culture) => 66 | value.ToString(culture.DateTimeFormat.ShortDatePattern); 67 | 68 | public static string CultureDateTimeOffsetDateFormatter(DateTimeOffset value, CultureInfo culture) => 69 | value.ToString(culture.DateTimeFormat.ShortDatePattern); 70 | 71 | public static string CultureDateTimeFormatter(DateTime value, CultureInfo culture) => 72 | value.ToString(culture.DateTimeFormat.FullDateTimePattern); 73 | 74 | public static string CultureDateTimeOffsetFormatter(DateTimeOffset value, CultureInfo culture) => 75 | value.ToString(culture.DateTimeFormat.FullDateTimePattern); 76 | 77 | public static FontFamily ToFontFamily(string value) => new(value); 78 | 79 | public static string ToPercentageString(object value, int precision) 80 | { 81 | var p = "F" + precision; 82 | return value switch 83 | { 84 | uint i => (i * 100).ToString(p), 85 | int i => (i * 100).ToString(p), 86 | short i => (i * 100).ToString(p), 87 | ushort i => (i * 100).ToString(p), 88 | long i => (i * 100).ToString(p), 89 | ulong i => (i * 100).ToString(p), 90 | float i => (i * 100).ToString(p), 91 | double i => (i * 100).ToString(p), 92 | decimal i => (i * 100).ToString(p), 93 | _ => "NaN" 94 | } + "%"; 95 | } 96 | 97 | public static string PlusOneToString(int value) => (value + 1).ToString(); 98 | 99 | public static CommandBarLabelPosition LabelIsNullToVisibility(string? value) => 100 | value is null ? CommandBarLabelPosition.Collapsed : CommandBarLabelPosition.Default; 101 | 102 | public static ItemsViewSelectionMode ToSelectionMode(bool value) => 103 | value ? ItemsViewSelectionMode.Multiple : ItemsViewSelectionMode.None; 104 | 105 | public static string IntEllipsis(int value) => 106 | value < 1000 ? value.ToString() : $"{value / 1000d:0.#}k"; 107 | 108 | public static double DoubleComplementary(double value) => 1 - value; 109 | 110 | public static FontWeight ToFontWeight(Enum value) => value.GetHashCode() switch 111 | { 112 | 0 => FontWeights.Thin, 113 | 1 => FontWeights.ExtraLight, 114 | 2 => FontWeights.Light, 115 | 3 => FontWeights.SemiLight, 116 | 4 => FontWeights.Normal, 117 | 5 => FontWeights.Medium, 118 | 6 => FontWeights.SemiBold, 119 | 7 => FontWeights.Bold, 120 | 8 => FontWeights.ExtraBold, 121 | 9 => FontWeights.Black, 122 | 10 => FontWeights.ExtraBlack, 123 | _ => ThrowHelper.ArgumentOutOfRange(value) 124 | }; 125 | } 126 | -------------------------------------------------------------------------------- /TagsTree/Services/ExtensionMethods/FileModelHelper.cs: -------------------------------------------------------------------------------- 1 | using TagsTree.Models; 2 | 3 | namespace TagsTree.Services.ExtensionMethods; 4 | 5 | public static class FileModelHelper 6 | { 7 | public static void AddNew(this FileModel fileModel) 8 | { 9 | AppContext.Relations.NewFile(fileModel); 10 | AppContext.IdFile[fileModel.Id] = fileModel; 11 | } 12 | 13 | public static void Remove(this FileModel fileModel) 14 | { 15 | _ = AppContext.IdFile.Remove(fileModel); 16 | AppContext.Relations.DeleteFile(fileModel); 17 | } 18 | 19 | public static void MoveOrRename(this FileModel fileModel, string newFullName) => fileModel.Reload(newFullName); 20 | } 21 | -------------------------------------------------------------------------------- /TagsTree/Services/ExtensionMethods/FileSystemHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Windows.System; 4 | using Microsoft.UI.Xaml.Documents; 5 | using Microsoft.VisualBasic.FileIO; 6 | using TagsTree.Interfaces; 7 | using TagsTree.Models; 8 | 9 | namespace TagsTree.Services.ExtensionMethods; 10 | 11 | public static class FileSystemHelper 12 | { 13 | public static bool Exists(this string fullName) => File.Exists(fullName) || Directory.Exists(fullName); 14 | 15 | public static Hyperlink GetHyperlink(this string uri, string alt) 16 | { 17 | var hyperlink = new Hyperlink 18 | { 19 | NavigateUri = new Uri(uri), 20 | Inlines = { new Run { Text = alt } } 21 | }; 22 | 23 | hyperlink.Click += async (sender, _) => await Launcher.LaunchUriAsync(new(sender.NavigateUri.AbsolutePath)); 24 | 25 | return hyperlink; 26 | } 27 | 28 | public static async void Open(this IFullName fullName) 29 | { 30 | if (!await Launcher.LaunchUriAsync(new(fullName.FullName))) 31 | await ShowContentDialog.Information(true, "找不到文件(夹),源文件可能已被更改"); 32 | } 33 | 34 | public static async void Open(this string fullName) 35 | { 36 | if (!await Launcher.LaunchUriAsync(new(fullName))) 37 | await ShowContentDialog.Information(true, $"打开路径「{fullName}」时出现错误"); 38 | } 39 | 40 | public static async void OpenDirectory(this IFullName fullName) 41 | { 42 | if (!await Launcher.LaunchUriAsync(new(fullName.FullName))) 43 | await ShowContentDialog.Information(true, "找不到目录,源文件目录可能已被更改"); 44 | } 45 | 46 | public static void Move(this FileBase fileBase, string newFullName) 47 | { 48 | if (fileBase.IsFolder) 49 | FileSystem.MoveDirectory(fileBase.FullName, newFullName.GetPath(), UIOption.OnlyErrorDialogs); 50 | else 51 | FileSystem.MoveFile(fileBase.FullName, newFullName.GetPath(), UIOption.OnlyErrorDialogs); 52 | } 53 | 54 | public static void Copy(this string sourceDirectory, string destinationDirectory) => FileSystem.CopyDirectory(sourceDirectory, destinationDirectory); 55 | 56 | public static void Rename(this FileBase fileBase, string newFullName) 57 | { 58 | if (fileBase.IsFolder) 59 | FileSystem.RenameDirectory(fileBase.FullName, newFullName.GetName()); 60 | else 61 | FileSystem.RenameFile(fileBase.FullName, newFullName.GetName()); 62 | } 63 | 64 | public static void Delete(this FileBase fileBase) 65 | { 66 | if (fileBase.IsFolder) 67 | FileSystem.DeleteDirectory(fileBase.FullName, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin); 68 | else 69 | FileSystem.DeleteFile(fileBase.FullName, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin); 70 | } 71 | 72 | public static string GetInvalidNameChars => @"\\/:*?""<>|" + new string(Path.GetInvalidPathChars()); 73 | 74 | public static string GetInvalidPathChars => @"\/:*?""<>|" + new string(Path.GetInvalidPathChars()); 75 | 76 | public enum InvalidMode 77 | { 78 | Name = 0, 79 | Path = 1 80 | } 81 | 82 | public static string CountSize(FileInfo file) => " " + file.Length switch 83 | { 84 | < 1 << 10 => file.Length.ToString("F2") + "Byte", 85 | < 1 << 20 => ((double)file.Length / (1 << 10)).ToString("F2") + "KB", 86 | < 1 << 30 => ((double)file.Length / (1 << 20)).ToString("F2") + "MB", 87 | _ => ((double)file.Length / (1 << 30)).ToString("F2") + "GB" 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /TagsTree/Services/ExtensionMethods/FileViewModelHelper.cs: -------------------------------------------------------------------------------- 1 | using TagsTree.Views.ViewModels; 2 | 3 | namespace TagsTree.Services.ExtensionMethods; 4 | 5 | public static class FileViewModelHelper 6 | { 7 | public static void AddNewAndSave(this FileViewModel fileViewModel) 8 | { 9 | fileViewModel.FileModel.AddNew(); 10 | AppContext.SaveFiles(); 11 | AppContext.SaveRelations(); 12 | } 13 | 14 | public static void RemoveAndSave(this FileViewModel fileViewModel) 15 | { 16 | fileViewModel.FileModel.Remove(); 17 | AppContext.SaveFiles(); 18 | AppContext.SaveRelations(); 19 | } 20 | 21 | public static void MoveOrRenameAndSave(this FileViewModel fileViewModel, string newFullName) 22 | { 23 | fileViewModel.FileModel.MoveOrRename(newFullName); 24 | AppContext.SaveFiles(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /TagsTree/Services/ExtensionMethods/FullNameHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TagsTree.Services.ExtensionMethods; 4 | 5 | public static class FullNameHelper 6 | { 7 | public static string GetPartialPath(this string fullName) => fullName.Replace(AppContext.AppConfig.LibraryPath, "..."); 8 | 9 | public static string GetName(this string fullName) 10 | { 11 | var fullNameSpan = fullName.AsSpan(); 12 | return fullNameSpan[(fullNameSpan.LastIndexOf('\\') + 1)..].ToString(); 13 | } 14 | 15 | public static string GetPath(this string fullName) 16 | { 17 | var fullNameSpan = fullName.AsSpan(); 18 | return fullNameSpan.LastIndexOf('\\') is -1 19 | ? "" 20 | : fullNameSpan[..fullNameSpan.LastIndexOf('\\')].ToString(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /TagsTree/Services/ExtensionMethods/TagViewModelHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using TagsTree.Models; 5 | using TagsTree.Views.ViewModels; 6 | 7 | namespace TagsTree.Services.ExtensionMethods; 8 | 9 | public static class TagViewModelHelper 10 | { 11 | /// 12 | /// 补全标签的路径(为空则为根标签) 13 | /// 14 | /// 需要找的单个标签 15 | /// 搜索范围 16 | /// 找到的标签,若返回即没找到路径 17 | public static TagViewModel? GetTagViewModel(this string name, TagsTreeDictionary? range = null) 18 | { 19 | range ??= AppContext.Tags; 20 | var temp = name.Split('\\', StringSplitOptions.RemoveEmptyEntries); 21 | return temp.Length is 0 ? range.TagsDictionaryRoot : range.TagsDictionary.GetValueOrDefault(temp[^1]); 22 | } 23 | 24 | /// 25 | /// 分隔并获取标签 26 | /// 27 | /// 28 | /// 29 | /// 30 | public static IEnumerable GetTagViewModels(this string name, TagsTreeDictionary? range = null) 31 | { 32 | range ??= AppContext.Tags; 33 | foreach (var tagName in name.Split(' ', StringSplitOptions.RemoveEmptyEntries)) 34 | if (range.TagsDictionary.GetValueOrDefault(tagName) is { } tagModel) 35 | yield return tagModel; 36 | } 37 | 38 | /// 39 | /// 输入标签时的建议列表 40 | /// 41 | /// 目前输入的最后一个标签 42 | /// 标签间分隔符 43 | /// 搜索范围 44 | /// 标签建议列表 45 | public static IEnumerable TagSuggest(this string name, char separator, TagsTreeDictionary? range = null) 46 | { 47 | range ??= AppContext.Tags; 48 | var tempName = name.Split(separator, StringSplitOptions.RemoveEmptyEntries).LastOrDefault(); 49 | if (tempName is "" or null) 50 | yield break; 51 | foreach (var tag in range.TagsDictionaryValues.Where(tag => tag.Name.Contains(tempName))) 52 | yield return tag; 53 | foreach (var tag in range.TagsDictionaryValues.Where(tag => tag.Path.Contains(tempName) && !tag.Name.Contains(tempName))) 54 | yield return tag; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /TagsTree/Services/FilesObserver.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using TagsTree.Models; 5 | using TagsTree.Services.ExtensionMethods; 6 | 7 | namespace TagsTree.Services; 8 | 9 | public partial class FilesObserver : FileSystemWatcher 10 | { 11 | public FilesObserver() 12 | { 13 | IncludeSubdirectories = true; 14 | NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName; 15 | base.Created += Created; 16 | base.Renamed += Renamed; 17 | base.Deleted += Deleted; 18 | } 19 | 20 | public async Task FilesObserverChanged(string path) 21 | { 22 | // 路径是否存在 23 | if (!Directory.Exists(AppContext.AppConfig.LibraryPath)) 24 | { 25 | await ShowContentDialog.Information(true, $"路径「{AppContext.AppConfig.LibraryPath}」不存在,无法开启文件监视,请在设置修改正确路径后保存"); 26 | AppContext.FilesObserver.EnableRaisingEvents = false; 27 | } 28 | // 不能是错误路径 29 | Path = path; 30 | AppContext.FilesObserver.EnableRaisingEvents = AppContext.AppConfig.FilesObserverEnabled; 31 | } 32 | 33 | private static new void Created(object sender, FileSystemEventArgs e) 34 | => _ = App.MainWindow.DispatcherQueue.TryEnqueue(() => 35 | { 36 | if (AppContext.FilesChangedList.LastOrDefault() is { Type: FileChanged.ChangedType.Delete } item && item.Name == e.FullPath.GetName() && item.FullName != e.FullPath) 37 | { 38 | _ = AppContext.FilesChangedList.Remove(item); 39 | AppContext.FilesChangedList.Add(new(e.FullPath, FileChanged.ChangedType.Move, item.Path)); 40 | } 41 | else 42 | AppContext.FilesChangedList.Add(new(e.FullPath, FileChanged.ChangedType.Create)); 43 | }); 44 | 45 | private static new void Renamed(object sender, RenamedEventArgs e) => _ = App.MainWindow.DispatcherQueue.TryEnqueue 46 | ( 47 | () => AppContext.FilesChangedList.Add(new(e.FullPath, FileChanged.ChangedType.Rename, e.OldFullPath.GetName())) 48 | ); 49 | 50 | private static new void Deleted(object sender, FileSystemEventArgs e) => _ = App.MainWindow.DispatcherQueue.TryEnqueue 51 | ( 52 | () => AppContext.FilesChangedList.Add(new(e.FullPath, FileChanged.ChangedType.Delete)) 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /TagsTree/Services/IconsHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Microsoft.UI.Xaml.Media; 8 | using Microsoft.UI.Xaml.Media.Imaging; 9 | using TagsTree.Views.ViewModels; 10 | using Windows.Storage; 11 | using Windows.Storage.FileProperties; 12 | using Windows.Storage.Streams; 13 | 14 | namespace TagsTree.Services; 15 | 16 | public static class IconsHelper 17 | { 18 | /// 19 | /// 将已有文件列表里所有文件图标预加载 20 | /// 21 | public static void LoadFilesIcons() 22 | { 23 | using var ms1 = new MemoryStream(Properties.Resources.NotFound); 24 | _notFoundIcon = GetBitmapImage(ms1.AsRandomAccessStream()); 25 | 26 | using var ms2 = new MemoryStream(Properties.Resources.Loading); 27 | _loadingIcon = GetBitmapImage(ms2.AsRandomAccessStream()); 28 | 29 | using var ms3 = new MemoryStream(Properties.Resources.Folder); 30 | _iconList["文件夹"] = GetBitmapImage(ms3.AsRandomAccessStream()); 31 | 32 | using var ms4 = new MemoryStream(Properties.Resources.Link); 33 | _iconList["LNK"] = _iconList["URL"] = GetBitmapImage(ms4.AsRandomAccessStream()); 34 | 35 | foreach (var file in Directory.GetFiles(ApplicationData.Current.TemporaryFolder.Path, "Temp.*")) 36 | File.Delete(file); 37 | 38 | foreach (var extension in AppContext.IdFile.Values.Select(file => file.Extension).Where(extension => !_iconList.ContainsKey(extension)).Distinct()) 39 | { 40 | _iconRequest.Enqueue(new(extension)); 41 | _iconList[extension] = null; 42 | } 43 | 44 | _ = StartAsync(); 45 | } 46 | 47 | /// 48 | /// 同步获取图标,如果没有加载图标则先返回加载图片,在加载后更新UI 49 | /// 50 | /// 文件 51 | /// 图标 52 | public static ImageSource GetIcon(this FileViewModel fileViewModel) 53 | { 54 | if (!fileViewModel.Exists) 55 | return _notFoundIcon; 56 | // 如果图标列表已经有该扩展名项 57 | if (_iconList.TryGetValue(fileViewModel.Extension, out var icon)) 58 | // 且加载完成 59 | if (icon is not null) 60 | return icon; 61 | else 62 | { 63 | _iconRequest.First(iconGetter => iconGetter.Extension == fileViewModel.Extension).CallBack += 64 | fileViewModel.IconChanged; 65 | return _loadingIcon; 66 | } 67 | 68 | _iconRequest.Enqueue(new(fileViewModel.Extension, fileViewModel.IconChanged)); 69 | _iconList[fileViewModel.Extension] = null; 70 | if (_iconRequest.Count <= 1) 71 | _ = StartAsync(); 72 | return _loadingIcon; 73 | } 74 | 75 | /// 76 | /// 异步处理请求加载图标的列表里所有图标的加载 77 | /// 78 | private static async Task StartAsync() 79 | { 80 | while (_iconRequest.TryPeek(out var item)) 81 | { 82 | _iconList[item.Extension] = await CreateIcon(item.Extension); 83 | item.CallBack(); 84 | _ = _iconRequest.TryDequeue(out _); 85 | } 86 | } 87 | 88 | /// 89 | /// 从流中获取图标 90 | /// 91 | /// 流 92 | /// 图标 93 | private static async Task GetBitmapImageAsync(IRandomAccessStream iRandomAccessStream) 94 | { 95 | var bitmapImage = new BitmapImage(); 96 | await bitmapImage.SetSourceAsync(iRandomAccessStream); 97 | return bitmapImage; 98 | } 99 | 100 | /// 101 | /// 从流中获取图标 102 | /// 103 | /// 流 104 | /// 图标 105 | private static BitmapImage GetBitmapImage(IRandomAccessStream iRandomAccessStream) 106 | { 107 | var bitmapImage = new BitmapImage(); 108 | bitmapImage.SetSource(iRandomAccessStream); 109 | return bitmapImage; 110 | } 111 | 112 | /// 113 | /// 根据后缀名获取图标 114 | /// 115 | /// 后缀名 116 | /// 图标 117 | private static async Task CreateIcon(string extension) 118 | { 119 | var tempFile = await ApplicationData.Current.TemporaryFolder.CreateFileAsync("Temp." + extension, CreationCollisionOption.FailIfExists); 120 | using var storageItemThumbnail = await tempFile.GetThumbnailAsync(ThumbnailMode.SingleItem, Size); 121 | await tempFile.DeleteAsync(); 122 | return await GetBitmapImageAsync(storageItemThumbnail); 123 | } 124 | 125 | /// 126 | /// 包含扩展名和对应的回调函数(用于更新UI) 127 | /// 128 | private class IconGetter(string extension, Action callBack) 129 | { 130 | public IconGetter(string extension) : this(extension, () => { }) 131 | { 132 | } 133 | 134 | public readonly string Extension = extension; 135 | 136 | public Action CallBack = callBack; 137 | } 138 | 139 | /// 140 | /// 图标边长 141 | /// 142 | private const uint Size = 32; 143 | 144 | /// 145 | /// 文件不存在的图标 146 | /// 147 | private static BitmapImage _notFoundIcon = null!; 148 | 149 | /// 150 | /// 加载中的图标 151 | /// 152 | private static BitmapImage _loadingIcon = null!; 153 | 154 | /// 155 | /// 请求加载图标的列表 156 | /// 157 | private static readonly ConcurrentQueue _iconRequest = new(); 158 | 159 | /// 160 | /// 图标字典,键是扩展名,值是图标 161 | /// 162 | private static readonly Dictionary _iconList = []; 163 | } 164 | -------------------------------------------------------------------------------- /TagsTree/Services/Serialization.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text.Json; 4 | using System.Threading.Tasks; 5 | 6 | namespace TagsTree.Services; 7 | 8 | public static class Serialization 9 | { 10 | /// 11 | /// 将Json文件反序列化为某个类 12 | /// 13 | /// 带无参构造的类 14 | /// Json文件位置 15 | /// 返回文件中的数据,如果没有则返回新实例 16 | public static T Deserialize(string path) where T : new() 17 | { 18 | try 19 | { 20 | using var stream = File.OpenRead(path); 21 | return JsonSerializer.Deserialize(stream) ?? new T(); 22 | } 23 | catch (Exception) 24 | { 25 | return new(); 26 | } 27 | } 28 | 29 | /// 30 | /// 异步将Json文件反序列化为某个类 31 | /// 32 | /// 带无参构造的类 33 | /// Json文件位置 34 | /// 返回文件中的数据,如果没有则返回新实例 35 | public static async ValueTask DeserializeAsync(string path) where T : new() 36 | { 37 | try 38 | { 39 | await using var stream = File.OpenRead(path); 40 | return await JsonSerializer.DeserializeAsync(stream) ?? new T(); 41 | } 42 | catch (Exception) 43 | { 44 | return new(); 45 | } 46 | } 47 | 48 | /// 49 | /// 将某个类序列化为Json文件 50 | /// 51 | /// 泛型类 52 | /// Json文件路径 53 | /// 需要转化的对象 54 | /// 55 | public static void Serialize(string path, T obj) 56 | { 57 | using var stream = File.Create(path); 58 | JsonSerializer.Serialize(stream, obj); 59 | } 60 | 61 | /// 62 | /// 异步将某个类序列化为Json文件 63 | /// 64 | /// 泛型类 65 | /// Json文件路径 66 | /// 需要转化的对象 67 | /// 68 | public static async ValueTask SerializeAsync(string path, T obj) 69 | { 70 | await using var stream = File.Create(path); 71 | await JsonSerializer.SerializeAsync(stream, obj); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /TagsTree/Services/ShowContentDialog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.UI.Xaml.Controls; 4 | 5 | namespace TagsTree.Services; 6 | 7 | public static class ShowContentDialog 8 | { 9 | /// 10 | /// 显示一条错误信息 11 | /// 12 | /// :错误,:提示 13 | /// 错误信息 14 | public static async Task Information(bool mode, object control) 15 | { 16 | var messageDialog = new ContentDialog 17 | { 18 | Title = mode ? "错误" : "提示", 19 | Content = control, 20 | CloseButtonText = "确定", 21 | DefaultButton = ContentDialogButton.Close, 22 | XamlRoot = App.MainWindow.Content.XamlRoot 23 | }; 24 | _ = await messageDialog.ShowAsync(); 25 | } 26 | 27 | /// 28 | /// 警告(错误)信息 29 | /// 选择确认结果(可选) 30 | /// 选择取消结果(可选) 31 | public static async Task Warning(string message, string okHint = "", string cancelHint = "") 32 | { 33 | var ok = okHint is "" ? "" : $"\n按“确认”{okHint}"; 34 | var cancel = okHint is "" ? "" : $"\n按“取消”{cancelHint}"; 35 | return await Warning(control: message + "\n" + ok + cancel); 36 | } 37 | 38 | /// 39 | /// 显示一条双项选择的警告(错误)信息 40 | /// 41 | /// 警告(错误)信息 42 | /// 选择确定则返回,取消返回 43 | public static async Task Warning(object control) 44 | { 45 | var result = false; 46 | var messageDialog = new ContentDialog 47 | { 48 | Title = "警告", 49 | Content = control, 50 | PrimaryButtonText = "确定", 51 | CloseButtonText = "取消", 52 | DefaultButton = ContentDialogButton.Close, 53 | XamlRoot = App.MainWindow.Content.XamlRoot 54 | }; 55 | messageDialog.PrimaryButtonClick += (_, _) => result = true; 56 | messageDialog.CloseButtonClick += (_, _) => result = false; 57 | _ = await messageDialog.ShowAsync(); 58 | return result; 59 | } 60 | 61 | /// 62 | /// 询问信息 63 | /// 选择是结果(可选) 64 | /// 选择否结果(可选) 65 | /// 选择取消结果(可选) 66 | public static async Task Question(string message, string yesHint = "", string noHint = "", string cancelHint = "") 67 | { 68 | var yes = yesHint is "" ? "" : $"\n按“是”{yesHint}"; 69 | var no = noHint is "" ? "" : $"\n按“否”{noHint}"; 70 | var cancel = cancelHint is "" ? "" : $"\n按“取消”{cancelHint}"; 71 | return await Question(control: message + "\n" + yes + no + cancel); 72 | } 73 | 74 | /// 75 | /// 显示一条三项选择的询问信息 76 | /// 77 | /// 询问信息 78 | /// 选择是则返回,否返回,取消返回 79 | public static async Task Question(object control) 80 | { 81 | bool? result = null; 82 | var messageDialog = new ContentDialog 83 | { 84 | Title = "提示", 85 | Content = control, 86 | PrimaryButtonText = "是", 87 | SecondaryButtonText = "否", 88 | CloseButtonText = "取消", 89 | DefaultButton = ContentDialogButton.Close, 90 | XamlRoot = App.MainWindow.Content.XamlRoot 91 | }; 92 | messageDialog.PrimaryButtonClick += (_, _) => result = true; 93 | messageDialog.SecondaryButtonClick += (_, _) => result = false; 94 | messageDialog.CloseButtonClick += (_, _) => result = null; 95 | _ = await messageDialog.ShowAsync(); 96 | return result; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /TagsTree/TagsTree.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | WinExe 4 | net8.0-windows10.0.22621.0 5 | 10.0.19041.0 6 | TagsTree 7 | app.manifest 8 | x86;x64;arm64 9 | win-x86;win-x64;win-arm64 10 | true 11 | enable 12 | preview 13 | zh-cn 14 | true 15 | true 16 | 17 | 18 | DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION;DISABLE_XAML_GENERATED_BINDING_DEBUG_OUTPUT 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 40 | 41 | 42 | 43 | 44 | 49 | 50 | true 51 | 52 | 53 | -------------------------------------------------------------------------------- /TagsTree/Views/FileEditTagsPage.xaml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | 41 | 42 | 43 | 44 | 45 | 46 | 50 | 51 | 52 | 56 | 57 | 58 | 59 | 60 | 61 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /TagsTree/Views/FileEditTagsPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Xaml; 2 | using Microsoft.UI.Xaml.Controls; 3 | using Microsoft.UI.Xaml.Input; 4 | using Microsoft.UI.Xaml.Navigation; 5 | using TagsTree.Services; 6 | using TagsTree.Views.ViewModels; 7 | using WinUI3Utilities; 8 | 9 | namespace TagsTree.Views; 10 | 11 | public partial class FileEditTagsPage : Page 12 | { 13 | public FileEditTagsPage() => InitializeComponent(); 14 | 15 | private FileEditTagsViewModel _vm = null!; 16 | 17 | protected override void OnNavigatedTo(NavigationEventArgs e) 18 | { 19 | _vm = new(e.Parameter.To()); 20 | } 21 | 22 | #region 事件处理 23 | 24 | private async void AddTagClicked(object sender, DoubleTappedRoutedEventArgs e) 25 | { 26 | var newTag = sender.To().GetTag(); 27 | foreach (var tagExisted in _vm.VirtualTags) 28 | if (tagExisted.Name == newTag.Name) 29 | { 30 | this.CreateTeachingTip().ShowAndHide($"已拥有该标签「{newTag.Name}」", TeachingTipSeverity.Error); 31 | return; 32 | } 33 | else if (newTag.HasChildTag(tagExisted)) 34 | { 35 | this.CreateTeachingTip().ShowAndHide($"已拥有「{newTag.Name}」的下级标签「{tagExisted.Name}」或更多", TeachingTipSeverity.Error); 36 | return; 37 | } 38 | else if (tagExisted.HasChildTag(newTag)) 39 | { 40 | if (await ShowContentDialog.Warning($"「{newTag.Name}」将会覆盖上级标签「{tagExisted.Name}」,是否继续?")) 41 | { 42 | _ = _vm.VirtualTags.Remove(tagExisted); 43 | _vm.VirtualTags.Add(newTag); 44 | _vm.IsSaveEnabled = true; 45 | } 46 | this.CreateTeachingTip().ShowAndHide($"「{newTag.Name}」覆盖上级标签「{tagExisted.Name}」"); 47 | return; 48 | } 49 | 50 | _vm.VirtualTags.Add(newTag); 51 | _vm.IsSaveEnabled = true; 52 | } 53 | 54 | private void DeleteTagClicked(object sender, DoubleTappedRoutedEventArgs e) 55 | { 56 | _ = _vm.VirtualTags.Remove(sender.To().GetTag()); 57 | _vm.IsSaveEnabled = true; 58 | } 59 | 60 | private void SaveClicked(object sender, RoutedEventArgs e) 61 | { 62 | foreach (var tag in AppContext.Tags.TagsDictionaryValues) 63 | AppContext.Relations[tag.Id, _vm.FileViewModel.Id] = _vm.VirtualTags.Contains(tag); 64 | _vm.FileViewModel.TagsChanged(); 65 | AppContext.SaveRelations(); 66 | _vm.IsSaveEnabled = false; 67 | this.CreateTeachingTip().ShowAndHide("已保存更改"); 68 | } 69 | 70 | #endregion 71 | } 72 | -------------------------------------------------------------------------------- /TagsTree/Views/FileImporterPage.xaml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 20 | 24 | 25 | 26 | 32 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | 53 | 59 | 65 | 66 | 67 | 68 | 74 | 75 | 80 | 85 | 86 | 90 | 91 | 97 | 98 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /TagsTree/Views/FileImporterPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.UI.Xaml; 7 | using Microsoft.UI.Xaml.Controls; 8 | using TagsTree.Interfaces; 9 | using TagsTree.Models; 10 | using TagsTree.Services.ExtensionMethods; 11 | using TagsTree.Views.ViewModels; 12 | using WinUI3Utilities; 13 | 14 | namespace TagsTree.Views; 15 | 16 | public partial class FileImporterPage : Page, ITypeGetter 17 | { 18 | public FileImporterPage() => InitializeComponent(); 19 | 20 | public static Type TypeGetter => typeof(FileImporterPage); 21 | 22 | private readonly FileImporterViewModel _vm = new(); 23 | 24 | #region 事件处理 25 | 26 | private async void ImportClicked(object sender, RoutedEventArgs e) 27 | { 28 | _vm.Processing = true; 29 | await Task.Yield(); 30 | var temp = new List(); 31 | var dictionary = new Dictionary(); 32 | 33 | if (sender.To().Name is { } mode) 34 | if (mode is nameof(SelectFiles)) 35 | { 36 | if (await App.MainWindow.PickMultipleFilesAsync() is { } files and not { Count: 0 }) 37 | if (FileViewModel.IsValidPath(files[0].Path.GetPath())) 38 | { 39 | foreach (var fileViewModelModel in _vm.FileViewModels) 40 | dictionary[fileViewModelModel.FullName] = true; 41 | if (FileViewModel.IsValidPath(files[0].Path.GetPath())) 42 | temp.AddRange(files.Where(file => !dictionary.ContainsKey(false + file.Path)) 43 | .Select(file => new FileViewModel(file.Path))); 44 | } 45 | } 46 | else if (await App.MainWindow.PickSingleFolderAsync() is { } folder) 47 | { 48 | foreach (var fileViewModelModel in _vm.FileViewModels) 49 | dictionary[fileViewModelModel.FullName] = true; 50 | if (mode is nameof(SelectFolders)) 51 | { 52 | if (FileViewModel.IsValidPath(folder.Path.GetPath())) 53 | if (!dictionary.ContainsKey(true + folder.Path)) 54 | temp.Add(new(folder.Path)); 55 | } 56 | else if (FileViewModel.IsValidPath(folder.Path)) 57 | switch (mode) 58 | { 59 | case nameof(PathFiles): 60 | temp.AddRange(new DirectoryInfo(folder.Path).GetFiles() 61 | .Where(fileInfo => !dictionary.ContainsKey(false + fileInfo.FullName)) 62 | .Select(fileInfo => new FileViewModel(fileInfo.FullName))); 63 | break; 64 | case nameof(PathFolders): 65 | temp.AddRange(new DirectoryInfo(folder.Path).GetDirectories() 66 | .Where(directoryInfo => !dictionary.ContainsKey(true + directoryInfo.FullName)) 67 | .Select(directoryInfo => new FileViewModel(directoryInfo.FullName))); 68 | break; 69 | case nameof(PathBoth): 70 | { 71 | temp.AddRange(new DirectoryInfo(folder.Path).GetFiles() 72 | .Where(fileInfo => !dictionary.ContainsKey(false + fileInfo.FullName)) 73 | .Select(fileInfo => new FileViewModel(fileInfo.FullName))); 74 | temp.AddRange(new DirectoryInfo(folder.Path).GetDirectories() 75 | .Where(directoryInfo => !dictionary.ContainsKey(true + directoryInfo.FullName)) 76 | .Select(directoryInfo => new FileViewModel(directoryInfo.FullName))); 77 | } 78 | 79 | break; 80 | case nameof(All): 81 | void RecursiveReadFiles(string folderName) 82 | { 83 | temp.AddRange(new DirectoryInfo(folderName).GetFiles() 84 | .Where(fileInfo => !dictionary!.ContainsKey(false + fileInfo.FullName)) 85 | .Select(fileInfo => new FileViewModel(fileInfo.FullName))); 86 | foreach (var directoryInfo in new DirectoryInfo(folderName).GetDirectories()) 87 | RecursiveReadFiles(directoryInfo.FullName); 88 | } 89 | 90 | RecursiveReadFiles(folder.Path); 91 | break; 92 | } 93 | } 94 | 95 | _vm.FileViewModels = [..temp]; 96 | _vm.Processing = false; 97 | } 98 | 99 | private void DeleteClicked(object sender, RoutedEventArgs e) => _vm.FileViewModels.Clear(); 100 | 101 | private void SaveClicked(object sender, RoutedEventArgs e) 102 | { 103 | var saved = 0; 104 | _vm.Processing = true; 105 | 106 | var dictionary = new Dictionary(); 107 | foreach (var fileModel in AppContext.IdFile.Values) 108 | dictionary[fileModel.FullName] = true; 109 | foreach (var fileViewModel in _vm.FileViewModels) 110 | if (!dictionary.ContainsKey(fileViewModel.FullName)) 111 | { 112 | FileModel.FromFullName(fileViewModel.FullName).AddNew(); 113 | ++saved; 114 | } 115 | 116 | _vm.Processing = false; 117 | 118 | AppContext.SaveFiles(); 119 | AppContext.SaveRelations(); 120 | this.CreateTeachingTip().ShowAndHide($"导入「{saved}/{_vm.FileViewModels.Count}」个文件"); 121 | _vm.FileViewModels.Clear(); 122 | } 123 | 124 | private void ContextDeleteClicked(object sender, RoutedEventArgs e) 125 | { 126 | foreach (var fileViewModel in FileImporterDataGird.SelectedItems.Cast()) 127 | _ = _vm.FileViewModels.Remove(fileViewModel); 128 | } 129 | 130 | #endregion 131 | } 132 | -------------------------------------------------------------------------------- /TagsTree/Views/FilePropertiesPage.xaml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 17 | 22 | 26 | 30 | 34 | 35 | 40 | 45 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 65 | 72 | 79 | 86 | 94 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /TagsTree/Views/FilePropertiesPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using CommunityToolkit.WinUI.Controls; 4 | using Microsoft.UI.Xaml.Controls; 5 | using Microsoft.UI.Xaml.Media.Animation; 6 | using Microsoft.UI.Xaml.Navigation; 7 | using TagsTree.Services; 8 | using TagsTree.Services.ExtensionMethods; 9 | using TagsTree.Views.ViewModels; 10 | using Windows.ApplicationModel.DataTransfer; 11 | using Microsoft.UI.Xaml; 12 | using WinUI3Utilities; 13 | 14 | namespace TagsTree.Views; 15 | 16 | public partial class FilePropertiesPage : Page 17 | { 18 | public FilePropertiesPage() => InitializeComponent(); 19 | 20 | private readonly FilePropertiesPageViewModel _vm = new(); 21 | 22 | #region 事件处理 23 | 24 | protected override void OnNavigatedTo(NavigationEventArgs e) => _vm.FileViewModel = e.Parameter.To(); 25 | 26 | private void OpenClicked(object sender, RoutedEventArgs e) => _vm.FileViewModel.Open(); 27 | 28 | private void OpenExplorerClicked(object sender, RoutedEventArgs e) => _vm.FileViewModel.OpenDirectory(); 29 | 30 | private void EditTagsClicked(object sender, RoutedEventArgs e) => App.MainWindow.GotoPage(_vm.FileViewModel); 31 | 32 | private async void RemoveClicked(object sender, RoutedEventArgs e) 33 | { 34 | if (!await ShowContentDialog.Warning("是否从软件移除该文件?")) 35 | return; 36 | Remove(_vm.FileViewModel); 37 | } 38 | 39 | private async void RenameClicked(object sender, RoutedEventArgs e) 40 | { 41 | InputName.Load($"文件重命名 {_vm.FileViewModel.Name}", cd => 42 | { 43 | if (_vm.FileViewModel.Name == cd.Text) 44 | return "新文件名与原文件名一致!"; 45 | 46 | var newFullName = _vm.FileViewModel.Path + @"\" + cd.Text; 47 | if (_vm.FileViewModel.IsFolder ? Directory.Exists(newFullName) : File.Exists(newFullName)) 48 | { 49 | var isFolder = _vm.FileViewModel.IsFolder ? "夹" : ""; 50 | return $"新文件{isFolder}名与目录中其他文件{isFolder}同名!"; 51 | } 52 | 53 | return null; 54 | }, FileSystemHelper.InvalidMode.Name, _vm.FileViewModel.Name); 55 | if (await InputName.ShowAsync()) 56 | return; 57 | var newFullName = _vm.FileViewModel.Path + @"\" + InputName.Text; 58 | _vm.FileViewModel.FileModel.Rename(newFullName); 59 | _vm.FileViewModel.MoveOrRenameAndSave(newFullName); 60 | // 相当于对FileViewModel的所有属性OnPropertyChanged 61 | _vm.RaiseOnPropertyChanged(); 62 | } 63 | 64 | private async void MoveClicked(object sender, RoutedEventArgs e) 65 | { 66 | if (await App.MainWindow.PickSingleFolderAsync() is not { } folder) 67 | return; 68 | if (_vm.FileViewModel.Path == folder.Path) 69 | { 70 | await ShowContentDialog.Information(true, "新目录与原目录一致!"); 71 | return; 72 | } 73 | 74 | if (folder.Path.Contains(_vm.FileViewModel.Path)) 75 | { 76 | await ShowContentDialog.Information(true, "不能将其移动到原目录下!"); 77 | return; 78 | } 79 | 80 | var newFullName = folder.Path + @"\" + _vm.FileViewModel.Name; 81 | if (newFullName.Exists()) 82 | { 83 | await ShowContentDialog.Information(true, "新名称与目录下其他文件(夹)同名!"); 84 | return; 85 | } 86 | 87 | _vm.FileViewModel.FileModel.Move(newFullName); 88 | _vm.FileViewModel.MoveOrRenameAndSave(newFullName); 89 | _vm.RaiseOnPropertyChanged(); 90 | } 91 | 92 | private async void DeleteClicked(object sender, RoutedEventArgs e) 93 | { 94 | if (!await ShowContentDialog.Warning("是否删除该文件?")) 95 | return; 96 | _vm.FileViewModel.FileModel.Delete(); 97 | Remove(_vm.FileViewModel); 98 | } 99 | 100 | private void CopyClicked(object sender, RoutedEventArgs e) 101 | { 102 | var dataPackage = new DataPackage(); 103 | dataPackage.SetText(sender.To().Header.To()); 104 | Clipboard.SetContent(dataPackage); 105 | } 106 | 107 | #endregion 108 | 109 | #region 操作 110 | 111 | private static void Remove(FileViewModel fileViewModel) 112 | { 113 | App.MainWindow.Frame.GoBack(new SlideNavigationTransitionInfo()); 114 | fileViewModel.RemoveAndSave(); 115 | TagSearchFilesPage.FileRemoved(fileViewModel); 116 | } 117 | 118 | #endregion 119 | } 120 | -------------------------------------------------------------------------------- /TagsTree/Views/FilesObserverPage.xaml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 18 | 23 | 28 | 33 | 38 | 39 | 44 | 45 | 52 | 56 | 57 | 61 | 62 | 63 | 64 | 68 | 72 | 76 | 80 | 81 | 82 | 83 | 84 | 88 | 89 | 90 | 94 | 95 | 96 | 97 | 101 | 102 | 103 | 107 | 108 | 109 | 110 | 114 | 115 | 116 | 120 | 121 | 122 | 123 | 127 | 128 | 129 | 133 | 134 | 135 | 136 | 140 | 141 | 142 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /TagsTree/Views/IndexPage.xaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /TagsTree/Views/IndexPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.UI.Xaml.Controls; 3 | using TagsTree.Interfaces; 4 | 5 | namespace TagsTree.Views; 6 | 7 | public sealed partial class IndexPage : Page, ITypeGetter 8 | { 9 | public IndexPage() 10 | { 11 | InitializeComponent(); 12 | TagSearchBox.ResetQuerySubmitted(QuerySubmitted); 13 | } 14 | 15 | public static Type TypeGetter => typeof(IndexPage); 16 | 17 | #region 事件处理 18 | 19 | private static void QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs e) => App.MainWindow.GotoPage(e.QueryText); 20 | 21 | #endregion 22 | } 23 | -------------------------------------------------------------------------------- /TagsTree/Views/SelectTagToEditPage.xaml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 19 | 23 | 24 | 25 | 26 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /TagsTree/Views/SelectTagToEditPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.UI.Xaml; 3 | using Microsoft.UI.Xaml.Controls; 4 | using TagsTree.Interfaces; 5 | using TagsTree.Services; 6 | using TagsTree.Services.ExtensionMethods; 7 | using TagsTree.Views.ViewModels; 8 | using WinUI3Utilities; 9 | 10 | namespace TagsTree.Views; 11 | 12 | public partial class SelectTagToEditPage : Page, ITypeGetter 13 | { 14 | public SelectTagToEditPage() => InitializeComponent(); 15 | 16 | public static Type TypeGetter => typeof(SelectTagToEditPage); 17 | 18 | private readonly SelectTagToEditPageViewModel _vm = new(); 19 | 20 | #region 事件处理 21 | 22 | private void TagsOnItemInvoked(TreeView sender, TreeViewItemInvokedEventArgs e) => _vm.Path = e.InvokedItem.To()?.FullName ?? _vm.Path; 23 | 24 | private async void ConfirmClicked(object sender, RoutedEventArgs e) 25 | { 26 | if (_vm.Path.GetTagViewModel() is not { } pathTagModel) 27 | { 28 | await ShowContentDialog.Information(true, "「标签路径」不存在!"); 29 | return; 30 | } 31 | 32 | if (pathTagModel == AppContext.Tags.TagsDictionaryRoot) 33 | { 34 | await ShowContentDialog.Information(true, "「标签路径」不能为空!"); 35 | return; 36 | } 37 | 38 | App.MainWindow.GotoPage(pathTagModel); 39 | } 40 | 41 | #endregion 42 | } 43 | -------------------------------------------------------------------------------- /TagsTree/Views/SettingsPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Windows.System; 4 | using CommunityToolkit.WinUI.Controls; 5 | using CommunityToolkit.Mvvm.ComponentModel; 6 | using Microsoft.UI.Xaml; 7 | using Microsoft.UI.Xaml.Controls; 8 | using TagsTree.Interfaces; 9 | using TagsTree.Services.ExtensionMethods; 10 | using TagsTree.Views.ViewModels; 11 | using WinUI3Utilities; 12 | 13 | namespace TagsTree.Views; 14 | 15 | [INotifyPropertyChanged] 16 | public partial class SettingsPage : Page, ITypeGetter 17 | { 18 | public SettingsPage() 19 | { 20 | Current = this; 21 | InitializeComponent(); 22 | } 23 | 24 | public static Type TypeGetter => typeof(SettingsPage); 25 | 26 | public static SettingsPage Current { get; private set; } = null!; 27 | 28 | #pragma warning disable CA1822 29 | private SettingsViewModel Vm => AppContext.SettingViewModel; 30 | #pragma warning restore CA1822 31 | 32 | #region 事件处理 33 | 34 | private async void NavigateUriClicked(object sender, RoutedEventArgs e) 35 | { 36 | _ = await Launcher.LaunchUriAsync(new(sender.To().GetTag())); 37 | } 38 | 39 | private void ThemeChecked(object sender, RoutedEventArgs e) 40 | { 41 | var selectedTheme = sender.To().GetTag() switch 42 | { 43 | 1 => ElementTheme.Light, 44 | 2 => ElementTheme.Dark, 45 | _ => ElementTheme.Default 46 | }; 47 | 48 | if (App.MainWindow.Content is FrameworkElement rootElement) 49 | rootElement.RequestedTheme = selectedTheme; 50 | 51 | AppContext.AppConfig.Theme = selectedTheme.To(); 52 | } 53 | 54 | private async void LibraryPathClicked(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs e) 55 | => sender.Text = (await App.MainWindow.PickSingleFolderAsync())?.Path; 56 | 57 | private async void ExportClicked(object sender, RoutedEventArgs e) 58 | { 59 | if (await App.MainWindow.PickSingleFolderAsync() is { } folder) 60 | AppContext.AppLocalFolder.Copy(folder.Path); 61 | } 62 | 63 | private async void ImportClicked(object sender, RoutedEventArgs e) 64 | { 65 | if (await App.MainWindow.PickSingleFolderAsync() is { } folder) 66 | folder.Path.Copy(AppContext.AppLocalFolder); 67 | } 68 | 69 | private void OpenDirectoryClicked(object sender, RoutedEventArgs e) => AppContext.AppLocalFolder.Open(); 70 | 71 | private async void LibraryPathSaved(object sender, RoutedEventArgs e) 72 | { 73 | var asb = sender.To().Description.To(); 74 | if (!Directory.Exists(asb.Text)) 75 | { 76 | asb.Description = "路径错误!请填写正确、完整、存在的文件夹路径!"; 77 | return; 78 | } 79 | 80 | asb.Description = ""; 81 | 82 | Vm.LibraryPath = asb.Text; 83 | asb.Text = ""; 84 | Vm.ConfigSetChanged(); 85 | 86 | await App.MainWindow.ConfigIsSet(); 87 | } 88 | 89 | private void SetDefaultAppConfigClicked(object sender, RoutedEventArgs e) 90 | { 91 | AppContext.SetDefaultAppConfig(); 92 | OnPropertyChanged(nameof(Vm)); 93 | } 94 | 95 | private new void Unloaded(object sender, RoutedEventArgs e) => AppContext.SaveConfiguration(Vm.AppConfig); 96 | 97 | #endregion 98 | } 99 | -------------------------------------------------------------------------------- /TagsTree/Views/TagEditFilesPage.xaml: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 21 | 25 | 26 | 30 | 31 | 32 | 38 | 39 | 44 | 45 | 46 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /TagsTree/Views/TagEditFilesPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using CommunityToolkit.WinUI.UI.Controls; 4 | using Microsoft.UI.Xaml; 5 | using Microsoft.UI.Xaml.Controls; 6 | using Microsoft.UI.Xaml.Media.Animation; 7 | using Microsoft.UI.Xaml.Navigation; 8 | using TagsTree.Models; 9 | using TagsTree.Services.ExtensionMethods; 10 | using TagsTree.Views.ViewModels; 11 | using WinUI3Utilities; 12 | 13 | namespace TagsTree.Views; 14 | 15 | public sealed partial class TagEditFilesPage : Page 16 | { 17 | public TagEditFilesPage() => InitializeComponent(); 18 | 19 | private readonly TagEditFilesViewModel _vm = new(); 20 | 21 | #region 事件处理 22 | 23 | protected override void OnNavigatedTo(NavigationEventArgs e) 24 | { 25 | _vm.TagViewModel = e.Parameter.To(); 26 | _vm.FileViewModels = [.. RelationsDataTable.GetFileModels().Select(fileModel => new FileViewModel(fileModel, _vm.TagViewModel))]; 27 | } 28 | 29 | private void ResultChanged(IEnumerable newResult) => _vm.FileViewModels = [.. newResult.Select(fileModel => new FileViewModel(fileModel, _vm.TagViewModel.Parent))]; 30 | 31 | private void Selected(object sender, SelectionChangedEventArgs e) 32 | { 33 | var dg = sender.To(); 34 | if (dg.SelectedItem.To() is not { } item) 35 | return; 36 | item.SelectedFlip(); 37 | dg.SelectedIndex = -1; 38 | } 39 | 40 | private void SaveClicked(object sender, RoutedEventArgs e) 41 | { 42 | foreach (var fileViewModel in _vm.FileViewModels) 43 | if (fileViewModel.Selected != fileViewModel.SelectedOriginal) 44 | { 45 | switch (fileViewModel.SelectedOriginal) 46 | { 47 | case true: 48 | AppContext.Relations[_vm.TagViewModel.Id, fileViewModel.Id] = false; 49 | break; 50 | // 如果原本有上级标签,则覆盖相应上级标签 51 | case false: 52 | AppContext.Relations[_vm.TagViewModel.Id, fileViewModel.Id] = true; 53 | foreach (var tagViewModel in fileViewModel.Tags.GetTagViewModels()) 54 | if (tagViewModel.HasChildTag(_vm.TagViewModel)) 55 | { 56 | AppContext.Relations[tagViewModel.Id, fileViewModel.Id] = false; 57 | break; 58 | } 59 | break; 60 | // 如果原本是null,则删除fileViewModel拥有的相应子标签 61 | case null: 62 | foreach (var tag in fileViewModel.GetAncestorTags(_vm.TagViewModel)) 63 | AppContext.Relations[tag.Id, fileViewModel.Id] = false; 64 | break; 65 | } 66 | 67 | fileViewModel.TagsChanged(); 68 | } 69 | 70 | AppContext.SaveRelations(); 71 | this.CreateTeachingTip().ShowAndHide("已保存更改"); 72 | App.MainWindow.Frame.GoBack(new SlideNavigationTransitionInfo()); 73 | } 74 | 75 | #endregion 76 | } 77 | -------------------------------------------------------------------------------- /TagsTree/Views/TagSearchFilesPage.xaml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 19 | 26 | 32 | 33 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /TagsTree/Views/TagSearchFilesPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text.RegularExpressions; 4 | using CommunityToolkit.WinUI.UI.Controls; 5 | using Microsoft.UI.Xaml; 6 | using Microsoft.UI.Xaml.Controls; 7 | using Microsoft.UI.Xaml.Input; 8 | using Microsoft.UI.Xaml.Navigation; 9 | using TagsTree.Models; 10 | using TagsTree.Services; 11 | using TagsTree.Services.ExtensionMethods; 12 | using TagsTree.Views.ViewModels; 13 | using WinUI3Utilities; 14 | 15 | namespace TagsTree.Views; 16 | 17 | public partial class TagSearchFilesPage : Page 18 | { 19 | public TagSearchFilesPage() 20 | { 21 | _vm = new(); 22 | _current = this; 23 | InitializeComponent(); 24 | } 25 | 26 | protected override void OnNavigatedTo(NavigationEventArgs e) 27 | { 28 | TbSearch.SubmitQuery(TbSearch.Text = e.Parameter.To()); 29 | } 30 | 31 | private readonly TagSearchFilesViewModel _vm; 32 | 33 | #region 事件处理 34 | 35 | private void ResultChanged(IEnumerable newResult) => _vm.ResultCallBack = [.. newResult.Select(fileModel => new FileViewModel(fileModel))]; 36 | 37 | private void TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs e) => sender.Text = Regex.Replace(sender.Text, $@"[{FileSystemHelper.GetInvalidNameChars} ]+", ""); 38 | 39 | private void QuerySubmitted(AutoSuggestBox autoSuggestBox, AutoSuggestBoxQuerySubmittedEventArgs e) => _vm.FileViewModels = e.QueryText is "" ? _vm.ResultCallBack : RelationsDataTable.FuzzySearchName(e.QueryText, _vm.ResultCallBack); 40 | 41 | private void ContextOpenClicked(object sender, RoutedEventArgs e) => sender.To().GetDataContext().Open(); 42 | 43 | private void ContextOpenExplorerClicked(object sender, RoutedEventArgs e) => sender.To().GetDataContext().OpenDirectory(); 44 | 45 | private async void ContextRemoveClicked(object sender, RoutedEventArgs e) 46 | { 47 | var fileViewModel = sender.To().GetDataContext(); 48 | // 打开确认框会关闭菜单,导致DataContext变为null,所以提前记录 49 | if (!await ShowContentDialog.Warning("是否从软件移除该文件?")) 50 | return; 51 | fileViewModel.RemoveAndSave(); 52 | _ = _vm.FileViewModels.Remove(fileViewModel); 53 | } 54 | 55 | private void ContextPropertiesClicked(object sender, RoutedEventArgs e) => App.MainWindow.GotoPage(sender.To().GetDataContext()); 56 | 57 | private void ContextPropertiesDoubleTapped(object sender, DoubleTappedRoutedEventArgs e) 58 | { 59 | if (sender.To().SelectedItem is FileViewModel fileViewModel) 60 | App.MainWindow.GotoPage(fileViewModel); 61 | } 62 | 63 | #endregion 64 | 65 | #region 操作 66 | 67 | private static TagSearchFilesPage _current = null!; 68 | 69 | public static void FileRemoved(FileViewModel removedItem) => _ = _current._vm.FileViewModels.Remove(removedItem); 70 | 71 | #endregion 72 | } 73 | -------------------------------------------------------------------------------- /TagsTree/Views/TagsManagerPage.xaml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 19 | 23 | 27 | 31 | 35 | 36 | 41 | 42 | 48 | 52 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 70 | 75 | 81 | 86 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 102 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /TagsTree/Views/TagsManagerPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.RegularExpressions; 4 | using System.Threading.Tasks; 5 | using Microsoft.UI.Xaml; 6 | using Microsoft.UI.Xaml.Controls; 7 | using TagsTree.Interfaces; 8 | using TagsTree.Services.ExtensionMethods; 9 | using TagsTree.Views.ViewModels; 10 | using WinUI3Utilities; 11 | 12 | namespace TagsTree.Views; 13 | 14 | public partial class TagsManagerPage : Page, ITypeGetter 15 | { 16 | public TagsManagerPage() 17 | { 18 | Current = this; 19 | InitializeComponent(); 20 | } 21 | 22 | public static Type TypeGetter => typeof(TagsManagerPage); 23 | 24 | public static TagsManagerPage Current { get; private set; } = null!; 25 | 26 | // TODO: TextBox绑定更新慢 27 | public readonly TagsManagerViewModel Vm = new(); 28 | 29 | #region 事件处理 30 | 31 | private TagViewModel? _tempPath; 32 | 33 | private void OnDragItemsCompleted(TreeView sender, TreeViewDragItemsCompletedEventArgs e) 34 | { 35 | if (e.NewParentItem.To() == _tempPath) 36 | this.CreateTeachingTip().ShowAndHide($"移动标签 {e.Items[0].To().Name} 到原位置", TeachingTipSeverity.Information); 37 | else 38 | { 39 | Vm.IsSaveEnabled = true; 40 | this.CreateTeachingTip().ShowAndHide($"移动标签 {e.Items[0].To().Name}"); 41 | } 42 | 43 | _tempPath = null; 44 | } 45 | 46 | private void OnDragItemsStarting(TreeView sender, TreeViewDragItemsStartingEventArgs e) => _tempPath = e.Items[0].To().Parent; 47 | 48 | private void NameChanged(object sender, TextChangedEventArgs e) => Vm.Name = Regex.Replace(Vm.Name, $@"[{FileSystemHelper.GetInvalidNameChars}]+", ""); 49 | 50 | private void TagsOnItemInvoked(TreeView sender, TreeViewItemInvokedEventArgs e) => Vm.Path = (e.InvokedItem as TagViewModel)?.FullName ?? Vm.Path; 51 | 52 | private void NewClicked(object sender, RoutedEventArgs e) 53 | { 54 | if (ExistenceCheck(Vm.Path, "标签路径") is not { } pathTagModel) 55 | return; 56 | 57 | var result = NewTagCheck(Vm.Name); 58 | if (result is not null) 59 | { 60 | this.CreateTeachingTip().ShowAndHide(result, TeachingTipSeverity.Error); 61 | return; 62 | } 63 | 64 | NewTag(Vm.Name, pathTagModel); 65 | Vm.Name = ""; 66 | } 67 | 68 | private void MoveClicked(object sender, RoutedEventArgs e) 69 | { 70 | if (ExistenceCheck(Vm.Path, "标签路径") is not { } pathTagModel) 71 | return; 72 | 73 | if (ExistenceCheck(Vm.Name, "标签名称") is not { } nameTagModel) 74 | return; 75 | 76 | MoveTag(nameTagModel, pathTagModel); 77 | Vm.Name = ""; 78 | } 79 | 80 | private void RenameClicked(object sender, RoutedEventArgs e) 81 | { 82 | if (Vm.Path is "") 83 | { 84 | this.CreateTeachingTip().ShowAndHide("未输入希望重命名的标签!", TeachingTipSeverity.Error); 85 | return; 86 | } 87 | 88 | if (ExistenceCheck(Vm.Path, "标签路径") is not { } pathTagModel) 89 | return; 90 | 91 | var result = NewTagCheck(Vm.Name); 92 | if (result is not null) 93 | { 94 | this.CreateTeachingTip().ShowAndHide(result, TeachingTipSeverity.Error); 95 | return; 96 | } 97 | 98 | RenameTag(Vm.Name, pathTagModel); 99 | Vm.Name = ""; 100 | Vm.Path = ""; 101 | } 102 | 103 | private void DeleteClicked(object sender, RoutedEventArgs e) 104 | { 105 | if (Vm.Path is "") 106 | { 107 | this.CreateTeachingTip().ShowAndHide("未输入希望删除的标签!", TeachingTipSeverity.Error); 108 | return; 109 | } 110 | 111 | if (ExistenceCheck(Vm.Path, "标签路径") is not { } pathTagModel) 112 | return; 113 | 114 | DeleteTag(pathTagModel); 115 | Vm.Name = ""; 116 | } 117 | 118 | private async void SaveClicked(object sender, RoutedEventArgs e) 119 | { 120 | await Task.Yield(); 121 | AppContext.Tags = Vm.TagsSource; 122 | AppContext.SaveTags(); 123 | foreach (var (mode, tagViewModel) in _buffer) 124 | if (mode) 125 | AppContext.Relations.NewTag(tagViewModel); 126 | else 127 | AppContext.Relations.DeleteTag(tagViewModel); 128 | _buffer.Clear(); 129 | AppContext.SaveRelations(); 130 | Vm.IsSaveEnabled = false; 131 | this.CreateTeachingTip().ShowAndHide("已保存"); 132 | } 133 | 134 | private async void ContextNewClicked(object sender, RoutedEventArgs e) 135 | { 136 | InputName.Load($"新建子标签 {sender.To().GetTag().Name}", cd => NewTagCheck(cd.Text), FileSystemHelper.InvalidMode.Name); 137 | if (!await InputName.ShowAsync()) 138 | NewTag(InputName.Text, sender.To().GetTag()); 139 | } 140 | 141 | private async void RootContextNewClicked(object sender, RoutedEventArgs e) 142 | { 143 | InputName.Load("新建根标签", cd => NewTagCheck(cd.Text), FileSystemHelper.InvalidMode.Name); 144 | if (!await InputName.ShowAsync()) 145 | NewTag(InputName.Text, Vm.TagsSource.TagsTree); 146 | } 147 | 148 | private void ContextCutClicked(object sender, RoutedEventArgs e) => Vm.ClipBoard = sender.To().GetTag(); 149 | 150 | private async void ContextRenameClicked(object sender, RoutedEventArgs e) 151 | { 152 | InputName.Load($"标签重命名 {sender.To().GetTag().Name}", cd => NewTagCheck(cd.Text), FileSystemHelper.InvalidMode.Name); 153 | if (!await InputName.ShowAsync()) 154 | RenameTag(InputName.Text, sender.To().GetTag()); 155 | } 156 | 157 | private void ContextPasteClicked(object sender, RoutedEventArgs e) => MoveTag(Vm.ClipBoard!, sender.To().GetTag()); 158 | 159 | private void RootContextPasteClicked(object sender, RoutedEventArgs e) => MoveTag(Vm.ClipBoard!, Vm.TagsSource.TagsTree); 160 | 161 | private void ContextDeleteClicked(object sender, RoutedEventArgs e) => DeleteTag(sender.To().GetTag()); 162 | 163 | #endregion 164 | 165 | /// 166 | /// 暂存关系表的变化
167 | /// 表示添加,表示删除 168 | ///
169 | private readonly List<(bool, TagViewModel)> _buffer = []; 170 | 171 | #region 操作 172 | 173 | private void NewTag(string name, TagViewModel path) 174 | { 175 | _buffer.Add(new(true, Vm.TagsSource.AddTag(path, name))); 176 | Vm.IsSaveEnabled = true; 177 | this.CreateTeachingTip().ShowAndHide($"新建标签 {name}"); 178 | } 179 | 180 | private void MoveTag(TagViewModel name, TagViewModel path) 181 | { 182 | if (path == name.Parent) 183 | { 184 | this.CreateTeachingTip().ShowAndHide($"移动标签 {name.Name} 到原位置", TeachingTipSeverity.Information); 185 | return; 186 | } 187 | 188 | if (name == path || Vm.TagsSource.TagsDictionary.GetValueOrDefault(name.Id)!.HasChildTag(Vm.TagsSource.TagsDictionary.GetValueOrDefault(path.Id)!)) 189 | { 190 | this.CreateTeachingTip().ShowAndHide("禁止将标签移动到自己目录下!", TeachingTipSeverity.Error); 191 | return; 192 | } 193 | 194 | Vm.ClipBoard = null; 195 | Vm.TagsSource.MoveTag(name, path); 196 | Vm.IsSaveEnabled = true; 197 | this.CreateTeachingTip().ShowAndHide($"移动标签 {name.Name}"); 198 | } 199 | 200 | private void RenameTag(string name, TagViewModel path) 201 | { 202 | Vm.TagsSource.RenameTag(path, name); 203 | Vm.IsSaveEnabled = true; 204 | this.CreateTeachingTip().ShowAndHide($"重命名标签 {name}"); 205 | } 206 | 207 | private void DeleteTag(TagViewModel path) 208 | { 209 | Vm.TagsSource.DeleteTag(path); 210 | _buffer.Add(new(false, path)); 211 | Vm.IsSaveEnabled = true; 212 | this.CreateTeachingTip().ShowAndHide($"删除标签 {path.Name}"); 213 | } 214 | 215 | private string? NewTagCheck(string name) 216 | => name is "" 217 | ? "标签名称不能为空!" 218 | : name.GetTagViewModel(Vm.TagsSource) is not null 219 | ? "与现有标签重名!" 220 | : null; 221 | 222 | private TagViewModel? ExistenceCheck(string path, string label) 223 | { 224 | var pathTagModel = path.GetTagViewModel(Vm.TagsSource); 225 | if (pathTagModel is null) 226 | this.CreateTeachingTip().ShowAndHide($"「{label}」不存在!", TeachingTipSeverity.Error); 227 | return pathTagModel; 228 | } 229 | 230 | #endregion 231 | } 232 | -------------------------------------------------------------------------------- /TagsTree/Views/ViewModels/Controls/InputContentDialogViewModels.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | 3 | namespace TagsTree.Views.ViewModels.Controls; 4 | 5 | public partial class InputContentDialogViewModels : ObservableObject 6 | { 7 | /// 8 | /// 输入的格式 9 | /// 10 | [ObservableProperty] private string _warningText = ""; 11 | 12 | /// 13 | /// 错误提示 14 | /// 15 | [ObservableProperty] private string _message = ""; 16 | 17 | /// 18 | /// 打开错误提示 19 | /// 20 | [ObservableProperty] private bool _isOpen = false; 21 | } 22 | -------------------------------------------------------------------------------- /TagsTree/Views/ViewModels/Controls/TagCompleteBoxViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using CommunityToolkit.Mvvm.ComponentModel; 3 | using TagsTree.Controls; 4 | using TagsTree.Services.ExtensionMethods; 5 | 6 | namespace TagsTree.Views.ViewModels.Controls; 7 | 8 | public partial class TagCompleteBoxViewModel(TagCompleteBox tcb) : ObservableObject 9 | { 10 | [ObservableProperty] 11 | [NotifyPropertyChangedFor(nameof(Tags))] 12 | [NotifyPropertyChangedFor(nameof(SuggestionTags))] 13 | private string _path = ""; 14 | 15 | /// 16 | /// BreadcrumbBar中最后一个item无法点击,需要多加个空元素 17 | /// 18 | public IEnumerable Tags => [..Path.Split('\\'), ""]; 19 | 20 | public IEnumerable SuggestionTags => Path.TagSuggest('\\', tcb.TagsSource); 21 | 22 | [ObservableProperty] 23 | [NotifyPropertyChangedFor(nameof(IsFocused))] 24 | private bool _state = true; 25 | 26 | public bool IsFocused => Path is "" || State; 27 | } 28 | -------------------------------------------------------------------------------- /TagsTree/Views/ViewModels/FileEditTagsViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using CommunityToolkit.Mvvm.ComponentModel; 3 | 4 | namespace TagsTree.Views.ViewModels; 5 | 6 | public partial class FileEditTagsViewModel(FileViewModel fileViewModel) : ObservableObject 7 | { 8 | public FileEditTagsViewModel() : this(FileViewModel.DefaultFileViewModel) 9 | { 10 | } 11 | 12 | [ObservableProperty] private bool _isSaveEnabled; 13 | 14 | public static ObservableCollection TagsSource => AppContext.Tags.TagsTree.SubTags; 15 | 16 | [ObservableProperty] 17 | [NotifyPropertyChangedFor(nameof(VirtualTags))] 18 | private FileViewModel _fileViewModel = fileViewModel; 19 | 20 | public ObservableCollection VirtualTags { get; } = [..AppContext.Relations.GetTags(fileViewModel.Id)]; 21 | } 22 | -------------------------------------------------------------------------------- /TagsTree/Views/ViewModels/FileImporterViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using CommunityToolkit.Mvvm.ComponentModel; 3 | 4 | namespace TagsTree.Views.ViewModels; 5 | 6 | public partial class FileImporterViewModel : ObservableObject 7 | { 8 | public FileImporterViewModel() => FileViewModels.CollectionChanged += (_, _) => OnPropertyChanged(nameof(DeleteSaveEnabled)); 9 | 10 | [ObservableProperty] 11 | [NotifyPropertyChangedFor(nameof(Processed))] 12 | [NotifyPropertyChangedFor(nameof(DeleteSaveEnabled))] 13 | private bool _processing; 14 | 15 | public bool Processed => !Processing; 16 | 17 | public bool DeleteSaveEnabled => !Processing && FileViewModels.Count is not 0; 18 | 19 | [ObservableProperty] private ObservableCollection _fileViewModels = []; 20 | } 21 | -------------------------------------------------------------------------------- /TagsTree/Views/ViewModels/FilePropertiesPageViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | 3 | namespace TagsTree.Views.ViewModels; 4 | 5 | public partial class FilePropertiesPageViewModel : ObservableObject 6 | { 7 | [ObservableProperty] private FileViewModel _fileViewModel = FileViewModel.DefaultFileViewModel; 8 | 9 | public void RaiseOnPropertyChanged() => OnPropertyChanged(nameof(FileViewModel)); 10 | } 11 | -------------------------------------------------------------------------------- /TagsTree/Views/ViewModels/FileViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | using CommunityToolkit.Mvvm.ComponentModel; 6 | using Microsoft.UI.Xaml.Media; 7 | using TagsTree.Interfaces; 8 | using TagsTree.Models; 9 | using TagsTree.Services; 10 | using TagsTree.Services.ExtensionMethods; 11 | using WinUI3Utilities; 12 | 13 | namespace TagsTree.Views.ViewModels; 14 | 15 | public class FileViewModel : ObservableObject, IFileModel 16 | { 17 | public static readonly FileViewModel DefaultFileViewModel = new(""); 18 | 19 | public FileModel FileModel { get; } 20 | 21 | /// 22 | /// 复制构造,可从后端创建的对象 23 | /// 24 | /// 后端FileModel 25 | /// 如果指定tag,则判断有无tag 26 | public FileViewModel(FileModel fileModel, TagViewModel? tag = null) 27 | { 28 | FileModel = fileModel; 29 | if (tag is not null) 30 | Selected = SelectedOriginal = FileModel.HasTag(tag); 31 | } 32 | 33 | /// 34 | /// 虚拟构造(文件对象不存在于) 35 | /// 36 | /// 文件路径 37 | public FileViewModel(string fullName) => FileModel = new(-1, fullName.GetName(), fullName.GetPath()); 38 | 39 | private readonly WeakReference _fileSystemInfo = new(null); 40 | 41 | #region FileModel 42 | 43 | public int Id => FileModel.Id is -1 ? ThrowHelper.Exception() : FileModel.Id; 44 | 45 | public string Name => FileModel.Name; 46 | 47 | public string Path => FileModel.Path; 48 | 49 | public string FullName => FileModel.FullName; 50 | 51 | public string Extension => FileModel.Extension; 52 | 53 | public bool Exists => FileModel.Exists; 54 | 55 | public string Tags => FileModel.Tags; 56 | 57 | public IEnumerable PathTags => FileModel.PathTags; 58 | 59 | public string PartialPath => FileModel.PartialPath; 60 | 61 | public bool IsFolder => FileModel.IsFolder; 62 | 63 | public bool PathContains(PathTagModel pathTag) => FileModel.PathContains(pathTag); 64 | 65 | public IEnumerable GetAncestorTags(TagViewModel parentTag) => FileModel.GetAncestorTags(parentTag); 66 | 67 | #endregion 68 | 69 | private FileSystemInfo FileSystemInfo 70 | { 71 | get 72 | { 73 | if (!_fileSystemInfo.TryGetTarget(out var value) || value.FullName != FileModel.FullName) 74 | _fileSystemInfo.SetTarget(value = FileModel.IsFolder ? new DirectoryInfo(FileModel.FullName) : new FileInfo(FileModel.FullName)); 75 | return value; 76 | } 77 | } 78 | 79 | public ImageSource Icon => this.GetIcon(); 80 | 81 | public string DateOfModification => FileModel.Exists ? FileSystemInfo.LastWriteTime.ToString(CultureInfo.CurrentCulture) : ""; 82 | 83 | public string Size => FileModel is { Exists: true, IsFolder: false } ? FileSystemHelper.CountSize((FileInfo)FileSystemInfo) : ""; 84 | 85 | public static bool IsValidPath(string path) => FileModel.IsValidPath(path); 86 | 87 | public void IconChanged() => OnPropertyChanged(nameof(Icon)); 88 | 89 | public void TagsChanged() => OnPropertyChanged(nameof(Tags)); 90 | 91 | /// 92 | /// 表示拥有提供的标签
93 | /// 表示拥有的标签是提供的标签的子标签
94 | /// 表示既不拥有提供的标签,拥有的标签也不是提供标签的子标签 95 | ///
96 | public bool? Selected { get; private set; } 97 | 98 | /// 99 | /// 表示拥有提供的标签
100 | /// 表示拥有的标签是提供的标签的子标签
101 | /// 表示既不拥有提供的标签,拥有的标签也不是提供标签的子标签 102 | ///
103 | public bool? SelectedOriginal { get; } 104 | 105 | public void SelectedFlip() 106 | { 107 | Selected = Selected == SelectedOriginal ? Selected is false : SelectedOriginal; 108 | OnPropertyChanged(nameof(Selected)); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /TagsTree/Views/ViewModels/FilesObserverViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Collections.Specialized; 3 | using System.Linq; 4 | using CommunityToolkit.Mvvm.ComponentModel; 5 | using TagsTree.Models; 6 | 7 | namespace TagsTree.Views.ViewModels; 8 | 9 | public partial class FilesObserverViewModel : ObservableObject 10 | { 11 | public ObservableCollection FilesChangedList { get; } 12 | 13 | public int FirstId => FilesChangedList.Count is 0 ? 0 : FilesChangedList[0].Id; 14 | 15 | public int LastId => FilesChangedList.Count is 0 ? 1 : FilesChangedList[^1].Id; 16 | 17 | public FilesObserverViewModel(ObservableCollection filesChangedList) 18 | { 19 | FilesChangedList = filesChangedList; 20 | FilesChangedList.CollectionChanged += (_, e) => 21 | { 22 | if (e.Action is NotifyCollectionChangedAction.Remove) 23 | FileChanged.Num = FilesChangedList.LastOrDefault() is { } fileChanged ? fileChanged.Id + 1 : 1; 24 | IsSaveEnabled = true; 25 | OnPropertyChanged(nameof(FirstId)); 26 | OnPropertyChanged(nameof(LastId)); 27 | OnPropertyChanged(nameof(IsListNotEmpty)); 28 | OnPropertyChanged(nameof(IsMultipleItems)); 29 | }; 30 | } 31 | 32 | [ObservableProperty] private bool _isSaveEnabled; 33 | 34 | public bool IsListNotEmpty => FilesChangedList.Count is not 0; 35 | 36 | public bool IsMultipleItems => FilesChangedList.Count > 1; 37 | } 38 | -------------------------------------------------------------------------------- /TagsTree/Views/ViewModels/SelectTagToEditPageViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using CommunityToolkit.Mvvm.ComponentModel; 3 | 4 | namespace TagsTree.Views.ViewModels; 5 | 6 | public partial class SelectTagToEditPageViewModel : ObservableObject 7 | { 8 | [ObservableProperty] private ObservableCollection _allTags = AppContext.Tags.TagsTree.SubTags; 9 | 10 | [ObservableProperty] private string _path = ""; 11 | } 12 | -------------------------------------------------------------------------------- /TagsTree/Views/ViewModels/SettingsViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using CommunityToolkit.Mvvm.ComponentModel; 3 | using Microsoft.UI.Xaml.Controls; 4 | using WinUI3Utilities.Attributes; 5 | 6 | namespace TagsTree.Views.ViewModels; 7 | 8 | [SettingsViewModel(nameof(AppConfig))] 9 | public partial class SettingsViewModel : ObservableObject 10 | { 11 | public bool ConfigSet => Directory.Exists(LibraryPath); 12 | 13 | public bool IsFileObserverItemEnabled => AppConfig.FilesObserverEnabled && ConfigSet; 14 | 15 | /// 16 | /// 事件响应时还没有改变绑定的值,所以直接在绑定值里调用事件 17 | /// 18 | public bool FilesObserverEnabled 19 | { 20 | get => AppConfig.FilesObserverEnabled; 21 | set 22 | { 23 | _ = SetProperty(AppConfig.FilesObserverEnabled, value, AppConfig, (setting, @value) => setting.FilesObserverEnabled = @value); 24 | OnPropertyChanged(nameof(IsFileObserverItemEnabled)); 25 | } 26 | } 27 | 28 | public void ConfigSetChanged() 29 | { 30 | OnPropertyChanged(nameof(ConfigSet)); 31 | OnPropertyChanged(nameof(IsFileObserverItemEnabled)); 32 | } 33 | 34 | public AppConfig AppConfig => AppContext.AppConfig; 35 | } 36 | -------------------------------------------------------------------------------- /TagsTree/Views/ViewModels/TagEditFilesViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using CommunityToolkit.Mvvm.ComponentModel; 4 | 5 | namespace TagsTree.Views.ViewModels; 6 | 7 | public partial class TagEditFilesViewModel : ObservableObject 8 | { 9 | [ObservableProperty] 10 | [NotifyPropertyChangedFor(nameof(Tags))] 11 | private TagViewModel _tagViewModel = null!; 12 | 13 | public IEnumerable Tags => (TagViewModel?.FullName + '\\').Split('\\'); 14 | 15 | [ObservableProperty] private ObservableCollection _fileViewModels = []; 16 | } 17 | -------------------------------------------------------------------------------- /TagsTree/Views/ViewModels/TagSearchFilesViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using CommunityToolkit.Mvvm.ComponentModel; 3 | 4 | namespace TagsTree.Views.ViewModels; 5 | 6 | public partial class TagSearchFilesViewModel : ObservableObject 7 | { 8 | private ObservableCollection _resultCallBack = []; 9 | 10 | [ObservableProperty] private ObservableCollection _fileViewModels = []; 11 | 12 | public ObservableCollection ResultCallBack 13 | { 14 | get => _resultCallBack; 15 | set 16 | { 17 | if (Equals(_resultCallBack, value)) 18 | return; 19 | _resultCallBack = value; 20 | FileViewModels = value; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /TagsTree/Views/ViewModels/TagViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.Collections.Specialized; 4 | using System.Text.Json.Serialization; 5 | using CommunityToolkit.Mvvm.ComponentModel; 6 | using TagsTree.Models; 7 | using WinUI3Utilities; 8 | 9 | namespace TagsTree.Views.ViewModels; 10 | 11 | [INotifyPropertyChanged] 12 | public partial class TagViewModel : TagModel 13 | { 14 | public new string Name 15 | { 16 | get => base.Name; 17 | set 18 | { 19 | if (base.Name == value) 20 | return; 21 | base.Name = value; 22 | OnPropertyChanged(); 23 | } 24 | } 25 | 26 | [JsonIgnore] 27 | public TagViewModel? Parent 28 | { 29 | get => (TagViewModel?)BaseParent; 30 | set => BaseParent = value; 31 | } 32 | 33 | public ObservableCollection SubTags { get; set; } = []; 34 | 35 | /// 36 | /// 反序列化专用,不要调用该构造器 37 | /// 38 | [JsonConstructor] 39 | public TagViewModel(int id, string name, ObservableCollection? subTags = null) : base(id, name) => InitializeSubTags(subTags); 40 | 41 | /// 42 | /// 创建新的 43 | /// 44 | /// 标签名 45 | /// 标签路径 46 | /// 子标签 47 | public TagViewModel(string name, TagViewModel? parent, ObservableCollection? subTags = null) : base(name, parent) => InitializeSubTags(subTags); 48 | 49 | private void InitializeSubTags(ObservableCollection? subTags = null) 50 | { 51 | SubTags.CollectionChanged += OnSubTagsCollectionChanged; 52 | if (subTags is not null) 53 | foreach (var subTag in subTags) 54 | SubTags.Add(subTag); 55 | } 56 | 57 | /// 58 | /// 集合改变时候更改父标签 59 | /// 60 | /// 61 | private void OnSubTagsCollectionChanged(object? o, NotifyCollectionChangedEventArgs e) 62 | { 63 | switch (e.Action) 64 | { 65 | case NotifyCollectionChangedAction.Add: 66 | if (e.NewItems is not null) 67 | foreach (TagViewModel newItem in e.NewItems) 68 | newItem.Parent = this; 69 | break; 70 | case NotifyCollectionChangedAction.Remove: 71 | if (e.OldItems is not null) 72 | foreach (TagViewModel newItem in e.OldItems) 73 | newItem.Parent = null; 74 | break; 75 | case NotifyCollectionChangedAction.Replace: 76 | case NotifyCollectionChangedAction.Reset: 77 | if (e.OldItems is not null) 78 | foreach (TagViewModel newItem in e.OldItems) 79 | newItem.Parent = null; 80 | if (e.NewItems is not null) 81 | foreach (TagViewModel newItem in e.NewItems) 82 | newItem.Parent = this; 83 | break; 84 | case NotifyCollectionChangedAction.Move: 85 | break; 86 | default: 87 | ThrowHelper.ArgumentOutOfRange(e); 88 | break; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /TagsTree/Views/ViewModels/TagsManagerViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | using TagsTree.Models; 3 | 4 | namespace TagsTree.Views.ViewModels; 5 | 6 | public sealed partial class TagsManagerViewModel : ObservableObject 7 | { 8 | public TagsManagerViewModel() 9 | { 10 | TagsSource.DeserializeTree(AppContext.TagsPath); 11 | TagsSource.LoadDictionary(); 12 | } 13 | 14 | public TagsTreeDictionary TagsSource { get; } = new(); 15 | 16 | [ObservableProperty] private string _name = ""; 17 | 18 | [ObservableProperty] private string _path = ""; 19 | 20 | public bool CanPaste => ClipBoard is not null; 21 | 22 | [ObservableProperty] 23 | [NotifyPropertyChangedFor(nameof(CanPaste))] 24 | private TagViewModel? _clipBoard; 25 | 26 | [ObservableProperty] private bool _isSaveEnabled; 27 | } 28 | -------------------------------------------------------------------------------- /TagsTree/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | true/PM 22 | PerMonitorV2, PerMonitor 23 | 24 | 25 | 26 | --------------------------------------------------------------------------------