From 5e7fc75b61b784fdb76774467ba180e7192388d2 Mon Sep 17 00:00:00 2001 From: Livinglist Date: Tue, 13 Aug 2024 14:41:23 -0700 Subject: [PATCH 1/7] feat: fetch from web instead of api. --- assets/remote-config-dev.json | 9 +- assets/remote-config.json | 9 +- ios/Podfile.lock | 2 +- lib/blocs/stories/stories_bloc.dart | 104 ++----- lib/blocs/stories/stories_state.dart | 6 - .../remote_config/remote_config_state.dart | 36 +++ lib/models/story_type.dart | 18 +- lib/repositories/hacker_news_repository.dart | 2 +- .../hacker_news_web_repository.dart | 146 ++++++++++ .../remote_config_repository.dart | 25 +- lib/screens/widgets/stories_list_view.dart | 2 +- lib/utils/log_util.dart | 2 +- pubspec.lock | 257 ++++++++++-------- pubspec.yaml | 3 +- 14 files changed, 402 insertions(+), 219 deletions(-) diff --git a/assets/remote-config-dev.json b/assets/remote-config-dev.json index 082d1f18..80c8512c 100644 --- a/assets/remote-config-dev.json +++ b/assets/remote-config-dev.json @@ -3,5 +3,12 @@ "commentTextSelector": "td > table > tbody > tr > td.default > div.comment > div.commtext", "commentHeadSelector": "td > table > tbody > tr > td.default > div > span > a", "commentAgeSelector": "td > table > tbody > tr > td.default > div > span > span.age", - "commentIndentSelector": "td > table > tbody > tr > td.ind" + "commentIndentSelector": "td > table > tbody > tr > td.ind", + "storySelector": "#hnmain > tbody > tr > td > table > tbody > .athing", + "subtextSelector": "#hnmain > tbody > tr > td > table > tbody > tr > .subtext", + "titlelineSelector": ".title > .titleline > a", + "pointSelector": ".subline > .score", + "userSelector": ".subline > .hnuser", + "ageSelector": ".subline > .age", + "cmtCountSelector": ".subline > a" } diff --git a/assets/remote-config.json b/assets/remote-config.json index 082d1f18..80c8512c 100644 --- a/assets/remote-config.json +++ b/assets/remote-config.json @@ -3,5 +3,12 @@ "commentTextSelector": "td > table > tbody > tr > td.default > div.comment > div.commtext", "commentHeadSelector": "td > table > tbody > tr > td.default > div > span > a", "commentAgeSelector": "td > table > tbody > tr > td.default > div > span > span.age", - "commentIndentSelector": "td > table > tbody > tr > td.ind" + "commentIndentSelector": "td > table > tbody > tr > td.ind", + "storySelector": "#hnmain > tbody > tr > td > table > tbody > .athing", + "subtextSelector": "#hnmain > tbody > tr > td > table > tbody > tr > .subtext", + "titlelineSelector": ".title > .titleline > a", + "pointSelector": ".subline > .score", + "userSelector": ".subline > .hnuser", + "ageSelector": ".subline > .age", + "cmtCountSelector": ".subline > a" } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9006792d..6df62436 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -159,7 +159,7 @@ SPEC CHECKSUMS: synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 - webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36 + webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 PODFILE CHECKSUM: f03c7c11cf2b623592c89c68c628682778bb78b4 diff --git a/lib/blocs/stories/stories_bloc.dart b/lib/blocs/stories/stories_bloc.dart index 598efeb0..943ee2a5 100644 --- a/lib/blocs/stories/stories_bloc.dart +++ b/lib/blocs/stories/stories_bloc.dart @@ -1,10 +1,8 @@ import 'dart:async'; -import 'dart:math'; import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:equatable/equatable.dart'; -import 'package:hacki/config/constants.dart'; import 'package:hacki/config/locator.dart'; import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/models/models.dart'; @@ -23,6 +21,7 @@ class StoriesBloc extends Bloc { required FilterCubit filterCubit, OfflineRepository? offlineRepository, HackerNewsRepository? hackerNewsRepository, + HackerNewsWebRepository? hackerNewsWebRepository, PreferenceRepository? preferenceRepository, Logger? logger, }) : _preferenceCubit = preferenceCubit, @@ -31,6 +30,8 @@ class StoriesBloc extends Bloc { offlineRepository ?? locator.get(), _hackerNewsRepository = hackerNewsRepository ?? locator.get(), + _hackerNewsWebRepository = + hackerNewsWebRepository ?? locator.get(), _preferenceRepository = preferenceRepository ?? locator.get(), _logger = logger ?? locator.get(), @@ -61,39 +62,19 @@ class StoriesBloc extends Bloc { final FilterCubit _filterCubit; final OfflineRepository _offlineRepository; final HackerNewsRepository _hackerNewsRepository; + final HackerNewsWebRepository _hackerNewsWebRepository; final PreferenceRepository _preferenceRepository; final Logger _logger; DeviceScreenType? deviceScreenType; StreamSubscription? _preferenceSubscription; - static const int _smallPageSize = 10; - static const int _largePageSize = 20; - static const int _tabletSmallPageSize = 15; - static const int _tabletLargePageSize = 25; static const String _logPrefix = '[StoriesBloc]'; Future onInitialize( StoriesInitialize event, Emitter emit, ) async { - _preferenceSubscription ??= _preferenceCubit.stream - .distinct((PreferenceState previous, PreferenceState next) { - return previous.isComplexStoryTileEnabled == - next.isComplexStoryTileEnabled; - }) - .debounceTime(AppDurations.oneSecond) - .listen((PreferenceState event) { - final bool isComplexTile = event.isComplexStoryTileEnabled; - final int pageSize = getPageSize(isComplexTile: isComplexTile); - - if (pageSize != state.currentPageSize) { - add(StoriesPageSizeChanged(pageSize: pageSize)); - } - }); - final bool isComplexTile = _preferenceCubit.state.isComplexStoryTileEnabled; - final int pageSize = getPageSize(isComplexTile: isComplexTile); emit( const StoriesState.init().copyWith( - currentPageSize: pageSize, downloadStatus: state.downloadStatus, storiesDownloaded: state.storiesDownloaded, storiesToBeDownloaded: state.storiesToBeDownloaded, @@ -121,9 +102,7 @@ class StoriesBloc extends Bloc { .copyWithStatusUpdated(type: type, to: Status.inProgress), ); _offlineRepository - .getCachedStoriesStream( - ids: ids.sublist(0, min(ids.length, state.currentPageSize)), - ) + .getCachedStoriesStream(ids: ids) .listen((Story story) => add(StoryLoaded(story: story, type: type))) .onDone(() => add(StoryLoadingCompleted(type: type))); } else { @@ -135,16 +114,24 @@ class StoriesBloc extends Bloc { .copyWithCurrentPageUpdated(type: type, to: 0) .copyWithStatusUpdated(type: type, to: Status.inProgress), ); - await _hackerNewsRepository - .fetchStoriesStream( - ids: ids.sublist(0, state.currentPageSize), - sequential: _preferenceCubit.state.isComplexStoryTileEnabled || - _preferenceCubit.state.isFaviconEnabled, - ) + + await _hackerNewsWebRepository + .fetchStoriesStream(event.type, page: 1) .listen((Story story) { add(StoryLoaded(story: story, type: type)); }).asFuture(); add(StoryLoadingCompleted(type: type)); + + // await _hackerNewsRepository + // .fetchStoriesStream( + // ids: ids.sublist(0, state.currentPageSize), + // sequential: _preferenceCubit.state.isComplexStoryTileEnabled || + // _preferenceCubit.state.isFaviconEnabled, + // ) + // .listen((Story story) { + // add(StoryLoaded(story: story, type: type)); + // }).asFuture(); + // add(StoryLoadingCompleted(type: type)); } } @@ -185,51 +172,24 @@ class StoriesBloc extends Bloc { ); final int currentPage = state.currentPageByType[event.type]!; - final int len = state.storyIdsByType[event.type]!.length; emit( state.copyWithCurrentPageUpdated(type: event.type, to: currentPage + 1), ); - final int currentPageSize = state.currentPageSize; - final int lower = currentPageSize * (currentPage + 1); - int upper = currentPageSize + lower; - if (len > lower) { - if (len < upper) { - upper = len; - } - - if (state.isOfflineReading) { - _offlineRepository - .getCachedStoriesStream( - ids: state.storyIdsByType[event.type]!.sublist( - lower, - upper, - ), - ) - .listen( - (Story story) => add(StoryLoaded(story: story, type: event.type)), - ) - .onDone(() => add(StoryLoadingCompleted(type: event.type))); - } else { - _hackerNewsRepository - .fetchStoriesStream( - ids: state.storyIdsByType[event.type]!.sublist( - lower, - upper, - ), - ) - .listen( - (Story story) => add(StoryLoaded(story: story, type: event.type)), - ) - .onDone(() => add(StoryLoadingCompleted(type: event.type))); - } - } else { + if (state.isOfflineReading) { emit( state.copyWithStatusUpdated( type: event.type, to: Status.success, ), ); + } else { + _hackerNewsWebRepository + .fetchStoriesStream(event.type, page: currentPage + 1) + .listen( + (Story story) => add(StoryLoaded(story: story, type: event.type)), + ) + .onDone(() => add(StoryLoadingCompleted(type: event.type))); } } @@ -520,16 +480,6 @@ class StoriesBloc extends Bloc { bool hasRead(Story story) => state.readStoriesIds.contains(story.id); - int getPageSize({required bool isComplexTile}) { - int pageSize = isComplexTile ? _smallPageSize : _largePageSize; - - if (deviceScreenType != DeviceScreenType.mobile) { - pageSize = isComplexTile ? _tabletSmallPageSize : _tabletLargePageSize; - } - - return pageSize; - } - @override Future close() async { await _preferenceSubscription?.cancel(); diff --git a/lib/blocs/stories/stories_state.dart b/lib/blocs/stories/stories_state.dart index 48264041..bdc07aca 100644 --- a/lib/blocs/stories/stories_state.dart +++ b/lib/blocs/stories/stories_state.dart @@ -17,7 +17,6 @@ class StoriesState extends Equatable { required this.readStoriesIds, required this.isOfflineReading, required this.downloadStatus, - required this.currentPageSize, required this.storiesDownloaded, required this.storiesToBeDownloaded, }); @@ -53,7 +52,6 @@ class StoriesState extends Equatable { }, }) : isOfflineReading = false, downloadStatus = StoriesDownloadStatus.idle, - currentPageSize = 0, readStoriesIds = const {}, storiesDownloaded = 0, storiesToBeDownloaded = 0; @@ -65,7 +63,6 @@ class StoriesState extends Equatable { final Set readStoriesIds; final StoriesDownloadStatus downloadStatus; final bool isOfflineReading; - final int currentPageSize; final int storiesDownloaded; final int storiesToBeDownloaded; @@ -77,7 +74,6 @@ class StoriesState extends Equatable { Set? readStoriesIds, StoriesDownloadStatus? downloadStatus, bool? isOfflineReading, - int? currentPageSize, int? storiesDownloaded, int? storiesToBeDownloaded, }) { @@ -89,7 +85,6 @@ class StoriesState extends Equatable { readStoriesIds: readStoriesIds ?? this.readStoriesIds, isOfflineReading: isOfflineReading ?? this.isOfflineReading, downloadStatus: downloadStatus ?? this.downloadStatus, - currentPageSize: currentPageSize ?? this.currentPageSize, storiesDownloaded: storiesDownloaded ?? this.storiesDownloaded, storiesToBeDownloaded: storiesToBeDownloaded ?? this.storiesToBeDownloaded, @@ -179,7 +174,6 @@ class StoriesState extends Equatable { readStoriesIds, isOfflineReading, downloadStatus, - currentPageSize, storiesDownloaded, storiesToBeDownloaded, ]; diff --git a/lib/cubits/remote_config/remote_config_state.dart b/lib/cubits/remote_config/remote_config_state.dart index 9441ec3d..92f61ae5 100644 --- a/lib/cubits/remote_config/remote_config_state.dart +++ b/lib/cubits/remote_config/remote_config_state.dart @@ -10,6 +10,42 @@ final class RemoteConfigState extends Equatable { @protected final Map data; + String get storySelector => getString( + key: 'storySelector', + fallback: '''#hnmain > tbody > tr > td > table > tbody > .athing''', + ); + + String get subtextSelector => getString( + key: 'subtextSelector', + fallback: + '''#hnmain > tbody > tr > td > table > tbody > tr > .subtext''', + ); + + String get titlelineSelector => getString( + key: 'titlelineSelector', + fallback: '''.title > .titleline > a''', + ); + + String get pointSelector => getString( + key: 'pointSelector', + fallback: '''.subline > .score''', + ); + + String get userSelector => getString( + key: 'userSelector', + fallback: '''.subline > .hnuser''', + ); + + String get ageSelector => getString( + key: 'ageSelector', + fallback: '''.subline > .age''', + ); + + String get cmtCountSelector => getString( + key: 'cmtCountSelector', + fallback: '''.subline > a''', + ); + String get athingComtrSelector => getString( key: 'athingComtrSelector', fallback: diff --git a/lib/models/story_type.dart b/lib/models/story_type.dart index 4def627c..3dbe2900 100644 --- a/lib/models/story_type.dart +++ b/lib/models/story_type.dart @@ -1,13 +1,17 @@ enum StoryType { - top('topstories'), - best('beststories'), - latest('newstories'), - ask('askstories'), - show('showstories'); + top('topstories', ''), + best('beststories', 'best'), + latest('newstories', 'newest'), + ask('askstories', 'ask'), + show('showstories', 'show'); - const StoryType(this.path); + const StoryType( + this.apiPathParam, + this.webPathParam, + ); - final String path; + final String apiPathParam; + final String webPathParam; String get label { switch (this) { diff --git a/lib/repositories/hacker_news_repository.dart b/lib/repositories/hacker_news_repository.dart index 799c90b0..331fadae 100644 --- a/lib/repositories/hacker_news_repository.dart +++ b/lib/repositories/hacker_news_repository.dart @@ -118,7 +118,7 @@ class HackerNewsRepository { /// Fetch ids of stories of a certain [StoryType]. Future> fetchStoryIds({required StoryType type}) async { final List ids = await _firebaseClient - .get('$_baseUrl${type.path}.json') + .get('$_baseUrl${type.apiPathParam}.json') .then((dynamic val) { final List ids = (val as List).cast(); return ids; diff --git a/lib/repositories/hacker_news_web_repository.dart b/lib/repositories/hacker_news_web_repository.dart index 51e36290..04fbb6a1 100644 --- a/lib/repositories/hacker_news_web_repository.dart +++ b/lib/repositories/hacker_news_web_repository.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:collection/collection.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; @@ -41,6 +42,151 @@ class HackerNewsWebRepository { 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1', }; + static const String _storiesBaseUrl = 'https://news.ycombinator.com'; + + String get _storySelector => _remoteConfigCubit.state.storySelector; + + String get _titlelineSelector => _remoteConfigCubit.state.titlelineSelector; + + String get _subtextSelector => _remoteConfigCubit.state.subtextSelector; + + String get _pointSelector => _remoteConfigCubit.state.pointSelector; + + String get _userSelector => _remoteConfigCubit.state.userSelector; + + String get _ageSelector => _remoteConfigCubit.state.ageSelector; + + String get _cmtCountSelector => _remoteConfigCubit.state.cmtCountSelector; + + Stream fetchStoriesStream( + StoryType storyType, { + required int page, + }) async* { + Future> fetchElements( + int page, + ) async { + try { + final Uri url = Uri.parse( + storyType == StoryType.top + ? '$_storiesBaseUrl?p=$page' + : '$_storiesBaseUrl/${storyType.webPathParam}?p=$page', + ); + final Options option = Options( + headers: _headers, + persistentConnection: true, + ); + + /// Be more conservative while user is on wifi. + final Response response = await _dio.getUri( + url, + options: option, + ); + + final String data = response.data ?? ''; + final Document document = parse(data); + final List elements = + document.querySelectorAll(_storySelector); + final List subtextElements = + document.querySelectorAll(_subtextSelector); + return List<(Element, Element)>.generate( + min(elements.length, subtextElements.length), + (int index) => + (elements.elementAt(index), subtextElements.elementAt(index)), + ); + } on DioException catch (e) { + if (e.response?.statusCode == HttpStatus.forbidden) { + throw RateLimitedWithFallbackException(); + } + throw GenericException(); + } + } + + final Set fetchedCommentIds = {}; + final Iterable<(Element, Element)> elements = await fetchElements(page); + + while (elements.isNotEmpty) { + for (final (Element, Element) element in elements) { + final Element titleElement = element.$1; + final Element subtextElement = element.$2; + + /// Get id. + final String? idStr = titleElement.attributes['id']; + final int? id = int.tryParse(idStr ?? ''); + + /// Get user. + final Element? userElement = + subtextElement.querySelector(_userSelector); + final String? user = userElement?.nodes.firstOrNull?.text; + + /// Get post date. + final Element? postDateElement = + subtextElement.querySelector(_ageSelector); + final String? dateStr = postDateElement?.attributes['title']; + final int? timestamp = dateStr == null + ? null + : DateTime.parse(dateStr) + .copyWith(isUtc: true) + .millisecondsSinceEpoch; + + /// Get descendants. + final Element? cmtCountElement = + subtextElement.querySelectorAll(_cmtCountSelector).lastOrNull; + final String cmtCountStr = cmtCountElement?.nodes.firstOrNull?.text + ?.split('\u{00A0}') + .firstOrNull ?? + ''; + final int cmtCount = int.tryParse(cmtCountStr) ?? 0; + + /// Get title; + final Element? titlelineElement = + titleElement.querySelector(_titlelineSelector); + final String title = titlelineElement?.nodes.firstOrNull?.text ?? ''; + final String url = titlelineElement?.attributes['href'] ?? ''; + + /// Get points. + final Element? ptElement = subtextElement.querySelector(_pointSelector); + + /// Example: "80 points" + final String? pointsStr = ptElement?.nodes.firstOrNull?.text; + final int? points = + int.tryParse(pointsStr?.split(' ').firstOrNull ?? ''); + + final Story story = Story( + id: id ?? 0, + time: timestamp ?? 0, + score: points ?? 0, + by: user ?? '', + text: '', + kids: const [], + hidden: false, + descendants: cmtCount, + title: title, + type: '', + url: storyType == StoryType.ask ? '$_itemBaseUrl$id' : url, + parts: const [], + ); + + /// Skip any comment with no valid id or timestamp. + if (story.id == 0 || timestamp == 0) { + continue; + } + + /// Duplicate comment means we are done fetching all the comments. + if (fetchedCommentIds.contains(story.id)) return; + + fetchedCommentIds.add(story.id); + yield story; + } + + /// Due to rate limiting, we have a short break here. + await Future.delayed(AppDurations.twoSeconds); + + page++; + //elements = await fetchElements(page); + return; + } + } + static const String _favoritesBaseUrl = 'https://news.ycombinator.com/favorites?id='; static const String _aThingSelector = diff --git a/lib/repositories/remote_config_repository.dart b/lib/repositories/remote_config_repository.dart index 6b0c9618..56920e22 100644 --- a/lib/repositories/remote_config_repository.dart +++ b/lib/repositories/remote_config_repository.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; class RemoteConfigRepository { RemoteConfigRepository({Dio? dio}) : _dio = dio ?? Dio(); @@ -11,13 +12,21 @@ class RemoteConfigRepository { 'https://raw.githubusercontent.com/Livinglist/Hacki/master/assets/'; Future> fetchRemoteConfig() async { - const String fileName = - kReleaseMode ? 'remote-config.json' : 'remote-config-dev.json'; - final Response response = await _dio.get( - '$_path$fileName', - ); - final String data = response.data as String? ?? ''; - final Map json = jsonDecode(data) as Map; - return json; + if (kReleaseMode) { + const String fileName = 'remote-config.json'; + final Response response = await _dio.get( + '$_path$fileName', + ); + final String data = response.data as String? ?? ''; + final Map json = jsonDecode(data) as Map; + return json; + } else { + const String fileName = 'remote-config-dev.json'; + final String data = await rootBundle.loadString('assets/$fileName'); + final Map json = jsonDecode(data) as Map; + return json; + } } } diff --git a/lib/screens/widgets/stories_list_view.dart b/lib/screens/widgets/stories_list_view.dart index 9cf40c1c..98ba5a4b 100644 --- a/lib/screens/widgets/stories_list_view.dart +++ b/lib/screens/widgets/stories_list_view.dart @@ -86,7 +86,7 @@ class _StoriesListViewState extends State return ItemsListView( showOfflineBanner: true, markReadStories: preferenceState.isMarkReadStoriesEnabled, - showWebPreviewOnStoryTile: + showWebPreviewOnStoryTile: storyType != StoryType.ask && preferenceState.isComplexStoryTileEnabled, showMetadataOnStoryTile: preferenceState.isMetadataEnabled, showFavicon: preferenceState.isFaviconEnabled, diff --git a/lib/utils/log_util.dart b/lib/utils/log_util.dart index c2a18978..a66b3cbc 100644 --- a/lib/utils/log_util.dart +++ b/lib/utils/log_util.dart @@ -13,7 +13,7 @@ abstract class LogUtil { printTime: true, ) : PrettyPrinter( - printTime: true, + dateTimeFormat: DateTimeFormat.dateAndTime, ); static LogOutput logOutput(File outputFile) => MultiOutput( diff --git a/pubspec.lock b/pubspec.lock index 0449d8a0..f70dd458 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,15 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" adaptive_theme: dependency: "direct main" description: @@ -21,10 +26,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.7.0" animations: dependency: "direct main" description: @@ -37,10 +42,10 @@ packages: dependency: transitive description: name: ansicolor - sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" archive: dependency: transitive description: @@ -109,26 +114,26 @@ packages: dependency: "direct main" description: name: cached_network_image - sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" + sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.0" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.1.1" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" + sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" characters: dependency: transitive description: @@ -165,18 +170,18 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: db7a4e143dc72cc3cb2044ef9b052a7ebfe729513e6a82943bc3526f784365b8 + sha256: "2056db5241f96cdc0126bd94459fc4cdc13876753768fc7a31c425e50a7177d0" url: "https://pub.dev" source: hosted - version: "6.0.3" + version: "6.0.5" connectivity_plus_platform_interface: dependency: transitive description: name: connectivity_plus_platform_interface - sha256: b6a56efe1e6675be240de39107281d4034b64ac23438026355b4234042a35adb + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" convert: dependency: transitive description: @@ -189,26 +194,26 @@ packages: dependency: transitive description: name: coverage - sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" + sha256: "576aaab8b1abdd452e0f656c3e73da9ead9d7880e15bdc494189d9c1a1baf0db" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.0" cross_file: dependency: transitive description: name: cross_file - sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.4+1" + version: "0.3.4+2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1dceb0cf05cb63a7852c11560060e53ec2f182079a16ced6f4395c5b0875baf8" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.4" csslib: dependency: transitive description: @@ -229,18 +234,18 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91 + sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 url: "https://pub.dev" source: hosted - version: "10.1.0" + version: "10.1.2" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" diff_match_patch: dependency: transitive description: @@ -253,10 +258,18 @@ packages: dependency: "direct main" description: name: dio - sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" + sha256: "0dfb6b6a1979dac1c1245e17cef824d7b452ea29bd33d3467269f9bef3715fb0" + url: "https://pub.dev" + source: hosted + version: "5.6.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" url: "https://pub.dev" source: hosted - version: "5.4.3+1" + version: "2.0.0" equatable: dependency: "direct main" description: @@ -294,10 +307,10 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file: dependency: transitive description: @@ -323,18 +336,18 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2 + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a url: "https://pub.dev" source: hosted - version: "8.1.5" + version: "8.1.6" flutter_cache_manager: dependency: "direct main" description: name: flutter_cache_manager - sha256: "395d6b7831f21f3b989ebedbb785545932adb9afe2622c1ffacf7f4b53a7e544" + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.4.1" flutter_driver: dependency: "direct dev" description: flutter @@ -424,26 +437,26 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "40e6fbd2da7dcc7ed78432c5cdab1559674b4af035fddbfb2f9a8f9c2112fcef" + sha256: c500d5d9e7e553f06b61877ca6b9c8b92c570a4c8db371038702e8ce57f8a50f url: "https://pub.dev" source: hosted - version: "17.1.2" + version: "17.2.2" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af url: "https://pub.dev" source: hosted - version: "4.0.0+1" + version: "4.0.1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "340abf67df238f7f0ef58f4a26d2a83e1ab74c77ab03cd2b2d5018ac64db30b7" + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "7.2.0" flutter_material_color_picker: dependency: "direct main" description: @@ -521,10 +534,10 @@ packages: dependency: "direct main" description: name: flutter_slidable - sha256: "673403d2eeef1f9e8483bd6d8d92aae73b1d8bd71f382bc3930f699c731bc27c" + sha256: "2c5611c0b44e20d180e4342318e1bbc28b0a44ad2c442f5df16962606fd3e8e3" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" flutter_test: dependency: "direct dev" description: flutter @@ -576,10 +589,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: abec47eb8c8c36ebf41d0a4c64dbbe7f956e39a012b3aafc530e951bdc12fe3f + sha256: ddc16d34b0d74cb313986918c0f0885a7ba2fc24d8fb8419de75f0015144ccfe url: "https://pub.dev" source: hosted - version: "14.1.4" + version: "14.2.3" hive: dependency: "direct main" description: @@ -608,10 +621,10 @@ packages: dependency: "direct main" description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -724,10 +737,10 @@ packages: dependency: "direct main" description: name: logger - sha256: af05cc8714f356fd1f3888fb6741cbe9fbe25cdb6eedbab80e1a6db21047d4a4 + sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" logging: dependency: transitive description: @@ -736,6 +749,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" matcher: dependency: transitive description: @@ -780,10 +801,10 @@ packages: dependency: "direct dev" description: name: mocktail - sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6 + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" nested: dependency: transitive description: @@ -812,10 +833,10 @@ packages: dependency: transitive description: name: octo_image - sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" package_config: dependency: transitive description: @@ -828,18 +849,18 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 + sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "8.0.2" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" path: dependency: "direct main" description: @@ -852,18 +873,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: "direct main" description: name: path_provider_android - sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514" + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" url: "https://pub.dev" source: hosted - version: "2.2.5" + version: "2.2.10" path_provider_foundation: dependency: "direct main" description: @@ -892,10 +913,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" petitparser: dependency: transitive description: @@ -932,10 +953,10 @@ packages: dependency: "direct main" description: name: pretty_dio_logger - sha256: "00b80053063935cf9a6190da344c5373b9d0e92da4c944c878ff2fbef0ef6dc2" + sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" process: dependency: transitive description: @@ -973,10 +994,10 @@ packages: dependency: transitive description: name: qr - sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" qr_code_scanner: dependency: "direct main" description: @@ -1005,10 +1026,10 @@ packages: dependency: "direct main" description: name: responsive_builder - sha256: a38ba9ba86c9daf08904674553034b651377b1d685d10ee450d8350ae51f76ec + sha256: "64a5ef3fbe3628e4588a0c2391c3186300e76f58621d8135cc77aac816255a3e" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.1" rxdart: dependency: "direct main" description: @@ -1029,10 +1050,10 @@ packages: dependency: "direct main" description: name: sembast - sha256: dbe19600cff55d43f19405be79138c3fd2c08a87b0b152b18609b9427d113a64 + sha256: a49ce14fb0d81bee9f8941061a38f4b790d19c0ab01abe35a529c1fcef0512a1 url: "https://pub.dev" source: hosted - version: "3.7.1" + version: "3.7.2" share_plus: dependency: "direct main" description: @@ -1053,58 +1074,58 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + sha256: c272f9cabca5a81adc9b0894381e9c1def363e980f960fa903c604c471b22f68 url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.1" shared_preferences_android: dependency: "direct main" description: name: shared_preferences_android - sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577" + sha256: a7e8467e9181cef109f601e3f65765685786c1a738a83d7fbbde377589c0d974 url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.1" shared_preferences_foundation: dependency: "direct main" description: name: shared_preferences_foundation - sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shelf: dependency: transitive description: @@ -1133,10 +1154,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" shimmer: dependency: "direct main" description: @@ -1289,10 +1310,10 @@ packages: dependency: transitive description: name: timezone - sha256: a6ccda4a69a442098b602c44e61a1e2b4bf6f5516e875bbf0f427d5df14745d5 + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" url: "https://pub.dev" source: hosted - version: "0.9.3" + version: "0.9.4" typed_data: dependency: transitive description: @@ -1321,34 +1342,34 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" url: "https://pub.dev" source: hosted - version: "6.2.6" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf + sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79 url: "https://pub.dev" source: hosted - version: "6.3.3" + version: "6.3.9" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.0" url_launcher_macos: dependency: transitive description: @@ -1369,26 +1390,26 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" uuid: dependency: transitive description: name: uuid - sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" + sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.4.2" vector_math: dependency: transitive description: @@ -1425,10 +1446,10 @@ packages: dependency: "direct main" description: name: wakelock_plus - sha256: "4fa83a128b4127619e385f686b4f080a5d2de46cff8e8c94eccac5fcf76550e5" + sha256: bf4ee6f17a2fa373ed3753ad0e602b7603f8c75af006d5b9bdade263928c0484 url: "https://pub.dev" source: hosted - version: "1.2.7" + version: "1.2.8" wakelock_plus_platform_interface: dependency: transitive description: @@ -1453,14 +1474,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "3.0.1" webdriver: dependency: transitive description: @@ -1489,10 +1518,10 @@ packages: dependency: transitive description: name: webview_flutter_android - sha256: f42447ca49523f11d8f70abea55ea211b3cafe172dd7a0e7ac007bb35dd356dc + sha256: c66651fba15f9d7ddd31daec42da8d6bce46c85610a7127e3ebcb39a4395c3c9 url: "https://pub.dev" source: hosted - version: "3.16.4" + version: "3.16.6" webview_flutter_platform_interface: dependency: transitive description: @@ -1505,26 +1534,26 @@ packages: dependency: transitive description: name: webview_flutter_wkwebview - sha256: "7affdf9d680c015b11587181171d3cad8093e449db1f7d9f0f08f4f33d24f9a0" + sha256: "9c62cc46fa4f2d41e10ab81014c1de470a6c6f26051a2de32111b2ee55287feb" url: "https://pub.dev" source: hosted - version: "3.13.1" + version: "3.14.0" win32: dependency: transitive description: name: win32 - sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "5.5.1" + version: "5.5.4" win32_registry: dependency: transitive description: name: win32_registry - sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" + sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.1.4" workmanager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 0c43a6b7..d94a5bd3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,7 +54,7 @@ dependencies: path: components/in_app_review intl: ^0.19.0 linkify: ^5.0.0 - logger: ^2.3.0 + logger: ^2.4.0 memoize: ^3.0.0 package_info_plus: ^8.0.0 path: ^1.8.2 @@ -104,6 +104,7 @@ flutter: assets: - assets/images/ + - assets/remote-config-dev.json fonts: - family: RobotoSlab From 9d2f0e77fbd09972044571f30816478015804025 Mon Sep 17 00:00:00 2001 From: Livinglist Date: Tue, 13 Aug 2024 14:43:18 -0700 Subject: [PATCH 2/7] remove comments. --- lib/blocs/stories/stories_bloc.dart | 11 ----------- lib/repositories/hacker_news_web_repository.dart | 5 +---- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/lib/blocs/stories/stories_bloc.dart b/lib/blocs/stories/stories_bloc.dart index 943ee2a5..0afd520e 100644 --- a/lib/blocs/stories/stories_bloc.dart +++ b/lib/blocs/stories/stories_bloc.dart @@ -121,17 +121,6 @@ class StoriesBloc extends Bloc { add(StoryLoaded(story: story, type: type)); }).asFuture(); add(StoryLoadingCompleted(type: type)); - - // await _hackerNewsRepository - // .fetchStoriesStream( - // ids: ids.sublist(0, state.currentPageSize), - // sequential: _preferenceCubit.state.isComplexStoryTileEnabled || - // _preferenceCubit.state.isFaviconEnabled, - // ) - // .listen((Story story) { - // add(StoryLoaded(story: story, type: type)); - // }).asFuture(); - // add(StoryLoadingCompleted(type: type)); } } diff --git a/lib/repositories/hacker_news_web_repository.dart b/lib/repositories/hacker_news_web_repository.dart index 04fbb6a1..d26daec0 100644 --- a/lib/repositories/hacker_news_web_repository.dart +++ b/lib/repositories/hacker_news_web_repository.dart @@ -161,7 +161,7 @@ class HackerNewsWebRepository { hidden: false, descendants: cmtCount, title: title, - type: '', + type: 'story', url: storyType == StoryType.ask ? '$_itemBaseUrl$id' : url, parts: const [], ); @@ -180,9 +180,6 @@ class HackerNewsWebRepository { /// Due to rate limiting, we have a short break here. await Future.delayed(AppDurations.twoSeconds); - - page++; - //elements = await fetchElements(page); return; } } From f740016d44afd3255a8d858841468f46a65f18b0 Mon Sep 17 00:00:00 2001 From: Livinglist Date: Tue, 13 Aug 2024 14:48:45 -0700 Subject: [PATCH 3/7] update. --- lib/models/story_type.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/models/story_type.dart b/lib/models/story_type.dart index 3dbe2900..a8727ccc 100644 --- a/lib/models/story_type.dart +++ b/lib/models/story_type.dart @@ -10,7 +10,12 @@ enum StoryType { this.webPathParam, ); + /// The path param used in the official Hacker News API. + /// e.g. https://hacker-news.firebaseio.com/v0/{apiPathParam}.json final String apiPathParam; + + /// The path param used in the HN web. + /// e.g. https://news.ycombinator.com/{webPathParam} final String webPathParam; String get label { From 93eaf1e04c1326162f871b9b94146baf846ce4b7 Mon Sep 17 00:00:00 2001 From: Livinglist Date: Tue, 13 Aug 2024 15:00:14 -0700 Subject: [PATCH 4/7] update. --- fastlane/metadata/android/en-US/changelogs/148.txt | 1 + pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/148.txt diff --git a/fastlane/metadata/android/en-US/changelogs/148.txt b/fastlane/metadata/android/en-US/changelogs/148.txt new file mode 100644 index 00000000..fd5c5531 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/148.txt @@ -0,0 +1 @@ +- UX improvements. \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index d94a5bd3..2556e605 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: hacki description: A Hacker News reader. -version: 2.8.3+148 +version: 2.9.0+148 publish_to: none environment: From 77f691cef28db3508c25dad6a205fb2611959be5 Mon Sep 17 00:00:00 2001 From: Livinglist Date: Tue, 13 Aug 2024 15:00:56 -0700 Subject: [PATCH 5/7] format. --- lib/repositories/remote_config_repository.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/repositories/remote_config_repository.dart b/lib/repositories/remote_config_repository.dart index 56920e22..5257c830 100644 --- a/lib/repositories/remote_config_repository.dart +++ b/lib/repositories/remote_config_repository.dart @@ -18,14 +18,14 @@ class RemoteConfigRepository { '$_path$fileName', ); final String data = response.data as String? ?? ''; - final Map json = jsonDecode(data) as Map; + final Map json = + jsonDecode(data) as Map; return json; } else { const String fileName = 'remote-config-dev.json'; final String data = await rootBundle.loadString('assets/$fileName'); - final Map json = jsonDecode(data) as Map; + final Map json = + jsonDecode(data) as Map; return json; } } From 77d0ecb9b6694d4c44a93335b57227e532bae8ab Mon Sep 17 00:00:00 2001 From: Livinglist Date: Tue, 13 Aug 2024 15:23:44 -0700 Subject: [PATCH 6/7] add err handling. --- lib/blocs/stories/stories_bloc.dart | 75 ++++++++++++++++++++++++++-- lib/blocs/stories/stories_event.dart | 28 +++++++++-- 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/lib/blocs/stories/stories_bloc.dart b/lib/blocs/stories/stories_bloc.dart index 0afd520e..d423a4f0 100644 --- a/lib/blocs/stories/stories_bloc.dart +++ b/lib/blocs/stories/stories_bloc.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; @@ -68,6 +69,7 @@ class StoriesBloc extends Bloc { DeviceScreenType? deviceScreenType; StreamSubscription? _preferenceSubscription; static const String _logPrefix = '[StoriesBloc]'; + static const int apiPageSize = 30; Future onInitialize( StoriesInitialize event, @@ -105,7 +107,7 @@ class StoriesBloc extends Bloc { .getCachedStoriesStream(ids: ids) .listen((Story story) => add(StoryLoaded(story: story, type: type))) .onDone(() => add(StoryLoadingCompleted(type: type))); - } else { + } else if (event.useApi) { final List ids = await _hackerNewsRepository.fetchStoryIds(type: type); emit( @@ -115,9 +117,34 @@ class StoriesBloc extends Bloc { .copyWithStatusUpdated(type: type, to: Status.inProgress), ); + await _hackerNewsRepository + .fetchStoriesStream( + ids: ids.sublist(0, apiPageSize), + sequential: _preferenceCubit.state.isComplexStoryTileEnabled || + _preferenceCubit.state.isFaviconEnabled, + ) + .listen((Story story) { + add(StoryLoaded(story: story, type: type)); + }).asFuture(); + } else { + emit( + state + .copyWithCurrentPageUpdated(type: type, to: 0) + .copyWithStatusUpdated(type: type, to: Status.inProgress), + ); + await _hackerNewsWebRepository .fetchStoriesStream(event.type, page: 1) - .listen((Story story) { + .handleError((dynamic e) { + _logger.e('$_logPrefix error loading stories $e'); + + switch (e.runtimeType) { + case RateLimitedException: + case RateLimitedWithFallbackException: + case PossibleParsingException: + add(event.copyWith(useApi: true)); + } + }).listen((Story story) { add(StoryLoaded(story: story, type: type)); }).asFuture(); add(StoryLoadingCompleted(type: type)); @@ -150,7 +177,10 @@ class StoriesBloc extends Bloc { } } - void onLoadMore(StoriesLoadMore event, Emitter emit) { + Future onLoadMore( + StoriesLoadMore event, + Emitter emit, + ) async { if (state.statusByType[event.type] == Status.inProgress) return; emit( @@ -160,9 +190,9 @@ class StoriesBloc extends Bloc { ), ); - final int currentPage = state.currentPageByType[event.type]!; + final int currentPage = state.currentPageByType[event.type]! + 1; emit( - state.copyWithCurrentPageUpdated(type: event.type, to: currentPage + 1), + state.copyWithCurrentPageUpdated(type: event.type, to: currentPage), ); if (state.isOfflineReading) { @@ -172,9 +202,44 @@ class StoriesBloc extends Bloc { to: Status.success, ), ); + } else if (event.useApi) { + late final int length; + final List? ids = state.storyIdsByType[event.type]; + + if (ids?.isEmpty ?? true) { + final List ids = + await _hackerNewsRepository.fetchStoryIds(type: event.type); + length = ids.length; + } else { + length = ids!.length; + } + + final int lower = apiPageSize * currentPage; + final int upper = min(length, lower + apiPageSize); + _hackerNewsRepository + .fetchStoriesStream( + ids: state.storyIdsByType[event.type]!.sublist( + lower, + upper, + ), + ) + .listen( + (Story story) => add(StoryLoaded(story: story, type: event.type)), + ) + .onDone(() => add(StoryLoadingCompleted(type: event.type))); } else { _hackerNewsWebRepository .fetchStoriesStream(event.type, page: currentPage + 1) + .handleError((dynamic e) { + _logger.e('$_logPrefix error loading more stories $e'); + + switch (e.runtimeType) { + case RateLimitedException: + case RateLimitedWithFallbackException: + case PossibleParsingException: + add(event.copyWith(useApi: true)); + } + }) .listen( (Story story) => add(StoryLoaded(story: story, type: event.type)), ) diff --git a/lib/blocs/stories/stories_event.dart b/lib/blocs/stories/stories_event.dart index ee4d295c..4211164a 100644 --- a/lib/blocs/stories/stories_event.dart +++ b/lib/blocs/stories/stories_event.dart @@ -6,15 +6,25 @@ abstract class StoriesEvent extends Equatable { } class LoadStories extends StoriesEvent { - LoadStories({required this.type, this.isRefreshing = false}); + LoadStories({ + required this.type, + this.isRefreshing = false, + this.useApi = false, + }); final StoryType type; final bool isRefreshing; + final bool useApi; + + LoadStories copyWith({required bool useApi}) { + return LoadStories(type: type, isRefreshing: isRefreshing, useApi: useApi); + } @override List get props => [ type, isRefreshing, + useApi, ]; } @@ -33,12 +43,24 @@ class StoriesRefresh extends StoriesEvent { } class StoriesLoadMore extends StoriesEvent { - StoriesLoadMore({required this.type}); + StoriesLoadMore({ + required this.type, + this.useApi = false, + }); final StoryType type; + final bool useApi; + + StoriesLoadMore copyWith({required bool useApi}) { + return StoriesLoadMore(type: type, useApi: useApi); + } + @override - List get props => [type]; + List get props => [ + type, + useApi, + ]; } class StoriesDownload extends StoriesEvent { From a604a0f2794c34f69a84da740b303e6a465fac46 Mon Sep 17 00:00:00 2001 From: Livinglist Date: Tue, 13 Aug 2024 16:12:21 -0700 Subject: [PATCH 7/7] fix paginations for latest stories. --- assets/remote-config-dev.json | 3 +- assets/remote-config.json | 3 +- lib/blocs/stories/stories_bloc.dart | 4 +-- .../remote_config/remote_config_state.dart | 6 ++++ .../hacker_news_web_repository.dart | 36 ++++++++++++++++--- 5 files changed, 43 insertions(+), 9 deletions(-) diff --git a/assets/remote-config-dev.json b/assets/remote-config-dev.json index 80c8512c..5c58b137 100644 --- a/assets/remote-config-dev.json +++ b/assets/remote-config-dev.json @@ -10,5 +10,6 @@ "pointSelector": ".subline > .score", "userSelector": ".subline > .hnuser", "ageSelector": ".subline > .age", - "cmtCountSelector": ".subline > a" + "cmtCountSelector": ".subline > a", + "moreLinkSelector": "#hnmain > tbody > tr:nth-child(3) > td > table > tbody > tr > td.title > a" } diff --git a/assets/remote-config.json b/assets/remote-config.json index 80c8512c..5c58b137 100644 --- a/assets/remote-config.json +++ b/assets/remote-config.json @@ -10,5 +10,6 @@ "pointSelector": ".subline > .score", "userSelector": ".subline > .hnuser", "ageSelector": ".subline > .age", - "cmtCountSelector": ".subline > a" + "cmtCountSelector": ".subline > a", + "moreLinkSelector": "#hnmain > tbody > tr:nth-child(3) > td > table > tbody > tr > td.title > a" } diff --git a/lib/blocs/stories/stories_bloc.dart b/lib/blocs/stories/stories_bloc.dart index d423a4f0..ae7ee5f5 100644 --- a/lib/blocs/stories/stories_bloc.dart +++ b/lib/blocs/stories/stories_bloc.dart @@ -39,7 +39,7 @@ class StoriesBloc extends Bloc { super(const StoriesState.init()) { on( onLoadStories, - transformer: concurrent(), + transformer: sequential(), ); on(onInitialize); on(onRefresh); @@ -229,7 +229,7 @@ class StoriesBloc extends Bloc { .onDone(() => add(StoryLoadingCompleted(type: event.type))); } else { _hackerNewsWebRepository - .fetchStoriesStream(event.type, page: currentPage + 1) + .fetchStoriesStream(event.type, page: currentPage) .handleError((dynamic e) { _logger.e('$_logPrefix error loading more stories $e'); diff --git a/lib/cubits/remote_config/remote_config_state.dart b/lib/cubits/remote_config/remote_config_state.dart index 92f61ae5..28af95af 100644 --- a/lib/cubits/remote_config/remote_config_state.dart +++ b/lib/cubits/remote_config/remote_config_state.dart @@ -46,6 +46,12 @@ final class RemoteConfigState extends Equatable { fallback: '''.subline > a''', ); + String get moreLinkSelector => getString( + key: 'moreLinkSelector', + fallback: + ''''#hnmain > tbody > tr:nth-child(3) > td > table > tbody > tr > td.title > a''', + ); + String get athingComtrSelector => getString( key: 'athingComtrSelector', fallback: diff --git a/lib/repositories/hacker_news_web_repository.dart b/lib/repositories/hacker_news_web_repository.dart index d26daec0..e724f121 100644 --- a/lib/repositories/hacker_news_web_repository.dart +++ b/lib/repositories/hacker_news_web_repository.dart @@ -58,6 +58,10 @@ class HackerNewsWebRepository { String get _cmtCountSelector => _remoteConfigCubit.state.cmtCountSelector; + String get _moreLinkSelector => _remoteConfigCubit.state.moreLinkSelector; + + static final Map _next = {}; + Stream fetchStoriesStream( StoryType storyType, { required int page, @@ -66,11 +70,16 @@ class HackerNewsWebRepository { int page, ) async { try { - final Uri url = Uri.parse( - storyType == StoryType.top - ? '$_storiesBaseUrl?p=$page' - : '$_storiesBaseUrl/${storyType.webPathParam}?p=$page', - ); + final String urlStr = switch (storyType) { + StoryType.top => '$_storiesBaseUrl?p=$page', + StoryType.best || + StoryType.ask || + StoryType.show => + '$_storiesBaseUrl/${storyType.webPathParam}?p=$page', + StoryType.latest => + '$_storiesBaseUrl/${storyType.webPathParam}?next=${_next[page]}' + }; + final Uri url = Uri.parse(urlStr); final Options option = Options( headers: _headers, persistentConnection: true, @@ -88,6 +97,23 @@ class HackerNewsWebRepository { document.querySelectorAll(_storySelector); final List subtextElements = document.querySelectorAll(_subtextSelector); + + if (storyType == StoryType.latest) { + /// Get the next id for latest stories. + final Element? moreLinkElement = + document.querySelector(_moreLinkSelector); + + /// Example: "newest?next=41240344&n=31" + final String? href = moreLinkElement?.attributes['href']; + final String? nextIdStr = + href?.split('&n').firstOrNull?.split('=').lastOrNull; + final int? nextId = int.tryParse(nextIdStr ?? ''); + + if (nextId != null) { + _next[page + 1] = nextId; + } + } + return List<(Element, Element)>.generate( min(elements.length, subtextElements.length), (int index) =>