44using CommunityToolkit . Mvvm . Input ;
55using Microsoft . EntityFrameworkCore ;
66using System ;
7+ using System . Collections . Generic ;
78using System . Collections . ObjectModel ;
89using System . Linq ;
910using System . Threading ;
@@ -17,6 +18,10 @@ public partial class MainWindowViewModel : ViewModelBase
1718 private readonly IFolderPickerService _folderPickerService ;
1819 private readonly IDbContextFactory < FileAppDbContext > _dbContextFactory ;
1920 private CancellationTokenSource ? _cts ;
21+ // Guards against cascading reloads when programmatically resetting CurrentPage
22+ private bool _suppressPageReload ;
23+
24+ public static IReadOnlyList < int > PageSizes { get ; } = [ 25 , 50 , 75 , 100 , 125 , 150 , 175 , 200 ] ;
2025
2126 [ ObservableProperty ]
2227 [ NotifyPropertyChangedFor ( nameof ( CanScan ) ) ]
@@ -36,6 +41,32 @@ public partial class MainWindowViewModel : ViewModelBase
3641 [ ObservableProperty ]
3742 private int _totalFilesProcessed ;
3843
44+ [ ObservableProperty ]
45+ [ NotifyPropertyChangedFor ( nameof ( TotalPages ) , nameof ( PagingInfo ) ) ]
46+ [ NotifyCanExecuteChangedFor ( nameof ( FirstPageCommand ) ) ]
47+ [ NotifyCanExecuteChangedFor ( nameof ( PreviousPageCommand ) ) ]
48+ [ NotifyCanExecuteChangedFor ( nameof ( NextPageCommand ) ) ]
49+ [ NotifyCanExecuteChangedFor ( nameof ( LastPageCommand ) ) ]
50+ private int _pageSize = 50 ;
51+
52+ [ ObservableProperty ]
53+ [ NotifyPropertyChangedFor ( nameof ( PagingInfo ) ) ]
54+ [ NotifyCanExecuteChangedFor ( nameof ( FirstPageCommand ) ) ]
55+ [ NotifyCanExecuteChangedFor ( nameof ( PreviousPageCommand ) ) ]
56+ [ NotifyCanExecuteChangedFor ( nameof ( NextPageCommand ) ) ]
57+ [ NotifyCanExecuteChangedFor ( nameof ( LastPageCommand ) ) ]
58+ private int _currentPage = 1 ;
59+
60+ [ ObservableProperty ]
61+ [ NotifyPropertyChangedFor ( nameof ( TotalPages ) , nameof ( PagingInfo ) ) ]
62+ [ NotifyCanExecuteChangedFor ( nameof ( NextPageCommand ) ) ]
63+ [ NotifyCanExecuteChangedFor ( nameof ( LastPageCommand ) ) ]
64+ private int _totalFileCount ;
65+
66+ public int TotalPages => TotalFileCount == 0 ? 1 : ( int ) Math . Ceiling ( ( double ) TotalFileCount / PageSize ) ;
67+
68+ public string PagingInfo => $ "PAGE { CurrentPage } OF { TotalPages } [{ TotalFileCount } FILES]";
69+
3970 public bool CanScan => ! IsScanning && ! string . IsNullOrWhiteSpace ( SelectedFolderPath ) ;
4071
4172 public ObservableCollection < string > StatusMessages { get ; } = [ ] ;
@@ -71,7 +102,11 @@ private async Task StartScan()
71102 StatusMessages . Clear ( ) ;
72103 ScannedFiles . Clear ( ) ;
73104 TotalFilesProcessed = 0 ;
105+ TotalFileCount = 0 ;
74106 CurrentScanFolder = string . Empty ;
107+ _suppressPageReload = true ;
108+ CurrentPage = 1 ;
109+ _suppressPageReload = false ;
75110
76111 _cts = new CancellationTokenSource ( ) ;
77112
@@ -107,19 +142,65 @@ private void Cancel()
107142 _cts ? . Cancel ( ) ;
108143 }
109144
145+ [ RelayCommand ( CanExecute = nameof ( CanGoFirst ) ) ]
146+ private void FirstPage ( ) { CurrentPage = 1 ; }
147+ private bool CanGoFirst ( ) => CurrentPage > 1 ;
148+
149+ [ RelayCommand ( CanExecute = nameof ( CanGoPrevious ) ) ]
150+ private void PreviousPage ( ) { CurrentPage -- ; }
151+ private bool CanGoPrevious ( ) => CurrentPage > 1 ;
152+
153+ [ RelayCommand ( CanExecute = nameof ( CanGoNext ) ) ]
154+ private void NextPage ( ) { CurrentPage ++ ; }
155+ private bool CanGoNext ( ) => CurrentPage < TotalPages ;
156+
157+ [ RelayCommand ( CanExecute = nameof ( CanGoLast ) ) ]
158+ private void LastPage ( ) { CurrentPage = TotalPages ; }
159+ private bool CanGoLast ( ) => CurrentPage < TotalPages ;
160+
161+ partial void OnCurrentPageChanged ( int value )
162+ {
163+ if ( ! _suppressPageReload )
164+ _ = LoadScannedFilesAsync ( ) ;
165+ }
166+
167+ partial void OnPageSizeChanged ( int value )
168+ {
169+ _suppressPageReload = true ;
170+ CurrentPage = 1 ;
171+ _suppressPageReload = false ;
172+ _ = LoadScannedFilesAsync ( ) ;
173+ }
174+
110175 private async Task LoadScannedFilesAsync ( )
111176 {
112- var root = SelectedFolderPath ;
113- // Normalise so the StartsWith check works regardless of trailing separator
114- var prefix = root . TrimEnd ( System . IO . Path . DirectorySeparatorChar ,
115- System . IO . Path . AltDirectorySeparatorChar )
177+ if ( string . IsNullOrWhiteSpace ( SelectedFolderPath ) )
178+ return ;
179+
180+ var prefix = SelectedFolderPath . TrimEnd ( System . IO . Path . DirectorySeparatorChar ,
181+ System . IO . Path . AltDirectorySeparatorChar )
116182 + System . IO . Path . DirectorySeparatorChar ;
117183
118184 await using var db = await _dbContextFactory . CreateDbContextAsync ( ) ;
119- var files = await db . ScannedFiles
185+
186+ var query = db . ScannedFiles
120187 . Where ( f => f . FullPath . StartsWith ( prefix ) )
121188 . OrderBy ( f => f . FolderPath )
122- . ThenBy ( f => f . FileName )
189+ . ThenBy ( f => f . FileName ) ;
190+
191+ TotalFileCount = await query . CountAsync ( ) ;
192+
193+ // Clamp current page if the page count shrank (e.g. after a page-size increase)
194+ if ( CurrentPage > TotalPages )
195+ {
196+ _suppressPageReload = true ;
197+ CurrentPage = TotalPages ;
198+ _suppressPageReload = false ;
199+ }
200+
201+ var files = await query
202+ . Skip ( ( CurrentPage - 1 ) * PageSize )
203+ . Take ( PageSize )
123204 . ToListAsync ( ) ;
124205
125206 ScannedFiles . Clear ( ) ;
0 commit comments