Compare commits

...

4 Commits

Author SHA1 Message Date
56e442b09f v0.2.26 (#63)
* added integration test.

* replace listview with listview.builder

* added cache.

* bumped version.

* bumped version.

* updated github action.

* bumped version.

* fixed time machine cubit.

* fixed time machine cubit.

* reverted changes.

* removed keepAliveMixin
2022-06-28 17:32:50 -07:00
9069efcced improved logging. 2022-06-28 12:08:02 -07:00
bf6a5667dc fixed naming. 2022-06-28 12:01:36 -07:00
cff73a010b v0.2.25 (#62)
* bumped version.

* improved cache.

* improved comment cache.

* updated default val for navigationMode.
2022-06-28 00:08:07 -07:00
37 changed files with 702 additions and 462 deletions

View File

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
JAVA_VERSION: "11.0" JAVA_VERSION: "11.0"
FLUTTER_VERSION: "3.0.0" FLUTTER_VERSION: "3.0.3"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-java@v2 - uses: actions/setup-java@v2
@ -20,7 +20,7 @@ jobs:
java-version: '17' java-version: '17'
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with: with:
flutter-version: '3.0.0' flutter-version: '3.0.3'
channel: 'stable' channel: 'stable'
- run: flutter pub get - run: flutter pub get
- run: flutter analyze - run: flutter analyze

View File

@ -0,0 +1,2 @@
- Offline mode now includes web pages.
- You can now sort comments in story screen.

View File

@ -0,0 +1,2 @@
- Offline mode now includes web pages.
- You can now sort comments in story screen.

View File

@ -0,0 +1,46 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:hacki/main.dart' as app;
import 'package:hacki/screens/widgets/story_tile.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('performance test', () {
testWidgets('scrolling performance on ItemScreen',
(WidgetTester tester) async {
await app.main(testing: true);
await tester.pump();
final Finder bestStoryTabFinder = find.text('BEST');
await tester.tap(bestStoryTabFinder);
await tester.pumpAndSettle(const Duration(seconds: 3));
final Finder storyTileFinder = find.byType(StoryTile);
await tester.tap(storyTileFinder.first);
await tester.pumpAndSettle(const Duration(seconds: 3));
TestGesture gesture = await tester.startGesture(const Offset(0, 300));
await gesture.moveBy(const Offset(0, -300));
await tester.pump();
gesture = await tester.startGesture(const Offset(0, 300));
await gesture.moveBy(const Offset(0, -300));
await tester.pump();
gesture = await tester.startGesture(const Offset(0, 300));
await gesture.moveBy(const Offset(0, -300));
await tester.pump();
gesture = await tester.startGesture(const Offset(0, 300));
await gesture.moveBy(const Offset(0, 900));
await tester.pump();
gesture = await tester.startGesture(const Offset(0, 300));
await gesture.moveBy(const Offset(0, -900));
await tester.pump();
});
});
}

View File

