mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
cff73a010b | |||
f0d6cac3fd |
@ -1 +0,0 @@
|
||||
- Bugfixes.
|
@ -1 +0,0 @@
|
||||
- Updates to UI.
|
@ -1 +0,0 @@
|
||||
- Updates to UI.
|
@ -1 +0,0 @@
|
||||
- Updates to UI.
|
@ -1 +0,0 @@
|
||||
- Tapping on comments in notification and history screen will lead you directly to the comment.
|
@ -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.
|
@ -1 +0,0 @@
|
||||
- Added offline mode.
|
@ -1,2 +0,0 @@
|
||||
- Added offline mode.
|
||||
- Bugfixes.
|
@ -1,2 +0,0 @@
|
||||
- Added offline mode.
|
||||
- Bugfixes.
|
@ -1,2 +0,0 @@
|
||||
- Added offline mode.
|
||||
- Bugfixes.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
2
fastlane/metadata/android/en-US/changelogs/66.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/66.txt
Normal file
@ -0,0 +1,2 @@
|
||||
- Offline mode now includes web pages.
|
||||
- You can now sort comments in story screen.
|
2
fastlane/metadata/android/en-US/changelogs/67.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/67.txt
Normal file
@ -0,0 +1,2 @@
|
||||
- Offline mode now includes web pages.
|
||||
- You can now sort comments in story screen.
|
@ -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 = "";
|
||||
|
@ -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());
|
||||
}
|
||||
|
31
lib/config/custom_log_filter.dart
Normal file
31
lib/config/custom_log_filter.dart
Normal 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;
|
||||
}
|
||||
}
|
@ -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>>(),
|
||||
);
|
||||
|
@ -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(
|
||||
|
@ -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();
|
||||
|
@ -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!,
|
||||
);
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
12
lib/extensions/context_extension.dart
Normal file
12
lib/extensions/context_extension.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
export 'context_extension.dart';
|
||||
export 'date_time_extension.dart';
|
||||
export 'int_extension.dart';
|
||||
export 'list_extension.dart';
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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.''';
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
);
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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++;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
3
lib/services/caches/caches.dart
Normal file
3
lib/services/caches/caches.dart
Normal file
@ -0,0 +1,3 @@
|
||||
export 'collapse_cache.dart';
|
||||
export 'comment_cache.dart';
|
||||
export 'draft_cache.dart';
|
@ -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];
|
||||
}
|
13
lib/services/caches/comment_cache.dart
Normal file
13
lib/services/caches/comment_cache.dart
Normal 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();
|
||||
}
|
||||
}
|
10
lib/services/caches/draft_cache.dart
Normal file
10
lib/services/caches/draft_cache.dart
Normal 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];
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
export 'cache_service.dart';
|
||||
export 'caches/caches.dart';
|
||||
export 'custom_bloc_observer.dart';
|
||||
export 'fetcher.dart';
|
||||
export 'firebase_client.dart';
|
||||
|
@ -19,7 +19,7 @@ abstract class LinkUtil {
|
||||
}) {
|
||||
if (offlineReading) {
|
||||
locator
|
||||
.get<CacheRepository>()
|
||||
.get<OfflineRepository>()
|
||||
.hasCachedWebPage(url: link)
|
||||
.then((bool cached) {
|
||||
if (cached) {
|
||||
|
@ -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:
|
||||
|
Reference in New Issue
Block a user