@@ -36,8 +36,9 @@ class FavoritesViewModel extends ChangeNotifier {
3636 Map <FavoriteTypeFilter , List <AggregatedItem >> _rowItems = {};
3737 Map <FavoriteTypeFilter , List <AggregatedItem >> get rowItems => _rowItems;
3838
39- int get totalCount =>
40- _rowItems.values.fold (0 , (sum, list) => sum + list.length);
39+ int get totalCount => _viewStyle == FavoritesViewStyle .home
40+ ? _rowItems.values.fold (0 , (sum, list) => sum + list.length)
41+ : _gridTotalCount;
4142
4243 late LibrarySortBy _sortBy;
4344 LibrarySortBy get sortBy => _sortBy;
@@ -64,6 +65,23 @@ class FavoritesViewModel extends ChangeNotifier {
6465
6566 ImageApi get imageApi => _client.imageApi;
6667
68+ late FavoritesViewStyle _viewStyle;
69+ FavoritesViewStyle get viewStyle => _viewStyle;
70+
71+ List <AggregatedItem > _gridItems = const [];
72+ List <AggregatedItem > get gridItems => _gridItems;
73+
74+ int _gridTotalCount = 0 ;
75+ int get gridTotalCount => _gridTotalCount;
76+
77+ bool _loadingMoreGrid = false ;
78+ bool get loadingMoreGrid => _loadingMoreGrid;
79+
80+ bool get hasMoreGrid => _gridItems.length < _gridTotalCount;
81+
82+ final Map <FavoriteTypeFilter , int > _rowTotalCounts = {};
83+ final Set <FavoriteTypeFilter > _inFlightPagingRowTypes = {};
84+
6785 FavoritesViewModel ({
6886 required MediaServerClient client,
6987 required UserPreferences prefs,
@@ -75,6 +93,7 @@ class FavoritesViewModel extends ChangeNotifier {
7593 _sortDirection = _prefs.get (UserPreferences .librarySortDirection (_prefKey));
7694 _imageType = _prefs.get (UserPreferences .libraryImageType (_prefKey));
7795 _posterSize = _prefs.resolveLibraryPosterSize ();
96+ _viewStyle = _prefs.get (UserPreferences .favoritesViewStyle);
7897 }
7998
8099 void setFocusedItem (AggregatedItem ? item) {
@@ -84,6 +103,13 @@ class FavoritesViewModel extends ChangeNotifier {
84103 if (item != null ) _loadFocusedRatings (item);
85104 }
86105
106+ Future <void > setViewStyle (FavoritesViewStyle value) async {
107+ if (_viewStyle == value) return ;
108+ _viewStyle = value;
109+ await _prefs.set (UserPreferences .favoritesViewStyle, value);
110+ await load ();
111+ }
112+
87113 Future <void > _loadFocusedRatings (AggregatedItem item) async {
88114 if (! _prefs.get (UserPreferences .enableAdditionalRatings)) return ;
89115 var tmdbId = item.tmdbId;
@@ -117,18 +143,28 @@ class FavoritesViewModel extends ChangeNotifier {
117143 Future <void > load () async {
118144 _state = FavoritesState .loading;
119145 _rowItems = {};
146+ _rowTotalCounts.clear ();
147+ _gridItems = const [];
148+ _gridTotalCount = 0 ;
120149 _tmdbIdByItemId.clear ();
121150 notifyListeners ();
122151
123152 try {
124- final results = await Future .wait (
125- rowTypes.map ((type) => _fetchRowItems (type)),
126- );
127- final map = < FavoriteTypeFilter , List <AggregatedItem >> {};
128- for (var i = 0 ; i < rowTypes.length; i++ ) {
129- if (results[i].isNotEmpty) map[rowTypes[i]] = results[i];
153+ if (_viewStyle == FavoritesViewStyle .home) {
154+ final results = await Future .wait (
155+ rowTypes.map ((type) => _fetchRowItems (type, startIndex: 0 )),
156+ );
157+ final map = < FavoriteTypeFilter , List <AggregatedItem >> {};
158+ for (var i = 0 ; i < rowTypes.length; i++ ) {
159+ if (results[i].$1.isNotEmpty) {
160+ map[rowTypes[i]] = results[i].$1;
161+ _rowTotalCounts[rowTypes[i]] = results[i].$2;
162+ }
163+ }
164+ _rowItems = map;
165+ } else {
166+ await _fetchGridPage (0 );
130167 }
131- _rowItems = map;
132168 _state = FavoritesState .ready;
133169 } catch (e) {
134170 _errorMessage = e.toString ();
@@ -137,7 +173,10 @@ class FavoritesViewModel extends ChangeNotifier {
137173 notifyListeners ();
138174 }
139175
140- Future <List <AggregatedItem >> _fetchRowItems (FavoriteTypeFilter type) async {
176+ Future <(List <AggregatedItem >, int )> _fetchRowItems (
177+ FavoriteTypeFilter type, {
178+ int startIndex = 0 ,
179+ }) async {
141180 try {
142181 final sortValue = _sortBy.apiValue;
143182 final sortOrder = _sortDirection == SortDirection .ascending
@@ -148,6 +187,7 @@ class FavoritesViewModel extends ChangeNotifier {
148187 response = await _client.itemsApi.getItems (
149188 sortBy: sortValue,
150189 sortOrder: sortOrder,
190+ startIndex: startIndex,
151191 limit: _rowLimit,
152192 recursive: true ,
153193 isFavorite: true ,
@@ -163,6 +203,7 @@ class FavoritesViewModel extends ChangeNotifier {
163203 response = await _client.itemsApi.getItems (
164204 sortBy: fallbackSort,
165205 sortOrder: sortOrder,
206+ startIndex: startIndex,
166207 limit: _rowLimit,
167208 recursive: true ,
168209 isFavorite: true ,
@@ -172,7 +213,7 @@ class FavoritesViewModel extends ChangeNotifier {
172213 );
173214 }
174215 final rawItems = (response['Items' ] as List ? ) ?? [];
175- return rawItems
216+ final items = rawItems
176217 .cast <Map <String , dynamic >>()
177218 .map (
178219 (raw) => AggregatedItem (
@@ -182,9 +223,109 @@ class FavoritesViewModel extends ChangeNotifier {
182223 ),
183224 )
184225 .toList ();
226+ final totalCount = response['TotalRecordCount' ] as int ? ?? items.length;
227+ return (items, totalCount);
228+ } catch (_) {
229+ return (const < AggregatedItem > [], 0 );
230+ }
231+ }
232+
233+ Future <void > loadMoreForType (FavoriteTypeFilter type) async {
234+ final items = _rowItems[type];
235+ if (items == null || items.isEmpty) return ;
236+ final total = _rowTotalCounts[type] ?? 0 ;
237+ if (items.length >= total) return ;
238+ if (_inFlightPagingRowTypes.contains (type)) return ;
239+
240+ _inFlightPagingRowTypes.add (type);
241+ try {
242+ final currentOffset = items.length;
243+ final (newItems, totalCount) = await _fetchRowItems (type, startIndex: currentOffset);
244+ _rowTotalCounts[type] = totalCount;
245+
246+ if (newItems.isNotEmpty) {
247+ _rowItems[type] = [...items, ...newItems];
248+ notifyListeners ();
249+ }
185250 } catch (_) {
186- return const [];
251+ } finally {
252+ _inFlightPagingRowTypes.remove (type);
253+ }
254+ }
255+
256+ Future <void > _fetchGridPage (int startIndex) async {
257+ final sortValue = _sortBy.apiValue;
258+ final sortOrder = _sortDirection == SortDirection .ascending
259+ ? 'Ascending'
260+ : 'Descending' ;
261+ final pageSize = startIndex == 0 ? 75 : 48 ;
262+
263+ Map <String , dynamic > response;
264+ try {
265+ response = await _client.itemsApi.getItems (
266+ sortBy: sortValue,
267+ sortOrder: sortOrder,
268+ startIndex: startIndex,
269+ limit: pageSize,
270+ recursive: true ,
271+ isFavorite: true ,
272+ fields: _browseFields,
273+ );
274+ } on DioException catch (e) {
275+ final statusCode = e.response? .statusCode ?? 0 ;
276+ if (statusCode < 500 ) rethrow ;
277+ final fallbackSort = sortValue.toLowerCase ().contains ('isfolder' )
278+ ? 'SortName'
279+ : sortValue;
280+ response = await _client.itemsApi.getItems (
281+ sortBy: fallbackSort,
282+ sortOrder: sortOrder,
283+ startIndex: startIndex,
284+ limit: pageSize,
285+ recursive: true ,
286+ isFavorite: true ,
287+ fields: _browseFields,
288+ enableTotalRecordCount: false ,
289+ );
290+ }
291+
292+ final rawItems = (response['Items' ] as List ? ) ?? [];
293+ final totalFromServer = response['TotalRecordCount' ] as int ? ;
294+ if (totalFromServer != null ) {
295+ _gridTotalCount = totalFromServer;
296+ } else {
297+ _gridTotalCount = startIndex + rawItems.length + (rawItems.length == pageSize ? 1 : 0 );
187298 }
299+
300+ final mapped = rawItems
301+ .cast <Map <String , dynamic >>()
302+ .map (
303+ (raw) => AggregatedItem (
304+ id: raw['Id' ] as String ,
305+ serverId: _client.baseUrl,
306+ rawData: raw,
307+ ),
308+ )
309+ .toList ();
310+
311+ if (startIndex == 0 ) {
312+ _gridItems = mapped;
313+ } else {
314+ _gridItems = [..._gridItems, ...mapped];
315+ }
316+ }
317+
318+ Future <void > loadMoreGrid () async {
319+ if (_loadingMoreGrid || ! hasMoreGrid) return ;
320+ _loadingMoreGrid = true ;
321+ notifyListeners ();
322+
323+ try {
324+ await _fetchGridPage (_gridItems.length);
325+ } catch (_) {}
326+
327+ _loadingMoreGrid = false ;
328+ notifyListeners ();
188329 }
189330
190331 Future <void > setSortBy (LibrarySortBy value) async {
0 commit comments