Fix fewest-viewers sort via GQL VIEWER_COUNT_ASC.
All checks were successful
Actions probe / probe (push) Successful in 0s
Release APK / Build & publish release (push) Successful in 8m16s

Helix directories are viewer-desc only; local reverse of top pages wrongly showed high-viewer streams for ascending sort.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
copilot 2026-10-01 12:03:12 +02:00
parent 47150d0ab3
commit 5a787e0134
4 changed files with 68 additions and 44 deletions

View file

@ -260,12 +260,6 @@ abstract class ListStoreBase with Store {
try {
final languages = _languageFilter;
final isAsc = sortOrder == StreamViewerSort.viewersAsc;
// Asc needs a deeper Helix scan so client-side re-sort can surface
// lower-viewer streams (Helix itself is always viewer-desc).
final minCount = isAsc ? 80 : 50;
final maxPages = isAsc
? (_streamsCursor == null ? 20 : 10)
: (_streamsCursor == null ? 12 : 8);
final StreamsTwitch newStreams;
switch (listType) {
@ -276,36 +270,56 @@ abstract class ListStoreBase with Store {
);
break;
case ListType.top:
newStreams = await twitchApi.getDirectoryStreams(
languages: languages,
cursor: _streamsCursor,
minCount: minCount,
maxPages: maxPages,
);
if (isAsc && twitchGqlApi != null) {
// Helix is always viewer-desc — fewest must be requested via GQL.
newStreams = await twitchGqlApi!.getStreams(
sort: StreamViewerSort.viewersAsc,
languages: languages.isEmpty ? null : languages,
cursor: _streamsCursor,
);
} else {
newStreams = await twitchApi.getDirectoryStreams(
languages: languages,
cursor: _streamsCursor,
);
}
break;
case ListType.category:
newStreams = await twitchApi.getDirectoryStreams(
languages: languages,
gameId: categoryId!,
cursor: _streamsCursor,
minCount: minCount,
maxPages: maxPages,
);
if (isAsc && twitchGqlApi != null) {
newStreams = await twitchGqlApi!.getGameStreams(
gameId: categoryId!,
sort: StreamViewerSort.viewersAsc,
languages: languages.isEmpty ? null : languages,
cursor: _streamsCursor,
);
} else {
newStreams = await twitchApi.getDirectoryStreams(
languages: languages,
gameId: categoryId!,
cursor: _streamsCursor,
);
}
break;
case ListType.tag:
// Helix + client tag filter (GQL freeformTags can't paginate without
// a non-bot Client-Integrity token).
newStreams = await twitchApi.getDirectoryStreams(
languages: languages,
freeformTag: freeformTag,
cursor: _streamsCursor,
minCount: minCount,
maxPages: maxPages,
);
if (isAsc && twitchGqlApi != null) {
newStreams = await twitchGqlApi!.getStreams(
freeformTags: [freeformTag!],
sort: StreamViewerSort.viewersAsc,
languages: languages.isEmpty ? null : languages,
cursor: _streamsCursor,
);
} else {
// Desc: Helix + tag filter (GQL page-2 needs Client-Integrity).
newStreams = await twitchApi.getDirectoryStreams(
languages: languages,
freeformTag: freeformTag,
cursor: _streamsCursor,
);
}
break;
}
// Empty continuation ⇒ end of directory.
// Empty continuation ⇒ end of directory (incl. GQL integrity dead-ends).
final isContinuation = _streamsCursor != null;
if (isContinuation && newStreams.data.isEmpty) {
_streamsCursor = null;
@ -313,18 +327,25 @@ abstract class ListStoreBase with Store {
return;
}
// Keep API order. Only sanitize each ASC page — Twitch ASC can briefly
// mis-order within a page; never re-sort a Helix desc directory locally.
final pageData = isAsc
? (List<StreamTwitch>.of(newStreams.data)..sort(
(a, b) => a.viewerCount.compareTo(b.viewerCount),
))
: newStreams.data;
if (_streamsCursor == null) {
_allStreams = newStreams.data.asObservable();
_allStreams = pageData.asObservable();
} else {
final existingIds = _allStreams.map((s) => s.userId).toSet();
_allStreams.addAll(
newStreams.data.where((s) => !existingIds.contains(s.userId)),
pageData.where((s) => !existingIds.contains(s.userId)),
);
}
final next = newStreams.pagination['cursor'];
_streamsCursor = (next != null && next.isNotEmpty) ? next : null;
_applyViewerSort();
_error = null;
} on SocketException {
_error = 'Unable to connect to Twitch';
@ -351,15 +372,6 @@ abstract class ListStoreBase with Store {
}
}
void _applyViewerSort() {
final sorted = _allStreams.toList();
sorted.sort((a, b) {
final cmp = a.viewerCount.compareTo(b.viewerCount);
return sortOrder == StreamViewerSort.viewersAsc ? cmp : -cmp;
});
_allStreams = sorted.asObservable();
}
/// Persists viewer sort and reloads via the settings reaction.
@action
Future<void> setSortOrder(StreamViewerSort order) async {