Fix tag pagination and persist viewer sort.
All checks were successful
Actions probe / probe (push) Successful in 0s
Release APK / Build & publish release (push) Successful in 7m59s

Load tag (and language) directories via Helix with client-side tag matching so scroll can continue past the GQL integrity wall. Re-sort lists locally after each fetch and store streamViewerSort in Settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
copilot 2026-10-01 11:41:25 +02:00
parent 9d392d8d6a
commit 47150d0ab3
12 changed files with 214 additions and 113 deletions

View file

@ -49,11 +49,9 @@ 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. Settings → General → Stream languages (multi-select chips). Default `de`. Applied to Top, category, and tag lists.
- One language: Helix `language=` (paginates cleanly, `first=100`) - Helix `language=` when one language; multi-language / tags via Helix over-fetch + client filter
- Several languages: Helix over-fetch + client filter (still paginated) - Viewer sort is persisted (`streamViewerSort`); lists are re-sorted client-side after each fetch (Twitch GQL `VIEWER_COUNT_ASC` is unreliable)
- Tags / ascending sort: GQL with `languages: [Language!]`; page-2 often hits Twitch integrity — treated as end-of-list after the first page of 30 - Tag directories use Helix `tags[]` matching (not GQL) so infinite scroll works without Client-Integrity
Also fixed infinite scroll: larger Helix pages, defensive cursors, scroll near-end load-more, and stop disposing shared Home scroll controllers.
## Build ## Build

View file

@ -248,29 +248,42 @@ class TwitchApi extends BaseApiClient {
return StreamsTwitch.fromJson(data); return StreamsTwitch.fromJson(data);
} }
/// Fetches Helix stream pages until [minCount] streams match [languages] /// Fetches Helix directory pages until enough streams match [languages] and
/// (or the directory is exhausted). Preserves Helix viewer order. /// optional [freeformTag] (case-insensitive tag name), or the directory ends.
/// ///
/// Used when the user selected more than one language — Helix only accepts a /// Helix is always viewer-desc and supports at most one `language` query
/// single `language` query param. /// param; tags are filtered client-side. Prefer this over GQL for pagination
Future<StreamsTwitch> getStreamsMatchingLanguages({ /// (GQL page-2 requires a non-bot Client-Integrity token).
required List<String> languages, Future<StreamsTwitch> getDirectoryStreams({
List<String> languages = const [],
String? gameId, String? gameId,
String? freeformTag,
String? cursor, String? cursor,
int minCount = 40, int minCount = 50,
int maxPages = 8, int maxPages = 12,
}) async { }) async {
final wanted = languages final wantedLangs = languages
.map((l) => l.trim().toLowerCase()) .map((l) => l.trim().toLowerCase())
.where((l) => l.isNotEmpty) .where((l) => l.isNotEmpty)
.toSet(); .toSet();
if (wanted.isEmpty) { final tagNeedle = freeformTag?.trim().toLowerCase();
return gameId == null final hasTag = tagNeedle != null && tagNeedle.isNotEmpty;
? getTopStreams(cursor: cursor)
: getStreamsUnderCategory(gameId: gameId, cursor: cursor); bool matches(StreamTwitch stream) {
if (wantedLangs.isNotEmpty &&
!wantedLangs.contains(stream.language.toLowerCase())) {
return false;
} }
if (wanted.length == 1) { if (hasTag) {
final lang = wanted.first; final hit = stream.tags.any((t) => t.toLowerCase() == tagNeedle);
if (!hit) return false;
}
return true;
}
// Fast path: no tag filter and 0–1 languages → one Helix call.
if (!hasTag && wantedLangs.length <= 1) {
final lang = wantedLangs.isEmpty ? null : wantedLangs.first;
return gameId == null return gameId == null
? getTopStreams(cursor: cursor, language: lang) ? getTopStreams(cursor: cursor, language: lang)
: getStreamsUnderCategory( : getStreamsUnderCategory(
@ -280,6 +293,11 @@ class TwitchApi extends BaseApiClient {
); );
} }
// When filtering by tag and exactly one language, still pass language to
// Helix so each page is denser in matches.
final helixLanguage =
wantedLangs.length == 1 ? wantedLangs.first : null;
final matched = <StreamTwitch>[]; final matched = <StreamTwitch>[];
String? pageCursor = cursor; String? pageCursor = cursor;
String? nextCursor; String? nextCursor;
@ -287,14 +305,16 @@ class TwitchApi extends BaseApiClient {
while (matched.length < minCount && pages < maxPages) { while (matched.length < minCount && pages < maxPages) {
final page = gameId == null final page = gameId == null
? await getTopStreams(cursor: pageCursor) ? await getTopStreams(cursor: pageCursor, language: helixLanguage)
: await getStreamsUnderCategory(gameId: gameId, cursor: pageCursor); : await getStreamsUnderCategory(
gameId: gameId,
cursor: pageCursor,
language: helixLanguage,
);
pages++; pages++;
nextCursor = page.pagination['cursor']; nextCursor = page.pagination['cursor'];
for (final stream in page.data) { for (final stream in page.data) {
if (wanted.contains(stream.language.toLowerCase())) { if (matches(stream)) matched.add(stream);
matched.add(stream);
}
} }
if (nextCursor == null || page.data.isEmpty) { if (nextCursor == null || page.data.isEmpty) {
nextCursor = null; nextCursor = null;
@ -309,6 +329,22 @@ class TwitchApi extends BaseApiClient {
); );
} }
/// Deprecated name — use [getDirectoryStreams].
Future<StreamsTwitch> getStreamsMatchingLanguages({
required List<String> languages,
String? gameId,
String? cursor,
int minCount = 40,
int maxPages = 8,
}) =>
getDirectoryStreams(
languages: languages,
gameId: gameId,
cursor: cursor,
minCount: minCount,
maxPages: maxPages,
);
/// Returns a [StreamTwitch] object containing the stream info associated with the given [userLogin]. /// Returns a [StreamTwitch] object containing the stream info associated with the given [userLogin].
Future<StreamTwitch> getStream({required String userLogin}) async { Future<StreamTwitch> getStream({required String userLogin}) async {
final data = await get<JsonMap>( final data = await get<JsonMap>(

View file

@ -340,6 +340,7 @@ class TwitchGqlApi extends BaseApiClient {
// Helix-style placeholder so existing thumbnail widgets keep working. // Helix-style placeholder so existing thumbnail widgets keep working.
'https://static-cdn.jtvnw.net/previews-ttv/live_user_$login-{width}x{height}.jpg', 'https://static-cdn.jtvnw.net/previews-ttv/live_user_$login-{width}x{height}.jpg',
language, language,
const [],
); );
} }
} }

View file

@ -19,6 +19,10 @@ class StreamTwitch {
@JsonKey(defaultValue: '') @JsonKey(defaultValue: '')
final String language; final String language;
/// Freeform tags from Helix (e.g. `Deutsch`, `English`).
@JsonKey(defaultValue: <String>[])
final List<String> tags;
const StreamTwitch( const StreamTwitch(
this.userId, this.userId,
this.userLogin, this.userLogin,
@ -30,6 +34,7 @@ class StreamTwitch {
this.startedAt, this.startedAt,
this.thumbnailUrl, [ this.thumbnailUrl, [
this.language = '', this.language = '',
this.tags = const [],
]); ]);
factory StreamTwitch.fromJson(Map<String, dynamic> json) => factory StreamTwitch.fromJson(Map<String, dynamic> json) =>

View file

@ -17,6 +17,7 @@ StreamTwitch _$StreamTwitchFromJson(Map<String, dynamic> json) => StreamTwitch(
json['started_at'] as String, json['started_at'] as String,
json['thumbnail_url'] as String, json['thumbnail_url'] as String,
json['language'] as String? ?? '', json['language'] as String? ?? '',
(json['tags'] as List<dynamic>?)?.map((e) => e as String).toList() ?? [],
); );
StreamsTwitch _$StreamsTwitchFromJson(Map<String, dynamic> json) => StreamsTwitch _$StreamsTwitchFromJson(Map<String, dynamic> json) =>

View file

@ -1,28 +1,24 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:frosty/apis/twitch_gql_api.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:frosty/screens/home/stream_list/stream_list_store.dart'; import 'package:frosty/screens/home/stream_list/stream_list_store.dart';
import 'package:frosty/screens/home/stream_list/streams_list.dart'; import 'package:frosty/screens/home/stream_list/streams_list.dart';
import 'package:frosty/screens/settings/stores/settings_store.dart';
import 'package:frosty/widgets/blurred_container.dart'; import 'package:frosty/widgets/blurred_container.dart';
import 'package:frosty/widgets/stream_sort_button.dart'; import 'package:frosty/widgets/stream_sort_button.dart';
import 'package:provider/provider.dart';
/// Streams matching a freeform tag (search → tap tag). /// Streams matching a freeform tag (search → tap tag).
class TagStreams extends StatefulWidget { class TagStreams extends StatelessWidget {
static const routeName = 'TagStreams'; static const routeName = 'TagStreams';
final String tagName; final String tagName;
const TagStreams({super.key, required this.tagName}); const TagStreams({super.key, required this.tagName});
@override
State<TagStreams> createState() => _TagStreamsState();
}
class _TagStreamsState extends State<TagStreams> {
var _sortOrder = StreamViewerSort.viewersDesc;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final settingsStore = context.watch<SettingsStore>();
return Scaffold( return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor, backgroundColor: theme.scaffoldBackgroundColor,
@ -31,14 +27,15 @@ class _TagStreamsState extends State<TagStreams> {
body: Stack( body: Stack(
children: [ children: [
Positioned.fill( Positioned.fill(
child: StreamsList( child: Observer(
key: ValueKey(_sortOrder), builder: (_) => StreamsList(
listType: ListType.tag, listType: ListType.tag,
freeformTag: widget.tagName, freeformTag: tagName,
sortOrder: _sortOrder, sortOrder: settingsStore.streamViewerSort,
showJumpButton: true, showJumpButton: true,
), ),
), ),
),
Positioned( Positioned(
top: 0, top: 0,
left: 0, left: 0,
@ -61,15 +58,18 @@ class _TagStreamsState extends State<TagStreams> {
), ),
Expanded( Expanded(
child: Text( child: Text(
widget.tagName, tagName,
style: theme.textTheme.titleLarge, style: theme.textTheme.titleLarge,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
StreamSortButton( Observer(
sortOrder: _sortOrder, builder: (_) => StreamSortButton(
onChanged: (order) => setState(() => _sortOrder = order), sortOrder: settingsStore.streamViewerSort,
onChanged: (order) =>
settingsStore.streamViewerSort = order,
),
), ),
], ],
), ),

View file

@ -214,11 +214,18 @@ abstract class ListStoreBase with Store {
_getCategoryDetails(); _getCategoryDetails();
} }
// Reload directory lists when the language filter changes. // Reload directory lists when the language filter or sort changes.
if (listType != ListType.followed) { if (listType != ListType.followed) {
sortOrder = settingsStore.streamViewerSort;
_languageFilterReactionDisposer = reaction( _languageFilterReactionDisposer = reaction(
(_) => settingsStore.streamLanguages.toList(), (_) => [
(_) => refreshStreams(), ...settingsStore.streamLanguages,
settingsStore.streamViewerSort,
],
(_) {
sortOrder = settingsStore.streamViewerSort;
refreshStreams();
},
); );
} }
@ -252,6 +259,14 @@ abstract class ListStoreBase with Store {
try { try {
final languages = _languageFilter; 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; final StreamsTwitch newStreams;
switch (listType) { switch (listType) {
case ListType.followed: case ListType.followed:
@ -261,50 +276,36 @@ abstract class ListStoreBase with Store {
); );
break; break;
case ListType.top: case ListType.top:
// Ascending sort needs GQL (Helix is always desc). newStreams = await twitchApi.getDirectoryStreams(
if (sortOrder == StreamViewerSort.viewersAsc &&
twitchGqlApi != null) {
newStreams = await twitchGqlApi!.getStreams(
sort: sortOrder,
languages: languages.isEmpty ? null : languages,
cursor: _streamsCursor,
);
} else {
newStreams = await twitchApi.getStreamsMatchingLanguages(
languages: languages, languages: languages,
cursor: _streamsCursor, cursor: _streamsCursor,
minCount: minCount,
maxPages: maxPages,
); );
}
break; break;
case ListType.category: case ListType.category:
if (sortOrder == StreamViewerSort.viewersAsc && newStreams = await twitchApi.getDirectoryStreams(
twitchGqlApi != null) {
newStreams = await twitchGqlApi!.getGameStreams(
gameId: categoryId!,
sort: sortOrder,
languages: languages.isEmpty ? null : languages,
cursor: _streamsCursor,
);
} else {
newStreams = await twitchApi.getStreamsMatchingLanguages(
languages: languages, languages: languages,
gameId: categoryId!, gameId: categoryId!,
cursor: _streamsCursor, cursor: _streamsCursor,
minCount: minCount,
maxPages: maxPages,
); );
}
break; break;
case ListType.tag: case ListType.tag:
newStreams = await twitchGqlApi!.getStreams( // Helix + client tag filter (GQL freeformTags can't paginate without
freeformTags: [freeformTag!], // a non-bot Client-Integrity token).
sort: sortOrder, newStreams = await twitchApi.getDirectoryStreams(
languages: languages.isEmpty ? null : languages, languages: languages,
freeformTag: freeformTag,
cursor: _streamsCursor, cursor: _streamsCursor,
minCount: minCount,
maxPages: maxPages,
); );
break; break;
} }
// GQL page-2 often returns empty `data` under IntegrityCheckFailed // Empty continuation ⇒ end of directory.
// without throwing — treat empty continuation pages as end-of-list.
final isContinuation = _streamsCursor != null; final isContinuation = _streamsCursor != null;
if (isContinuation && newStreams.data.isEmpty) { if (isContinuation && newStreams.data.isEmpty) {
_streamsCursor = null; _streamsCursor = null;
@ -315,7 +316,6 @@ abstract class ListStoreBase with Store {
if (_streamsCursor == null) { if (_streamsCursor == null) {
_allStreams = newStreams.data.asObservable(); _allStreams = newStreams.data.asObservable();
} else { } else {
// Dedupe in case of overlapping over-fetch pages.
final existingIds = _allStreams.map((s) => s.userId).toSet(); final existingIds = _allStreams.map((s) => s.userId).toSet();
_allStreams.addAll( _allStreams.addAll(
newStreams.data.where((s) => !existingIds.contains(s.userId)), newStreams.data.where((s) => !existingIds.contains(s.userId)),
@ -324,11 +324,11 @@ abstract class ListStoreBase with Store {
final next = newStreams.pagination['cursor']; final next = newStreams.pagination['cursor'];
_streamsCursor = (next != null && next.isNotEmpty) ? next : null; _streamsCursor = (next != null && next.isNotEmpty) ? next : null;
_applyViewerSort();
_error = null; _error = null;
} on SocketException { } on SocketException {
_error = 'Unable to connect to Twitch'; _error = 'Unable to connect to Twitch';
debugPrint('Streams SocketException: No internet connection'); debugPrint('Streams SocketException: No internet connection');
// Stop retry storms on failed continuations.
if (_streamsCursor != null && _allStreams.isNotEmpty) { if (_streamsCursor != null && _allStreams.isNotEmpty) {
_streamsCursor = null; _streamsCursor = null;
} }
@ -337,7 +337,6 @@ abstract class ListStoreBase with Store {
debugPrint('Streams ApiException: $e'); debugPrint('Streams ApiException: $e');
if (_streamsCursor != null && _allStreams.isNotEmpty) { if (_streamsCursor != null && _allStreams.isNotEmpty) {
_streamsCursor = null; _streamsCursor = null;
// Don't surface integrity / page-2 failures as a full-list error.
if (_allStreams.isNotEmpty) _error = null; if (_allStreams.isNotEmpty) _error = null;
} }
} catch (e) { } catch (e) {
@ -352,12 +351,19 @@ abstract class ListStoreBase with Store {
} }
} }
/// Changes viewer sort and reloads from the first page. 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 @action
Future<void> setSortOrder(StreamViewerSort order) async { Future<void> setSortOrder(StreamViewerSort order) async {
if (sortOrder == order) return; settingsStore.streamViewerSort = order;
sortOrder = order;
await refreshStreams();
} }
@action @action

View file

@ -27,7 +27,7 @@ class CategoryStreams extends StatefulWidget {
} }
class _CategoryStreamsState extends State<CategoryStreams> { class _CategoryStreamsState extends State<CategoryStreams> {
var _sortOrder = StreamViewerSort.viewersDesc; late final _settingsStore = context.read<SettingsStore>();
late final _listStore = ListStore( late final _listStore = ListStore(
listType: ListType.category, listType: ListType.category,
@ -35,7 +35,8 @@ class _CategoryStreamsState extends State<CategoryStreams> {
authStore: context.read<AuthStore>(), authStore: context.read<AuthStore>(),
twitchApi: context.read<TwitchApi>(), twitchApi: context.read<TwitchApi>(),
twitchGqlApi: context.read<TwitchGqlApi>(), twitchGqlApi: context.read<TwitchGqlApi>(),
settingsStore: context.read<SettingsStore>(), settingsStore: _settingsStore,
sortOrder: _settingsStore.streamViewerSort,
); );
@override @override
@ -50,14 +51,15 @@ class _CategoryStreamsState extends State<CategoryStreams> {
children: [ children: [
// Stream list content behind the pinned elements // Stream list content behind the pinned elements
Positioned.fill( Positioned.fill(
child: StreamsList( child: Observer(
key: ValueKey(_sortOrder), builder: (_) => StreamsList(
listType: ListType.category, listType: ListType.category,
categoryId: widget.categoryId, categoryId: widget.categoryId,
sortOrder: _sortOrder, sortOrder: _settingsStore.streamViewerSort,
showJumpButton: true, showJumpButton: true,
), ),
), ),
),
// Single blurred background spanning app bar and category card // Single blurred background spanning app bar and category card
Positioned( Positioned(
top: 0, top: 0,
@ -84,10 +86,12 @@ class _CategoryStreamsState extends State<CategoryStreams> {
onPressed: Navigator.of(context).pop, onPressed: Navigator.of(context).pop,
), ),
const Spacer(), const Spacer(),
StreamSortButton( Observer(
sortOrder: _sortOrder, builder: (_) => StreamSortButton(
sortOrder: _settingsStore.streamViewerSort,
onChanged: (order) => onChanged: (order) =>
setState(() => _sortOrder = order), _settingsStore.streamViewerSort = order,
),
), ),
], ],
), ),

View file

@ -1,11 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:frosty/apis/twitch_gql_api.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:frosty/screens/home/home_store.dart'; import 'package:frosty/screens/home/home_store.dart';
import 'package:frosty/screens/home/stream_list/stream_list_store.dart'; import 'package:frosty/screens/home/stream_list/stream_list_store.dart';
import 'package:frosty/screens/home/stream_list/streams_list.dart'; import 'package:frosty/screens/home/stream_list/streams_list.dart';
import 'package:frosty/screens/home/top/categories/categories.dart'; import 'package:frosty/screens/home/top/categories/categories.dart';
import 'package:frosty/screens/settings/stores/settings_store.dart';
import 'package:frosty/widgets/blurred_container.dart'; import 'package:frosty/widgets/blurred_container.dart';
import 'package:frosty/widgets/stream_sort_button.dart'; import 'package:frosty/widgets/stream_sort_button.dart';
import 'package:provider/provider.dart';
class TopSection extends StatefulWidget { class TopSection extends StatefulWidget {
final HomeStore homeStore; final HomeStore homeStore;
@ -19,7 +21,6 @@ class TopSection extends StatefulWidget {
class _TopSectionState extends State<TopSection> class _TopSectionState extends State<TopSection>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late final _tabBarController = TabController(length: 2, vsync: this); late final _tabBarController = TabController(length: 2, vsync: this);
var _sortOrder = StreamViewerSort.viewersDesc;
@override @override
void initState() { void initState() {
@ -32,6 +33,7 @@ class _TopSectionState extends State<TopSection>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final settingsStore = context.watch<SettingsStore>();
final showSort = _tabBarController.index == 0; final showSort = _tabBarController.index == 0;
return Stack( return Stack(
@ -40,13 +42,14 @@ class _TopSectionState extends State<TopSection>
child: TabBarView( child: TabBarView(
controller: _tabBarController, controller: _tabBarController,
children: [ children: [
StreamsList( Observer(
key: ValueKey(_sortOrder), builder: (_) => StreamsList(
listType: ListType.top, listType: ListType.top,
sortOrder: _sortOrder, sortOrder: settingsStore.streamViewerSort,
scrollController: scrollController:
widget.homeStore.topSectionScrollControllers[0], widget.homeStore.topSectionScrollControllers[0],
), ),
),
Categories( Categories(
scrollController: scrollController:
widget.homeStore.topSectionScrollControllers[1], widget.homeStore.topSectionScrollControllers[1],
@ -78,9 +81,12 @@ class _TopSectionState extends State<TopSection>
), ),
), ),
if (showSort) if (showSort)
StreamSortButton( Observer(
sortOrder: _sortOrder, builder: (_) => StreamSortButton(
onChanged: (order) => setState(() => _sortOrder = order), sortOrder: settingsStore.streamViewerSort,
onChanged: (order) =>
settingsStore.streamViewerSort = order,
),
), ),
], ],
), ),

View file

@ -1,3 +1,4 @@
import 'package:frosty/apis/twitch_gql_api.dart';
import 'package:frosty/screens/channel/chat/stores/chat_tabs_store.dart'; import 'package:frosty/screens/channel/chat/stores/chat_tabs_store.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
@ -60,6 +61,16 @@ abstract class _SettingsStoreBase with Store {
@observable @observable
var streamLanguages = defaultStreamLanguages; var streamLanguages = defaultStreamLanguages;
/// Persistent viewer sort for Top / category / tag directories.
static const defaultStreamViewerSort = StreamViewerSort.viewersDesc;
@JsonKey(
defaultValue: defaultStreamViewerSort,
unknownEnumValue: StreamViewerSort.viewersDesc,
)
@observable
var streamViewerSort = defaultStreamViewerSort;
@action @action
void resetGeneralSettings() { void resetGeneralSettings() {
themeType = defaultThemeType; themeType = defaultThemeType;
@ -70,6 +81,7 @@ abstract class _SettingsStoreBase with Store {
launchUrlExternal = defaultLaunchUrlExternal; launchUrlExternal = defaultLaunchUrlExternal;
streamLanguages = defaultStreamLanguages; streamLanguages = defaultStreamLanguages;
streamViewerSort = defaultStreamViewerSort;
} }
// * Video Settings // * Video Settings

View file

@ -25,6 +25,13 @@ SettingsStore _$SettingsStoreFromJson(
?.map((e) => e as String) ?.map((e) => e as String)
.toList() ?? .toList() ??
['de'] ['de']
..streamViewerSort =
$enumDecodeNullable(
_$StreamViewerSortEnumMap,
json['streamViewerSort'],
unknownValue: StreamViewerSort.viewersDesc,
) ??
StreamViewerSort.viewersDesc
..showVideo = json['showVideo'] as bool? ?? true ..showVideo = json['showVideo'] as bool? ?? true
..defaultToHighestQuality = json['defaultToHighestQuality'] as bool? ?? false ..defaultToHighestQuality = json['defaultToHighestQuality'] as bool? ?? false
..useTextureRendering = json['useTextureRendering'] as bool? ?? true ..useTextureRendering = json['useTextureRendering'] as bool? ?? true
@ -110,6 +117,7 @@ Map<String, dynamic> _$SettingsStoreToJson(
'largeStreamCard': instance.largeStreamCard, 'largeStreamCard': instance.largeStreamCard,
'launchUrlExternal': instance.launchUrlExternal, 'launchUrlExternal': instance.launchUrlExternal,
'streamLanguages': instance.streamLanguages, 'streamLanguages': instance.streamLanguages,
'streamViewerSort': _$StreamViewerSortEnumMap[instance.streamViewerSort]!,
'showVideo': instance.showVideo, 'showVideo': instance.showVideo,
'defaultToHighestQuality': instance.defaultToHighestQuality, 'defaultToHighestQuality': instance.defaultToHighestQuality,
'useTextureRendering': instance.useTextureRendering, 'useTextureRendering': instance.useTextureRendering,
@ -165,6 +173,11 @@ const _$ThemeTypeEnumMap = {
ThemeType.dark: 'dark', ThemeType.dark: 'dark',
}; };
const _$StreamViewerSortEnumMap = {
StreamViewerSort.viewersDesc: 'viewersDesc',
StreamViewerSort.viewersAsc: 'viewersAsc',
};
const _$TimestampTypeEnumMap = { const _$TimestampTypeEnumMap = {
TimestampType.disabled: 'disabled', TimestampType.disabled: 'disabled',
TimestampType.twelve: 'twelve', TimestampType.twelve: 'twelve',
@ -302,6 +315,24 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
}); });
} }
late final _$streamViewerSortAtom = Atom(
name: '_SettingsStoreBase.streamViewerSort',
context: context,
);
@override
StreamViewerSort get streamViewerSort {
_$streamViewerSortAtom.reportRead();
return super.streamViewerSort;
}
@override
set streamViewerSort(StreamViewerSort value) {
_$streamViewerSortAtom.reportWrite(value, super.streamViewerSort, () {
super.streamViewerSort = value;
});
}
late final _$showVideoAtom = Atom( late final _$showVideoAtom = Atom(
name: '_SettingsStoreBase.showVideo', name: '_SettingsStoreBase.showVideo',
context: context, context: context,
@ -1286,6 +1317,7 @@ showThumbnails: ${showThumbnails},
largeStreamCard: ${largeStreamCard}, largeStreamCard: ${largeStreamCard},
launchUrlExternal: ${launchUrlExternal}, launchUrlExternal: ${launchUrlExternal},
streamLanguages: ${streamLanguages}, streamLanguages: ${streamLanguages},
streamViewerSort: ${streamViewerSort},
showVideo: ${showVideo}, showVideo: ${showVideo},
defaultToHighestQuality: ${defaultToHighestQuality}, defaultToHighestQuality: ${defaultToHighestQuality},
useTextureRendering: ${useTextureRendering}, useTextureRendering: ${useTextureRendering},

View file

@ -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 # 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 # 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. # of the product and file versions while build-number is used as the build suffix.
version: 5.2.4+74 version: 5.2.5+75
environment: environment:
sdk: ^3.9.0 sdk: ^3.9.0