├── .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 |
--------------------------------------------------------------------------------