@ -19,6 +19,8 @@ PODS:
- FMDB (2.7.5): - FMDB (2.7.5):
- FMDB/standard (= 2.7.5) - FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5) - FMDB/standard (2.7.5)
- integration_test (0.0.1):
- Flutter
- OrderedSet (5.0.0) - OrderedSet (5.0.0)
- path_provider_ios (0.0.1): - path_provider_ios (0.0.1):
- Flutter - Flutter
@ -50,6 +52,7 @@ DEPENDENCIES:
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`) - flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
@ -80,6 +83,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_secure_storage/ios" :path: ".symlinks/plugins/flutter_secure_storage/ios"
flutter_siri_suggestions: flutter_siri_suggestions:
:path: ".symlinks/plugins/flutter_siri_suggestions/ios" :path: ".symlinks/plugins/flutter_siri_suggestions/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
path_provider_ios: path_provider_ios:
:path: ".symlinks/plugins/path_provider_ios/ios" :path: ".symlinks/plugins/path_provider_ios/ios"
receive_sharing_intent: receive_sharing_intent:
@ -109,6 +114,7 @@ SPEC CHECKSUMS:
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825

View File

@ -568,7 +568,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = QMWX3X2NF7; DEVELOPMENT_TEAM = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -577,7 +577,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.2.24; MARKETING_VERSION = 0.2.26;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki; PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -705,7 +705,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = QMWX3X2NF7; DEVELOPMENT_TEAM = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -714,7 +714,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.2.24; MARKETING_VERSION = 0.2.26;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki; PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -736,7 +736,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = QMWX3X2NF7; DEVELOPMENT_TEAM = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -745,7 +745,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.2.24; MARKETING_VERSION = 0.2.26;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki; PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -17,15 +17,18 @@ part 'stories_state.dart';
class StoriesBloc extends Bloc<StoriesEvent, StoriesState> { class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
StoriesBloc({ StoriesBloc({
required PreferenceCubit preferenceCubit, required PreferenceCubit preferenceCubit,
CacheRepository? cacheRepository, OfflineRepository? offlineRepository,
StoriesRepository? storiesRepository, StoriesRepository? storiesRepository,
PreferenceRepository? preferenceRepository, PreferenceRepository? preferenceRepository,
Logger? logger,
}) : _preferenceCubit = preferenceCubit, }) : _preferenceCubit = preferenceCubit,
_cacheRepository = cacheRepository ?? locator.get<CacheRepository>(), _offlineRepository =
offlineRepository ?? locator.get<OfflineRepository>(),
_storiesRepository = _storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(), storiesRepository ?? locator.get<StoriesRepository>(),
_preferenceRepository = _preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>(), preferenceRepository ?? locator.get<PreferenceRepository>(),
_logger = logger ?? locator.get<Logger>(),
super(const StoriesState.init()) { super(const StoriesState.init()) {
on<StoriesInitialize>(onInitialize); on<StoriesInitialize>(onInitialize);
on<StoriesRefresh>(onRefresh); on<StoriesRefresh>(onRefresh);
@ -41,9 +44,10 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
} }
final PreferenceCubit _preferenceCubit; final PreferenceCubit _preferenceCubit;
final CacheRepository _cacheRepository; final OfflineRepository _offlineRepository;
final StoriesRepository _storiesRepository; final StoriesRepository _storiesRepository;
final PreferenceRepository _preferenceRepository; final PreferenceRepository _preferenceRepository;
final Logger _logger;
DeviceScreenType? deviceScreenType; DeviceScreenType? deviceScreenType;
StreamSubscription<PreferenceState>? _streamSubscription; StreamSubscription<PreferenceState>? _streamSubscription;
static const int _smallPageSize = 10; static const int _smallPageSize = 10;
@ -73,7 +77,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
add(StoriesPageSizeChanged(pageSize: pageSize)); add(StoriesPageSizeChanged(pageSize: pageSize));
} }
}); });
final bool hasCachedStories = await _cacheRepository.hasCachedStories; final bool hasCachedStories = await _offlineRepository.hasCachedStories;
final bool isComplexTile = _preferenceCubit.state.showComplexStoryTile; final bool isComplexTile = _preferenceCubit.state.showComplexStoryTile;
final int pageSize = _getPageSize(isComplexTile: isComplexTile); final int pageSize = _getPageSize(isComplexTile: isComplexTile);
emit( emit(
@ -92,13 +96,13 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
required Emitter<StoriesState> emit, required Emitter<StoriesState> emit,
}) async { }) async {
if (state.offlineReading) { if (state.offlineReading) {
final List<int> ids = await _cacheRepository.getCachedStoryIds(of: of); final List<int> ids = await _offlineRepository.getCachedStoryIds(of: of);
emit( emit(
state state
.copyWithStoryIdsUpdated(of: of, to: ids) .copyWithStoryIdsUpdated(of: of, to: ids)
.copyWithCurrentPageUpdated(of: of, to: 0), .copyWithCurrentPageUpdated(of: of, to: 0),
); );
_cacheRepository _offlineRepository
.getCachedStoriesStream( .getCachedStoriesStream(
ids: ids.sublist(0, min(ids.length, state.currentPageSize)), ids: ids.sublist(0, min(ids.length, state.currentPageSize)),
) )
@ -169,7 +173,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
} }
if (state.offlineReading) { if (state.offlineReading) {
_cacheRepository _offlineRepository
.getCachedStoriesStream( .getCachedStoriesStream(
ids: state.storyIdsByType[event.type]!.sublist( ids: state.storyIdsByType[event.type]!.sublist(
lower, lower,
@ -243,9 +247,9 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
), ),
); );
await _cacheRepository.deleteAllStoryIds(); await _offlineRepository.deleteAllStoryIds();
await _cacheRepository.deleteAllStories(); await _offlineRepository.deleteAllStories();
await _cacheRepository.deleteAllComments(); await _offlineRepository.deleteAllComments();
final Set<int> prioritizedIds = <int>{}; final Set<int> prioritizedIds = <int>{};
final List<StoryType> prioritizedTypes = <StoryType>[...types] final List<StoryType> prioritizedTypes = <StoryType>[...types]
@ -253,7 +257,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
for (final StoryType type in prioritizedTypes) { for (final StoryType type in prioritizedTypes) {
final List<int> ids = await _storiesRepository.fetchStoryIds(of: type); final List<int> ids = await _storiesRepository.fetchStoryIds(of: type);
await _cacheRepository.cacheStoryIds(of: type, ids: ids); await _offlineRepository.cacheStoryIds(of: type, ids: ids);
prioritizedIds.addAll(ids); prioritizedIds.addAll(ids);
} }
@ -275,7 +279,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
final List<int> ids = await _storiesRepository.fetchStoryIds( final List<int> ids = await _storiesRepository.fetchStoryIds(
of: StoryType.latest, of: StoryType.latest,
); );
await _cacheRepository.cacheStoryIds(of: StoryType.latest, ids: ids); await _offlineRepository.cacheStoryIds(of: StoryType.latest, ids: ids);
latestIds.addAll(ids); latestIds.addAll(ids);
await fetchAndCacheStories( await fetchAndCacheStories(
@ -314,11 +318,11 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
continue; continue;
} }
await _cacheRepository.cacheStory(story: story); await _offlineRepository.cacheStory(story: story);
if (story.url.isNotEmpty && includingWebPage) { if (story.url.isNotEmpty && includingWebPage) {
locator.get<Logger>().i('downloading ${story.url}'); _logger.i('downloading ${story.url}');
await _cacheRepository.cacheUrl(url: story.url); await _offlineRepository.cacheUrl(url: story.url);
} }
_storiesRepository _storiesRepository
@ -326,7 +330,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
.whereType<Comment>() .whereType<Comment>()
.listen( .listen(
(Comment comment) => unawaited( (Comment comment) => unawaited(
_cacheRepository.cacheComment(comment: comment), _offlineRepository.cacheComment(comment: comment),
), ),
) )
.onDone(() => add(StoryDownloaded(skipped: false))); .onDone(() => add(StoryDownloaded(skipped: false)));
@ -378,10 +382,10 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
StoriesExitOffline event, StoriesExitOffline event,
Emitter<StoriesState> emit, Emitter<StoriesState> emit,
) async { ) async {
await _cacheRepository.deleteAllStoryIds(); await _offlineRepository.deleteAllStoryIds();
await _cacheRepository.deleteAllStories(); await _offlineRepository.deleteAllStories();
await _cacheRepository.deleteAllComments(); await _offlineRepository.deleteAllComments();
await _cacheRepository.deleteAllWebPages(); await _offlineRepository.deleteAllWebPages();
emit(state.copyWith(offlineReading: false)); emit(state.copyWith(offlineReading: false));
add(StoriesInitialize()); add(StoriesInitialize());
} }

View File

@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:hacki/config/custom_log_filter.dart'; import 'package:hacki/config/custom_log_filter.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
@ -10,14 +11,18 @@ final GetIt locator = GetIt.instance;
/// Set up [GetIt] locator. /// Set up [GetIt] locator.
Future<void> setUpLocator() async { Future<void> setUpLocator() async {
locator locator
..registerSingleton<Logger>(Logger(filter: CustomLogFilter()))
..registerSingleton<StoriesRepository>(StoriesRepository()) ..registerSingleton<StoriesRepository>(StoriesRepository())
..registerSingleton<PreferenceRepository>(PreferenceRepository()) ..registerSingleton<PreferenceRepository>(PreferenceRepository())
..registerSingleton<SearchRepository>(SearchRepository()) ..registerSingleton<SearchRepository>(SearchRepository())
..registerSingleton<AuthRepository>(AuthRepository()) ..registerSingleton<AuthRepository>(AuthRepository())
..registerSingleton<PostRepository>(PostRepository()) ..registerSingleton<PostRepository>(PostRepository())
..registerSingleton<SembastRepository>(SembastRepository()) ..registerSingleton<SembastRepository>(SembastRepository())
..registerSingleton<CacheRepository>(CacheRepository()) ..registerSingleton<OfflineRepository>(OfflineRepository())
..registerSingleton<CacheService>(CacheService()) ..registerSingleton<DraftCache>(DraftCache())
..registerSingleton<CommentCache>(CommentCache())
..registerSingleton<LocalNotification>(LocalNotification()) ..registerSingleton<LocalNotification>(LocalNotification())
..registerSingleton<Logger>(Logger(filter: CustomLogFilter())); ..registerSingleton<RouteObserver<ModalRoute<dynamic>>>(
RouteObserver<ModalRoute<dynamic>>(),
);
} }

View File

@ -10,31 +10,31 @@ part 'collapse_state.dart';
class CollapseCubit extends Cubit<CollapseState> { class CollapseCubit extends Cubit<CollapseState> {
CollapseCubit({ CollapseCubit({
required int commentId, required int commentId,
CacheService? cacheService, CollapseCache? collapseCache,
}) : _commentId = commentId, }) : _commentId = commentId,
_cacheService = cacheService ?? locator.get<CacheService>(), _collapseCache = collapseCache ?? locator.get<CollapseCache>(),
super(const CollapseState.init()); super(const CollapseState.init());
final int _commentId; final int _commentId;
final CacheService _cacheService; final CollapseCache _collapseCache;
late final StreamSubscription<Map<int, Set<int>>> _streamSubscription; late final StreamSubscription<Map<int, Set<int>>> _streamSubscription;
void init() { void init() {
_streamSubscription = _streamSubscription =
_cacheService.hiddenComments.listen(hiddenCommentsStreamListener); _collapseCache.hiddenComments.listen(hiddenCommentsStreamListener);
emit( emit(
state.copyWith( state.copyWith(
collapsedCount: _cacheService.totalHidden(_commentId), collapsedCount: _collapseCache.totalHidden(_commentId),
collapsed: _cacheService.isCollapsed(_commentId), collapsed: _collapseCache.isCollapsed(_commentId),
hidden: _cacheService.isHidden(_commentId), hidden: _collapseCache.isHidden(_commentId),
), ),
); );
} }
void collapse() { void collapse() {
if (state.collapsed) { if (state.collapsed) {
_cacheService.uncollapse(_commentId); _collapseCache.uncollapse(_commentId);
emit( emit(
state.copyWith( state.copyWith(
@ -43,7 +43,7 @@ class CollapseCubit extends Cubit<CollapseState> {
), ),
); );
} else { } else {
final int count = _cacheService.collapse(_commentId); final int count = _collapseCache.collapse(_commentId);
emit( emit(
state.copyWith( state.copyWith(

View File

@ -16,22 +16,26 @@ part 'comments_state.dart';
class CommentsCubit extends Cubit<CommentsState> { class CommentsCubit extends Cubit<CommentsState> {
CommentsCubit({ CommentsCubit({
CacheService? cacheService, required CollapseCache collapseCache,
CacheRepository? cacheRepository, CommentCache? commentCache,
OfflineRepository? offlineRepository,
StoriesRepository? storiesRepository, StoriesRepository? storiesRepository,
SembastRepository? sembastRepository, SembastRepository? sembastRepository,
required bool offlineReading, required bool offlineReading,
required Item item, required Item item,
}) : _cacheService = cacheService ?? locator.get<CacheService>(), }) : _collapseCache = collapseCache,
_cacheRepository = cacheRepository ?? locator.get<CacheRepository>(), _commentCache = commentCache ?? locator.get<CommentCache>(),
_offlineRepository =
offlineRepository ?? locator.get<OfflineRepository>(),
_storiesRepository = _storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(), storiesRepository ?? locator.get<StoriesRepository>(),
_sembastRepository = _sembastRepository =
sembastRepository ?? locator.get<SembastRepository>(), sembastRepository ?? locator.get<SembastRepository>(),
super(CommentsState.init(offlineReading: offlineReading, item: item)); super(CommentsState.init(offlineReading: offlineReading, item: item));
final CacheService _cacheService; final CollapseCache _collapseCache;
final CacheRepository _cacheRepository; final CommentCache _commentCache;
final OfflineRepository _offlineRepository;
final StoriesRepository _storiesRepository; final StoriesRepository _storiesRepository;
final SembastRepository _sembastRepository; final SembastRepository _sembastRepository;
StreamSubscription<Comment>? _streamSubscription; StreamSubscription<Comment>? _streamSubscription;
@ -47,6 +51,7 @@ class CommentsCubit extends Cubit<CommentsState> {
Future<void> init({ Future<void> init({
bool onlyShowTargetComment = false, bool onlyShowTargetComment = false,
bool useCommentCache = false,
List<Comment>? targetParents, List<Comment>? targetParents,
}) async { }) async {
if (onlyShowTargetComment && (targetParents?.isNotEmpty ?? false)) { if (onlyShowTargetComment && (targetParents?.isNotEmpty ?? false)) {
@ -80,13 +85,16 @@ class CommentsCubit extends Cubit<CommentsState> {
emit(state.copyWith(item: updatedItem)); emit(state.copyWith(item: updatedItem));
if (state.offlineReading) { if (state.offlineReading) {
_streamSubscription = _cacheRepository _streamSubscription = _offlineRepository
.getCachedCommentsStream(ids: kids) .getCachedCommentsStream(ids: kids)
.listen(_onCommentFetched) .listen(_onCommentFetched)
..onDone(_onDone); ..onDone(_onDone);
} else { } else {
_streamSubscription = _storiesRepository _streamSubscription = _storiesRepository
.fetchCommentsStream(ids: kids) .fetchCommentsStream(
ids: kids,
getFromCache: useCommentCache ? _commentCache.getComment : null,
)
.listen(_onCommentFetched) .listen(_onCommentFetched)
..onDone(_onDone); ..onDone(_onDone);
} }
@ -102,9 +110,7 @@ class CommentsCubit extends Cubit<CommentsState> {
return; return;
} }
_cacheService _collapseCache.resetCollapsedComments();
..resetComments()
..resetCollapsedComments();
emit( emit(
state.copyWith( state.copyWith(
@ -179,7 +185,7 @@ class CommentsCubit extends Cubit<CommentsState> {
if (order == null) return; if (order == null) return;
_streamSubscription?.cancel(); _streamSubscription?.cancel();
emit(state.copyWith(order: order, comments: <Comment>[])); emit(state.copyWith(order: order, comments: <Comment>[]));
init(); init(useCommentCache: true);
} }
List<int> sortKids(List<int> kids) { List<int> sortKids(List<int> kids) {
@ -205,9 +211,8 @@ class CommentsCubit extends Cubit<CommentsState> {
void _onCommentFetched(Comment? comment) { void _onCommentFetched(Comment? comment) {
if (comment != null) { if (comment != null) {
_cacheService _collapseCache.addKid(comment.id, to: comment.parent);
..addKid(comment.id, to: comment.parent) _commentCache.cacheComment(comment);
..cacheComment(comment);
_sembastRepository.cacheComment(comment); _sembastRepository.cacheComment(comment);
final List<LinkifyElement> elements = _linkify( final List<LinkifyElement> elements = _linkify(
@ -227,7 +232,7 @@ class CommentsCubit extends Cubit<CommentsState> {
if (updatedComments.length >= _pageSize + _pageSize * state.currentPage && if (updatedComments.length >= _pageSize + _pageSize * state.currentPage &&
updatedComments.length <= updatedComments.length <=
_pageSize * 2 + _pageSize * state.currentPage) { _pageSize * 2 + _pageSize * state.currentPage) {
final bool isHidden = _cacheService.isHidden(comment.id); final bool isHidden = _collapseCache.isHidden(comment.id);
if (!isHidden) { if (!isHidden) {
_streamSubscription?.pause(); _streamSubscription?.pause();

View File

@ -8,19 +8,19 @@ import 'package:hacki/utils/debouncer.dart';
part 'edit_state.dart'; part 'edit_state.dart';
class EditCubit extends Cubit<EditState> { class EditCubit extends Cubit<EditState> {
EditCubit({CacheService? cacheService}) EditCubit({DraftCache? draftCache})
: _cacheService = cacheService ?? locator.get<CacheService>(), : _draftCache = draftCache ?? locator.get<DraftCache>(),
_debouncer = Debouncer(delay: const Duration(seconds: 1)), _debouncer = Debouncer(delay: const Duration(seconds: 1)),
super(const EditState.init()); super(const EditState.init());
final CacheService _cacheService; final DraftCache _draftCache;
final Debouncer _debouncer; final Debouncer _debouncer;
void onReplyTapped(Item item) { void onReplyTapped(Item item) {
emit( emit(
EditState( EditState(
replyingTo: item, replyingTo: item,
text: _cacheService.getDraft(replyingTo: item.id), text: _draftCache.getDraft(replyingTo: item.id),
), ),
); );
} }
@ -44,7 +44,7 @@ class EditCubit extends Cubit<EditState> {
void onReplySubmittedSuccessfully() { void onReplySubmittedSuccessfully() {
if (state.replyingTo != null) { if (state.replyingTo != null) {
_cacheService.removeDraft(replyingTo: state.replyingTo!.id); _draftCache.removeDraft(replyingTo: state.replyingTo!.id);
} }
emit(const EditState.init()); emit(const EditState.init());
} }
@ -54,7 +54,7 @@ class EditCubit extends Cubit<EditState> {
if (state.replyingTo != null) { if (state.replyingTo != null) {
final int? id = state.replyingTo?.id; final int? id = state.replyingTo?.id;
_debouncer.run(() { _debouncer.run(() {
_cacheService.cacheDraft( _draftCache.cacheDraft(
text: text, text: text,
replyingTo: id!, replyingTo: id!,
); );

View File

@ -3,18 +3,24 @@ import 'package:equatable/equatable.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/screens/screens.dart'; import 'package:hacki/screens/screens.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
import 'package:logger/logger.dart';
part 'split_view_state.dart'; part 'split_view_state.dart';
class SplitViewCubit extends Cubit<SplitViewState> { class SplitViewCubit extends Cubit<SplitViewState> {
SplitViewCubit({CacheService? cacheService}) SplitViewCubit({
: _cacheService = cacheService ?? locator.get<CacheService>(), CommentCache? commentCache,
Logger? logger,
}) : _commentCache = commentCache ?? locator.get<CommentCache>(),
_logger = logger ?? locator.get<Logger>(),
super(const SplitViewState.init()); super(const SplitViewState.init());
final CacheService _cacheService; final Logger _logger;
final CommentCache _commentCache;
void updateItemScreenArgs(ItemScreenArgs args) { void updateItemScreenArgs(ItemScreenArgs args) {
_cacheService.resetCollapsedComments(); _logger.i('resetting comments in CommentCache');
_commentCache.resetComments();
emit(state.copyWith(itemScreenArgs: args)); emit(state.copyWith(itemScreenArgs: args));
} }

View File

@ -3,34 +3,34 @@ import 'package:equatable/equatable.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/models/models.dart' show Comment; import 'package:hacki/models/models.dart' show Comment;
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/services/cache_service.dart'; import 'package:hacki/services/services.dart';
part 'time_machine_state.dart'; part 'time_machine_state.dart';
class TimeMachineCubit extends Cubit<TimeMachineState> { class TimeMachineCubit extends Cubit<TimeMachineState> {
TimeMachineCubit({ TimeMachineCubit({
SembastRepository? sembastRepository, SembastRepository? sembastRepository,
CacheService? cacheService, CommentCache? commentCache,
}) : _sembastRepository = }) : _sembastRepository =
sembastRepository ?? locator.get<SembastRepository>(), sembastRepository ?? locator.get<SembastRepository>(),
_cacheService = cacheService ?? locator.get<CacheService>(), _commentCache = commentCache ?? locator.get<CommentCache>(),
super(TimeMachineState.init()); super(TimeMachineState.init());
final SembastRepository _sembastRepository; final SembastRepository _sembastRepository;
final CacheService _cacheService; final CommentCache _commentCache;
Future<void> activateTimeMachine(Comment comment) async { Future<void> activateTimeMachine(Comment comment) async {
emit(state.copyWith(parents: <Comment>[])); emit(state.copyWith(parents: <Comment>[]));
final List<Comment> parents = <Comment>[]; final List<Comment> parents = <Comment>[];
Comment? parent = _cacheService.getComment(comment.parent); Comment? parent = _commentCache.getComment(comment.parent);
parent ??= await _sembastRepository.getCachedComment(id: comment.parent); parent ??= await _sembastRepository.getCachedComment(id: comment.parent);
while (parent != null) { while (parent != null) {
parents.insert(0, parent); parents.insert(0, parent);
final int parentId = parent.parent; final int parentId = parent.parent;
parent = _cacheService.getComment(parentId); parent = _commentCache.getComment(parentId);
parent ??= await _sembastRepository.getCachedComment(id: parentId); parent ??= await _sembastRepository.getCachedComment(id: parentId);
} }

View File

@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
extension TryReadContext on BuildContext {
T? tryRead<T>() {
try {
return read<T>();
} catch (_) {
return null;
}
}
}

View File

@ -1,3 +1,4 @@
export 'context_extension.dart';
export 'date_time_extension.dart'; export 'date_time_extension.dart';
export 'int_extension.dart'; export 'int_extension.dart';
export 'list_extension.dart'; export 'list_extension.dart';

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
extension WidgetModifier on Widget { extension WidgetModifier on Widget {
Widget padding([EdgeInsetsGeometry value = const EdgeInsets.all(12)]) { Widget padded([EdgeInsetsGeometry value = const EdgeInsets.all(12)]) {
return Padding( return Padding(
padding: value, padding: value,
child: this, child: this,

View File

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:feature_discovery/feature_discovery.dart'; import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -14,6 +15,7 @@ import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/repositories/repositories.dart' show PreferenceRepository; import 'package:hacki/repositories/repositories.dart' show PreferenceRepository;
import 'package:hacki/screens/screens.dart'; import 'package:hacki/screens/screens.dart';
import 'package:hacki/services/custom_bloc_observer.dart';
import 'package:hacki/services/fetcher.dart'; import 'package:hacki/services/fetcher.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -30,9 +32,13 @@ final BehaviorSubject<String?> selectNotificationSubject =
final BehaviorSubject<String?> siriSuggestionSubject = final BehaviorSubject<String?> siriSuggestionSubject =
BehaviorSubject<String?>(); BehaviorSubject<String?>();
Future<void> main() async { late final bool isTesting;
Future<void> main({bool testing = false}) async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
isTesting = testing;
if (Platform.isIOS) { if (Platform.isIOS) {
unawaited( unawaited(
Workmanager().initialize( Workmanager().initialize(
@ -86,25 +92,26 @@ Future<void> main() async {
final bool trueDarkMode = final bool trueDarkMode =
prefs.getBool(PreferenceRepository.trueDarkModeKey) ?? false; prefs.getBool(PreferenceRepository.trueDarkModeKey) ?? false;
// Uncomment code below for running with logging. if (kReleaseMode) {
// BlocOverrides.runZoned(
// () {
// runApp(
// HackiApp(
// savedThemeMode: savedThemeMode,
// trueDarkMode: trueDarkMode,
// ),
// );
// },
// blocObserver: CustomBlocObserver(),
// );
runApp( runApp(
HackiApp( HackiApp(
savedThemeMode: savedThemeMode, savedThemeMode: savedThemeMode,
trueDarkMode: trueDarkMode, trueDarkMode: trueDarkMode,
), ),
); );
} else {
BlocOverrides.runZoned(
() {
runApp(
HackiApp(
savedThemeMode: savedThemeMode,
trueDarkMode: trueDarkMode,
),
);
},
blocObserver: CustomBlocObserver(),
);
}
} }
class HackiApp extends StatelessWidget { class HackiApp extends StatelessWidget {
@ -221,6 +228,9 @@ class HackiApp extends StatelessWidget {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: useTrueDark ? trueDarkTheme : theme, theme: useTrueDark ? trueDarkTheme : theme,
navigatorKey: navigatorKey, navigatorKey: navigatorKey,
navigatorObservers: <NavigatorObserver>[
locator.get<RouteObserver<ModalRoute<dynamic>>>(),
],
onGenerateRoute: CustomRouter.onGenerateRoute, onGenerateRoute: CustomRouter.onGenerateRoute,
initialRoute: HomeScreen.routeName, initialRoute: HomeScreen.routeName,
), ),

View File

@ -11,11 +11,14 @@ class AuthRepository extends PostableRepository {
AuthRepository({ AuthRepository({
Dio? dio, Dio? dio,
PreferenceRepository? preferenceRepository, PreferenceRepository? preferenceRepository,
Logger? logger,
}) : _preferenceRepository = }) : _preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>(), preferenceRepository ?? locator.get<PreferenceRepository>(),
_logger = logger ?? locator.get<Logger>(),
super(dio: dio); super(dio: dio);
final PreferenceRepository _preferenceRepository; final PreferenceRepository _preferenceRepository;
final Logger _logger;
static const String _authority = 'news.ycombinator.com'; static const String _authority = 'news.ycombinator.com';
@ -45,7 +48,7 @@ class AuthRepository extends PostableRepository {
password: password, password: password,
); );
} catch (_) { } catch (_) {
locator.get<Logger>().e(_); _logger.e(_);
return false; return false;
} }
} }

View File

@ -5,20 +5,22 @@ import 'package:hive/hive.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
/// [CacheRepository] is for storing stories and comments for offline reading. /// [OfflineRepository] is for storing stories and comments for offline reading.
/// It's using [Hive] as its database which is being stored in temp directory. /// It's using [Hive] as its database which is being stored in temp directory.
class CacheRepository { class OfflineRepository {
CacheRepository({ OfflineRepository({
Future<Box<List<int>>>? storyIdBox, Future<Box<List<int>>>? storyIdBox,
Future<Box<Map<dynamic, dynamic>>>? storyBox, Future<Box<Map<dynamic, dynamic>>>? storyBox,
Future<LazyBox<String>>? webPageBox, Future<LazyBox<String>>? webPageBox,
Future<LazyBox<Map<dynamic, dynamic>>>? commentBox, Future<LazyBox<Map<dynamic, dynamic>>>? commentBox,
Logger? logger,
}) : _storyIdBox = storyIdBox ?? Hive.openBox<List<int>>(_storyIdBoxName), }) : _storyIdBox = storyIdBox ?? Hive.openBox<List<int>>(_storyIdBoxName),
_storyBox = _storyBox =
storyBox ?? Hive.openBox<Map<dynamic, dynamic>>(_storyBoxName), storyBox ?? Hive.openBox<Map<dynamic, dynamic>>(_storyBoxName),
_webPageBox = webPageBox ?? Hive.openLazyBox<String>(_webPageBoxName), _webPageBox = webPageBox ?? Hive.openLazyBox<String>(_webPageBoxName),
_commentBox = commentBox ?? _commentBox = commentBox ??
Hive.openLazyBox<Map<dynamic, dynamic>>(_commentBoxName); Hive.openLazyBox<Map<dynamic, dynamic>>(_commentBoxName),
_logger = logger ?? locator.get<Logger>();
static const String _storyIdBoxName = 'storyIdBox'; static const String _storyIdBoxName = 'storyIdBox';
static const String _storyBoxName = 'storyBox'; static const String _storyBoxName = 'storyBox';
@ -28,6 +30,7 @@ class CacheRepository {
final Future<Box<Map<dynamic, dynamic>>> _storyBox; final Future<Box<Map<dynamic, dynamic>>> _storyBox;
final Future<LazyBox<Map<dynamic, dynamic>>> _commentBox; final Future<LazyBox<Map<dynamic, dynamic>>> _commentBox;
final Future<LazyBox<String>> _webPageBox; final Future<LazyBox<String>> _webPageBox;
final Logger _logger;
Future<bool> get hasCachedStories => Future<bool> get hasCachedStories =>
_storyBox.then((Box<Map<dynamic, dynamic>> box) => box.isNotEmpty); _storyBox.then((Box<Map<dynamic, dynamic>> box) => box.isNotEmpty);
@ -41,7 +44,7 @@ class CacheRepository {
try { try {
box = await _storyIdBox; box = await _storyIdBox;
} catch (_) { } catch (_) {
locator.get<Logger>().e(_); _logger.e(_);
await Hive.deleteBoxFromDisk(_storyIdBoxName); await Hive.deleteBoxFromDisk(_storyIdBoxName);
box = await _storyIdBox; box = await _storyIdBox;
} }
@ -55,7 +58,7 @@ class CacheRepository {
try { try {
box = await _storyBox; box = await _storyBox;
} catch (_) { } catch (_) {
locator.get<Logger>().e(_); _logger.e(_);
await Hive.deleteBoxFromDisk(_storyBoxName); await Hive.deleteBoxFromDisk(_storyBoxName);
box = await _storyBox; box = await _storyBox;
} }
@ -69,7 +72,7 @@ class CacheRepository {
try { try {
box = await _webPageBox; box = await _webPageBox;
} catch (_) { } catch (_) {
locator.get<Logger>().e(_); _logger.e(_);
await Hive.deleteBoxFromDisk(_webPageBoxName); await Hive.deleteBoxFromDisk(_webPageBoxName);
box = await _webPageBox; box = await _webPageBox;
} }
@ -83,7 +86,7 @@ class CacheRepository {
final LazyBox<String> box = await _webPageBox; final LazyBox<String> box = await _webPageBox;
return box.get(url); return box.get(url);
} catch (_) { } catch (_) {
locator.get<Logger>().e(_); _logger.e(_);
await Hive.deleteBoxFromDisk(_webPageBoxName); await Hive.deleteBoxFromDisk(_webPageBoxName);
return null; return null;
} }
@ -94,7 +97,7 @@ class CacheRepository {
final LazyBox<String> box = await _webPageBox; final LazyBox<String> box = await _webPageBox;
return box.containsKey(url); return box.containsKey(url);
} catch (_) { } catch (_) {
locator.get<Logger>().e(_); _logger.e(_);
await Hive.deleteBoxFromDisk(_webPageBoxName); await Hive.deleteBoxFromDisk(_webPageBoxName);
return false; return false;
} }
@ -106,7 +109,7 @@ class CacheRepository {
final List<int>? ids = box.get(of.name); final List<int>? ids = box.get(of.name);
return ids ?? <int>[]; return ids ?? <int>[];
} catch (_) { } catch (_) {
locator.get<Logger>().e(_); _logger.e(_);
await Hive.deleteBoxFromDisk(_storyIdBoxName); await Hive.deleteBoxFromDisk(_storyIdBoxName);
return <int>[]; return <int>[];
} }
@ -118,7 +121,7 @@ class CacheRepository {
try { try {
box = await _storyBox; box = await _storyBox;
} catch (_) { } catch (_) {
locator.get<Logger>().e(_); _logger.e(_);
await Hive.deleteBoxFromDisk(_storyBoxName); await Hive.deleteBoxFromDisk(_storyBoxName);
return; return;
} }
@ -143,7 +146,7 @@ class CacheRepository {
try { try {
box = await _storyBox; box = await _storyBox;
} catch (_) { } catch (_) {
locator.get<Logger>().e(_); _logger.e(_);
await Hive.deleteBoxFromDisk(_storyBoxName); await Hive.deleteBoxFromDisk(_storyBoxName);
return null; return null;
} }
@ -162,7 +165,7 @@ class CacheRepository {
try { try {
box = await _commentBox; box = await _commentBox;
} catch (_) { } catch (_) {
locator.get<Logger>().e(_); _logger.e(_);
await Hive.deleteBoxFromDisk(_commentBoxName); await Hive.deleteBoxFromDisk(_commentBoxName);
box = await _commentBox; box = await _commentBox;
} }
@ -180,7 +183,7 @@ class CacheRepository {
final Comment comment = Comment.fromJson(json.cast<String, dynamic>()); final Comment comment = Comment.fromJson(json.cast<String, dynamic>());
return comment; return comment;
} catch (_) { } catch (_) {
locator.get<Logger>().e(_); _logger.e(_);
await Hive.deleteBoxFromDisk(_commentBoxName); await Hive.deleteBoxFromDisk(_commentBoxName);
return null; return null;
} }
@ -210,7 +213,7 @@ class CacheRepository {
final Box<List<int>> box = await _storyIdBox; final Box<List<int>> box = await _storyIdBox;
return box.clear(); return box.clear();
} catch (_) { } catch (_) {
locator.get<Logger>().e(_); _logger.e(_);
await Hive.deleteBoxFromDisk(_storyIdBoxName); await Hive.deleteBoxFromDisk(_storyIdBoxName);
return 0; return 0;
} }
@ -221,7 +224,7 @@ class CacheRepository {
final Box<Map<dynamic, dynamic>> box = await _storyBox; final Box<Map<dynamic, dynamic>> box = await _storyBox;
return box.clear(); return box.clear();
} catch (_) { } catch (_) {
locator.get<Logger>().e(_); _logger.e(_);
await Hive.deleteBoxFromDisk(_storyBoxName); await Hive.deleteBoxFromDisk(_storyBoxName);
return 0; return 0;
} }
@ -232,7 +235,7 @@ class CacheRepository {
final LazyBox<Map<dynamic, dynamic>> box = await _commentBox; final LazyBox<Map<dynamic, dynamic>> box = await _commentBox;
return box.clear(); return box.clear();
} catch (_) { } catch (_) {
locator.get<Logger>().e(_); _logger.e(_);
await Hive.deleteBoxFromDisk(_commentBoxName); await Hive.deleteBoxFromDisk(_commentBoxName);
return 0; return 0;
} }
@ -243,7 +246,7 @@ class CacheRepository {
final LazyBox<String> box = await _webPageBox; final LazyBox<String> box = await _webPageBox;
return box.clear(); return box.clear();
} catch (_) { } catch (_) {
locator.get<Logger>().e(_); _logger.e(_);
await Hive.deleteBoxFromDisk(_webPageBoxName); await Hive.deleteBoxFromDisk(_webPageBoxName);
return 0; return 0;
} }

View File

@ -12,9 +12,11 @@ class PreferenceRepository {
SyncedSharedPreferences? syncedPrefs, SyncedSharedPreferences? syncedPrefs,
Future<SharedPreferences>? prefs, Future<SharedPreferences>? prefs,
FlutterSecureStorage? secureStorage, FlutterSecureStorage? secureStorage,
Logger? logger,
}) : _syncedPrefs = syncedPrefs ?? SyncedSharedPreferences.instance, }) : _syncedPrefs = syncedPrefs ?? SyncedSharedPreferences.instance,
_prefs = prefs ?? SharedPreferences.getInstance(), _prefs = prefs ?? SharedPreferences.getInstance(),
_secureStorage = secureStorage ?? const FlutterSecureStorage(); _secureStorage = secureStorage ?? const FlutterSecureStorage(),
_logger = logger ?? locator.get<Logger>();
static const String _usernameKey = 'username'; static const String _usernameKey = 'username';
static const String _passwordKey = 'password'; static const String _passwordKey = 'password';
@ -43,7 +45,8 @@ class PreferenceRepository {
static const bool _notificationModeDefaultValue = true; static const bool _notificationModeDefaultValue = true;
static const bool _displayModeDefaultValue = true; static const bool _displayModeDefaultValue = true;
static const bool _navigationModeDefaultValue = true; static const bool _navigationModeDefaultValueIOS = true;
static const bool _navigationModeDefaultValueAndroid = false;
static const bool _eyeCandyModeDefaultValue = false; static const bool _eyeCandyModeDefaultValue = false;
static const bool _trueDarkModeDefaultValue = false; static const bool _trueDarkModeDefaultValue = false;
static const bool _readerModeDefaultValue = true; static const bool _readerModeDefaultValue = true;
@ -54,6 +57,7 @@ class PreferenceRepository {
final SyncedSharedPreferences _syncedPrefs; final SyncedSharedPreferences _syncedPrefs;
final Future<SharedPreferences> _prefs; final Future<SharedPreferences> _prefs;
final FlutterSecureStorage _secureStorage; final FlutterSecureStorage _secureStorage;
final Logger _logger;
Future<bool> get loggedIn async => await username != null; Future<bool> get loggedIn async => await username != null;
@ -84,7 +88,10 @@ class PreferenceRepository {
Future<bool> get shouldShowWebFirst async => _prefs.then( Future<bool> get shouldShowWebFirst async => _prefs.then(
(SharedPreferences prefs) => (SharedPreferences prefs) =>
prefs.getBool(_navigationModeKey) ?? _navigationModeDefaultValue, prefs.getBool(_navigationModeKey) ??
(Platform.isAndroid
? _navigationModeDefaultValueAndroid
: _navigationModeDefaultValueIOS),
); );
Future<bool> get shouldShowEyeCandy async => _prefs.then( Future<bool> get shouldShowEyeCandy async => _prefs.then(
@ -160,7 +167,7 @@ class PreferenceRepository {
aOptions: androidOptions, aOptions: androidOptions,
); );
} catch (_) { } catch (_) {
locator.get<Logger>().e(_); _logger.e(_);
} }
rethrow; rethrow;
@ -188,8 +195,10 @@ class PreferenceRepository {
Future<void> toggleNavigationMode() async { Future<void> toggleNavigationMode() async {
final SharedPreferences prefs = await _prefs; final SharedPreferences prefs = await _prefs;
final bool currentMode = final bool currentMode = prefs.getBool(_navigationModeKey) ??
prefs.getBool(_navigationModeKey) ?? _navigationModeDefaultValue; (Platform.isAndroid
? _navigationModeDefaultValueAndroid
: _navigationModeDefaultValueIOS);
await prefs.setBool(_navigationModeKey, !currentMode); await prefs.setBool(_navigationModeKey, !currentMode);
} }

View File

@ -1,5 +1,5 @@
export 'auth_repository.dart'; export 'auth_repository.dart';
export 'cache_repository.dart'; export 'offline_repository.dart';
export 'post_repository.dart'; export 'post_repository.dart';
export 'preference_repository.dart'; export 'preference_repository.dart';
export 'search_repository.dart'; export 'search_repository.dart';

View File

@ -51,9 +51,12 @@ class StoriesRepository {
Stream<Comment> fetchCommentsStream({ Stream<Comment> fetchCommentsStream({
required List<int> ids, required List<int> ids,
int level = 0, int level = 0,
Comment? Function(int)? getFromCache,
}) async* { }) async* {
for (final int id in ids) { for (final int id in ids) {
final Comment? comment = await _firebaseClient Comment? comment = getFromCache?.call(id)?.copyWith(level: level);
comment ??= await _firebaseClient
.get('${_baseUrl}item/$id.json') .get('${_baseUrl}item/$id.json')
.then((dynamic json) => _parseJson(json as Map<String, dynamic>?)) .then((dynamic json) => _parseJson(json as Map<String, dynamic>?))
.then((Map<String, dynamic>? json) async { .then((Map<String, dynamic>? json) async {
@ -69,6 +72,7 @@ class StoriesRepository {
yield* fetchCommentsStream( yield* fetchCommentsStream(
ids: comment.kids, ids: comment.kids,
level: level + 1, level: level + 1,
getFromCache: getFromCache,
); );
} }
} }

View File

@ -26,6 +26,7 @@ import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart'; import 'package:hacki/utils/utils.dart';
import 'package:logger/logger.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:responsive_builder/responsive_builder.dart'; import 'package:responsive_builder/responsive_builder.dart';
@ -46,8 +47,7 @@ class HomeScreen extends StatefulWidget {
} }
class _HomeScreenState extends State<HomeScreen> class _HomeScreenState extends State<HomeScreen>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin, RouteAware {
final CacheService cacheService = locator.get<CacheService>();
final Throttle featureDiscoveryDismissThrottle = Throttle( final Throttle featureDiscoveryDismissThrottle = Throttle(
delay: _throttleDelay, delay: _throttleDelay,
); );
@ -61,6 +61,19 @@ class _HomeScreenState extends State<HomeScreen>
static const Duration _throttleDelay = Duration(seconds: 1); static const Duration _throttleDelay = Duration(seconds: 1);
@override
void didPopNext() {
super.didPopNext();
if (context.read<StoriesBloc>().deviceScreenType ==
DeviceScreenType.mobile) {
locator.get<Logger>().i('resetting comments in CommentCache');
Future<void>.delayed(
const Duration(milliseconds: 500),
locator.get<CommentCache>().resetComments,
);
}
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -88,13 +101,25 @@ class _HomeScreenState extends State<HomeScreen>
siriSuggestionSubject.stream.listen(onSiriSuggestionTapped); siriSuggestionSubject.stream.listen(onSiriSuggestionTapped);
} }
SchedulerBinding.instance.addPostFrameCallback((_) { SchedulerBinding.instance
..addPostFrameCallback((_) {
if (!isTesting) {
FeatureDiscovery.discoverFeatures( FeatureDiscovery.discoverFeatures(
context, context,
const <String>{ const <String>{
Constants.featureLogIn, Constants.featureLogIn,
}, },
); );
}
})
..addPostFrameCallback((_) {
final ModalRoute<dynamic>? route = ModalRoute.of(context);
if (route == null) return;
locator
.get<RouteObserver<ModalRoute<dynamic>>>()
.subscribe(this, route);
}); });
tabController = TabController(vsync: this, length: 6) tabController = TabController(vsync: this, length: 6)

View File

@ -1,3 +1,5 @@
// ignore_for_file: comment_references
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:feature_discovery/feature_discovery.dart'; import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -38,6 +40,7 @@ class ItemScreenArgs extends Equatable {
const ItemScreenArgs({ const ItemScreenArgs({
required this.item, required this.item,
this.onlyShowTargetComment = false, this.onlyShowTargetComment = false,
this.useCommentCache = false,
this.targetComments, this.targetComments,
}); });
@ -45,11 +48,17 @@ class ItemScreenArgs extends Equatable {
final bool onlyShowTargetComment; final bool onlyShowTargetComment;
final List<Comment>? targetComments; final List<Comment>? targetComments;
/// when a user is trying to view a sub-thread from a main thread, we don't
/// need to fetch comments from [StoryRepository] since we have some, if not
/// all, comments cached in [CommentCache].
final bool useCommentCache;
@override @override
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
item, item,
onlyShowTargetComment, onlyShowTargetComment,
targetComments, targetComments,
useCommentCache,
]; ];
} }
@ -66,8 +75,8 @@ class ItemScreen extends StatefulWidget {
static Route<dynamic> route(ItemScreenArgs args) { static Route<dynamic> route(ItemScreenArgs args) {
return MaterialPageRoute<ItemScreen>( return MaterialPageRoute<ItemScreen>(
settings: const RouteSettings(name: routeName), settings: const RouteSettings(name: routeName),
builder: (BuildContext context) => RepositoryProvider<CacheService>( builder: (BuildContext context) => RepositoryProvider<CollapseCache>(
create: (BuildContext context) => CacheService(), create: (BuildContext context) => CollapseCache(),
lazy: false, lazy: false,
child: MultiBlocProvider( child: MultiBlocProvider(
providers: <BlocProvider<dynamic>>[ providers: <BlocProvider<dynamic>>[
@ -76,10 +85,11 @@ class ItemScreen extends StatefulWidget {
offlineReading: offlineReading:
context.read<StoriesBloc>().state.offlineReading, context.read<StoriesBloc>().state.offlineReading,
item: args.item, item: args.item,
cacheService: context.read<CacheService>(), collapseCache: context.read<CollapseCache>(),
)..init( )..init(
onlyShowTargetComment: args.onlyShowTargetComment, onlyShowTargetComment: args.onlyShowTargetComment,
targetParents: args.targetComments, targetParents: args.targetComments,
useCommentCache: args.useCommentCache,
), ),
), ),
BlocProvider<EditCubit>( BlocProvider<EditCubit>(
@ -106,8 +116,9 @@ class ItemScreen extends StatefulWidget {
return true; return true;
} }
}, },
child: RepositoryProvider<CacheService>( child: RepositoryProvider<CollapseCache>(
create: (BuildContext context) => CacheService(), create: (BuildContext context) => CollapseCache(),
lazy: false,
child: MultiBlocProvider( child: MultiBlocProvider(
key: ValueKey<ItemScreenArgs>(args), key: ValueKey<ItemScreenArgs>(args),
providers: <BlocProvider<dynamic>>[ providers: <BlocProvider<dynamic>>[
@ -116,7 +127,7 @@ class ItemScreen extends StatefulWidget {
offlineReading: offlineReading:
context.read<StoriesBloc>().state.offlineReading, context.read<StoriesBloc>().state.offlineReading,
item: args.item, item: args.item,
cacheService: context.read<CacheService>(), collapseCache: context.read<CollapseCache>(),
)..init( )..init(
onlyShowTargetComment: args.onlyShowTargetComment, onlyShowTargetComment: args.onlyShowTargetComment,
targetParents: args.targetComments, targetParents: args.targetComments,
@ -171,6 +182,7 @@ class _ItemScreenState extends State<ItemScreen> {
super.initState(); super.initState();
SchedulerBinding.instance.addPostFrameCallback((_) { SchedulerBinding.instance.addPostFrameCallback((_) {
if (!isTesting) {
FeatureDiscovery.discoverFeatures( FeatureDiscovery.discoverFeatures(
context, context,
const <String>{ const <String>{
@ -179,6 +191,7 @@ class _ItemScreenState extends State<ItemScreen> {
Constants.featureOpenStoryInWebView, Constants.featureOpenStoryInWebView,
}, },
); );
}
}); });
scrollController.addListener(() { scrollController.addListener(() {
@ -191,7 +204,6 @@ class _ItemScreenState extends State<ItemScreen> {
@override @override
void dispose() { void dispose() {
locator.get<CacheService>().resetComments();
refreshController.dispose(); refreshController.dispose();
commentEditingController.dispose(); commentEditingController.dispose();
scrollController.dispose(); scrollController.dispose();
@ -296,8 +308,12 @@ class _ItemScreenState extends State<ItemScreen> {
} }
}, },
onLoading: context.read<CommentsCubit>().loadMore, onLoading: context.read<CommentsCubit>().loadMore,
child: ListView( child: ListView.builder(
primary: false, primary: false,
itemCount: state.comments.length + 2,
itemBuilder: (BuildContext context, int index) {
if (index == 0) {
return Column(
children: <Widget>[ children: <Widget>[
SizedBox( SizedBox(
height: topPadding, height: topPadding,
@ -409,13 +425,13 @@ class _ItemScreenState extends State<ItemScreen> {
child: SelectableLinkify( child: SelectableLinkify(
text: state.item.text, text: state.item.text,
style: TextStyle( style: TextStyle(
fontSize: fontSize: MediaQuery.of(context)
MediaQuery.of(context).textScaleFactor * .textScaleFactor *
TextDimens.pt15, TextDimens.pt15,
), ),
linkStyle: TextStyle( linkStyle: TextStyle(
fontSize: fontSize: MediaQuery.of(context)
MediaQuery.of(context).textScaleFactor * .textScaleFactor *
TextDimens.pt15, TextDimens.pt15,
color: Palette.orange, color: Palette.orange,
), ),
@ -433,7 +449,8 @@ class _ItemScreenState extends State<ItemScreen> {
if (state.item.isPoll) if (state.item.isPoll)
BlocProvider<PollCubit>( BlocProvider<PollCubit>(
create: (BuildContext context) => create: (BuildContext context) =>
PollCubit(story: state.item as Story)..init(), PollCubit(story: state.item as Story)
..init(),
child: PollView( child: PollView(
onLoginTapped: onLoginTapped, onLoginTapped: onLoginTapped,
), ),
@ -483,7 +500,8 @@ class _ItemScreenState extends State<ItemScreen> {
? const SizedBox( ? const SizedBox(
height: Dimens.pt12, height: Dimens.pt12,
width: Dimens.pt12, width: Dimens.pt12,
child: CustomCircularProgressIndicator( child:
CustomCircularProgressIndicator(
strokeWidth: Dimens.pt2, strokeWidth: Dimens.pt2,
), ),
) )
@ -494,7 +512,8 @@ class _ItemScreenState extends State<ItemScreen> {
DropdownButton<CommentsOrder>( DropdownButton<CommentsOrder>(
value: state.order, value: state.order,
underline: const SizedBox.shrink(), underline: const SizedBox.shrink(),
items: const <DropdownMenuItem<CommentsOrder>>[ items: const <
DropdownMenuItem<CommentsOrder>>[
DropdownMenuItem<CommentsOrder>( DropdownMenuItem<CommentsOrder>(
value: CommentsOrder.natural, value: CommentsOrder.natural,
child: Text( child: Text(
@ -523,8 +542,9 @@ class _ItemScreenState extends State<ItemScreen> {
), ),
), ),
], ],
onChanged: onChanged: context
context.read<CommentsCubit>().onOrderChanged, .read<CommentsCubit>()
.onOrderChanged,
), ),
const SizedBox( const SizedBox(
width: Dimens.pt4, width: Dimens.pt4,
@ -536,7 +556,8 @@ class _ItemScreenState extends State<ItemScreen> {
), ),
], ],
if (state.comments.isEmpty && if (state.comments.isEmpty &&
state.status == CommentsStatus.allLoaded) ...<Widget>[ state.status ==
CommentsStatus.allLoaded) ...<Widget>[
const SizedBox( const SizedBox(
height: 240, height: 240,
), ),
@ -547,8 +568,26 @@ class _ItemScreenState extends State<ItemScreen> {
), ),
), ),
], ],
for (final Comment comment in state.comments) ],
FadeIn( );
} else if (index == state.comments.length + 1) {
if ((state.status == CommentsStatus.allLoaded &&
state.comments.isNotEmpty) ||
state.onlyShowTargetComment) {
return SizedBox(
height: 240,
child: Center(
child: Text(happyFace),
),
);
} else {
return const SizedBox.shrink();
}
}
index = index - 1;
final Comment comment = state.comments.elementAt(index);
return FadeIn(
key: ValueKey<String>('${comment.id}-FadeIn'), key: ValueKey<String>('${comment.id}-FadeIn'),
child: CommentTile( child: CommentTile(
comment: comment, comment: comment,
@ -563,11 +602,7 @@ class _ItemScreenState extends State<ItemScreen> {
} }
if (cmt.id != if (cmt.id !=
context context.read<EditCubit>().state.replyingTo?.id) {
.read<EditCubit>()
.state
.replyingTo
?.id) {
commentEditingController.clear(); commentEditingController.clear();
} }
@ -587,17 +622,8 @@ class _ItemScreenState extends State<ItemScreen> {
onStoryLinkTapped: onStoryLinkTapped, onStoryLinkTapped: onStoryLinkTapped,
onRightMoreTapped: onRightMoreTapped, onRightMoreTapped: onRightMoreTapped,
), ),
), );
if ((state.status == CommentsStatus.allLoaded && },
state.comments.isNotEmpty) ||
state.onlyShowTargetComment)
SizedBox(
height: 240,
child: Center(
child: Text(happyFace),
),
)
],
), ),
); );
@ -753,7 +779,10 @@ class _ItemScreenState extends State<ItemScreen> {
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
goToItemScreen( goToItemScreen(
args: ItemScreenArgs(item: comment), args: ItemScreenArgs(
item: comment,
useCommentCache: true,
),
forceNewScreen: true, forceNewScreen: true,
); );
}, },

View File

@ -410,7 +410,7 @@ class _ProfileScreenState extends State<ProfileScreen>
showAboutDialog( showAboutDialog(
context: context, context: context,
applicationName: 'Hacki', applicationName: 'Hacki',
applicationVersion: 'v0.2.24', applicationVersion: 'v0.2.26',
applicationIcon: ClipRRect( applicationIcon: ClipRRect(
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular( Radius.circular(
@ -680,7 +680,7 @@ class _ProfileScreenState extends State<ProfileScreen>
.get<SembastRepository>() .get<SembastRepository>()
.deleteAllCachedComments() .deleteAllCachedComments()
.whenComplete( .whenComplete(
locator.get<CacheRepository>().deleteAll, locator.get<OfflineRepository>().deleteAll,
) )
.whenComplete( .whenComplete(
locator.get<PreferenceRepository>().clearAllReadStories, locator.get<PreferenceRepository>().clearAllReadStories,

View File

@ -34,7 +34,7 @@ class _WebViewScreenState extends State<WebViewScreen> {
), ),
body: WebView( body: WebView(
onWebViewCreated: (WebViewController controller) async { onWebViewCreated: (WebViewController controller) async {
final String? html = await locator.get<CacheRepository>().getHtml( final String? html = await locator.get<OfflineRepository>().getHtml(
url: widget.url, url: widget.url,
); );

View File

@ -44,7 +44,7 @@ class CommentTile extends StatelessWidget {
lazy: false, lazy: false,
create: (_) => CollapseCubit( create: (_) => CollapseCubit(
commentId: comment.id, commentId: comment.id,
cacheService: context.read<CacheService>(), collapseCache: context.tryRead<CollapseCache>() ?? CollapseCache(),
)..init(), )..init(),
child: BlocBuilder<CollapseCubit, CollapseState>( child: BlocBuilder<CollapseCubit, CollapseState>(
builder: (BuildContext context, CollapseState state) { builder: (BuildContext context, CollapseState state) {
@ -340,7 +340,12 @@ class CommentTile extends StatelessWidget {
); );
} }
static final Map<int, Color> _colors = <int, Color>{};
Color _getColor(int level) { Color _getColor(int level) {
final int initialLevel = level;
if (_colors[initialLevel] != null) return _colors[initialLevel]!;
while (level >= 10) { while (level >= 10) {
level = level - 10; level = level - 10;
} }
@ -361,6 +366,7 @@ class CommentTile extends StatelessWidget {
1, 1,
); );
_colors[initialLevel] = color;
return color; return color;
} }
} }

View File

@ -140,7 +140,7 @@ class WebAnalyzer {
while (comment == null && index < story.kids.length) { while (comment == null && index < story.kids.length) {
comment = await locator comment = await locator
.get<CacheRepository>() .get<OfflineRepository>()
.getCachedComment(id: story.kids.elementAt(index)); .getCachedComment(id: story.kids.elementAt(index));
index++; index++;
} }

View File

@ -0,0 +1,3 @@
export 'collapse_cache.dart';
export 'comment_cache.dart';
export 'draft_cache.dart';

View File

@ -1,9 +1,6 @@
import 'package:hacki/models/models.dart' show Comment;
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
class CacheService { class CollapseCache {
final Map<int, Comment> _comments = <int, Comment>{};
final Map<int, String> _drafts = <int, String>{};
final Map<int, Set<int>> _kids = <int, Set<int>>{}; final Map<int, Set<int>> _kids = <int, Set<int>>{};
final Set<int> _collapsed = <int>{}; final Set<int> _collapsed = <int>{};
final Map<int, Set<int>> _hidden = <int, Set<int>>{}; final Map<int, Set<int>> _hidden = <int, Set<int>>{};
@ -76,19 +73,4 @@ class CacheService {
bool isCollapsed(int commentId) => _collapsed.contains(commentId); bool isCollapsed(int commentId) => _collapsed.contains(commentId);
int totalHidden(int commentId) => _hidden[commentId]?.length ?? 0; int totalHidden(int commentId) => _hidden[commentId]?.length ?? 0;
void cacheComment(Comment comment) => _comments[comment.id] = comment;
Comment? getComment(int id) => _comments[id];
void resetComments() {
_comments.clear();
}
void removeDraft({required int replyingTo}) => _drafts.remove(replyingTo);
void cacheDraft({required String text, required int replyingTo}) =>
_drafts[replyingTo] = text;
String? getDraft({required int replyingTo}) => _drafts[replyingTo];
} }

View File

@ -0,0 +1,13 @@
import 'package:hacki/models/models.dart' show Comment;
class CommentCache {
static final Map<int, Comment> _comments = <int, Comment>{};
void cacheComment(Comment comment) => _comments[comment.id] = comment;
Comment? getComment(int id) => _comments[id];
void resetComments() {
_comments.clear();
}
}

View File

@ -0,0 +1,10 @@
class DraftCache {
static final Map<int, String> _drafts = <int, String>{};
void removeDraft({required int replyingTo}) => _drafts.remove(replyingTo);
void cacheDraft({required String text, required int replyingTo}) =>
_drafts[replyingTo] = text;
String? getDraft({required int replyingTo}) => _drafts[replyingTo];
}

View File

@ -3,15 +3,30 @@ import 'package:hacki/config/locator.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
class CustomBlocObserver extends BlocObserver { class CustomBlocObserver extends BlocObserver {
@override
void onCreate(BlocBase<dynamic> bloc) {
locator.get<Logger>().v('$bloc created');
super.onCreate(bloc);
}
@override @override
void onEvent( void onEvent(
Bloc<dynamic, dynamic> bloc, Bloc<dynamic, dynamic> bloc,
Object? event, Object? event,
) { ) {
locator.get<Logger>().d(event); locator.get<Logger>().v(event);
super.onEvent(bloc, event); super.onEvent(bloc, event);
} }
@override
void onTransition(
Bloc<dynamic, dynamic> bloc,
Transition<dynamic, dynamic> transition,
) {
locator.get<Logger>().v(transition);
super.onTransition(bloc, transition);
}
@override @override
void onError( void onError(
BlocBase<dynamic> bloc, BlocBase<dynamic> bloc,

View File

@ -1,4 +1,4 @@
export 'cache_service.dart'; export 'caches/caches.dart';
export 'custom_bloc_observer.dart'; export 'custom_bloc_observer.dart';
export 'fetcher.dart'; export 'fetcher.dart';
export 'firebase_client.dart'; export 'firebase_client.dart';

View File

@ -19,7 +19,7 @@ abstract class LinkUtil {
}) { }) {
if (offlineReading) { if (offlineReading) {
locator locator
.get<CacheRepository>() .get<OfflineRepository>()
.hasCachedWebPage(url: link) .hasCachedWebPage(url: link)
.then((bool cached) { .then((bool cached) {
if (cached) { if (cached) {

View File

@ -14,7 +14,7 @@ packages:
name: adaptive_theme name: adaptive_theme
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.0" version: "3.1.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
@ -22,6 +22,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.1.0" version: "4.1.0"
archive:
dependency: transitive
description:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.11"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -175,14 +182,14 @@ packages:
name: coverage name: coverage
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.2" version: "1.2.0"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
name: crypto name: crypto
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.2" version: "3.0.1"
csslib: csslib:
dependency: transitive dependency: transitive
description: description:
@ -196,7 +203,7 @@ packages:
name: dbus name: dbus
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.7.5" version: "0.7.6"
diff_match_patch: diff_match_patch:
dependency: transitive dependency: transitive
description: description:
@ -281,6 +288,11 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.3.0" version: "3.3.0"
flutter_driver:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_fadein: flutter_fadein:
dependency: "direct main" dependency: "direct main"
description: description:
@ -410,6 +422,11 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.3" version: "2.1.3"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
gbk_codec: gbk_codec:
dependency: "direct main" dependency: "direct main"
description: description:
@ -473,6 +490,11 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.1" version: "4.0.1"
integration_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@ -605,7 +627,7 @@ packages:
name: path_provider_android name: path_provider_android
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.15" version: "2.0.16"
path_provider_ios: path_provider_ios:
dependency: "direct main" dependency: "direct main"
description: description:
@ -747,7 +769,7 @@ packages:
name: share_plus name: share_plus
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.9" version: "4.0.10"
share_plus_linux: share_plus_linux:
dependency: transitive dependency: transitive
description: description:
@ -935,6 +957,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
sync_http:
dependency: transitive
description:
name: sync_http
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
synced_shared_preferences: synced_shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
@ -997,7 +1026,7 @@ packages:
name: typed_data name: typed_data
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.1" version: "1.3.0"
universal_platform: universal_platform:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1088,7 +1117,7 @@ packages:
name: vm_service name: vm_service
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "8.3.0" version: "8.2.2"
wakelock: wakelock:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1138,6 +1167,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
webdriver:
dependency: transitive
description:
name: webdriver
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
webkit_inspection_protocol: webkit_inspection_protocol:
dependency: transitive dependency: transitive
description: description:
@ -1210,4 +1246,4 @@ packages:
version: "3.1.1" version: "3.1.1"
sdks: sdks:
dart: ">=2.17.0 <3.0.0" dart: ">=2.17.0 <3.0.0"
flutter: ">=3.0.0" flutter: ">=3.0.3"

View File

@ -1,10 +1,11 @@
name: hacki name: hacki
description: A Hacker News reader. description: A Hacker News reader.
version: 0.2.24+66 version: 0.2.26+68
publish_to: none publish_to: none
environment: environment:
sdk: ">=2.17.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"
flutter: "3.0.3"
dependencies: dependencies:
adaptive_theme: ^3.0.0 adaptive_theme: ^3.0.0
@ -74,8 +75,10 @@ dev_dependencies:
bloc_test: ^9.0.3 bloc_test: ^9.0.3
flutter_test: flutter_test:
sdk: flutter sdk: flutter
integration_test:
sdk: flutter
mocktail: ^0.3.0 mocktail: ^0.3.0
very_good_analysis: ^2.3.0 very_good_analysis: ^2.4.0
flutter: flutter:
uses-material-design: true uses-material-design: true