Fix tag pagination and persist viewer sort.
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:
parent
9d392d8d6a
commit
47150d0ab3
12 changed files with 214 additions and 113 deletions
|
|
@ -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.
|
||||
|
||||
- One language: Helix `language=` (paginates cleanly, `first=100`)
|
||||
- Several languages: Helix over-fetch + client filter (still paginated)
|
||||
- Tags / ascending sort: GQL with `languages: [Language!]`; page-2 often hits Twitch integrity — treated as end-of-list after the first page of 30
|
||||
|
||||
Also fixed infinite scroll: larger Helix pages, defensive cursors, scroll near-end load-more, and stop disposing shared Home scroll controllers.
|
||||
- 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
|
||||
|
||||
## Build
|
||||
|
||||
|
|
|
|||
|
|
@ -248,29 +248,42 @@ class TwitchApi extends BaseApiClient {
|
|||
return StreamsTwitch.fromJson(data);
|
||||
}
|
||||
|
||||
/// Fetches Helix stream pages until [minCount] streams match [languages]
|
||||
/// (or the directory is exhausted). Preserves Helix viewer order.
|
||||
/// Fetches Helix directory pages until enough streams match [languages] and
|
||||
/// optional [freeformTag] (case-insensitive tag name), or the directory ends.
|
||||
///
|
||||
/// Used when the user selected more than one language — Helix only accepts a
|
||||
/// single `language` query param.
|
||||
Future<StreamsTwitch> getStreamsMatchingLanguages({
|
||||
required List<String> languages,
|
||||
/// Helix is always viewer-desc and supports at most one `language` query
|
||||
/// param; tags are filtered client-side. Prefer this over GQL for pagination
|
||||
/// (GQL page-2 requires a non-bot Client-Integrity token).
|
||||
Future<StreamsTwitch> getDirectoryStreams({
|
||||
List<String> languages = const [],
|
||||
String? gameId,
|
||||
String? freeformTag,
|
||||
String? cursor,
|
||||
int minCount = 40,
|
||||
int maxPages = 8,
|
||||
int minCount = 50,
|
||||
int maxPages = 12,
|
||||
}) async {
|
||||
final wanted = languages
|
||||
final wantedLangs = languages
|
||||
.map((l) => l.trim().toLowerCase())
|
||||
.where((l) => l.isNotEmpty)
|
||||
.toSet();
|
||||
if (wanted.isEmpty) {
|
||||
return gameId == null
|
||||
? getTopStreams(cursor: cursor)
|
||||
: getStreamsUnderCategory(gameId: gameId, cursor: cursor);
|
||||
final tagNeedle = freeformTag?.trim().toLowerCase();
|
||||
final hasTag = tagNeedle != null && tagNeedle.isNotEmpty;
|
||||
|
||||
bool matches(StreamTwitch stream) {
|
||||
if (wantedLangs.isNotEmpty &&
|
||||
!wantedLangs.contains(stream.language.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
if (hasTag) {
|
||||
final hit = stream.tags.any((t) => t.toLowerCase() == tagNeedle);
|
||||
if (!hit) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (wanted.length == 1) {
|
||||
final lang = wanted.first;
|
||||
|
||||
// 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
|
||||
? getTopStreams(cursor: cursor, language: lang)
|
||||
: 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>[];
|
||||
String? pageCursor = cursor;
|
||||
String? nextCursor;
|
||||
|
|
@ -287,14 +305,16 @@ class TwitchApi extends BaseApiClient {
|
|||
|
||||
while (matched.length < minCount && pages < maxPages) {
|
||||
final page = gameId == null
|
||||
? await getTopStreams(cursor: pageCursor)
|
||||
: await getStreamsUnderCategory(gameId: gameId, cursor: pageCursor);
|
||||
? await getTopStreams(cursor: pageCursor, language: helixLanguage)
|
||||
: await getStreamsUnderCategory(
|
||||
gameId: gameId,
|
||||
cursor: pageCursor,
|
||||
language: helixLanguage,
|
||||
);
|
||||
pages++;
|
||||
nextCursor = page.pagination['cursor'];
|
||||
for (final stream in page.data) {
|
||||
if (wanted.contains(stream.language.toLowerCase())) {
|
||||
matched.add(stream);
|
||||
}
|
||||
if (matches(stream)) matched.add(stream);
|
||||
}
|
||||
if (nextCursor == null || page.data.isEmpty) {
|
||||
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].
|
||||
Future<StreamTwitch> getStream({required String userLogin}) async {
|
||||
final data = await get<JsonMap>(
|
||||
|
|
|
|||
|
|
@ -340,6 +340,7 @@ class TwitchGqlApi extends BaseApiClient {
|
|||
// Helix-style placeholder so existing thumbnail widgets keep working.
|
||||
'https://static-cdn.jtvnw.net/previews-ttv/live_user_$login-{width}x{height}.jpg',
|
||||
language,
|
||||
const [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ class StreamTwitch {
|
|||
@JsonKey(defaultValue: '')
|
||||
final String language;
|
||||
|
||||
/// Freeform tags from Helix (e.g. `Deutsch`, `English`).
|
||||
@JsonKey(defaultValue: <String>[])
|
||||
final List<String> tags;
|
||||
|
||||
const StreamTwitch(
|
||||
this.userId,
|
||||
this.userLogin,
|
||||
|
|
@ -30,6 +34,7 @@ class StreamTwitch {
|
|||
this.startedAt,
|
||||
this.thumbnailUrl, [
|
||||
this.language = '',
|
||||
this.tags = const [],
|
||||
]);
|
||||
|
||||
factory StreamTwitch.fromJson(Map<String, dynamic> json) =>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ StreamTwitch _$StreamTwitchFromJson(Map<String, dynamic> json) => StreamTwitch(
|
|||
json['started_at'] as String,
|
||||
json['thumbnail_url'] as String,
|
||||
json['language'] as String? ?? '',
|
||||
(json['tags'] as List<dynamic>?)?.map((e) => e as String).toList() ?? [],
|
||||
);
|
||||
|
||||
StreamsTwitch _$StreamsTwitchFromJson(Map<String, dynamic> json) =>
|
||||
|
|
|
|||
|
|
@ -1,28 +1,24 @@
|
|||
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/streams_list.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/widgets/blurred_container.dart';
|
||||
import 'package:frosty/widgets/stream_sort_button.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
/// Streams matching a freeform tag (search → tap tag).
|
||||
class TagStreams extends StatefulWidget {
|
||||
class TagStreams extends StatelessWidget {
|
||||
static const routeName = 'TagStreams';
|
||||
|
||||
final String tagName;
|
||||
|
||||
const TagStreams({super.key, required this.tagName});
|
||||
|
||||
@override
|
||||
State<TagStreams> createState() => _TagStreamsState();
|
||||
}
|
||||
|
||||
class _TagStreamsState extends State<TagStreams> {
|
||||
var _sortOrder = StreamViewerSort.viewersDesc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final settingsStore = context.watch<SettingsStore>();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: theme.scaffoldBackgroundColor,
|
||||
|
|
@ -31,12 +27,13 @@ class _TagStreamsState extends State<TagStreams> {
|
|||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: StreamsList(
|
||||
key: ValueKey(_sortOrder),
|
||||
listType: ListType.tag,
|
||||
freeformTag: widget.tagName,
|
||||
sortOrder: _sortOrder,
|
||||
showJumpButton: true,
|
||||
child: Observer(
|
||||
builder: (_) => StreamsList(
|
||||
listType: ListType.tag,
|
||||
freeformTag: tagName,
|
||||
sortOrder: settingsStore.streamViewerSort,
|
||||
showJumpButton: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
|
|
@ -61,15 +58,18 @@ class _TagStreamsState extends State<TagStreams> {
|
|||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.tagName,
|
||||
tagName,
|
||||
style: theme.textTheme.titleLarge,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
StreamSortButton(
|
||||
sortOrder: _sortOrder,
|
||||
onChanged: (order) => setState(() => _sortOrder = order),
|
||||
Observer(
|
||||
builder: (_) => StreamSortButton(
|
||||
sortOrder: settingsStore.streamViewerSort,
|
||||
onChanged: (order) =>
|
||||
settingsStore.streamViewerSort = order,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -214,11 +214,18 @@ abstract class ListStoreBase with Store {
|
|||
_getCategoryDetails();
|
||||
}
|
||||
|
||||
// Reload directory lists when the language filter changes.
|
||||
// Reload directory lists when the language filter or sort changes.
|
||||
if (listType != ListType.followed) {
|
||||
sortOrder = settingsStore.streamViewerSort;
|
||||
_languageFilterReactionDisposer = reaction(
|
||||
(_) => settingsStore.streamLanguages.toList(),
|
||||
(_) => refreshStreams(),
|
||||
(_) => [
|
||||
...settingsStore.streamLanguages,
|
||||
settingsStore.streamViewerSort,
|
||||
],
|
||||
(_) {
|
||||
sortOrder = settingsStore.streamViewerSort;
|
||||
refreshStreams();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -252,6 +259,14 @@ 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) {
|
||||
case ListType.followed:
|
||||
|
|
@ -261,50 +276,36 @@ abstract class ListStoreBase with Store {
|
|||
);
|
||||
break;
|
||||
case ListType.top:
|
||||
// Ascending sort needs GQL (Helix is always desc).
|
||||
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,
|
||||
cursor: _streamsCursor,
|
||||
);
|
||||
}
|
||||
newStreams = await twitchApi.getDirectoryStreams(
|
||||
languages: languages,
|
||||
cursor: _streamsCursor,
|
||||
minCount: minCount,
|
||||
maxPages: maxPages,
|
||||
);
|
||||
break;
|
||||
case ListType.category:
|
||||
if (sortOrder == StreamViewerSort.viewersAsc &&
|
||||
twitchGqlApi != null) {
|
||||
newStreams = await twitchGqlApi!.getGameStreams(
|
||||
gameId: categoryId!,
|
||||
sort: sortOrder,
|
||||
languages: languages.isEmpty ? null : languages,
|
||||
cursor: _streamsCursor,
|
||||
);
|
||||
} else {
|
||||
newStreams = await twitchApi.getStreamsMatchingLanguages(
|
||||
languages: languages,
|
||||
gameId: categoryId!,
|
||||
cursor: _streamsCursor,
|
||||
);
|
||||
}
|
||||
newStreams = await twitchApi.getDirectoryStreams(
|
||||
languages: languages,
|
||||
gameId: categoryId!,
|
||||
cursor: _streamsCursor,
|
||||
minCount: minCount,
|
||||
maxPages: maxPages,
|
||||
);
|
||||
break;
|
||||
case ListType.tag:
|
||||
newStreams = await twitchGqlApi!.getStreams(
|
||||
freeformTags: [freeformTag!],
|
||||
sort: sortOrder,
|
||||
languages: languages.isEmpty ? null : languages,
|
||||
// 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,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// GQL page-2 often returns empty `data` under IntegrityCheckFailed
|
||||
// without throwing — treat empty continuation pages as end-of-list.
|
||||
// Empty continuation ⇒ end of directory.
|
||||
final isContinuation = _streamsCursor != null;
|
||||
if (isContinuation && newStreams.data.isEmpty) {
|
||||
_streamsCursor = null;
|
||||
|
|
@ -315,7 +316,6 @@ abstract class ListStoreBase with Store {
|
|||
if (_streamsCursor == null) {
|
||||
_allStreams = newStreams.data.asObservable();
|
||||
} else {
|
||||
// Dedupe in case of overlapping over-fetch pages.
|
||||
final existingIds = _allStreams.map((s) => s.userId).toSet();
|
||||
_allStreams.addAll(
|
||||
newStreams.data.where((s) => !existingIds.contains(s.userId)),
|
||||
|
|
@ -324,11 +324,11 @@ abstract class ListStoreBase with Store {
|
|||
final next = newStreams.pagination['cursor'];
|
||||
_streamsCursor = (next != null && next.isNotEmpty) ? next : null;
|
||||
|
||||
_applyViewerSort();
|
||||
_error = null;
|
||||
} on SocketException {
|
||||
_error = 'Unable to connect to Twitch';
|
||||
debugPrint('Streams SocketException: No internet connection');
|
||||
// Stop retry storms on failed continuations.
|
||||
if (_streamsCursor != null && _allStreams.isNotEmpty) {
|
||||
_streamsCursor = null;
|
||||
}
|
||||
|
|
@ -337,7 +337,6 @@ abstract class ListStoreBase with Store {
|
|||
debugPrint('Streams ApiException: $e');
|
||||
if (_streamsCursor != null && _allStreams.isNotEmpty) {
|
||||
_streamsCursor = null;
|
||||
// Don't surface integrity / page-2 failures as a full-list error.
|
||||
if (_allStreams.isNotEmpty) _error = null;
|
||||
}
|
||||
} 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
|
||||
Future<void> setSortOrder(StreamViewerSort order) async {
|
||||
if (sortOrder == order) return;
|
||||
sortOrder = order;
|
||||
await refreshStreams();
|
||||
settingsStore.streamViewerSort = order;
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class CategoryStreams extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _CategoryStreamsState extends State<CategoryStreams> {
|
||||
var _sortOrder = StreamViewerSort.viewersDesc;
|
||||
late final _settingsStore = context.read<SettingsStore>();
|
||||
|
||||
late final _listStore = ListStore(
|
||||
listType: ListType.category,
|
||||
|
|
@ -35,7 +35,8 @@ class _CategoryStreamsState extends State<CategoryStreams> {
|
|||
authStore: context.read<AuthStore>(),
|
||||
twitchApi: context.read<TwitchApi>(),
|
||||
twitchGqlApi: context.read<TwitchGqlApi>(),
|
||||
settingsStore: context.read<SettingsStore>(),
|
||||
settingsStore: _settingsStore,
|
||||
sortOrder: _settingsStore.streamViewerSort,
|
||||
);
|
||||
|
||||
@override
|
||||
|
|
@ -50,12 +51,13 @@ class _CategoryStreamsState extends State<CategoryStreams> {
|
|||
children: [
|
||||
// Stream list content behind the pinned elements
|
||||
Positioned.fill(
|
||||
child: StreamsList(
|
||||
key: ValueKey(_sortOrder),
|
||||
listType: ListType.category,
|
||||
categoryId: widget.categoryId,
|
||||
sortOrder: _sortOrder,
|
||||
showJumpButton: true,
|
||||
child: Observer(
|
||||
builder: (_) => StreamsList(
|
||||
listType: ListType.category,
|
||||
categoryId: widget.categoryId,
|
||||
sortOrder: _settingsStore.streamViewerSort,
|
||||
showJumpButton: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Single blurred background spanning app bar and category card
|
||||
|
|
@ -84,10 +86,12 @@ class _CategoryStreamsState extends State<CategoryStreams> {
|
|||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
const Spacer(),
|
||||
StreamSortButton(
|
||||
sortOrder: _sortOrder,
|
||||
onChanged: (order) =>
|
||||
setState(() => _sortOrder = order),
|
||||
Observer(
|
||||
builder: (_) => StreamSortButton(
|
||||
sortOrder: _settingsStore.streamViewerSort,
|
||||
onChanged: (order) =>
|
||||
_settingsStore.streamViewerSort = order,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
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/stream_list/stream_list_store.dart';
|
||||
import 'package:frosty/screens/home/stream_list/streams_list.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/stream_sort_button.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class TopSection extends StatefulWidget {
|
||||
final HomeStore homeStore;
|
||||
|
|
@ -19,7 +21,6 @@ class TopSection extends StatefulWidget {
|
|||
class _TopSectionState extends State<TopSection>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final _tabBarController = TabController(length: 2, vsync: this);
|
||||
var _sortOrder = StreamViewerSort.viewersDesc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -32,6 +33,7 @@ class _TopSectionState extends State<TopSection>
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settingsStore = context.watch<SettingsStore>();
|
||||
final showSort = _tabBarController.index == 0;
|
||||
|
||||
return Stack(
|
||||
|
|
@ -40,12 +42,13 @@ class _TopSectionState extends State<TopSection>
|
|||
child: TabBarView(
|
||||
controller: _tabBarController,
|
||||
children: [
|
||||
StreamsList(
|
||||
key: ValueKey(_sortOrder),
|
||||
listType: ListType.top,
|
||||
sortOrder: _sortOrder,
|
||||
scrollController:
|
||||
widget.homeStore.topSectionScrollControllers[0],
|
||||
Observer(
|
||||
builder: (_) => StreamsList(
|
||||
listType: ListType.top,
|
||||
sortOrder: settingsStore.streamViewerSort,
|
||||
scrollController:
|
||||
widget.homeStore.topSectionScrollControllers[0],
|
||||
),
|
||||
),
|
||||
Categories(
|
||||
scrollController:
|
||||
|
|
@ -78,9 +81,12 @@ class _TopSectionState extends State<TopSection>
|
|||
),
|
||||
),
|
||||
if (showSort)
|
||||
StreamSortButton(
|
||||
sortOrder: _sortOrder,
|
||||
onChanged: (order) => setState(() => _sortOrder = order),
|
||||
Observer(
|
||||
builder: (_) => StreamSortButton(
|
||||
sortOrder: settingsStore.streamViewerSort,
|
||||
onChanged: (order) =>
|
||||
settingsStore.streamViewerSort = order,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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:json_annotation/json_annotation.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
|
@ -60,6 +61,16 @@ abstract class _SettingsStoreBase with Store {
|
|||
@observable
|
||||
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
|
||||
void resetGeneralSettings() {
|
||||
themeType = defaultThemeType;
|
||||
|
|
@ -70,6 +81,7 @@ abstract class _SettingsStoreBase with Store {
|
|||
|
||||
launchUrlExternal = defaultLaunchUrlExternal;
|
||||
streamLanguages = defaultStreamLanguages;
|
||||
streamViewerSort = defaultStreamViewerSort;
|
||||
}
|
||||
|
||||
// * Video Settings
|
||||
|
|
|
|||
|
|
@ -25,6 +25,13 @@ SettingsStore _$SettingsStoreFromJson(
|
|||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
['de']
|
||||
..streamViewerSort =
|
||||
$enumDecodeNullable(
|
||||
_$StreamViewerSortEnumMap,
|
||||
json['streamViewerSort'],
|
||||
unknownValue: StreamViewerSort.viewersDesc,
|
||||
) ??
|
||||
StreamViewerSort.viewersDesc
|
||||
..showVideo = json['showVideo'] as bool? ?? true
|
||||
..defaultToHighestQuality = json['defaultToHighestQuality'] as bool? ?? false
|
||||
..useTextureRendering = json['useTextureRendering'] as bool? ?? true
|
||||
|
|
@ -110,6 +117,7 @@ Map<String, dynamic> _$SettingsStoreToJson(
|
|||
'largeStreamCard': instance.largeStreamCard,
|
||||
'launchUrlExternal': instance.launchUrlExternal,
|
||||
'streamLanguages': instance.streamLanguages,
|
||||
'streamViewerSort': _$StreamViewerSortEnumMap[instance.streamViewerSort]!,
|
||||
'showVideo': instance.showVideo,
|
||||
'defaultToHighestQuality': instance.defaultToHighestQuality,
|
||||
'useTextureRendering': instance.useTextureRendering,
|
||||
|
|
@ -165,6 +173,11 @@ const _$ThemeTypeEnumMap = {
|
|||
ThemeType.dark: 'dark',
|
||||
};
|
||||
|
||||
const _$StreamViewerSortEnumMap = {
|
||||
StreamViewerSort.viewersDesc: 'viewersDesc',
|
||||
StreamViewerSort.viewersAsc: 'viewersAsc',
|
||||
};
|
||||
|
||||
const _$TimestampTypeEnumMap = {
|
||||
TimestampType.disabled: 'disabled',
|
||||
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(
|
||||
name: '_SettingsStoreBase.showVideo',
|
||||
context: context,
|
||||
|
|
@ -1286,6 +1317,7 @@ showThumbnails: ${showThumbnails},
|
|||
largeStreamCard: ${largeStreamCard},
|
||||
launchUrlExternal: ${launchUrlExternal},
|
||||
streamLanguages: ${streamLanguages},
|
||||
streamViewerSort: ${streamViewerSort},
|
||||
showVideo: ${showVideo},
|
||||
defaultToHighestQuality: ${defaultToHighestQuality},
|
||||
useTextureRendering: ${useTextureRendering},
|
||||
|
|
|
|||
|
|
@ -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.4+74
|
||||
version: 5.2.5+75
|
||||
|
||||
environment:
|
||||
sdk: ^3.9.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue