Compare commits

..

2 Commits

Author SHA1 Message Date
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
f0d6cac3fd v0.2.24 (#61)
* bumped version.

* improved collapse.

* improved download speed.

* improved err handling.

* improved error handling.

* improved error handling.

* improved logging.

* improved logging.

* bumped version.
2022-06-27 00:47:22 -07:00
66 changed files with 434 additions and 325 deletions

View File

@ -1 +0,0 @@
- Bugfixes.

View File

@ -1 +0,0 @@
- Updates to UI.

View File

@ -1 +0,0 @@
- Updates to UI.

View File

@ -1 +0,0 @@
- Updates to UI.

View File

@ -1 +0,0 @@
- Tapping on comments in notification and history screen will lead you directly to the comment.

View File

@ -1,3 +0,0 @@
- Tapping on comment in notification or history screen will now lead you directly to the comment.
- Fixed the bug where reply box cannot be expanded in editing mode.
- Fixed inconsistent font size in history screen.

View File

@ -1 +0,0 @@
- Added offline mode.

View File

@ -1,2 +0,0 @@
- Added offline mode.
- Bugfixes.

View File

@ -1,2 +0,0 @@
- Added offline mode.
- Bugfixes.

View File

@ -1,2 +0,0 @@
- Added offline mode.
- Bugfixes.

View File

@ -1,3 +0,0 @@
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,3 +0,0 @@
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,3 +0,0 @@
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,4 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.

View File

@ -1,5 +0,0 @@
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.
- Bugfixes.

View File

@ -1,6 +0,0 @@
- You can now add filters for searching.
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.
- Bugfixes.

View File

@ -1,6 +0,0 @@
- You can now add filters for searching.
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.
- Bugfixes.

View File

@ -1,6 +0,0 @@
- You can now add filters for searching.
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.
- Bugfixes.

View File

@ -1,6 +0,0 @@
- You can now add filters for searching.
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.
- Bugfixes.

View File

@ -1,7 +0,0 @@
- You can share links.
- You can now add filters for searching.
- You can now participate in polls.
- Pick up where you left off.
- Swipe left on comment tile to view its parents without scrolling all the way up.
- Huge performance boost.
- Bugfixes.

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

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

View File

@ -7,7 +7,9 @@ import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:logger/logger.dart';
import 'package:responsive_builder/responsive_builder.dart';
import 'package:rxdart/rxdart.dart';
part 'stories_event.dart';
part 'stories_state.dart';
@ -15,11 +17,12 @@ part 'stories_state.dart';
class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
StoriesBloc({
required PreferenceCubit preferenceCubit,
CacheRepository? cacheRepository,
OfflineRepository? offlineRepository,
StoriesRepository? storiesRepository,
PreferenceRepository? preferenceRepository,
}) : _preferenceCubit = preferenceCubit,
_cacheRepository = cacheRepository ?? locator.get<CacheRepository>(),
_offlineRepository =
offlineRepository ?? locator.get<OfflineRepository>(),
_storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(),
_preferenceRepository =
@ -39,7 +42,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
}
final PreferenceCubit _preferenceCubit;
final CacheRepository _cacheRepository;
final OfflineRepository _offlineRepository;
final StoriesRepository _storiesRepository;
final PreferenceRepository _preferenceRepository;
DeviceScreenType? deviceScreenType;
@ -71,7 +74,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
add(StoriesPageSizeChanged(pageSize: pageSize));
}
});
final bool hasCachedStories = await _cacheRepository.hasCachedStories;
final bool hasCachedStories = await _offlineRepository.hasCachedStories;
final bool isComplexTile = _preferenceCubit.state.showComplexStoryTile;
final int pageSize = _getPageSize(isComplexTile: isComplexTile);
emit(
@ -90,13 +93,13 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
required Emitter<StoriesState> emit,
}) async {
if (state.offlineReading) {
final List<int> ids = await _cacheRepository.getCachedStoryIds(of: of);
final List<int> ids = await _offlineRepository.getCachedStoryIds(of: of);
emit(
state
.copyWithStoryIdsUpdated(of: of, to: ids)
.copyWithCurrentPageUpdated(of: of, to: 0),
);
_cacheRepository
_offlineRepository
.getCachedStoriesStream(
ids: ids.sublist(0, min(ids.length, state.currentPageSize)),
)
@ -167,7 +170,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
}
if (state.offlineReading) {
_cacheRepository
_offlineRepository
.getCachedStoriesStream(
ids: state.storyIdsByType[event.type]!.sublist(
lower,
@ -241,9 +244,9 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
),
);
await _cacheRepository.deleteAllStoryIds();
await _cacheRepository.deleteAllStories();
await _cacheRepository.deleteAllComments();
await _offlineRepository.deleteAllStoryIds();
await _offlineRepository.deleteAllStories();
await _offlineRepository.deleteAllComments();
final Set<int> prioritizedIds = <int>{};
final List<StoryType> prioritizedTypes = <StoryType>[...types]
@ -251,7 +254,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
for (final StoryType type in prioritizedTypes) {
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);
}
@ -273,7 +276,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
final List<int> ids = await _storiesRepository.fetchStoryIds(
of: StoryType.latest,
);
await _cacheRepository.cacheStoryIds(of: StoryType.latest, ids: ids);
await _offlineRepository.cacheStoryIds(of: StoryType.latest, ids: ids);
latestIds.addAll(ids);
await fetchAndCacheStories(
@ -281,12 +284,6 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
includingWebPage: event.includingWebPage,
isPrioritized: false,
);
emit(
state.copyWith(
downloadStatus: StoriesDownloadStatus.finished,
),
);
} catch (_) {
emit(
state.copyWith(
@ -318,44 +315,53 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
continue;
}
await _cacheRepository.cacheStory(story: story);
await _offlineRepository.cacheStory(story: story);
if (story.url.isNotEmpty && includingWebPage) {
await _cacheRepository.cacheUrl(url: story.url);
locator.get<Logger>().i('downloading ${story.url}');
await _offlineRepository.cacheUrl(url: story.url);
}
final Completer<void> completer = Completer<void>();
_storiesRepository
.fetchAllChildrenComments(ids: story.kids)
.listen((Comment? comment) async {
if (comment != null) {
await _cacheRepository.cacheComment(comment: comment);
}
}).onDone(() {
completer.complete();
add(StoryDownloaded(skipped: false));
});
await completer.future;
.whereType<Comment>()
.listen(
(Comment comment) => unawaited(
_offlineRepository.cacheComment(comment: comment),
),
)
.onDone(() => add(StoryDownloaded(skipped: false)));
}
}
void onStoryDownloaded(StoryDownloaded event, Emitter<StoriesState> emit) {
if (event.skipped) {
final int updatedStoriesToBeDownloaded = state.storiesToBeDownloaded - 1;
emit(
state.copyWith(
storiesToBeDownloaded: state.storiesToBeDownloaded - 1,
storiesToBeDownloaded: updatedStoriesToBeDownloaded,
downloadStatus:
state.storiesDownloaded == updatedStoriesToBeDownloaded
? StoriesDownloadStatus.finished
: null,
),
);
} else {
final int updatedStoriesDownloaded = state.storiesDownloaded + 1;
final int updatedStoriesToBeDownloaded =
updatedStoriesDownloaded > state.storiesToBeDownloaded
? state.storiesToBeDownloaded + 1
: state.storiesToBeDownloaded;
emit(
state.copyWith(
storiesDownloaded: updatedStoriesDownloaded,
storiesToBeDownloaded:
updatedStoriesDownloaded > state.storiesToBeDownloaded
? state.storiesToBeDownloaded + 1
: state.storiesToBeDownloaded,
storiesToBeDownloaded: updatedStoriesToBeDownloaded,
downloadStatus:
updatedStoriesDownloaded == updatedStoriesToBeDownloaded
? StoriesDownloadStatus.finished
: null,
),
);
}
@ -373,10 +379,10 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
StoriesExitOffline event,
Emitter<StoriesState> emit,
) async {
await _cacheRepository.deleteAllStoryIds();
await _cacheRepository.deleteAllStories();
await _cacheRepository.deleteAllComments();
await _cacheRepository.deleteAllWebPages();
await _offlineRepository.deleteAllStoryIds();
await _offlineRepository.deleteAllStories();
await _offlineRepository.deleteAllComments();
await _offlineRepository.deleteAllWebPages();
emit(state.copyWith(offlineReading: false));
add(StoriesInitialize());
}

View File

@ -0,0 +1,31 @@
import 'package:logger/logger.dart';
class CustomLogFilter extends LogFilter {
@override
// ignore: overridden_fields
Level? level = Level.verbose;
/// The minimal level allowed in production.
static const Level _minimalLevel = Level.info;
@override
bool shouldLog(LogEvent event) {
bool shouldLog = false;
if (event.level.index >= _minimalLevel.index) {
return true;
}
assert(
() {
if (event.level.index >= level!.index) {
shouldLog = true;
}
return true;
}(),
'',
);
return shouldLog;
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:hacki/config/custom_log_filter.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/services/services.dart';
import 'package:logger/logger.dart';
@ -16,10 +17,11 @@ Future<void> setUpLocator() async {
..registerSingleton<AuthRepository>(AuthRepository())
..registerSingleton<PostRepository>(PostRepository())
..registerSingleton<SembastRepository>(SembastRepository())
..registerSingleton<CacheRepository>(CacheRepository())
..registerSingleton<CacheService>(CacheService())
..registerSingleton<OfflineRepository>(OfflineRepository())
..registerSingleton<DraftCache>(DraftCache())
..registerSingleton<CommentCache>(CommentCache())
..registerSingleton<LocalNotification>(LocalNotification())
..registerSingleton<Logger>(Logger())
..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> {
CollapseCubit({
required int commentId,
CacheService? cacheService,
CollapseCache? collapseCache,
}) : _commentId = commentId,
_cacheService = cacheService ?? locator.get<CacheService>(),
_collapseCache = collapseCache ?? locator.get<CollapseCache>(),
super(const CollapseState.init());
final int _commentId;
final CacheService _cacheService;
final CollapseCache _collapseCache;
late final StreamSubscription<Map<int, Set<int>>> _streamSubscription;
void init() {
_streamSubscription =
_cacheService.hiddenComments.listen(hiddenCommentsStreamListener);
_collapseCache.hiddenComments.listen(hiddenCommentsStreamListener);
emit(
state.copyWith(
collapsedCount: _cacheService.totalHidden(_commentId),
collapsed: _cacheService.isCollapsed(_commentId),
hidden: _cacheService.isHidden(_commentId),
collapsedCount: _collapseCache.totalHidden(_commentId),
collapsed: _collapseCache.isCollapsed(_commentId),
hidden: _collapseCache.isHidden(_commentId),
),
);
}
void collapse() {
if (state.collapsed) {
_cacheService.uncollapse(_commentId);
_collapseCache.uncollapse(_commentId);
emit(
state.copyWith(
@ -43,7 +43,7 @@ class CollapseCubit extends Cubit<CollapseState> {
),
);
} else {
final int count = _cacheService.collapse(_commentId);
final int count = _collapseCache.collapse(_commentId);
emit(
state.copyWith(

View File

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

View File

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

View File

@ -3,18 +3,24 @@ import 'package:equatable/equatable.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/screens/screens.dart';
import 'package:hacki/services/services.dart';
import 'package:logger/logger.dart';
part 'split_view_state.dart';
class SplitViewCubit extends Cubit<SplitViewState> {
SplitViewCubit({CacheService? cacheService})
: _cacheService = cacheService ?? locator.get<CacheService>(),
SplitViewCubit({
CommentCache? commentCache,
Logger? logger,
}) : _commentCache = commentCache ?? locator.get<CommentCache>(),
_logger = logger ?? locator.get<Logger>(),
super(const SplitViewState.init());
final CacheService _cacheService;
final Logger _logger;
final CommentCache _commentCache;
void updateItemScreenArgs(ItemScreenArgs args) {
_cacheService.resetCollapsedComments();
_logger.i('resetting comments in CommentCache');
_commentCache.resetComments();
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/models/models.dart' show Comment;
import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/services/cache_service.dart';
import 'package:hacki/services/services.dart';
part 'time_machine_state.dart';
class TimeMachineCubit extends Cubit<TimeMachineState> {
TimeMachineCubit({
SembastRepository? sembastRepository,
CacheService? cacheService,
CommentCache? commentCache,
}) : _sembastRepository =
sembastRepository ?? locator.get<SembastRepository>(),
_cacheService = cacheService ?? locator.get<CacheService>(),
_commentCache = commentCache ?? locator.get<CommentCache>(),
super(TimeMachineState.init());
final SembastRepository _sembastRepository;
final CacheService _cacheService;
final CommentCache _commentCache;
Future<void> activateTimeMachine(Comment comment) async {
emit(state.copyWith(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);
while (parent != null) {
parents.insert(0, parent);
final int parentId = parent.parent;
parent = _cacheService.getComment(parentId);
parent = _commentCache.getComment(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 'int_extension.dart';
export 'list_extension.dart';

View File

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

View File

@ -5,6 +5,7 @@ import 'package:hacki/config/locator.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/postable_repository.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:logger/logger.dart';
class AuthRepository extends PostableRepository {
AuthRepository({
@ -44,6 +45,7 @@ class AuthRepository extends PostableRepository {
password: password,
);
} catch (_) {
locator.get<Logger>().e(_);
return false;
}
}

View File

@ -1,20 +1,22 @@
import 'package:flutter/foundation.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/models/models.dart';
import 'package:hive/hive.dart';
import 'package:http/http.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.
class CacheRepository {
CacheRepository({
class OfflineRepository {
OfflineRepository({
Future<Box<List<int>>>? storyIdBox,
Future<Box<Map<dynamic, dynamic>>>? storyBox,
Future<Box<String>>? webPageBox,
Future<LazyBox<String>>? webPageBox,
Future<LazyBox<Map<dynamic, dynamic>>>? commentBox,
}) : _storyIdBox = storyIdBox ?? Hive.openBox<List<int>>(_storyIdBoxName),
_storyBox =
storyBox ?? Hive.openBox<Map<dynamic, dynamic>>(_storyBoxName),
_webPageBox = webPageBox ?? Hive.openBox<String>(_webPageBoxName),
_webPageBox = webPageBox ?? Hive.openLazyBox<String>(_webPageBoxName),
_commentBox = commentBox ??
Hive.openLazyBox<Map<dynamic, dynamic>>(_commentBoxName);
@ -25,7 +27,7 @@ class CacheRepository {
final Future<Box<List<int>>> _storyIdBox;
final Future<Box<Map<dynamic, dynamic>>> _storyBox;
final Future<LazyBox<Map<dynamic, dynamic>>> _commentBox;
final Future<Box<String>> _webPageBox;
final Future<LazyBox<String>> _webPageBox;
Future<bool> get hasCachedStories =>
_storyBox.then((Box<Map<dynamic, dynamic>> box) => box.isNotEmpty);
@ -34,39 +36,92 @@ class CacheRepository {
required StoryType of,
required List<int> ids,
}) async {
final Box<List<int>> box = await _storyIdBox;
late final Box<List<int>> box;
try {
box = await _storyIdBox;
} catch (_) {
locator.get<Logger>().e(_);
await Hive.deleteBoxFromDisk(_storyIdBoxName);
box = await _storyIdBox;
}
return box.put(of.name, ids);
}
Future<void> cacheStory({required Story story}) async {
final Box<Map<dynamic, dynamic>> box = await _storyBox;
late final Box<Map<dynamic, dynamic>> box;
try {
box = await _storyBox;
} catch (_) {
locator.get<Logger>().e(_);
await Hive.deleteBoxFromDisk(_storyBoxName);
box = await _storyBox;
}
return box.put(story.id.toString(), story.toJson());
}
Future<void> cacheUrl({required String url}) async {
final Box<String> box = await _webPageBox;
final String html = await compute(downloadWebPage, url);
late final LazyBox<String> box;
try {
box = await _webPageBox;
} catch (_) {
locator.get<Logger>().e(_);
await Hive.deleteBoxFromDisk(_webPageBoxName);
box = await _webPageBox;
}
final String html = await compute(_downloadWebPage, url);
return box.put(url, html);
}
Future<String?> getHtml({required String url}) async {
final Box<String> box = await _webPageBox;
return box.get(url);
try {
final LazyBox<String> box = await _webPageBox;
return box.get(url);
} catch (_) {
locator.get<Logger>().e(_);
await Hive.deleteBoxFromDisk(_webPageBoxName);
return null;
}
}
Future<bool> hasCachedWebPage({required String url}) async {
final Box<String> box = await _webPageBox;
return box.containsKey(url);
try {
final LazyBox<String> box = await _webPageBox;
return box.containsKey(url);
} catch (_) {
locator.get<Logger>().e(_);
await Hive.deleteBoxFromDisk(_webPageBoxName);
return false;
}
}
Future<List<int>> getCachedStoryIds({required StoryType of}) async {
final Box<List<int>> box = await _storyIdBox;
final List<int>? ids = box.get(of.name);
return ids ?? <int>[];
try {
final Box<List<int>> box = await _storyIdBox;
final List<int>? ids = box.get(of.name);
return ids ?? <int>[];
} catch (_) {
locator.get<Logger>().e(_);
await Hive.deleteBoxFromDisk(_storyIdBoxName);
return <int>[];
}
}
Stream<Story> getCachedStoriesStream({required List<int> ids}) async* {
final Box<Map<dynamic, dynamic>> box = await _storyBox;
late final Box<Map<dynamic, dynamic>> box;
try {
box = await _storyBox;
} catch (_) {
locator.get<Logger>().e(_);
await Hive.deleteBoxFromDisk(_storyBoxName);
return;
}
for (final int id in ids) {
final Map<dynamic, dynamic>? json = box.get(id.toString());
@ -83,7 +138,16 @@ class CacheRepository {
}
Future<Story?> getCachedStory({required int id}) async {
final Box<Map<dynamic, dynamic>> box = await _storyBox;
late final Box<Map<dynamic, dynamic>> box;
try {
box = await _storyBox;
} catch (_) {
locator.get<Logger>().e(_);
await Hive.deleteBoxFromDisk(_storyBoxName);
return null;
}
final Map<dynamic, dynamic>? json = box.get(id.toString());
if (json == null) {
return null;
@ -93,18 +157,33 @@ class CacheRepository {
}
Future<void> cacheComment({required Comment comment}) async {
final LazyBox<Map<dynamic, dynamic>> box = await _commentBox;
late final LazyBox<Map<dynamic, dynamic>> box;
try {
box = await _commentBox;
} catch (_) {
locator.get<Logger>().e(_);
await Hive.deleteBoxFromDisk(_commentBoxName);
box = await _commentBox;
}
return box.put(comment.id.toString(), comment.toJson());
}
Future<Comment?> getCachedComment({required int id}) async {
final LazyBox<Map<dynamic, dynamic>> box = await _commentBox;
final Map<dynamic, dynamic>? json = await box.get(id.toString());
if (json == null) {
try {
final LazyBox<Map<dynamic, dynamic>> box = await _commentBox;
final Map<dynamic, dynamic>? json = await box.get(id.toString());
if (json == null) {
return null;
}
final Comment comment = Comment.fromJson(json.cast<String, dynamic>());
return comment;
} catch (_) {
locator.get<Logger>().e(_);
await Hive.deleteBoxFromDisk(_commentBoxName);
return null;
}
final Comment comment = Comment.fromJson(json.cast<String, dynamic>());
return comment;
}
Stream<Comment> getCachedCommentsStream({
@ -127,23 +206,47 @@ class CacheRepository {
}
Future<int> deleteAllStoryIds() async {
final Box<List<int>> box = await _storyIdBox;
return box.clear();
try {
final Box<List<int>> box = await _storyIdBox;
return box.clear();
} catch (_) {
locator.get<Logger>().e(_);
await Hive.deleteBoxFromDisk(_storyIdBoxName);
return 0;
}
}
Future<int> deleteAllStories() async {
final Box<Map<dynamic, dynamic>> box = await _storyBox;
return box.clear();
try {
final Box<Map<dynamic, dynamic>> box = await _storyBox;
return box.clear();
} catch (_) {
locator.get<Logger>().e(_);
await Hive.deleteBoxFromDisk(_storyBoxName);
return 0;
}
}
Future<int> deleteAllComments() async {
final LazyBox<Map<dynamic, dynamic>> box = await _commentBox;
return box.clear();
try {
final LazyBox<Map<dynamic, dynamic>> box = await _commentBox;
return box.clear();
} catch (_) {
locator.get<Logger>().e(_);
await Hive.deleteBoxFromDisk(_commentBoxName);
return 0;
}
}
Future<int> deleteAllWebPages() async {
final Box<String> box = await _webPageBox;
return box.clear();
try {
final LazyBox<String> box = await _webPageBox;
return box.clear();
} catch (_) {
locator.get<Logger>().e(_);
await Hive.deleteBoxFromDisk(_webPageBoxName);
return 0;
}
}
Future<int> deleteAll() async {
@ -153,12 +256,13 @@ class CacheRepository {
.whenComplete(deleteAllWebPages);
}
static Future<String> downloadWebPage(String link) async {
static Future<String> _downloadWebPage(String link) async {
try {
final Client client = Client();
final Uri url = Uri.parse(link);
final Response response = await client.get(url);
final String body = response.body;
client.close();
return body;
} catch (_) {
return '''Web page not available.''';

View File

@ -2,6 +2,8 @@ import 'dart:async';
import 'dart:io';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hacki/config/locator.dart';
import 'package:logger/logger.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:synced_shared_preferences/synced_shared_preferences.dart';
@ -41,7 +43,8 @@ class PreferenceRepository {
static const bool _notificationModeDefaultValue = 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 _trueDarkModeDefaultValue = false;
static const bool _readerModeDefaultValue = true;
@ -82,7 +85,10 @@ class PreferenceRepository {
Future<bool> get shouldShowWebFirst async => _prefs.then(
(SharedPreferences prefs) =>
prefs.getBool(_navigationModeKey) ?? _navigationModeDefaultValue,
prefs.getBool(_navigationModeKey) ??
(Platform.isAndroid
? _navigationModeDefaultValueAndroid
: _navigationModeDefaultValueIOS),
);
Future<bool> get shouldShowEyeCandy async => _prefs.then(
@ -153,9 +159,13 @@ class PreferenceRepository {
aOptions: androidOptions,
);
} catch (_) {
await _secureStorage.deleteAll(
aOptions: androidOptions,
);
try {
await _secureStorage.deleteAll(
aOptions: androidOptions,
);
} catch (_) {
locator.get<Logger>().e(_);
}
rethrow;
}
@ -182,8 +192,10 @@ class PreferenceRepository {
Future<void> toggleNavigationMode() async {
final SharedPreferences prefs = await _prefs;
final bool currentMode =
prefs.getBool(_navigationModeKey) ?? _navigationModeDefaultValue;
final bool currentMode = prefs.getBool(_navigationModeKey) ??
(Platform.isAndroid
? _navigationModeDefaultValueAndroid
: _navigationModeDefaultValueIOS);
await prefs.setBool(_navigationModeKey, !currentMode);
}

View File

@ -51,9 +51,12 @@ class StoriesRepository {
Stream<Comment> fetchCommentsStream({
required List<int> ids,
int level = 0,
Comment? Function(int)? getFromCache,
}) async* {
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')
.then((dynamic json) => _parseJson(json as Map<String, dynamic>?))
.then((Map<String, dynamic>? json) async {
@ -69,6 +72,7 @@ class StoriesRepository {
yield* fetchCommentsStream(
ids: comment.kids,
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/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
import 'package:logger/logger.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:responsive_builder/responsive_builder.dart';
@ -47,7 +48,6 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen>
with SingleTickerProviderStateMixin, RouteAware {
final CacheService cacheService = locator.get<CacheService>();
final Throttle featureDiscoveryDismissThrottle = Throttle(
delay: _throttleDelay,
);
@ -66,9 +66,10 @@ class _HomeScreenState extends State<HomeScreen>
super.didPopNext();
if (context.read<StoriesBloc>().deviceScreenType ==
DeviceScreenType.mobile) {
locator.get<Logger>().i('resetting comments in CommentCache');
Future<void>.delayed(
const Duration(milliseconds: 500),
cacheService.resetCollapsedComments,
locator.get<CommentCache>().resetComments,
);
}
}

View File

@ -1,3 +1,5 @@
// ignore_for_file: comment_references
import 'package:equatable/equatable.dart';
import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/material.dart';
@ -38,6 +40,7 @@ class ItemScreenArgs extends Equatable {
const ItemScreenArgs({
required this.item,
this.onlyShowTargetComment = false,
this.useCommentCache = false,
this.targetComments,
});
@ -45,11 +48,17 @@ class ItemScreenArgs extends Equatable {
final bool onlyShowTargetComment;
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
List<Object?> get props => <Object?>[
item,
onlyShowTargetComment,
targetComments,
useCommentCache,
];
}
@ -66,25 +75,32 @@ class ItemScreen extends StatefulWidget {
static Route<dynamic> route(ItemScreenArgs args) {
return MaterialPageRoute<ItemScreen>(
settings: const RouteSettings(name: routeName),
builder: (BuildContext context) => MultiBlocProvider(
providers: <BlocProvider<dynamic>>[
BlocProvider<CommentsCubit>(
create: (_) => CommentsCubit(
offlineReading: context.read<StoriesBloc>().state.offlineReading,
item: args.item,
)..init(
onlyShowTargetComment: args.onlyShowTargetComment,
targetParents: args.targetComments,
),
builder: (BuildContext context) => RepositoryProvider<CollapseCache>(
create: (BuildContext context) => CollapseCache(),
lazy: false,
child: MultiBlocProvider(
providers: <BlocProvider<dynamic>>[
BlocProvider<CommentsCubit>(
create: (BuildContext context) => CommentsCubit(
offlineReading:
context.read<StoriesBloc>().state.offlineReading,
item: args.item,
collapseCache: context.read<CollapseCache>(),
)..init(
onlyShowTargetComment: args.onlyShowTargetComment,
targetParents: args.targetComments,
useCommentCache: args.useCommentCache,
),
),
BlocProvider<EditCubit>(
lazy: false,
create: (BuildContext context) => EditCubit(),
),
],
child: ItemScreen(
item: args.item,
parentComments: args.targetComments ?? <Comment>[],
),
BlocProvider<EditCubit>(
lazy: false,
create: (BuildContext context) => EditCubit(),
),
],
child: ItemScreen(
item: args.item,
parentComments: args.targetComments ?? <Comment>[],
),
),
);
@ -100,27 +116,33 @@ class ItemScreen extends StatefulWidget {
return true;
}
},
child: MultiBlocProvider(
key: ValueKey<ItemScreenArgs>(args),
providers: <BlocProvider<dynamic>>[
BlocProvider<CommentsCubit>(
create: (BuildContext context) => CommentsCubit(
offlineReading: context.read<StoriesBloc>().state.offlineReading,
item: args.item,
)..init(
onlyShowTargetComment: args.onlyShowTargetComment,
targetParents: args.targetComments,
),
child: RepositoryProvider<CollapseCache>(
create: (BuildContext context) => CollapseCache(),
lazy: false,
child: MultiBlocProvider(
key: ValueKey<ItemScreenArgs>(args),
providers: <BlocProvider<dynamic>>[
BlocProvider<CommentsCubit>(
create: (BuildContext context) => CommentsCubit(
offlineReading:
context.read<StoriesBloc>().state.offlineReading,
item: args.item,
collapseCache: context.read<CollapseCache>(),
)..init(
onlyShowTargetComment: args.onlyShowTargetComment,
targetParents: args.targetComments,
),
),
BlocProvider<EditCubit>(
lazy: false,
create: (BuildContext context) => EditCubit(),
),
],
child: ItemScreen(
item: args.item,
parentComments: args.targetComments ?? <Comment>[],
splitViewEnabled: true,
),
BlocProvider<EditCubit>(
lazy: false,
create: (BuildContext context) => EditCubit(),
),
],
child: ItemScreen(
item: args.item,
parentComments: args.targetComments ?? <Comment>[],
splitViewEnabled: true,
),
),
);
@ -180,7 +202,6 @@ class _ItemScreenState extends State<ItemScreen> {
@override
void dispose() {
locator.get<CacheService>().resetComments();
refreshController.dispose();
commentEditingController.dispose();
scrollController.dispose();
@ -742,7 +763,10 @@ class _ItemScreenState extends State<ItemScreen> {
onTap: () {
Navigator.pop(context);
goToItemScreen(
args: ItemScreenArgs(item: comment),
args: ItemScreenArgs(
item: comment,
useCommentCache: true,
),
forceNewScreen: true,
);
},

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/services/services.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
@ -43,6 +44,7 @@ class CommentTile extends StatelessWidget {
lazy: false,
create: (_) => CollapseCubit(
commentId: comment.id,
collapseCache: context.tryRead<CollapseCache>() ?? CollapseCache(),
)..init(),
child: BlocBuilder<CollapseCubit, CollapseState>(
builder: (BuildContext context, CollapseState state) {

View File

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

View File

@ -23,7 +23,8 @@ class StoriesListView extends StatefulWidget {
State<StoriesListView> createState() => _StoriesListViewState();
}
class _StoriesListViewState extends State<StoriesListView> {
class _StoriesListViewState extends State<StoriesListView>
with AutomaticKeepAliveClientMixin {
final RefreshController refreshController = RefreshController();
@override
@ -34,6 +35,7 @@ class _StoriesListViewState extends State<StoriesListView> {
@override
Widget build(BuildContext context) {
super.build(context);
final StoryType storyType = widget.storyType;
final Widget header = widget.header;
final ValueChanged<Story> onStoryTapped = widget.onStoryTapped;
@ -91,4 +93,7 @@ class _StoriesListViewState extends State<StoriesListView> {
},
);
}
@override
bool get wantKeepAlive => true;
}

View File

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

View File

@ -1,12 +1,9 @@
import 'package:hacki/models/models.dart' show Comment;
import 'package:rxdart/rxdart.dart';
class CacheService {
static final Map<int, Comment> _comments = <int, Comment>{};
static final Map<int, String> _drafts = <int, String>{};
static final Map<int, Set<int>> _kids = <int, Set<int>>{};
static final Set<int> _collapsed = <int>{};
static final Map<int, Set<int>> _hidden = <int, Set<int>>{};
class CollapseCache {
final Map<int, Set<int>> _kids = <int, Set<int>>{};
final Set<int> _collapsed = <int>{};
final Map<int, Set<int>> _hidden = <int, Set<int>>{};
final PublishSubject<Map<int, Set<int>>> _hiddenCommentsSubject =
PublishSubject<Map<int, Set<int>>>();
@ -76,19 +73,4 @@ class CacheService {
bool isCollapsed(int commentId) => _collapsed.contains(commentId);
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

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

View File

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

View File

@ -1,6 +1,6 @@
name: hacki
description: A Hacker News reader.
version: 0.2.23+65
version: 0.2.25+67
publish_to: none
environment: