Fix fewest-viewers sort via GQL VIEWER_COUNT_ASC.
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:
parent
47150d0ab3
commit
5a787e0134
4 changed files with 68 additions and 44 deletions
|
|
@ -50,8 +50,10 @@ Key code: `lib/apis/twitch_gql_api.dart`, `lib/screens/home/search/`, `lib/widge
|
|||
Settings → General → Stream languages (multi-select chips). Default `de`. Applied to Top, category, and tag lists.
|
||||
|
||||
- Helix `language=` when one language; multi-language / tags via Helix over-fetch + client filter
|
||||
- Viewer sort is persisted (`streamViewerSort`); lists are re-sorted client-side after each fetch (Twitch GQL `VIEWER_COUNT_ASC` is unreliable)
|
||||
- Tag directories use Helix `tags[]` matching (not GQL) so infinite scroll works without Client-Integrity
|
||||
- Viewer sort is persisted (`streamViewerSort`)
|
||||
- **Most viewers (desc):** Helix directory order (paginates)
|
||||
- **Fewest viewers (asc):** Twitch GQL `VIEWER_COUNT_ASC` at request time (Helix cannot ascend; local reverse of Helix top pages was wrong)
|
||||
- Tag directories (desc): Helix `tags[]` matching for scroll; asc uses GQL with the same tag/language filters
|
||||
|
||||
## Build
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,13 @@
|
|||
## v5.2.6
|
||||
|
||||
October 1, 2026
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed “fewest viewers” showing high-viewer streams by sorting via Twitch GQL `VIEWER_COUNT_ASC` at request time instead of reversing Helix pages locally.
|
||||
|
||||
---
|
||||
|
||||
## v5.2.1
|
||||
|
||||
September 30, 2026
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
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,
|
||||
minCount: minCount,
|
||||
maxPages: maxPages,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case ListType.category:
|
||||
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,
|
||||
minCount: minCount,
|
||||
maxPages: maxPages,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case ListType.tag:
|
||||
// Helix + client tag filter (GQL freeformTags can't paginate without
|
||||
// a non-bot Client-Integrity token).
|
||||
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,
|
||||
minCount: minCount,
|
||||
maxPages: maxPages,
|
||||
);
|
||||
}
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 5.2.5+75
|
||||
version: 5.2.6+76
|
||||
|
||||
environment:
|
||||
sdk: ^3.9.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue