From 61e9abdeb2e5588338fdd60d581a251682ff42a9 Mon Sep 17 00:00:00 2001 From: gthvmt Date: Thu, 8 Jun 2023 23:19:03 +0200 Subject: [PATCH] :sparkles: Stop fetching emotes if no more are available Previously once the user scrolls to the end of the loaded emotes the application would always try to load more even if no more where available --- src/lib/models/seventv.dart | 53 +++++++++++++++------- src/lib/screens/browser.dart | 76 +++++++++++++++++--------------- src/lib/screens/stickerpack.dart | 12 +++-- 3 files changed, 85 insertions(+), 56 deletions(-) diff --git a/src/lib/models/seventv.dart b/src/lib/models/seventv.dart index 4e3c862..9f94671 100644 --- a/src/lib/models/seventv.dart +++ b/src/lib/models/seventv.dart @@ -45,6 +45,8 @@ class SevenTv { final _url = Uri.parse('https://7tv.io/v3/gql'); final _client = HttpClient(); + // TODO: refactor the request creation into a seperate method to reduce code redundancy + Stream> getTrending(int chunkSize) async* { var currentPage = 1; final variables = { @@ -62,17 +64,18 @@ class SevenTv { 'aspect_ratio': '' } }; - //TODO: figure out a way to break the loop - //we do get the emote count in the response, maybe figure out a way to read the stream until the count - //and return the transformed stream after. Then break once all emotes should have been loaded - //(currentPage*chunkSize >= emoteCount) - while (true) { + int countCollected = 0; + int countTotal = -1; + while (countTotal < 0 || countTotal > countCollected) { variables['page'] = currentPage; final req = await _client.postUrl(_url); req.headers.add(HttpHeaders.contentTypeHeader, ContentType.json.mimeType); req.write(jsonEncode({'query': _searchEmotesQuery, 'variables': variables})); final resp = await req.close(); - yield resp.transform(utf8.decoder).transform(EmoteTransformer()); + final transformer = EmoteTransformer(); + yield resp.transform(utf8.decoder).transform(transformer); + countTotal = transformer.countTotal; + countCollected += transformer.countCollected; currentPage++; } } @@ -94,17 +97,18 @@ class SevenTv { 'aspect_ratio': '' } }; - //TODO: figure out a way to break the loop - //we do get the emote count in the response, maybe figure out a way to read the stream until the count - //and return the transformed stream after. Then break once all emotes should have been loaded - //(currentPage*chunkSize >= emoteCount) - while (true) { + int countCollected = 0; + int countTotal = -1; + while (countTotal < 0 || countTotal > countCollected) { variables['page'] = currentPage; final req = await _client.postUrl(_url); req.headers.add(HttpHeaders.contentTypeHeader, ContentType.json.mimeType); req.write(jsonEncode({'query': _searchEmotesQuery, 'variables': variables})); final resp = await req.close(); - yield resp.transform(utf8.decoder).transform(EmoteTransformer()); + final transformer = EmoteTransformer(); + yield resp.transform(utf8.decoder).transform(transformer); + countTotal = transformer.countTotal; + countCollected += transformer.countCollected; currentPage++; } } @@ -112,7 +116,12 @@ class SevenTv { class EmoteTransformer implements StreamTransformer { final StreamController _controller = StreamController(); + int countCollected = 0; + int countTotal = -1; String _buffer = ''; + String _countBuffer = ''; + bool _collectCount = false; + int _currentDepth = 0; bool _inStrVal = false; int _itemsArrayDepth = -1; @@ -126,10 +135,20 @@ class EmoteTransformer implements StreamTransformer { Future onListen(String chunk) async { for (final c in chunk.split('')) { _buffer += c; + if (_collectCount) { + if (c == ',') { + countTotal = int.parse(_countBuffer); + _collectCount = false; + } else if (c != ' ') { + _countBuffer += c; + } + } if (c == '"') { _inStrVal = !_inStrVal; } else if (!_inStrVal) { - if (c == '{' || c == '[') { + if (countTotal < 0 && c == ':' && _buffer.replaceAll(' ', '').toLowerCase().endsWith('"count":')) { + _collectCount = true; + } else if (c == '{' || c == '[') { _currentDepth++; if (c == '[') { if (_buffer.replaceAll(' ', '').toLowerCase().endsWith('"items":[')) { @@ -141,6 +160,7 @@ class EmoteTransformer implements StreamTransformer { } else if (c == '}' || c == ']') { if (_currentDepth == _itemsArrayDepth + 1 && c == '}') { //emote object closed + countCollected++; _controller.add(Emote.fromJson(jsonDecode(_buffer.substring(_buffer.indexOf('{'))))); _buffer = ''; } @@ -194,10 +214,11 @@ class Emote { return data; } - Uri getMaxSizeUrl({Format format = Format.webp}) => - host!.getUrl(host!.files!.where((f) => f.format == format).reduce((a, b) => a.height > b.height ? a : b)); + Uri getMaxSizeUrl({Format format = Format.webp}) => host!.getUrl( + host!.files!.where((f) => f.format == format).reduce((a, b) => a.height > b.height ? a : b)); - File getMaxSizeFile({Format format = Format.webp}) => host!.files!.reduce((a, b) => a.height > b.height ? a : b); + File getMaxSizeFile({Format format = Format.webp}) => + host!.files!.reduce((a, b) => a.height > b.height ? a : b); } class Owner { diff --git a/src/lib/screens/browser.dart b/src/lib/screens/browser.dart index f2b9fa7..a1534f6 100644 --- a/src/lib/screens/browser.dart +++ b/src/lib/screens/browser.dart @@ -34,6 +34,7 @@ class _BrowserState extends State { bool _isSearchMode = false; bool _isLoading = false; StreamIterator>? _emoteStream; + bool _moreAvailable = true; static const _gridDelegate = SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 150.0, @@ -49,54 +50,56 @@ class _BrowserState extends State { _notificationService.initialize(); _scrollController.addListener(() async { if (_scrollController.offset == _scrollController.position.maxScrollExtent) { - await loadAdditional(); + await getEmotes(); } }); loadTrending(); } - Future loadAdditional() async { - var stream = _emoteStream; - if (stream != null) { - debugPrint('loading more emotes'); - if (await stream.moveNext()) { - await for (final emote in _emoteStream!.current) { - setState(() => _loadedEmotes.add(emote)); - } - } - } - } - Future loadTrending() async { setState(() { _isLoading = true; + _moreAvailable = true; _loadedEmotes.clear(); }); _emoteStream = StreamIterator(_api.getTrending(_chunkSize)); - if (await _emoteStream!.moveNext()) { - if (_isLoading) { - setState(() => _isLoading = false); - } - await for (final emote in _emoteStream!.current) { - setState(() => _loadedEmotes.add(emote)); - } - } + await getEmotes(); } Future search(searchText) async { setState(() { _isLoading = true; + _moreAvailable = true; _loadedEmotes.clear(); }); _emoteStream = StreamIterator(_api.search(searchText, _chunkSize)); - if (await _emoteStream!.moveNext()) { - if (_isLoading) { - setState(() => _isLoading = false); - } - await for (final emote in _emoteStream!.current) { - // log('got emote ${emote.name} - url is ${emote.host!.getUrl(emote.host!.files!.where((f) => f.format == Format.avif).reduce((a, b) => a.height > b.height ? a : b))}'); - setState(() => _loadedEmotes.add(emote)); - } + await getEmotes(); + } + + Future getEmotes() async { + if (!_moreAvailable) { + return; + } + var stream = _emoteStream; + if (stream != null) { + debugPrint('loading more emotes'); + try { + int fetchedEmoteCount = 0; + final streamIsExhausted = !(await stream.moveNext()); + if (_isLoading) { + setState(() => _isLoading = false); + } + if (!streamIsExhausted) { + await for (final emote in stream.current) { + fetchedEmoteCount++; + setState(() => _loadedEmotes.add(emote)); + } + } + if (fetchedEmoteCount < _chunkSize || streamIsExhausted) { + debugPrint('stream is exhausted'); + setState(() => _moreAvailable = false); + } + } catch (_) {} } } @@ -292,13 +295,14 @@ class _BrowserState extends State { ) ]), ), - SliverList( - delegate: SliverChildListDelegate([ - const Padding( - padding: EdgeInsets.symmetric(vertical: 20), - child: Center(child: CircularProgressIndicator()), - ) - ])) + if (_moreAvailable) + SliverList( + delegate: SliverChildListDelegate([ + const Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Center(child: CircularProgressIndicator()), + ) + ])) ], ), ), diff --git a/src/lib/screens/stickerpack.dart b/src/lib/screens/stickerpack.dart index 91edbf2..ab0226b 100644 --- a/src/lib/screens/stickerpack.dart +++ b/src/lib/screens/stickerpack.dart @@ -26,7 +26,8 @@ class _StickerPackState extends State { @override void initState() { super.initState(); - debugPrint('Stickers in stickerpack "${widget.stickerPack.name}" (${widget.stickerPack.identifier}):'); + debugPrint( + 'Stickers in stickerpack "${widget.stickerPack.name}" (${widget.stickerPack.identifier}):'); for (var sticker in widget.stickerPack.stickers) { debugPrint(jsonEncode(sticker.toJson())); } @@ -40,10 +41,12 @@ class _StickerPackState extends State { content: const Text( 'Deleting this sticker pack will remove all the stickers associated with it. Do you want to proceed?'), actions: [ - TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')), + TextButton( + onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')), TextButton( onPressed: () => Navigator.pop(ctx, true), - child: Text('Delete', style: TextStyle(color: Theme.of(ctx).colorScheme.error))) + child: + Text('Delete', style: TextStyle(color: Theme.of(ctx).colorScheme.error))) ])); if (!(delete ?? false)) { return false; @@ -68,7 +71,8 @@ class _StickerPackState extends State { SliverGrid( gridDelegate: StickerPack.gridDelegate, delegate: SliverChildListDelegate([ - for (final sticker in widget.stickerPack.stickers) Image.file(File(sticker.imagePath)), + for (final sticker in widget.stickerPack.stickers) + Image.file(File(sticker.imagePath)), // Stack( // children: [ // ],