@@ -22,8 +22,6 @@ namespace NETworkManager.ViewModels;
2222public class IPScannerHostViewModel : ViewModelBase , IProfileManager
2323{
2424 #region Variables
25-
26- private readonly IDialogCoordinator _dialogCoordinator ;
2725 private readonly DispatcherTimer _searchDispatcherTimer = new ( ) ;
2826
2927 public IInterTabClient InterTabClient { get ; }
@@ -129,6 +127,70 @@ public bool IsSearching
129127 OnPropertyChanged ( ) ;
130128 }
131129 }
130+
131+ private bool _profileFilterIsOpen ;
132+
133+ public bool ProfileFilterIsOpen
134+ {
135+ get => _profileFilterIsOpen ;
136+ set
137+ {
138+ if ( value == _profileFilterIsOpen )
139+ return ;
140+
141+ _profileFilterIsOpen = value ;
142+ OnPropertyChanged ( ) ;
143+ }
144+ }
145+
146+ public ICollectionView ProfileFilterTagsView { get ; }
147+
148+ private ObservableCollection < ProfileFilterTagsInfo > ProfileFilterTags { get ; } = [ ] ;
149+
150+ private bool _profileFilterTagsMatchAny = GlobalStaticConfiguration . Profile_TagsMatchAny ;
151+
152+ public bool ProfileFilterTagsMatchAny
153+ {
154+ get => _profileFilterTagsMatchAny ;
155+ set
156+ {
157+ if ( value == _profileFilterTagsMatchAny )
158+ return ;
159+
160+ _profileFilterTagsMatchAny = value ;
161+ OnPropertyChanged ( ) ;
162+ }
163+ }
164+
165+ private bool _profileFilterTagsMatchAll ;
166+
167+ public bool ProfileFilterTagsMatchAll
168+ {
169+ get => _profileFilterTagsMatchAll ;
170+ set
171+ {
172+ if ( value == _profileFilterTagsMatchAll )
173+ return ;
174+
175+ _profileFilterTagsMatchAll = value ;
176+ OnPropertyChanged ( ) ;
177+ }
178+ }
179+
180+ private bool _isProfileFilterSet ;
181+
182+ public bool IsProfileFilterSet
183+ {
184+ get => _isProfileFilterSet ;
185+ set
186+ {
187+ if ( value == _isProfileFilterSet )
188+ return ;
189+
190+ _isProfileFilterSet = value ;
191+ OnPropertyChanged ( ) ;
192+ }
193+ }
132194
133195 private bool _canProfileWidthChange = true ;
134196 private double _tempProfileWidth ;
@@ -184,14 +246,12 @@ public GridLength ProfileWidth
184246
185247 #region Constructor, load settings
186248
187- public IPScannerHostViewModel ( IDialogCoordinator instance )
249+ public IPScannerHostViewModel ( )
188250 {
189251 _isLoading = true ;
190252
191- _dialogCoordinator = instance ;
192-
193253 InterTabClient = new DragablzInterTabClient ( ApplicationName . IPScanner ) ;
194- InterTabPartition = ApplicationName . IPScanner . ToString ( ) ;
254+ InterTabPartition = nameof ( ApplicationName . IPScanner ) ;
195255
196256 var tabId = Guid . NewGuid ( ) ;
197257
@@ -201,7 +261,12 @@ public IPScannerHostViewModel(IDialogCoordinator instance)
201261 ] ;
202262
203263 // Profiles
204- SetProfilesView ( ) ;
264+ CreateTags ( ) ;
265+
266+ ProfileFilterTagsView = CollectionViewSource . GetDefaultView ( ProfileFilterTags ) ;
267+ ProfileFilterTagsView . SortDescriptions . Add ( new SortDescription ( nameof ( ProfileFilterTagsInfo . Name ) , ListSortDirection . Ascending ) ) ;
268+
269+ SetProfilesView ( new ProfileFilterInfo ( ) ) ;
205270
206271 ProfileManager . OnProfilesUpdated += ProfileManager_OnProfilesUpdated ;
207272
@@ -297,6 +362,36 @@ private void ClearSearchAction()
297362 {
298363 Search = string . Empty ;
299364 }
365+
366+ public ICommand OpenProfileFilterCommand => new RelayCommand ( _ => OpenProfileFilterAction ( ) ) ;
367+
368+ private void OpenProfileFilterAction ( )
369+ {
370+ ProfileFilterIsOpen = true ;
371+ }
372+
373+ public ICommand ApplyProfileFilterCommand => new RelayCommand ( _ => ApplyProfileFilterAction ( ) ) ;
374+
375+ private void ApplyProfileFilterAction ( )
376+ {
377+ RefreshProfiles ( ) ;
378+
379+ IsProfileFilterSet = true ;
380+ ProfileFilterIsOpen = false ;
381+ }
382+
383+ public ICommand ClearProfileFilterCommand => new RelayCommand ( _ => ClearProfileFilterAction ( ) ) ;
384+
385+ private void ClearProfileFilterAction ( )
386+ {
387+ foreach ( var tag in ProfileFilterTags )
388+ tag . IsSelected = false ;
389+
390+ RefreshProfiles ( ) ;
391+
392+ IsProfileFilterSet = false ;
393+ ProfileFilterIsOpen = false ;
394+ }
300395
301396 public ItemActionCallback CloseItemCommand => CloseItemAction ;
302397
@@ -364,37 +459,43 @@ public void OnViewHide()
364459 {
365460 _isViewActive = false ;
366461 }
367-
368- private void SetProfilesView ( ProfileInfo profile = null )
462+
463+ private void CreateTags ( )
369464 {
370- Profiles = new CollectionViewSource
371- {
372- Source = ProfileManager . Groups . SelectMany ( x => x . Profiles ) . Where ( x => x . IPScanner_Enabled )
373- . OrderBy ( x => x . Group ) . ThenBy ( x => x . Name )
374- } . View ;
465+ var tags = ProfileManager . Groups . SelectMany ( x => x . Profiles ) . Where ( x => x . IPScanner_Enabled ) . SelectMany ( x => x . TagsCollection ) . Distinct ( ) . ToList ( ) ;
375466
376- Profiles . GroupDescriptions . Add ( new PropertyGroupDescription ( nameof ( ProfileInfo . Group ) ) ) ;
467+ var tagSet = new HashSet < string > ( tags ) ;
377468
378- Profiles . Filter = o =>
469+ for ( var i = ProfileFilterTags . Count - 1 ; i >= 0 ; i -- )
379470 {
380- if ( string . IsNullOrEmpty ( Search ) )
381- return true ;
471+ if ( ! tagSet . Contains ( ProfileFilterTags [ i ] . Name ) )
472+ ProfileFilterTags . RemoveAt ( i ) ;
473+ }
382474
383- if ( o is not ProfileInfo info )
384- return false ;
475+ var existingTagNames = new HashSet < string > ( ProfileFilterTags . Select ( ft => ft . Name ) ) ;
385476
386- var search = Search . Trim ( ) ;
477+ foreach ( var tag in tags . Where ( tag => ! existingTagNames . Contains ( tag ) ) )
478+ {
479+ ProfileFilterTags . Add ( new ProfileFilterTagsInfo ( false , tag ) ) ;
480+ }
481+ }
387482
388- // Search by: Tag=xxx (exact match, ignore case)
389- /*
390- if (search.StartsWith(ProfileManager.TagIdentifier, StringComparison.OrdinalIgnoreCase))
391- return !string.IsNullOrEmpty(info.Tags) && info.PingMonitor_Enabled && info.Tags.Replace(" ", "").Split(';').Any(str => search.Substring(ProfileManager.TagIdentifier.Length, search.Length - ProfileManager.TagIdentifier.Length).Equals(str, StringComparison.OrdinalIgnoreCase));
392- */
483+ private void SetProfilesView ( ProfileFilterInfo filter , ProfileInfo profile = null )
484+ {
485+ Profiles = new CollectionViewSource
486+ {
487+ Source = ProfileManager . Groups . SelectMany ( x => x . Profiles ) . Where ( x => x . IPScanner_Enabled && (
488+ string . IsNullOrEmpty ( filter . Search ) || x . Name . IndexOf ( filter . Search , StringComparison . Ordinal ) > - 1 || x . IPScanner_HostOrIPRange . IndexOf ( filter . Search , StringComparison . Ordinal ) > - 1 ) && (
489+ // If no tags are selected, show all profiles
490+ ( ! filter . Tags . Any ( ) ) ||
491+ // Any tag can match
492+ ( filter . TagsFilterMatch == ProfileFilterTagsMatch . Any && filter . Tags . Any ( tag => x . TagsCollection . Contains ( tag ) ) ) ||
493+ // All tags must match
494+ ( filter . TagsFilterMatch == ProfileFilterTagsMatch . All && filter . Tags . All ( tag => x . TagsCollection . Contains ( tag ) ) ) )
495+ ) . OrderBy ( x => x . Group ) . ThenBy ( x => x . Name )
496+ } . View ;
393497
394- // Search by: Name, IPScanner_HostOrIPRange
395- return info . Name . IndexOf ( search , StringComparison . OrdinalIgnoreCase ) > - 1 ||
396- info . IPScanner_HostOrIPRange . IndexOf ( search , StringComparison . OrdinalIgnoreCase ) > - 1 ;
397- } ;
498+ Profiles . GroupDescriptions . Add ( new PropertyGroupDescription ( nameof ( ProfileInfo . Group ) ) ) ;
398499
399500 // Set specific profile or first if null
400501 SelectedProfile = null ;
@@ -411,7 +512,12 @@ private void RefreshProfiles()
411512 if ( ! _isViewActive )
412513 return ;
413514
414- SetProfilesView ( SelectedProfile ) ;
515+ SetProfilesView ( new ProfileFilterInfo
516+ {
517+ Search = Search ,
518+ Tags = [ .. ProfileFilterTags . Where ( x => x . IsSelected ) . Select ( x => x . Name ) ] ,
519+ TagsFilterMatch = ProfileFilterTagsMatchAny ? ProfileFilterTagsMatch . Any : ProfileFilterTagsMatch . All
520+ } , SelectedProfile ) ;
415521 }
416522
417523 #endregion
@@ -420,6 +526,8 @@ private void RefreshProfiles()
420526
421527 private void ProfileManager_OnProfilesUpdated ( object sender , EventArgs e )
422528 {
529+ CreateTags ( ) ;
530+
423531 RefreshProfiles ( ) ;
424532 }
425533
0 commit comments