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: [ // ],