Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
19f2107d95 | |||
c9b2d82dfc | |||
56e442b09f | |||
9069efcced | |||
bf6a5667dc | |||
cff73a010b | |||
f0d6cac3fd |
4
.github/workflows/github-actions.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
JAVA_VERSION: "11.0"
|
JAVA_VERSION: "11.0"
|
||||||
FLUTTER_VERSION: "3.0.0"
|
FLUTTER_VERSION: "3.0.3"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-java@v2
|
- uses: actions/setup-java@v2
|
||||||
@ -20,7 +20,7 @@ jobs:
|
|||||||
java-version: '17'
|
java-version: '17'
|
||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
flutter-version: '3.0.0'
|
flutter-version: '3.0.3'
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
- run: flutter pub get
|
- run: flutter pub get
|
||||||
- run: flutter analyze
|
- run: flutter analyze
|
||||||
|
Before Width: | Height: | Size: 935 KiB After Width: | Height: | Size: 820 KiB |
Before Width: | Height: | Size: 390 KiB After Width: | Height: | Size: 406 KiB |
@ -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
@ -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
@ -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/68.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
- Offline mode now includes web pages.
|
||||||
|
- You can now sort comments in story screen.
|
3
fastlane/metadata/android/en-US/changelogs/69.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
- Lazy loading.
|
||||||
|
- Offline mode now includes web pages.
|
||||||
|
- You can now sort comments in story screen.
|
3
fastlane/metadata/android/en-US/changelogs/70.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
- Lazy loading.
|
||||||
|
- Offline mode now includes web pages.
|
||||||
|
- You can now sort comments in story screen.
|
Before Width: | Height: | Size: 935 KiB After Width: | Height: | Size: 820 KiB |
Before Width: | Height: | Size: 390 KiB After Width: | Height: | Size: 406 KiB |
46
integration_test/app_test.dart
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:hacki/main.dart' as app;
|
||||||
|
import 'package:hacki/screens/widgets/story_tile.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group('performance test', () {
|
||||||
|
testWidgets('scrolling performance on ItemScreen',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await app.main(testing: true);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final Finder bestStoryTabFinder = find.text('BEST');
|
||||||
|
|
||||||
|
await tester.tap(bestStoryTabFinder);
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 3));
|
||||||
|
|
||||||
|
final Finder storyTileFinder = find.byType(StoryTile);
|
||||||
|
|
||||||
|
await tester.tap(storyTileFinder.first);
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 3));
|
||||||
|
|
||||||
|
TestGesture gesture = await tester.startGesture(const Offset(0, 300));
|
||||||
|
await gesture.moveBy(const Offset(0, -300));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
gesture = await tester.startGesture(const Offset(0, 300));
|
||||||
|
await gesture.moveBy(const Offset(0, -300));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
gesture = await tester.startGesture(const Offset(0, 300));
|
||||||
|
await gesture.moveBy(const Offset(0, -300));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
gesture = await tester.startGesture(const Offset(0, 300));
|
||||||
|
await gesture.moveBy(const Offset(0, 900));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
gesture = await tester.startGesture(const Offset(0, 300));
|
||||||
|
await gesture.moveBy(const Offset(0, -900));
|
||||||
|
await tester.pump();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -19,6 +19,8 @@ PODS:
|
|||||||
- FMDB (2.7.5):
|
- FMDB (2.7.5):
|
||||||
- FMDB/standard (= 2.7.5)
|
- FMDB/standard (= 2.7.5)
|
||||||
- FMDB/standard (2.7.5)
|
- FMDB/standard (2.7.5)
|
||||||
|
- integration_test (0.0.1):
|
||||||
|
- Flutter
|
||||||
- OrderedSet (5.0.0)
|
- OrderedSet (5.0.0)
|
||||||
- path_provider_ios (0.0.1):
|
- path_provider_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
@ -50,6 +52,7 @@ DEPENDENCIES:
|
|||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||||
- flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`)
|
- flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`)
|
||||||
|
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
@ -80,6 +83,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||||
flutter_siri_suggestions:
|
flutter_siri_suggestions:
|
||||||
:path: ".symlinks/plugins/flutter_siri_suggestions/ios"
|
:path: ".symlinks/plugins/flutter_siri_suggestions/ios"
|
||||||
|
integration_test:
|
||||||
|
:path: ".symlinks/plugins/integration_test/ios"
|
||||||
path_provider_ios:
|
path_provider_ios:
|
||||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||||
receive_sharing_intent:
|
receive_sharing_intent:
|
||||||
@ -109,6 +114,7 @@ SPEC CHECKSUMS:
|
|||||||
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
|
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
|
||||||
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
|
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
|
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
|
||||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
||||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||||
|
@ -577,7 +577,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.2.23;
|
MARKETING_VERSION = 0.2.28;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -714,7 +714,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.2.23;
|
MARKETING_VERSION = 0.2.28;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -745,7 +745,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.2.23;
|
MARKETING_VERSION = 0.2.28;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
@ -7,7 +7,9 @@ import 'package:hacki/config/locator.dart';
|
|||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
import 'package:responsive_builder/responsive_builder.dart';
|
import 'package:responsive_builder/responsive_builder.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
part 'stories_event.dart';
|
part 'stories_event.dart';
|
||||||
part 'stories_state.dart';
|
part 'stories_state.dart';
|
||||||
@ -15,15 +17,18 @@ part 'stories_state.dart';
|
|||||||
class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||||
StoriesBloc({
|
StoriesBloc({
|
||||||
required PreferenceCubit preferenceCubit,
|
required PreferenceCubit preferenceCubit,
|
||||||
CacheRepository? cacheRepository,
|
OfflineRepository? offlineRepository,
|
||||||
StoriesRepository? storiesRepository,
|
StoriesRepository? storiesRepository,
|
||||||
PreferenceRepository? preferenceRepository,
|
PreferenceRepository? preferenceRepository,
|
||||||
|
Logger? logger,
|
||||||
}) : _preferenceCubit = preferenceCubit,
|
}) : _preferenceCubit = preferenceCubit,
|
||||||
_cacheRepository = cacheRepository ?? locator.get<CacheRepository>(),
|
_offlineRepository =
|
||||||
|
offlineRepository ?? locator.get<OfflineRepository>(),
|
||||||
_storiesRepository =
|
_storiesRepository =
|
||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||||
_preferenceRepository =
|
_preferenceRepository =
|
||||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||||
|
_logger = logger ?? locator.get<Logger>(),
|
||||||
super(const StoriesState.init()) {
|
super(const StoriesState.init()) {
|
||||||
on<StoriesInitialize>(onInitialize);
|
on<StoriesInitialize>(onInitialize);
|
||||||
on<StoriesRefresh>(onRefresh);
|
on<StoriesRefresh>(onRefresh);
|
||||||
@ -39,9 +44,10 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final PreferenceCubit _preferenceCubit;
|
final PreferenceCubit _preferenceCubit;
|
||||||
final CacheRepository _cacheRepository;
|
final OfflineRepository _offlineRepository;
|
||||||
final StoriesRepository _storiesRepository;
|
final StoriesRepository _storiesRepository;
|
||||||
final PreferenceRepository _preferenceRepository;
|
final PreferenceRepository _preferenceRepository;
|
||||||
|
final Logger _logger;
|
||||||
DeviceScreenType? deviceScreenType;
|
DeviceScreenType? deviceScreenType;
|
||||||
StreamSubscription<PreferenceState>? _streamSubscription;
|
StreamSubscription<PreferenceState>? _streamSubscription;
|
||||||
static const int _smallPageSize = 10;
|
static const int _smallPageSize = 10;
|
||||||
@ -71,7 +77,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
add(StoriesPageSizeChanged(pageSize: pageSize));
|
add(StoriesPageSizeChanged(pageSize: pageSize));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
final bool hasCachedStories = await _cacheRepository.hasCachedStories;
|
final bool hasCachedStories = await _offlineRepository.hasCachedStories;
|
||||||
final bool isComplexTile = _preferenceCubit.state.showComplexStoryTile;
|
final bool isComplexTile = _preferenceCubit.state.showComplexStoryTile;
|
||||||
final int pageSize = _getPageSize(isComplexTile: isComplexTile);
|
final int pageSize = _getPageSize(isComplexTile: isComplexTile);
|
||||||
emit(
|
emit(
|
||||||
@ -90,13 +96,13 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
required Emitter<StoriesState> emit,
|
required Emitter<StoriesState> emit,
|
||||||
}) async {
|
}) async {
|
||||||
if (state.offlineReading) {
|
if (state.offlineReading) {
|
||||||
final List<int> ids = await _cacheRepository.getCachedStoryIds(of: of);
|
final List<int> ids = await _offlineRepository.getCachedStoryIds(of: of);
|
||||||
emit(
|
emit(
|
||||||
state
|
state
|
||||||
.copyWithStoryIdsUpdated(of: of, to: ids)
|
.copyWithStoryIdsUpdated(of: of, to: ids)
|
||||||
.copyWithCurrentPageUpdated(of: of, to: 0),
|
.copyWithCurrentPageUpdated(of: of, to: 0),
|
||||||
);
|
);
|
||||||
_cacheRepository
|
_offlineRepository
|
||||||
.getCachedStoriesStream(
|
.getCachedStoriesStream(
|
||||||
ids: ids.sublist(0, min(ids.length, state.currentPageSize)),
|
ids: ids.sublist(0, min(ids.length, state.currentPageSize)),
|
||||||
)
|
)
|
||||||
@ -167,7 +173,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state.offlineReading) {
|
if (state.offlineReading) {
|
||||||
_cacheRepository
|
_offlineRepository
|
||||||
.getCachedStoriesStream(
|
.getCachedStoriesStream(
|
||||||
ids: state.storyIdsByType[event.type]!.sublist(
|
ids: state.storyIdsByType[event.type]!.sublist(
|
||||||
lower,
|
lower,
|
||||||
@ -241,9 +247,9 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await _cacheRepository.deleteAllStoryIds();
|
await _offlineRepository.deleteAllStoryIds();
|
||||||
await _cacheRepository.deleteAllStories();
|
await _offlineRepository.deleteAllStories();
|
||||||
await _cacheRepository.deleteAllComments();
|
await _offlineRepository.deleteAllComments();
|
||||||
|
|
||||||
final Set<int> prioritizedIds = <int>{};
|
final Set<int> prioritizedIds = <int>{};
|
||||||
final List<StoryType> prioritizedTypes = <StoryType>[...types]
|
final List<StoryType> prioritizedTypes = <StoryType>[...types]
|
||||||
@ -251,7 +257,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
|
|
||||||
for (final StoryType type in prioritizedTypes) {
|
for (final StoryType type in prioritizedTypes) {
|
||||||
final List<int> ids = await _storiesRepository.fetchStoryIds(of: type);
|
final List<int> ids = await _storiesRepository.fetchStoryIds(of: type);
|
||||||
await _cacheRepository.cacheStoryIds(of: type, ids: ids);
|
await _offlineRepository.cacheStoryIds(of: type, ids: ids);
|
||||||
prioritizedIds.addAll(ids);
|
prioritizedIds.addAll(ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,7 +279,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
final List<int> ids = await _storiesRepository.fetchStoryIds(
|
final List<int> ids = await _storiesRepository.fetchStoryIds(
|
||||||
of: StoryType.latest,
|
of: StoryType.latest,
|
||||||
);
|
);
|
||||||
await _cacheRepository.cacheStoryIds(of: StoryType.latest, ids: ids);
|
await _offlineRepository.cacheStoryIds(of: StoryType.latest, ids: ids);
|
||||||
latestIds.addAll(ids);
|
latestIds.addAll(ids);
|
||||||
|
|
||||||
await fetchAndCacheStories(
|
await fetchAndCacheStories(
|
||||||
@ -281,12 +287,6 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
includingWebPage: event.includingWebPage,
|
includingWebPage: event.includingWebPage,
|
||||||
isPrioritized: false,
|
isPrioritized: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
downloadStatus: StoriesDownloadStatus.finished,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -318,44 +318,53 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _cacheRepository.cacheStory(story: story);
|
await _offlineRepository.cacheStory(story: story);
|
||||||
|
|
||||||
if (story.url.isNotEmpty && includingWebPage) {
|
if (story.url.isNotEmpty && includingWebPage) {
|
||||||
await _cacheRepository.cacheUrl(url: story.url);
|
_logger.i('downloading ${story.url}');
|
||||||
|
await _offlineRepository.cacheUrl(url: story.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Completer<void> completer = Completer<void>();
|
|
||||||
_storiesRepository
|
_storiesRepository
|
||||||
.fetchAllChildrenComments(ids: story.kids)
|
.fetchAllChildrenComments(ids: story.kids)
|
||||||
.listen((Comment? comment) async {
|
.whereType<Comment>()
|
||||||
if (comment != null) {
|
.listen(
|
||||||
await _cacheRepository.cacheComment(comment: comment);
|
(Comment comment) => unawaited(
|
||||||
}
|
_offlineRepository.cacheComment(comment: comment),
|
||||||
}).onDone(() {
|
),
|
||||||
completer.complete();
|
)
|
||||||
add(StoryDownloaded(skipped: false));
|
.onDone(() => add(StoryDownloaded(skipped: false)));
|
||||||
});
|
|
||||||
|
|
||||||
await completer.future;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onStoryDownloaded(StoryDownloaded event, Emitter<StoriesState> emit) {
|
void onStoryDownloaded(StoryDownloaded event, Emitter<StoriesState> emit) {
|
||||||
if (event.skipped) {
|
if (event.skipped) {
|
||||||
|
final int updatedStoriesToBeDownloaded = state.storiesToBeDownloaded - 1;
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
storiesToBeDownloaded: state.storiesToBeDownloaded - 1,
|
storiesToBeDownloaded: updatedStoriesToBeDownloaded,
|
||||||
|
downloadStatus:
|
||||||
|
state.storiesDownloaded == updatedStoriesToBeDownloaded
|
||||||
|
? StoriesDownloadStatus.finished
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final int updatedStoriesDownloaded = state.storiesDownloaded + 1;
|
final int updatedStoriesDownloaded = state.storiesDownloaded + 1;
|
||||||
|
final int updatedStoriesToBeDownloaded =
|
||||||
|
updatedStoriesDownloaded > state.storiesToBeDownloaded
|
||||||
|
? state.storiesToBeDownloaded + 1
|
||||||
|
: state.storiesToBeDownloaded;
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
storiesDownloaded: updatedStoriesDownloaded,
|
storiesDownloaded: updatedStoriesDownloaded,
|
||||||
storiesToBeDownloaded:
|
storiesToBeDownloaded: updatedStoriesToBeDownloaded,
|
||||||
updatedStoriesDownloaded > state.storiesToBeDownloaded
|
downloadStatus:
|
||||||
? state.storiesToBeDownloaded + 1
|
updatedStoriesDownloaded == updatedStoriesToBeDownloaded
|
||||||
: state.storiesToBeDownloaded,
|
? StoriesDownloadStatus.finished
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -373,10 +382,10 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
StoriesExitOffline event,
|
StoriesExitOffline event,
|
||||||
Emitter<StoriesState> emit,
|
Emitter<StoriesState> emit,
|
||||||
) async {
|
) async {
|
||||||
await _cacheRepository.deleteAllStoryIds();
|
await _offlineRepository.deleteAllStoryIds();
|
||||||
await _cacheRepository.deleteAllStories();
|
await _offlineRepository.deleteAllStories();
|
||||||
await _cacheRepository.deleteAllComments();
|
await _offlineRepository.deleteAllComments();
|
||||||
await _cacheRepository.deleteAllWebPages();
|
await _offlineRepository.deleteAllWebPages();
|
||||||
emit(state.copyWith(offlineReading: false));
|
emit(state.copyWith(offlineReading: false));
|
||||||
add(StoriesInitialize());
|
add(StoriesInitialize());
|
||||||
}
|
}
|
||||||
|
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:flutter/material.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:hacki/config/custom_log_filter.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
import 'package:hacki/services/services.dart';
|
import 'package:hacki/services/services.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
@ -10,16 +11,17 @@ final GetIt locator = GetIt.instance;
|
|||||||
/// Set up [GetIt] locator.
|
/// Set up [GetIt] locator.
|
||||||
Future<void> setUpLocator() async {
|
Future<void> setUpLocator() async {
|
||||||
locator
|
locator
|
||||||
|
..registerSingleton<Logger>(Logger(filter: CustomLogFilter()))
|
||||||
..registerSingleton<StoriesRepository>(StoriesRepository())
|
..registerSingleton<StoriesRepository>(StoriesRepository())
|
||||||
..registerSingleton<PreferenceRepository>(PreferenceRepository())
|
..registerSingleton<PreferenceRepository>(PreferenceRepository())
|
||||||
..registerSingleton<SearchRepository>(SearchRepository())
|
..registerSingleton<SearchRepository>(SearchRepository())
|
||||||
..registerSingleton<AuthRepository>(AuthRepository())
|
..registerSingleton<AuthRepository>(AuthRepository())
|
||||||
..registerSingleton<PostRepository>(PostRepository())
|
..registerSingleton<PostRepository>(PostRepository())
|
||||||
..registerSingleton<SembastRepository>(SembastRepository())
|
..registerSingleton<SembastRepository>(SembastRepository())
|
||||||
..registerSingleton<CacheRepository>(CacheRepository())
|
..registerSingleton<OfflineRepository>(OfflineRepository())
|
||||||
..registerSingleton<CacheService>(CacheService())
|
..registerSingleton<DraftCache>(DraftCache())
|
||||||
|
..registerSingleton<CommentCache>(CommentCache())
|
||||||
..registerSingleton<LocalNotification>(LocalNotification())
|
..registerSingleton<LocalNotification>(LocalNotification())
|
||||||
..registerSingleton<Logger>(Logger())
|
|
||||||
..registerSingleton<RouteObserver<ModalRoute<dynamic>>>(
|
..registerSingleton<RouteObserver<ModalRoute<dynamic>>>(
|
||||||
RouteObserver<ModalRoute<dynamic>>(),
|
RouteObserver<ModalRoute<dynamic>>(),
|
||||||
);
|
);
|
||||||
|
@ -10,31 +10,31 @@ part 'collapse_state.dart';
|
|||||||
class CollapseCubit extends Cubit<CollapseState> {
|
class CollapseCubit extends Cubit<CollapseState> {
|
||||||
CollapseCubit({
|
CollapseCubit({
|
||||||
required int commentId,
|
required int commentId,
|
||||||
CacheService? cacheService,
|
CollapseCache? collapseCache,
|
||||||
}) : _commentId = commentId,
|
}) : _commentId = commentId,
|
||||||
_cacheService = cacheService ?? locator.get<CacheService>(),
|
_collapseCache = collapseCache ?? locator.get<CollapseCache>(),
|
||||||
super(const CollapseState.init());
|
super(const CollapseState.init());
|
||||||
|
|
||||||
final int _commentId;
|
final int _commentId;
|
||||||
final CacheService _cacheService;
|
final CollapseCache _collapseCache;
|
||||||
late final StreamSubscription<Map<int, Set<int>>> _streamSubscription;
|
late final StreamSubscription<Map<int, Set<int>>> _streamSubscription;
|
||||||
|
|
||||||
void init() {
|
void init() {
|
||||||
_streamSubscription =
|
_streamSubscription =
|
||||||
_cacheService.hiddenComments.listen(hiddenCommentsStreamListener);
|
_collapseCache.hiddenComments.listen(hiddenCommentsStreamListener);
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
collapsedCount: _cacheService.totalHidden(_commentId),
|
collapsedCount: _collapseCache.totalHidden(_commentId),
|
||||||
collapsed: _cacheService.isCollapsed(_commentId),
|
collapsed: _collapseCache.isCollapsed(_commentId),
|
||||||
hidden: _cacheService.isHidden(_commentId),
|
hidden: _collapseCache.isHidden(_commentId),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void collapse() {
|
void collapse() {
|
||||||
if (state.collapsed) {
|
if (state.collapsed) {
|
||||||
_cacheService.uncollapse(_commentId);
|
_collapseCache.uncollapse(_commentId);
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -43,7 +43,7 @@ class CollapseCubit extends Cubit<CollapseState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final int count = _cacheService.collapse(_commentId);
|
final int count = _collapseCache.collapse(_commentId);
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
|
@ -11,31 +11,56 @@ import 'package:hacki/models/models.dart';
|
|||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
import 'package:hacki/screens/screens.dart';
|
import 'package:hacki/screens/screens.dart';
|
||||||
import 'package:hacki/services/services.dart';
|
import 'package:hacki/services/services.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
part 'comments_state.dart';
|
part 'comments_state.dart';
|
||||||
|
|
||||||
class CommentsCubit extends Cubit<CommentsState> {
|
class CommentsCubit extends Cubit<CommentsState> {
|
||||||
CommentsCubit({
|
CommentsCubit({
|
||||||
CacheService? cacheService,
|
required CollapseCache collapseCache,
|
||||||
CacheRepository? cacheRepository,
|
CommentCache? commentCache,
|
||||||
|
OfflineRepository? offlineRepository,
|
||||||
StoriesRepository? storiesRepository,
|
StoriesRepository? storiesRepository,
|
||||||
SembastRepository? sembastRepository,
|
SembastRepository? sembastRepository,
|
||||||
|
Logger? logger,
|
||||||
required bool offlineReading,
|
required bool offlineReading,
|
||||||
required Item item,
|
required Item item,
|
||||||
}) : _cacheService = cacheService ?? locator.get<CacheService>(),
|
required FetchMode defaultFetchMode,
|
||||||
_cacheRepository = cacheRepository ?? locator.get<CacheRepository>(),
|
required CommentsOrder defaultCommentsOrder,
|
||||||
|
}) : _collapseCache = collapseCache,
|
||||||
|
_commentCache = commentCache ?? locator.get<CommentCache>(),
|
||||||
|
_offlineRepository =
|
||||||
|
offlineRepository ?? locator.get<OfflineRepository>(),
|
||||||
_storiesRepository =
|
_storiesRepository =
|
||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||||
_sembastRepository =
|
_sembastRepository =
|
||||||
sembastRepository ?? locator.get<SembastRepository>(),
|
sembastRepository ?? locator.get<SembastRepository>(),
|
||||||
super(CommentsState.init(offlineReading: offlineReading, item: item));
|
_logger = logger ?? locator.get<Logger>(),
|
||||||
|
super(
|
||||||
|
CommentsState.init(
|
||||||
|
offlineReading: offlineReading,
|
||||||
|
item: item,
|
||||||
|
fetchMode: defaultFetchMode,
|
||||||
|
order: defaultCommentsOrder,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final CacheService _cacheService;
|
final CollapseCache _collapseCache;
|
||||||
final CacheRepository _cacheRepository;
|
final CommentCache _commentCache;
|
||||||
|
final OfflineRepository _offlineRepository;
|
||||||
final StoriesRepository _storiesRepository;
|
final StoriesRepository _storiesRepository;
|
||||||
final SembastRepository _sembastRepository;
|
final SembastRepository _sembastRepository;
|
||||||
|
final Logger _logger;
|
||||||
|
|
||||||
|
/// The [StreamSubscription] for stream (both lazy or eager)
|
||||||
|
/// fetching comments posted directly to the story.
|
||||||
StreamSubscription<Comment>? _streamSubscription;
|
StreamSubscription<Comment>? _streamSubscription;
|
||||||
|
|
||||||
|
/// The map of [StreamSubscription] for streams
|
||||||
|
/// fetching comments lazily. [int] is the id of parent comment.
|
||||||
|
final Map<int, StreamSubscription<Comment>> _streamSubscriptions =
|
||||||
|
<int, StreamSubscription<Comment>>{};
|
||||||
|
|
||||||
static const int _pageSize = 20;
|
static const int _pageSize = 20;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -47,6 +72,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
|
|
||||||
Future<void> init({
|
Future<void> init({
|
||||||
bool onlyShowTargetComment = false,
|
bool onlyShowTargetComment = false,
|
||||||
|
bool useCommentCache = false,
|
||||||
List<Comment>? targetParents,
|
List<Comment>? targetParents,
|
||||||
}) async {
|
}) async {
|
||||||
if (onlyShowTargetComment && (targetParents?.isNotEmpty ?? false)) {
|
if (onlyShowTargetComment && (targetParents?.isNotEmpty ?? false)) {
|
||||||
@ -59,7 +85,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
_streamSubscription = _storiesRepository
|
_streamSubscription = _storiesRepository
|
||||||
.fetchCommentsStream(
|
.fetchAllCommentsRecursivelyStream(
|
||||||
ids: targetParents!.last.kids,
|
ids: targetParents!.last.kids,
|
||||||
level: targetParents.last.level + 1,
|
level: targetParents.last.level + 1,
|
||||||
)
|
)
|
||||||
@ -69,7 +95,13 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(state.copyWith(status: CommentsStatus.loading));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: CommentsStatus.loading,
|
||||||
|
comments: <Comment>[],
|
||||||
|
currentPage: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final Item item = state.item;
|
final Item item = state.item;
|
||||||
final Item updatedItem = state.offlineReading
|
final Item updatedItem = state.offlineReading
|
||||||
@ -80,15 +112,31 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
emit(state.copyWith(item: updatedItem));
|
emit(state.copyWith(item: updatedItem));
|
||||||
|
|
||||||
if (state.offlineReading) {
|
if (state.offlineReading) {
|
||||||
_streamSubscription = _cacheRepository
|
_streamSubscription = _offlineRepository
|
||||||
.getCachedCommentsStream(ids: kids)
|
.getCachedCommentsStream(ids: kids)
|
||||||
.listen(_onCommentFetched)
|
.listen(_onCommentFetched)
|
||||||
..onDone(_onDone);
|
..onDone(_onDone);
|
||||||
} else {
|
} else {
|
||||||
_streamSubscription = _storiesRepository
|
switch (state.fetchMode) {
|
||||||
.fetchCommentsStream(ids: kids)
|
case FetchMode.lazy:
|
||||||
.listen(_onCommentFetched)
|
_streamSubscription = _storiesRepository
|
||||||
..onDone(_onDone);
|
.fetchCommentsStream(
|
||||||
|
ids: kids,
|
||||||
|
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
||||||
|
)
|
||||||
|
.listen(_onCommentFetched)
|
||||||
|
..onDone(_onDone);
|
||||||
|
break;
|
||||||
|
case FetchMode.eager:
|
||||||
|
_streamSubscription = _storiesRepository
|
||||||
|
.fetchAllCommentsRecursivelyStream(
|
||||||
|
ids: kids,
|
||||||
|
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
||||||
|
)
|
||||||
|
.listen(_onCommentFetched)
|
||||||
|
..onDone(_onDone);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,28 +150,47 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_cacheService
|
|
||||||
..resetComments()
|
|
||||||
..resetCollapsedComments();
|
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: CommentsStatus.loading,
|
status: CommentsStatus.loading,
|
||||||
comments: <Comment>[],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_collapseCache.resetCollapsedComments();
|
||||||
|
|
||||||
await _streamSubscription?.cancel();
|
await _streamSubscription?.cancel();
|
||||||
|
for (final int id in _streamSubscriptions.keys) {
|
||||||
|
await _streamSubscriptions[id]?.cancel();
|
||||||
|
}
|
||||||
|
_streamSubscriptions.clear();
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
comments: <Comment>[],
|
||||||
|
currentPage: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final Item item = state.item;
|
final Item item = state.item;
|
||||||
final Item updatedItem =
|
final Item updatedItem =
|
||||||
await _storiesRepository.fetchItemBy(id: item.id) ?? item;
|
await _storiesRepository.fetchItemBy(id: item.id) ?? item;
|
||||||
final List<int> kids = sortKids(updatedItem.kids);
|
final List<int> kids = sortKids(updatedItem.kids);
|
||||||
|
|
||||||
_streamSubscription = _storiesRepository
|
if (state.fetchMode == FetchMode.lazy) {
|
||||||
.fetchCommentsStream(ids: kids)
|
_streamSubscription = _storiesRepository
|
||||||
.listen(_onCommentFetched)
|
.fetchCommentsStream(
|
||||||
..onDone(_onDone);
|
ids: kids,
|
||||||
|
)
|
||||||
|
.listen(_onCommentFetched)
|
||||||
|
..onDone(_onDone);
|
||||||
|
} else {
|
||||||
|
_streamSubscription = _storiesRepository
|
||||||
|
.fetchAllCommentsRecursivelyStream(
|
||||||
|
ids: kids,
|
||||||
|
)
|
||||||
|
.listen(_onCommentFetched)
|
||||||
|
..onDone(_onDone);
|
||||||
|
}
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -138,17 +205,67 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
onlyShowTargetComment: false,
|
onlyShowTargetComment: false,
|
||||||
comments: <Comment>[],
|
|
||||||
item: story,
|
item: story,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadMore() {
|
/// [comment] is only used for lazy fetching.
|
||||||
if (_streamSubscription != null) {
|
void loadMore({Comment? comment}) {
|
||||||
emit(state.copyWith(status: CommentsStatus.loading));
|
switch (state.fetchMode) {
|
||||||
_streamSubscription?.resume();
|
case FetchMode.lazy:
|
||||||
|
if (comment == null) return;
|
||||||
|
if (_streamSubscriptions.containsKey(comment.id)) return;
|
||||||
|
|
||||||
|
final int level = comment.level + 1;
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
|
/// Ignoring because the subscription will be cancelled in close()
|
||||||
|
// ignore: cancel_subscriptions
|
||||||
|
final StreamSubscription<Comment> streamSubscription =
|
||||||
|
_storiesRepository
|
||||||
|
.fetchCommentsStream(ids: comment.kids)
|
||||||
|
.listen((Comment cmt) {
|
||||||
|
_collapseCache.addKid(cmt.id, to: cmt.parent);
|
||||||
|
_commentCache.cacheComment(cmt);
|
||||||
|
_sembastRepository.cacheComment(cmt);
|
||||||
|
|
||||||
|
final List<LinkifyElement> elements = _linkify(
|
||||||
|
cmt.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
final BuildableComment buildableComment =
|
||||||
|
BuildableComment.fromComment(cmt, elements: elements);
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
comments: <Comment>[...state.comments]..insert(
|
||||||
|
state.comments.indexOf(comment) + offset + 1,
|
||||||
|
buildableComment.copyWith(level: level),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
offset++;
|
||||||
|
})
|
||||||
|
..onDone(() {
|
||||||
|
_streamSubscriptions[comment.id]?.cancel();
|
||||||
|
_streamSubscriptions.remove(comment.id);
|
||||||
|
})
|
||||||
|
..onError((dynamic error) {
|
||||||
|
_logger.e(error);
|
||||||
|
_streamSubscriptions[comment.id]?.cancel();
|
||||||
|
_streamSubscriptions.remove(comment.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
_streamSubscriptions[comment.id] = streamSubscription;
|
||||||
|
break;
|
||||||
|
case FetchMode.eager:
|
||||||
|
if (_streamSubscription != null) {
|
||||||
|
emit(state.copyWith(status: CommentsStatus.loading));
|
||||||
|
_streamSubscription?.resume();
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,11 +292,30 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onOrderChanged(CommentsOrder? order) {
|
void onOrderChanged(CommentsOrder? order) {
|
||||||
HapticFeedback.selectionClick();
|
|
||||||
if (order == null) return;
|
if (order == null) return;
|
||||||
|
if (state.order == order) return;
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
_streamSubscription?.cancel();
|
_streamSubscription?.cancel();
|
||||||
emit(state.copyWith(order: order, comments: <Comment>[]));
|
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
|
||||||
init();
|
s.cancel();
|
||||||
|
}
|
||||||
|
_streamSubscriptions.clear();
|
||||||
|
emit(state.copyWith(order: order));
|
||||||
|
init(useCommentCache: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onFetchModeChanged(FetchMode? fetchMode) {
|
||||||
|
if (fetchMode == null) return;
|
||||||
|
if (state.fetchMode == fetchMode) return;
|
||||||
|
_collapseCache.resetCollapsedComments();
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
_streamSubscription?.cancel();
|
||||||
|
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
|
||||||
|
s.cancel();
|
||||||
|
}
|
||||||
|
_streamSubscriptions.clear();
|
||||||
|
emit(state.copyWith(fetchMode: fetchMode));
|
||||||
|
init(useCommentCache: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<int> sortKids(List<int> kids) {
|
List<int> sortKids(List<int> kids) {
|
||||||
@ -205,9 +341,8 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
|
|
||||||
void _onCommentFetched(Comment? comment) {
|
void _onCommentFetched(Comment? comment) {
|
||||||
if (comment != null) {
|
if (comment != null) {
|
||||||
_cacheService
|
_collapseCache.addKid(comment.id, to: comment.parent);
|
||||||
..addKid(comment.id, to: comment.parent)
|
_commentCache.cacheComment(comment);
|
||||||
..cacheComment(comment);
|
|
||||||
_sembastRepository.cacheComment(comment);
|
_sembastRepository.cacheComment(comment);
|
||||||
|
|
||||||
final List<LinkifyElement> elements = _linkify(
|
final List<LinkifyElement> elements = _linkify(
|
||||||
@ -224,21 +359,24 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
|
|
||||||
emit(state.copyWith(comments: updatedComments));
|
emit(state.copyWith(comments: updatedComments));
|
||||||
|
|
||||||
if (updatedComments.length >= _pageSize + _pageSize * state.currentPage &&
|
if (state.fetchMode == FetchMode.eager) {
|
||||||
updatedComments.length <=
|
if (updatedComments.length >=
|
||||||
_pageSize * 2 + _pageSize * state.currentPage) {
|
_pageSize + _pageSize * state.currentPage &&
|
||||||
final bool isHidden = _cacheService.isHidden(comment.id);
|
updatedComments.length <=
|
||||||
|
_pageSize * 2 + _pageSize * state.currentPage) {
|
||||||
|
final bool isHidden = _collapseCache.isHidden(comment.id);
|
||||||
|
|
||||||
if (!isHidden) {
|
if (!isHidden) {
|
||||||
_streamSubscription?.pause();
|
_streamSubscription?.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
currentPage: state.currentPage + 1,
|
||||||
|
status: CommentsStatus.loaded,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
currentPage: state.currentPage + 1,
|
|
||||||
status: CommentsStatus.loaded,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -271,6 +409,9 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
await _streamSubscription?.cancel();
|
await _streamSubscription?.cancel();
|
||||||
|
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
|
||||||
|
await s.cancel();
|
||||||
|
}
|
||||||
await super.close();
|
await super.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,11 @@ enum CommentsOrder {
|
|||||||
oldestFirst,
|
oldestFirst,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum FetchMode {
|
||||||
|
lazy,
|
||||||
|
eager,
|
||||||
|
}
|
||||||
|
|
||||||
class CommentsState extends Equatable {
|
class CommentsState extends Equatable {
|
||||||
const CommentsState({
|
const CommentsState({
|
||||||
required this.item,
|
required this.item,
|
||||||
@ -21,6 +26,7 @@ class CommentsState extends Equatable {
|
|||||||
required this.status,
|
required this.status,
|
||||||
required this.fetchParentStatus,
|
required this.fetchParentStatus,
|
||||||
required this.order,
|
required this.order,
|
||||||
|
required this.fetchMode,
|
||||||
required this.onlyShowTargetComment,
|
required this.onlyShowTargetComment,
|
||||||
required this.offlineReading,
|
required this.offlineReading,
|
||||||
required this.currentPage,
|
required this.currentPage,
|
||||||
@ -29,10 +35,11 @@ class CommentsState extends Equatable {
|
|||||||
CommentsState.init({
|
CommentsState.init({
|
||||||
required this.offlineReading,
|
required this.offlineReading,
|
||||||
required this.item,
|
required this.item,
|
||||||
|
required this.fetchMode,
|
||||||
|
required this.order,
|
||||||
}) : comments = <Comment>[],
|
}) : comments = <Comment>[],
|
||||||
status = CommentsStatus.init,
|
status = CommentsStatus.init,
|
||||||
fetchParentStatus = CommentsStatus.init,
|
fetchParentStatus = CommentsStatus.init,
|
||||||
order = CommentsOrder.natural,
|
|
||||||
onlyShowTargetComment = false,
|
onlyShowTargetComment = false,
|
||||||
currentPage = 0;
|
currentPage = 0;
|
||||||
|
|
||||||
@ -41,6 +48,7 @@ class CommentsState extends Equatable {
|
|||||||
final CommentsStatus status;
|
final CommentsStatus status;
|
||||||
final CommentsStatus fetchParentStatus;
|
final CommentsStatus fetchParentStatus;
|
||||||
final CommentsOrder order;
|
final CommentsOrder order;
|
||||||
|
final FetchMode fetchMode;
|
||||||
final bool onlyShowTargetComment;
|
final bool onlyShowTargetComment;
|
||||||
final bool offlineReading;
|
final bool offlineReading;
|
||||||
final int currentPage;
|
final int currentPage;
|
||||||
@ -51,6 +59,7 @@ class CommentsState extends Equatable {
|
|||||||
CommentsStatus? status,
|
CommentsStatus? status,
|
||||||
CommentsStatus? fetchParentStatus,
|
CommentsStatus? fetchParentStatus,
|
||||||
CommentsOrder? order,
|
CommentsOrder? order,
|
||||||
|
FetchMode? fetchMode,
|
||||||
bool? onlyShowTargetComment,
|
bool? onlyShowTargetComment,
|
||||||
bool? offlineReading,
|
bool? offlineReading,
|
||||||
int? currentPage,
|
int? currentPage,
|
||||||
@ -61,6 +70,7 @@ class CommentsState extends Equatable {
|
|||||||
fetchParentStatus: fetchParentStatus ?? this.fetchParentStatus,
|
fetchParentStatus: fetchParentStatus ?? this.fetchParentStatus,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
order: order ?? this.order,
|
order: order ?? this.order,
|
||||||
|
fetchMode: fetchMode ?? this.fetchMode,
|
||||||
onlyShowTargetComment:
|
onlyShowTargetComment:
|
||||||
onlyShowTargetComment ?? this.onlyShowTargetComment,
|
onlyShowTargetComment ?? this.onlyShowTargetComment,
|
||||||
offlineReading: offlineReading ?? this.offlineReading,
|
offlineReading: offlineReading ?? this.offlineReading,
|
||||||
@ -68,6 +78,8 @@ class CommentsState extends Equatable {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Set<int> get commentIds => comments.map((Comment e) => e.id).toSet();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
item,
|
item,
|
||||||
@ -75,6 +87,7 @@ class CommentsState extends Equatable {
|
|||||||
status,
|
status,
|
||||||
fetchParentStatus,
|
fetchParentStatus,
|
||||||
order,
|
order,
|
||||||
|
fetchMode,
|
||||||
onlyShowTargetComment,
|
onlyShowTargetComment,
|
||||||
offlineReading,
|
offlineReading,
|
||||||
currentPage,
|
currentPage,
|
||||||
|
@ -8,19 +8,19 @@ import 'package:hacki/utils/debouncer.dart';
|
|||||||
part 'edit_state.dart';
|
part 'edit_state.dart';
|
||||||
|
|
||||||
class EditCubit extends Cubit<EditState> {
|
class EditCubit extends Cubit<EditState> {
|
||||||
EditCubit({CacheService? cacheService})
|
EditCubit({DraftCache? draftCache})
|
||||||
: _cacheService = cacheService ?? locator.get<CacheService>(),
|
: _draftCache = draftCache ?? locator.get<DraftCache>(),
|
||||||
_debouncer = Debouncer(delay: const Duration(seconds: 1)),
|
_debouncer = Debouncer(delay: const Duration(seconds: 1)),
|
||||||
super(const EditState.init());
|
super(const EditState.init());
|
||||||
|
|
||||||
final CacheService _cacheService;
|
final DraftCache _draftCache;
|
||||||
final Debouncer _debouncer;
|
final Debouncer _debouncer;
|
||||||
|
|
||||||
void onReplyTapped(Item item) {
|
void onReplyTapped(Item item) {
|
||||||
emit(
|
emit(
|
||||||
EditState(
|
EditState(
|
||||||
replyingTo: item,
|
replyingTo: item,
|
||||||
text: _cacheService.getDraft(replyingTo: item.id),
|
text: _draftCache.getDraft(replyingTo: item.id),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -44,7 +44,7 @@ class EditCubit extends Cubit<EditState> {
|
|||||||
|
|
||||||
void onReplySubmittedSuccessfully() {
|
void onReplySubmittedSuccessfully() {
|
||||||
if (state.replyingTo != null) {
|
if (state.replyingTo != null) {
|
||||||
_cacheService.removeDraft(replyingTo: state.replyingTo!.id);
|
_draftCache.removeDraft(replyingTo: state.replyingTo!.id);
|
||||||
}
|
}
|
||||||
emit(const EditState.init());
|
emit(const EditState.init());
|
||||||
}
|
}
|
||||||
@ -54,7 +54,7 @@ class EditCubit extends Cubit<EditState> {
|
|||||||
if (state.replyingTo != null) {
|
if (state.replyingTo != null) {
|
||||||
final int? id = state.replyingTo?.id;
|
final int? id = state.replyingTo?.id;
|
||||||
_debouncer.run(() {
|
_debouncer.run(() {
|
||||||
_cacheService.cacheDraft(
|
_draftCache.cacheDraft(
|
||||||
text: text,
|
text: text,
|
||||||
replyingTo: id!,
|
replyingTo: id!,
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
|
import 'package:hacki/cubits/comments/comments_cubit.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
|
|
||||||
part 'preference_state.dart';
|
part 'preference_state.dart';
|
||||||
@ -33,6 +34,10 @@ class PreferenceCubit extends Cubit<PreferenceState> {
|
|||||||
.then((bool value) => emit(state.copyWith(markReadStories: value)));
|
.then((bool value) => emit(state.copyWith(markReadStories: value)));
|
||||||
_preferenceRepository.shouldShowMetadata
|
_preferenceRepository.shouldShowMetadata
|
||||||
.then((bool value) => emit(state.copyWith(showMetadata: value)));
|
.then((bool value) => emit(state.copyWith(showMetadata: value)));
|
||||||
|
_preferenceRepository.fetchMode
|
||||||
|
.then((FetchMode value) => emit(state.copyWith(fetchMode: value)));
|
||||||
|
_preferenceRepository.commentsOrder
|
||||||
|
.then((CommentsOrder value) => emit(state.copyWith(order: value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleNotificationMode() {
|
void toggleNotificationMode() {
|
||||||
@ -74,4 +79,16 @@ class PreferenceCubit extends Cubit<PreferenceState> {
|
|||||||
emit(state.copyWith(showMetadata: !state.showMetadata));
|
emit(state.copyWith(showMetadata: !state.showMetadata));
|
||||||
_preferenceRepository.toggleMetadataMode();
|
_preferenceRepository.toggleMetadataMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void selectFetchMode(FetchMode? fetchMode) {
|
||||||
|
if (fetchMode == null || state.fetchMode == fetchMode) return;
|
||||||
|
emit(state.copyWith(fetchMode: fetchMode));
|
||||||
|
_preferenceRepository.selectFetchMode(fetchMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void selectCommentsOrder(CommentsOrder? order) {
|
||||||
|
if (order == null || state.order == order) return;
|
||||||
|
emit(state.copyWith(order: order));
|
||||||
|
_preferenceRepository.selectCommentsOrder(order);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,8 @@ class PreferenceState extends Equatable {
|
|||||||
required this.useReader,
|
required this.useReader,
|
||||||
required this.markReadStories,
|
required this.markReadStories,
|
||||||
required this.showMetadata,
|
required this.showMetadata,
|
||||||
|
required this.fetchMode,
|
||||||
|
required this.order,
|
||||||
});
|
});
|
||||||
|
|
||||||
const PreferenceState.init()
|
const PreferenceState.init()
|
||||||
@ -20,7 +22,9 @@ class PreferenceState extends Equatable {
|
|||||||
useTrueDark = false,
|
useTrueDark = false,
|
||||||
useReader = false,
|
useReader = false,
|
||||||
markReadStories = false,
|
markReadStories = false,
|
||||||
showMetadata = false;
|
showMetadata = false,
|
||||||
|
fetchMode = FetchMode.eager,
|
||||||
|
order = CommentsOrder.natural;
|
||||||
|
|
||||||
final bool showNotification;
|
final bool showNotification;
|
||||||
final bool showComplexStoryTile;
|
final bool showComplexStoryTile;
|
||||||
@ -30,6 +34,8 @@ class PreferenceState extends Equatable {
|
|||||||
final bool useReader;
|
final bool useReader;
|
||||||
final bool markReadStories;
|
final bool markReadStories;
|
||||||
final bool showMetadata;
|
final bool showMetadata;
|
||||||
|
final FetchMode fetchMode;
|
||||||
|
final CommentsOrder order;
|
||||||
|
|
||||||
PreferenceState copyWith({
|
PreferenceState copyWith({
|
||||||
bool? showNotification,
|
bool? showNotification,
|
||||||
@ -40,6 +46,8 @@ class PreferenceState extends Equatable {
|
|||||||
bool? useReader,
|
bool? useReader,
|
||||||
bool? markReadStories,
|
bool? markReadStories,
|
||||||
bool? showMetadata,
|
bool? showMetadata,
|
||||||
|
FetchMode? fetchMode,
|
||||||
|
CommentsOrder? order,
|
||||||
}) {
|
}) {
|
||||||
return PreferenceState(
|
return PreferenceState(
|
||||||
showNotification: showNotification ?? this.showNotification,
|
showNotification: showNotification ?? this.showNotification,
|
||||||
@ -50,6 +58,8 @@ class PreferenceState extends Equatable {
|
|||||||
useReader: useReader ?? this.useReader,
|
useReader: useReader ?? this.useReader,
|
||||||
markReadStories: markReadStories ?? this.markReadStories,
|
markReadStories: markReadStories ?? this.markReadStories,
|
||||||
showMetadata: showMetadata ?? this.showMetadata,
|
showMetadata: showMetadata ?? this.showMetadata,
|
||||||
|
fetchMode: fetchMode ?? this.fetchMode,
|
||||||
|
order: order ?? this.order,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,5 +73,7 @@ class PreferenceState extends Equatable {
|
|||||||
useReader,
|
useReader,
|
||||||
markReadStories,
|
markReadStories,
|
||||||
showMetadata,
|
showMetadata,
|
||||||
|
fetchMode,
|
||||||
|
order,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -3,18 +3,24 @@ import 'package:equatable/equatable.dart';
|
|||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
import 'package:hacki/screens/screens.dart';
|
import 'package:hacki/screens/screens.dart';
|
||||||
import 'package:hacki/services/services.dart';
|
import 'package:hacki/services/services.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
part 'split_view_state.dart';
|
part 'split_view_state.dart';
|
||||||
|
|
||||||
class SplitViewCubit extends Cubit<SplitViewState> {
|
class SplitViewCubit extends Cubit<SplitViewState> {
|
||||||
SplitViewCubit({CacheService? cacheService})
|
SplitViewCubit({
|
||||||
: _cacheService = cacheService ?? locator.get<CacheService>(),
|
CommentCache? commentCache,
|
||||||
|
Logger? logger,
|
||||||
|
}) : _commentCache = commentCache ?? locator.get<CommentCache>(),
|
||||||
|
_logger = logger ?? locator.get<Logger>(),
|
||||||
super(const SplitViewState.init());
|
super(const SplitViewState.init());
|
||||||
|
|
||||||
final CacheService _cacheService;
|
final Logger _logger;
|
||||||
|
final CommentCache _commentCache;
|
||||||
|
|
||||||
void updateItemScreenArgs(ItemScreenArgs args) {
|
void updateItemScreenArgs(ItemScreenArgs args) {
|
||||||
_cacheService.resetCollapsedComments();
|
_logger.i('resetting comments in CommentCache');
|
||||||
|
_commentCache.resetComments();
|
||||||
emit(state.copyWith(itemScreenArgs: args));
|
emit(state.copyWith(itemScreenArgs: args));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,34 +3,34 @@ import 'package:equatable/equatable.dart';
|
|||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
import 'package:hacki/models/models.dart' show Comment;
|
import 'package:hacki/models/models.dart' show Comment;
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
import 'package:hacki/services/cache_service.dart';
|
import 'package:hacki/services/services.dart';
|
||||||
|
|
||||||
part 'time_machine_state.dart';
|
part 'time_machine_state.dart';
|
||||||
|
|
||||||
class TimeMachineCubit extends Cubit<TimeMachineState> {
|
class TimeMachineCubit extends Cubit<TimeMachineState> {
|
||||||
TimeMachineCubit({
|
TimeMachineCubit({
|
||||||
SembastRepository? sembastRepository,
|
SembastRepository? sembastRepository,
|
||||||
CacheService? cacheService,
|
CommentCache? commentCache,
|
||||||
}) : _sembastRepository =
|
}) : _sembastRepository =
|
||||||
sembastRepository ?? locator.get<SembastRepository>(),
|
sembastRepository ?? locator.get<SembastRepository>(),
|
||||||
_cacheService = cacheService ?? locator.get<CacheService>(),
|
_commentCache = commentCache ?? locator.get<CommentCache>(),
|
||||||
super(TimeMachineState.init());
|
super(TimeMachineState.init());
|
||||||
|
|
||||||
final SembastRepository _sembastRepository;
|
final SembastRepository _sembastRepository;
|
||||||
final CacheService _cacheService;
|
final CommentCache _commentCache;
|
||||||
|
|
||||||
Future<void> activateTimeMachine(Comment comment) async {
|
Future<void> activateTimeMachine(Comment comment) async {
|
||||||
emit(state.copyWith(parents: <Comment>[]));
|
emit(state.copyWith(parents: <Comment>[]));
|
||||||
|
|
||||||
final List<Comment> parents = <Comment>[];
|
final List<Comment> parents = <Comment>[];
|
||||||
Comment? parent = _cacheService.getComment(comment.parent);
|
Comment? parent = _commentCache.getComment(comment.parent);
|
||||||
parent ??= await _sembastRepository.getCachedComment(id: comment.parent);
|
parent ??= await _sembastRepository.getCachedComment(id: comment.parent);
|
||||||
|
|
||||||
while (parent != null) {
|
while (parent != null) {
|
||||||
parents.insert(0, parent);
|
parents.insert(0, parent);
|
||||||
|
|
||||||
final int parentId = parent.parent;
|
final int parentId = parent.parent;
|
||||||
parent = _cacheService.getComment(parentId);
|
parent = _commentCache.getComment(parentId);
|
||||||
parent ??= await _sembastRepository.getCachedComment(id: parentId);
|
parent ??= await _sembastRepository.getCachedComment(id: parentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
58
lib/extensions/context_extension.dart
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect? get rect {
|
||||||
|
final RenderBox? box = findRenderObject() as RenderBox?;
|
||||||
|
final Rect? rect =
|
||||||
|
box == null ? null : box.localToGlobal(Offset.zero) & box.size;
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
static double _screenWidth = 0;
|
||||||
|
static double _storyTileHeight = 0;
|
||||||
|
static int _storyTileMaxLines = 4;
|
||||||
|
static const double _screenWidthLowerBound = 428,
|
||||||
|
_screenWidthUpperBound = 850,
|
||||||
|
_picHeightLowerBound = 110,
|
||||||
|
_picHeightUpperBound = 128,
|
||||||
|
_smallPicHeight = 100,
|
||||||
|
_picHeightFactor = 0.3;
|
||||||
|
|
||||||
|
double get storyTileHeight {
|
||||||
|
final double screenWidth =
|
||||||
|
min(MediaQuery.of(this).size.height, MediaQuery.of(this).size.width);
|
||||||
|
|
||||||
|
if (screenWidth == _screenWidth) {
|
||||||
|
return _storyTileHeight;
|
||||||
|
} else {
|
||||||
|
_screenWidth = screenWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
final bool showSmallerPreviewPic = screenWidth > _screenWidthLowerBound &&
|
||||||
|
screenWidth < _screenWidthUpperBound;
|
||||||
|
final double height = showSmallerPreviewPic
|
||||||
|
? _smallPicHeight
|
||||||
|
: (screenWidth * _picHeightFactor)
|
||||||
|
.clamp(_picHeightLowerBound, _picHeightUpperBound);
|
||||||
|
final int maxLines = height == _smallPicHeight ? 3 : 4;
|
||||||
|
_storyTileMaxLines = maxLines;
|
||||||
|
|
||||||
|
_storyTileHeight = height;
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get storyTileMaxLines {
|
||||||
|
return _storyTileMaxLines;
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
export 'context_extension.dart';
|
||||||
export 'date_time_extension.dart';
|
export 'date_time_extension.dart';
|
||||||
export 'int_extension.dart';
|
export 'int_extension.dart';
|
||||||
export 'list_extension.dart';
|
export 'list_extension.dart';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
extension WidgetModifier on Widget {
|
extension WidgetModifier on Widget {
|
||||||
Widget padding([EdgeInsetsGeometry value = const EdgeInsets.all(12)]) {
|
Widget padded([EdgeInsetsGeometry value = const EdgeInsets.all(12)]) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: value,
|
padding: value,
|
||||||
child: this,
|
child: this,
|
||||||
|
@ -3,6 +3,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:adaptive_theme/adaptive_theme.dart';
|
import 'package:adaptive_theme/adaptive_theme.dart';
|
||||||
import 'package:feature_discovery/feature_discovery.dart';
|
import 'package:feature_discovery/feature_discovery.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@ -14,6 +15,7 @@ import 'package:hacki/config/locator.dart';
|
|||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart' show PreferenceRepository;
|
import 'package:hacki/repositories/repositories.dart' show PreferenceRepository;
|
||||||
import 'package:hacki/screens/screens.dart';
|
import 'package:hacki/screens/screens.dart';
|
||||||
|
import 'package:hacki/services/custom_bloc_observer.dart';
|
||||||
import 'package:hacki/services/fetcher.dart';
|
import 'package:hacki/services/fetcher.dart';
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
@ -30,9 +32,13 @@ final BehaviorSubject<String?> selectNotificationSubject =
|
|||||||
final BehaviorSubject<String?> siriSuggestionSubject =
|
final BehaviorSubject<String?> siriSuggestionSubject =
|
||||||
BehaviorSubject<String?>();
|
BehaviorSubject<String?>();
|
||||||
|
|
||||||
Future<void> main() async {
|
late final bool isTesting;
|
||||||
|
|
||||||
|
Future<void> main({bool testing = false}) async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
isTesting = testing;
|
||||||
|
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
unawaited(
|
unawaited(
|
||||||
Workmanager().initialize(
|
Workmanager().initialize(
|
||||||
@ -86,25 +92,26 @@ Future<void> main() async {
|
|||||||
final bool trueDarkMode =
|
final bool trueDarkMode =
|
||||||
prefs.getBool(PreferenceRepository.trueDarkModeKey) ?? false;
|
prefs.getBool(PreferenceRepository.trueDarkModeKey) ?? false;
|
||||||
|
|
||||||
// Uncomment code below for running with logging.
|
if (kReleaseMode) {
|
||||||
// BlocOverrides.runZoned(
|
runApp(
|
||||||
// () {
|
HackiApp(
|
||||||
// runApp(
|
savedThemeMode: savedThemeMode,
|
||||||
// HackiApp(
|
trueDarkMode: trueDarkMode,
|
||||||
// savedThemeMode: savedThemeMode,
|
),
|
||||||
// trueDarkMode: trueDarkMode,
|
);
|
||||||
// ),
|
} else {
|
||||||
// );
|
BlocOverrides.runZoned(
|
||||||
// },
|
() {
|
||||||
// blocObserver: CustomBlocObserver(),
|
runApp(
|
||||||
// );
|
HackiApp(
|
||||||
|
savedThemeMode: savedThemeMode,
|
||||||
runApp(
|
trueDarkMode: trueDarkMode,
|
||||||
HackiApp(
|
),
|
||||||
savedThemeMode: savedThemeMode,
|
);
|
||||||
trueDarkMode: trueDarkMode,
|
},
|
||||||
),
|
blocObserver: CustomBlocObserver(),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HackiApp extends StatelessWidget {
|
class HackiApp extends StatelessWidget {
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:hacki/models/item.dart';
|
import 'package:hacki/models/item.dart';
|
||||||
|
|
||||||
enum StoryType {
|
enum StoryType {
|
||||||
@ -113,9 +111,10 @@ class Story extends Item {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
final String prettyString =
|
// final String prettyString =
|
||||||
const JsonEncoder.withIndent(' ').convert(this);
|
// const JsonEncoder.withIndent(' ').convert(this);
|
||||||
return 'Story $prettyString';
|
// return 'Story $prettyString';
|
||||||
|
return 'Story $id';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -5,16 +5,20 @@ import 'package:hacki/config/locator.dart';
|
|||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/repositories/postable_repository.dart';
|
import 'package:hacki/repositories/postable_repository.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
class AuthRepository extends PostableRepository {
|
class AuthRepository extends PostableRepository {
|
||||||
AuthRepository({
|
AuthRepository({
|
||||||
Dio? dio,
|
Dio? dio,
|
||||||
PreferenceRepository? preferenceRepository,
|
PreferenceRepository? preferenceRepository,
|
||||||
|
Logger? logger,
|
||||||
}) : _preferenceRepository =
|
}) : _preferenceRepository =
|
||||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||||
|
_logger = logger ?? locator.get<Logger>(),
|
||||||
super(dio: dio);
|
super(dio: dio);
|
||||||
|
|
||||||
final PreferenceRepository _preferenceRepository;
|
final PreferenceRepository _preferenceRepository;
|
||||||
|
final Logger _logger;
|
||||||
|
|
||||||
static const String _authority = 'news.ycombinator.com';
|
static const String _authority = 'news.ycombinator.com';
|
||||||
|
|
||||||
@ -44,6 +48,7 @@ class AuthRepository extends PostableRepository {
|
|||||||
password: password,
|
password: password,
|
||||||
);
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
_logger.e(_);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,167 +0,0 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:hacki/models/models.dart';
|
|
||||||
import 'package:hive/hive.dart';
|
|
||||||
import 'package:http/http.dart';
|
|
||||||
|
|
||||||
/// [CacheRepository] 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({
|
|
||||||
Future<Box<List<int>>>? storyIdBox,
|
|
||||||
Future<Box<Map<dynamic, dynamic>>>? storyBox,
|
|
||||||
Future<Box<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),
|
|
||||||
_commentBox = commentBox ??
|
|
||||||
Hive.openLazyBox<Map<dynamic, dynamic>>(_commentBoxName);
|
|
||||||
|
|
||||||
static const String _storyIdBoxName = 'storyIdBox';
|
|
||||||
static const String _storyBoxName = 'storyBox';
|
|
||||||
static const String _commentBoxName = 'commentBox';
|
|
||||||
static const String _webPageBoxName = 'webPageBox';
|
|
||||||
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;
|
|
||||||
|
|
||||||
Future<bool> get hasCachedStories =>
|
|
||||||
_storyBox.then((Box<Map<dynamic, dynamic>> box) => box.isNotEmpty);
|
|
||||||
|
|
||||||
Future<void> cacheStoryIds({
|
|
||||||
required StoryType of,
|
|
||||||
required List<int> ids,
|
|
||||||
}) async {
|
|
||||||
final Box<List<int>> box = await _storyIdBox;
|
|
||||||
return box.put(of.name, ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> cacheStory({required Story story}) async {
|
|
||||||
final Box<Map<dynamic, dynamic>> 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);
|
|
||||||
return box.put(url, html);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String?> getHtml({required String url}) async {
|
|
||||||
final Box<String> box = await _webPageBox;
|
|
||||||
return box.get(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> hasCachedWebPage({required String url}) async {
|
|
||||||
final Box<String> box = await _webPageBox;
|
|
||||||
return box.containsKey(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
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>[];
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<Story> getCachedStoriesStream({required List<int> ids}) async* {
|
|
||||||
final Box<Map<dynamic, dynamic>> box = await _storyBox;
|
|
||||||
|
|
||||||
for (final int id in ids) {
|
|
||||||
final Map<dynamic, dynamic>? json = box.get(id.toString());
|
|
||||||
|
|
||||||
if (json == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Story story = Story.fromJson(json.cast<String, dynamic>());
|
|
||||||
yield story;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Story?> getCachedStory({required int id}) async {
|
|
||||||
final Box<Map<dynamic, dynamic>> box = await _storyBox;
|
|
||||||
final Map<dynamic, dynamic>? json = box.get(id.toString());
|
|
||||||
if (json == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final Story story = Story.fromJson(json.cast<String, dynamic>());
|
|
||||||
return story;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> cacheComment({required Comment comment}) async {
|
|
||||||
final LazyBox<Map<dynamic, dynamic>> 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) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final Comment comment = Comment.fromJson(json.cast<String, dynamic>());
|
|
||||||
return comment;
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<Comment> getCachedCommentsStream({
|
|
||||||
required List<int> ids,
|
|
||||||
int level = 0,
|
|
||||||
}) async* {
|
|
||||||
final LazyBox<Map<dynamic, dynamic>> box = await _commentBox;
|
|
||||||
|
|
||||||
for (final int id in ids) {
|
|
||||||
final Map<dynamic, dynamic>? json = await box.get(id.toString());
|
|
||||||
|
|
||||||
if (json != null) {
|
|
||||||
final Comment comment =
|
|
||||||
Comment.fromJson(json.cast<String, dynamic>(), level: level);
|
|
||||||
|
|
||||||
yield comment;
|
|
||||||
yield* getCachedCommentsStream(ids: comment.kids, level: level + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> deleteAllStoryIds() async {
|
|
||||||
final Box<List<int>> box = await _storyIdBox;
|
|
||||||
return box.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> deleteAllStories() async {
|
|
||||||
final Box<Map<dynamic, dynamic>> box = await _storyBox;
|
|
||||||
return box.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> deleteAllComments() async {
|
|
||||||
final LazyBox<Map<dynamic, dynamic>> box = await _commentBox;
|
|
||||||
return box.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> deleteAllWebPages() async {
|
|
||||||
final Box<String> box = await _webPageBox;
|
|
||||||
return box.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> deleteAll() async {
|
|
||||||
return deleteAllStoryIds()
|
|
||||||
.whenComplete(deleteAllStories)
|
|
||||||
.whenComplete(deleteAllComments)
|
|
||||||
.whenComplete(deleteAllWebPages);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
return body;
|
|
||||||
} catch (_) {
|
|
||||||
return '''Web page not available.''';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
274
lib/repositories/offline_repository.dart
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
/// [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 OfflineRepository {
|
||||||
|
OfflineRepository({
|
||||||
|
Future<Box<List<int>>>? storyIdBox,
|
||||||
|
Future<Box<Map<dynamic, dynamic>>>? storyBox,
|
||||||
|
Future<LazyBox<String>>? webPageBox,
|
||||||
|
Future<LazyBox<Map<dynamic, dynamic>>>? commentBox,
|
||||||
|
Logger? logger,
|
||||||
|
}) : _storyIdBox = storyIdBox ?? Hive.openBox<List<int>>(_storyIdBoxName),
|
||||||
|
_storyBox =
|
||||||
|
storyBox ?? Hive.openBox<Map<dynamic, dynamic>>(_storyBoxName),
|
||||||
|
_webPageBox = webPageBox ?? Hive.openLazyBox<String>(_webPageBoxName),
|
||||||
|
_commentBox = commentBox ??
|
||||||
|
Hive.openLazyBox<Map<dynamic, dynamic>>(_commentBoxName),
|
||||||
|
_logger = logger ?? locator.get<Logger>();
|
||||||
|
|
||||||
|
static const String _storyIdBoxName = 'storyIdBox';
|
||||||
|
static const String _storyBoxName = 'storyBox';
|
||||||
|
static const String _commentBoxName = 'commentBox';
|
||||||
|
static const String _webPageBoxName = 'webPageBox';
|
||||||
|
final Future<Box<List<int>>> _storyIdBox;
|
||||||
|
final Future<Box<Map<dynamic, dynamic>>> _storyBox;
|
||||||
|
final Future<LazyBox<Map<dynamic, dynamic>>> _commentBox;
|
||||||
|
final Future<LazyBox<String>> _webPageBox;
|
||||||
|
final Logger _logger;
|
||||||
|
|
||||||
|
Future<bool> get hasCachedStories =>
|
||||||
|
_storyBox.then((Box<Map<dynamic, dynamic>> box) => box.isNotEmpty);
|
||||||
|
|
||||||
|
Future<void> cacheStoryIds({
|
||||||
|
required StoryType of,
|
||||||
|
required List<int> ids,
|
||||||
|
}) async {
|
||||||
|
late final Box<List<int>> box;
|
||||||
|
|
||||||
|
try {
|
||||||
|
box = await _storyIdBox;
|
||||||
|
} catch (_) {
|
||||||
|
_logger.e(_);
|
||||||
|
await Hive.deleteBoxFromDisk(_storyIdBoxName);
|
||||||
|
box = await _storyIdBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
return box.put(of.name, ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cacheStory({required Story story}) async {
|
||||||
|
late final Box<Map<dynamic, dynamic>> box;
|
||||||
|
|
||||||
|
try {
|
||||||
|
box = await _storyBox;
|
||||||
|
} catch (_) {
|
||||||
|
_logger.e(_);
|
||||||
|
await Hive.deleteBoxFromDisk(_storyBoxName);
|
||||||
|
box = await _storyBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
return box.put(story.id.toString(), story.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cacheUrl({required String url}) async {
|
||||||
|
late final LazyBox<String> box;
|
||||||
|
|
||||||
|
try {
|
||||||
|
box = await _webPageBox;
|
||||||
|
} catch (_) {
|
||||||
|
_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 {
|
||||||
|
try {
|
||||||
|
final LazyBox<String> box = await _webPageBox;
|
||||||
|
return box.get(url);
|
||||||
|
} catch (_) {
|
||||||
|
_logger.e(_);
|
||||||
|
await Hive.deleteBoxFromDisk(_webPageBoxName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> hasCachedWebPage({required String url}) async {
|
||||||
|
try {
|
||||||
|
final LazyBox<String> box = await _webPageBox;
|
||||||
|
return box.containsKey(url);
|
||||||
|
} catch (_) {
|
||||||
|
_logger.e(_);
|
||||||
|
await Hive.deleteBoxFromDisk(_webPageBoxName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<int>> getCachedStoryIds({required StoryType of}) async {
|
||||||
|
try {
|
||||||
|
final Box<List<int>> box = await _storyIdBox;
|
||||||
|
final List<int>? ids = box.get(of.name);
|
||||||
|
return ids ?? <int>[];
|
||||||
|
} catch (_) {
|
||||||
|
_logger.e(_);
|
||||||
|
await Hive.deleteBoxFromDisk(_storyIdBoxName);
|
||||||
|
return <int>[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<Story> getCachedStoriesStream({required List<int> ids}) async* {
|
||||||
|
late final Box<Map<dynamic, dynamic>> box;
|
||||||
|
|
||||||
|
try {
|
||||||
|
box = await _storyBox;
|
||||||
|
} catch (_) {
|
||||||
|
_logger.e(_);
|
||||||
|
await Hive.deleteBoxFromDisk(_storyBoxName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final int id in ids) {
|
||||||
|
final Map<dynamic, dynamic>? json = box.get(id.toString());
|
||||||
|
|
||||||
|
if (json == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Story story = Story.fromJson(json.cast<String, dynamic>());
|
||||||
|
yield story;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Story?> getCachedStory({required int id}) async {
|
||||||
|
late final Box<Map<dynamic, dynamic>> box;
|
||||||
|
|
||||||
|
try {
|
||||||
|
box = await _storyBox;
|
||||||
|
} catch (_) {
|
||||||
|
_logger.e(_);
|
||||||
|
await Hive.deleteBoxFromDisk(_storyBoxName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<dynamic, dynamic>? json = box.get(id.toString());
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final Story story = Story.fromJson(json.cast<String, dynamic>());
|
||||||
|
return story;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cacheComment({required Comment comment}) async {
|
||||||
|
late final LazyBox<Map<dynamic, dynamic>> box;
|
||||||
|
|
||||||
|
try {
|
||||||
|
box = await _commentBox;
|
||||||
|
} catch (_) {
|
||||||
|
_logger.e(_);
|
||||||
|
await Hive.deleteBoxFromDisk(_commentBoxName);
|
||||||
|
box = await _commentBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
return box.put(comment.id.toString(), comment.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Comment?> getCachedComment({required int id}) async {
|
||||||
|
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 (_) {
|
||||||
|
_logger.e(_);
|
||||||
|
await Hive.deleteBoxFromDisk(_commentBoxName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<Comment> getCachedCommentsStream({
|
||||||
|
required List<int> ids,
|
||||||
|
int level = 0,
|
||||||
|
}) async* {
|
||||||
|
final LazyBox<Map<dynamic, dynamic>> box = await _commentBox;
|
||||||
|
|
||||||
|
for (final int id in ids) {
|
||||||
|
final Map<dynamic, dynamic>? json = await box.get(id.toString());
|
||||||
|
|
||||||
|
if (json != null) {
|
||||||
|
final Comment comment =
|
||||||
|
Comment.fromJson(json.cast<String, dynamic>(), level: level);
|
||||||
|
|
||||||
|
yield comment;
|
||||||
|
yield* getCachedCommentsStream(ids: comment.kids, level: level + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> deleteAllStoryIds() async {
|
||||||
|
try {
|
||||||
|
final Box<List<int>> box = await _storyIdBox;
|
||||||
|
return box.clear();
|
||||||
|
} catch (_) {
|
||||||
|
_logger.e(_);
|
||||||
|
await Hive.deleteBoxFromDisk(_storyIdBoxName);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> deleteAllStories() async {
|
||||||
|
try {
|
||||||
|
final Box<Map<dynamic, dynamic>> box = await _storyBox;
|
||||||
|
return box.clear();
|
||||||
|
} catch (_) {
|
||||||
|
_logger.e(_);
|
||||||
|
await Hive.deleteBoxFromDisk(_storyBoxName);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> deleteAllComments() async {
|
||||||
|
try {
|
||||||
|
final LazyBox<Map<dynamic, dynamic>> box = await _commentBox;
|
||||||
|
return box.clear();
|
||||||
|
} catch (_) {
|
||||||
|
_logger.e(_);
|
||||||
|
await Hive.deleteBoxFromDisk(_commentBoxName);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> deleteAllWebPages() async {
|
||||||
|
try {
|
||||||
|
final LazyBox<String> box = await _webPageBox;
|
||||||
|
return box.clear();
|
||||||
|
} catch (_) {
|
||||||
|
_logger.e(_);
|
||||||
|
await Hive.deleteBoxFromDisk(_webPageBoxName);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> deleteAll() async {
|
||||||
|
return deleteAllStoryIds()
|
||||||
|
.whenComplete(deleteAllStories)
|
||||||
|
.whenComplete(deleteAllComments)
|
||||||
|
.whenComplete(deleteAllWebPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,9 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:hacki/config/locator.dart';
|
||||||
|
import 'package:hacki/cubits/comments/comments_cubit.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:synced_shared_preferences/synced_shared_preferences.dart';
|
import 'package:synced_shared_preferences/synced_shared_preferences.dart';
|
||||||
|
|
||||||
@ -10,9 +13,11 @@ class PreferenceRepository {
|
|||||||
SyncedSharedPreferences? syncedPrefs,
|
SyncedSharedPreferences? syncedPrefs,
|
||||||
Future<SharedPreferences>? prefs,
|
Future<SharedPreferences>? prefs,
|
||||||
FlutterSecureStorage? secureStorage,
|
FlutterSecureStorage? secureStorage,
|
||||||
|
Logger? logger,
|
||||||
}) : _syncedPrefs = syncedPrefs ?? SyncedSharedPreferences.instance,
|
}) : _syncedPrefs = syncedPrefs ?? SyncedSharedPreferences.instance,
|
||||||
_prefs = prefs ?? SharedPreferences.getInstance(),
|
_prefs = prefs ?? SharedPreferences.getInstance(),
|
||||||
_secureStorage = secureStorage ?? const FlutterSecureStorage();
|
_secureStorage = secureStorage ?? const FlutterSecureStorage(),
|
||||||
|
_logger = logger ?? locator.get<Logger>();
|
||||||
|
|
||||||
static const String _usernameKey = 'username';
|
static const String _usernameKey = 'username';
|
||||||
static const String _passwordKey = 'password';
|
static const String _passwordKey = 'password';
|
||||||
@ -38,20 +43,26 @@ class PreferenceRepository {
|
|||||||
static const String _navigationModeKey = 'navigationMode';
|
static const String _navigationModeKey = 'navigationMode';
|
||||||
static const String _eyeCandyModeKey = 'eyeCandyMode';
|
static const String _eyeCandyModeKey = 'eyeCandyMode';
|
||||||
static const String _markReadStoriesModeKey = 'markReadStoriesMode';
|
static const String _markReadStoriesModeKey = 'markReadStoriesMode';
|
||||||
|
static const String _fetchModeKey = 'fetchMode';
|
||||||
|
static const String _commentsOrderKey = 'commentsOrder';
|
||||||
|
|
||||||
static const bool _notificationModeDefaultValue = true;
|
static const bool _notificationModeDefaultValue = true;
|
||||||
static const bool _displayModeDefaultValue = true;
|
static const bool _displayModeDefaultValue = true;
|
||||||
static const bool _navigationModeDefaultValue = true;
|
static const bool _navigationModeDefaultValueIOS = true;
|
||||||
|
static const bool _navigationModeDefaultValueAndroid = false;
|
||||||
static const bool _eyeCandyModeDefaultValue = false;
|
static const bool _eyeCandyModeDefaultValue = false;
|
||||||
static const bool _trueDarkModeDefaultValue = false;
|
static const bool _trueDarkModeDefaultValue = false;
|
||||||
static const bool _readerModeDefaultValue = true;
|
static const bool _readerModeDefaultValue = true;
|
||||||
static const bool _markReadStoriesModeDefaultValue = true;
|
static const bool _markReadStoriesModeDefaultValue = true;
|
||||||
static const bool _isFirstLaunchKeyDefaultValue = true;
|
static const bool _isFirstLaunchKeyDefaultValue = true;
|
||||||
static const bool _metadataModeDefaultValue = true;
|
static const bool _metadataModeDefaultValue = true;
|
||||||
|
static final int _fetchModeDefaultValue = FetchMode.eager.index;
|
||||||
|
static final int _commentsOrderDefaultValue = CommentsOrder.natural.index;
|
||||||
|
|
||||||
final SyncedSharedPreferences _syncedPrefs;
|
final SyncedSharedPreferences _syncedPrefs;
|
||||||
final Future<SharedPreferences> _prefs;
|
final Future<SharedPreferences> _prefs;
|
||||||
final FlutterSecureStorage _secureStorage;
|
final FlutterSecureStorage _secureStorage;
|
||||||
|
final Logger _logger;
|
||||||
|
|
||||||
Future<bool> get loggedIn async => await username != null;
|
Future<bool> get loggedIn async => await username != null;
|
||||||
|
|
||||||
@ -82,7 +93,10 @@ class PreferenceRepository {
|
|||||||
|
|
||||||
Future<bool> get shouldShowWebFirst async => _prefs.then(
|
Future<bool> get shouldShowWebFirst async => _prefs.then(
|
||||||
(SharedPreferences prefs) =>
|
(SharedPreferences prefs) =>
|
||||||
prefs.getBool(_navigationModeKey) ?? _navigationModeDefaultValue,
|
prefs.getBool(_navigationModeKey) ??
|
||||||
|
(Platform.isAndroid
|
||||||
|
? _navigationModeDefaultValueAndroid
|
||||||
|
: _navigationModeDefaultValueIOS),
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<bool> get shouldShowEyeCandy async => _prefs.then(
|
Future<bool> get shouldShowEyeCandy async => _prefs.then(
|
||||||
@ -111,6 +125,17 @@ class PreferenceRepository {
|
|||||||
_markReadStoriesModeDefaultValue,
|
_markReadStoriesModeDefaultValue,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<FetchMode> get fetchMode async => _prefs.then(
|
||||||
|
(SharedPreferences prefs) => FetchMode.values
|
||||||
|
.elementAt(prefs.getInt(_fetchModeKey) ?? _fetchModeDefaultValue),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<CommentsOrder> get commentsOrder async => _prefs.then(
|
||||||
|
(SharedPreferences prefs) => CommentsOrder.values.elementAt(
|
||||||
|
prefs.getInt(_commentsOrderKey) ?? _commentsOrderDefaultValue,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
Future<bool> hasPushed(int commentId) async =>
|
Future<bool> hasPushed(int commentId) async =>
|
||||||
_prefs.then((SharedPreferences prefs) {
|
_prefs.then((SharedPreferences prefs) {
|
||||||
final bool? val = prefs.getBool(_getPushNotificationKey(commentId));
|
final bool? val = prefs.getBool(_getPushNotificationKey(commentId));
|
||||||
@ -153,9 +178,13 @@ class PreferenceRepository {
|
|||||||
aOptions: androidOptions,
|
aOptions: androidOptions,
|
||||||
);
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
await _secureStorage.deleteAll(
|
try {
|
||||||
aOptions: androidOptions,
|
await _secureStorage.deleteAll(
|
||||||
);
|
aOptions: androidOptions,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
_logger.e(_);
|
||||||
|
}
|
||||||
|
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
@ -182,8 +211,10 @@ class PreferenceRepository {
|
|||||||
|
|
||||||
Future<void> toggleNavigationMode() async {
|
Future<void> toggleNavigationMode() async {
|
||||||
final SharedPreferences prefs = await _prefs;
|
final SharedPreferences prefs = await _prefs;
|
||||||
final bool currentMode =
|
final bool currentMode = prefs.getBool(_navigationModeKey) ??
|
||||||
prefs.getBool(_navigationModeKey) ?? _navigationModeDefaultValue;
|
(Platform.isAndroid
|
||||||
|
? _navigationModeDefaultValueAndroid
|
||||||
|
: _navigationModeDefaultValueIOS);
|
||||||
await prefs.setBool(_navigationModeKey, !currentMode);
|
await prefs.setBool(_navigationModeKey, !currentMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,6 +253,18 @@ class PreferenceRepository {
|
|||||||
await prefs.setBool(_metadataModeKey, !currentMode);
|
await prefs.setBool(_metadataModeKey, !currentMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> selectFetchMode(FetchMode fetchMode) async {
|
||||||
|
final SharedPreferences prefs = await _prefs;
|
||||||
|
final int index = fetchMode.index;
|
||||||
|
await prefs.setInt(_fetchModeKey, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> selectCommentsOrder(CommentsOrder order) async {
|
||||||
|
final SharedPreferences prefs = await _prefs;
|
||||||
|
final int index = order.index;
|
||||||
|
await prefs.setInt(_commentsOrderKey, index);
|
||||||
|
}
|
||||||
|
|
||||||
//#region fav
|
//#region fav
|
||||||
|
|
||||||
Future<List<int>> favList({required String of}) async {
|
Future<List<int>> favList({required String of}) async {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export 'auth_repository.dart';
|
export 'auth_repository.dart';
|
||||||
export 'cache_repository.dart';
|
export 'offline_repository.dart';
|
||||||
export 'post_repository.dart';
|
export 'post_repository.dart';
|
||||||
export 'preference_repository.dart';
|
export 'preference_repository.dart';
|
||||||
export 'search_repository.dart';
|
export 'search_repository.dart';
|
||||||
|
@ -51,9 +51,37 @@ class StoriesRepository {
|
|||||||
Stream<Comment> fetchCommentsStream({
|
Stream<Comment> fetchCommentsStream({
|
||||||
required List<int> ids,
|
required List<int> ids,
|
||||||
int level = 0,
|
int level = 0,
|
||||||
|
Comment? Function(int)? getFromCache,
|
||||||
}) async* {
|
}) async* {
|
||||||
for (final int id in ids) {
|
for (final int id in ids) {
|
||||||
final Comment? comment = await _firebaseClient
|
Comment? comment = getFromCache?.call(id)?.copyWith(level: level);
|
||||||
|
|
||||||
|
comment ??= await _firebaseClient
|
||||||
|
.get('${_baseUrl}item/$id.json')
|
||||||
|
.then((dynamic json) => _parseJson(json as Map<String, dynamic>?))
|
||||||
|
.then((Map<String, dynamic>? json) async {
|
||||||
|
if (json == null) return null;
|
||||||
|
|
||||||
|
final Comment comment = Comment.fromJson(json, level: level);
|
||||||
|
return comment;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (comment != null) {
|
||||||
|
yield comment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<Comment> fetchAllCommentsRecursivelyStream({
|
||||||
|
required List<int> ids,
|
||||||
|
int level = 0,
|
||||||
|
Comment? Function(int)? getFromCache,
|
||||||
|
}) async* {
|
||||||
|
for (final int id in ids) {
|
||||||
|
Comment? comment = getFromCache?.call(id)?.copyWith(level: level);
|
||||||
|
|
||||||
|
comment ??= await _firebaseClient
|
||||||
.get('${_baseUrl}item/$id.json')
|
.get('${_baseUrl}item/$id.json')
|
||||||
.then((dynamic json) => _parseJson(json as Map<String, dynamic>?))
|
.then((dynamic json) => _parseJson(json as Map<String, dynamic>?))
|
||||||
.then((Map<String, dynamic>? json) async {
|
.then((Map<String, dynamic>? json) async {
|
||||||
@ -66,9 +94,10 @@ class StoriesRepository {
|
|||||||
if (comment != null) {
|
if (comment != null) {
|
||||||
yield comment;
|
yield comment;
|
||||||
|
|
||||||
yield* fetchCommentsStream(
|
yield* fetchAllCommentsRecursivelyStream(
|
||||||
ids: comment.kids,
|
ids: comment.kids,
|
||||||
level: level + 1,
|
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/services/services.dart';
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
import 'package:hacki/utils/utils.dart';
|
import 'package:hacki/utils/utils.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||||
import 'package:responsive_builder/responsive_builder.dart';
|
import 'package:responsive_builder/responsive_builder.dart';
|
||||||
|
|
||||||
@ -47,7 +48,6 @@ class HomeScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _HomeScreenState extends State<HomeScreen>
|
class _HomeScreenState extends State<HomeScreen>
|
||||||
with SingleTickerProviderStateMixin, RouteAware {
|
with SingleTickerProviderStateMixin, RouteAware {
|
||||||
final CacheService cacheService = locator.get<CacheService>();
|
|
||||||
final Throttle featureDiscoveryDismissThrottle = Throttle(
|
final Throttle featureDiscoveryDismissThrottle = Throttle(
|
||||||
delay: _throttleDelay,
|
delay: _throttleDelay,
|
||||||
);
|
);
|
||||||
@ -66,9 +66,10 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
super.didPopNext();
|
super.didPopNext();
|
||||||
if (context.read<StoriesBloc>().deviceScreenType ==
|
if (context.read<StoriesBloc>().deviceScreenType ==
|
||||||
DeviceScreenType.mobile) {
|
DeviceScreenType.mobile) {
|
||||||
|
locator.get<Logger>().i('resetting comments in CommentCache');
|
||||||
Future<void>.delayed(
|
Future<void>.delayed(
|
||||||
const Duration(milliseconds: 500),
|
const Duration(milliseconds: 500),
|
||||||
cacheService.resetCollapsedComments,
|
locator.get<CommentCache>().resetComments,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,12 +103,14 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
|
|
||||||
SchedulerBinding.instance
|
SchedulerBinding.instance
|
||||||
..addPostFrameCallback((_) {
|
..addPostFrameCallback((_) {
|
||||||
FeatureDiscovery.discoverFeatures(
|
if (!isTesting) {
|
||||||
context,
|
FeatureDiscovery.discoverFeatures(
|
||||||
const <String>{
|
context,
|
||||||
Constants.featureLogIn,
|
const <String>{
|
||||||
},
|
Constants.featureLogIn,
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
..addPostFrameCallback((_) {
|
..addPostFrameCallback((_) {
|
||||||
final ModalRoute<dynamic>? route = ModalRoute.of(context);
|
final ModalRoute<dynamic>? route = ModalRoute.of(context);
|
||||||
@ -449,7 +452,10 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
locator.get<StoriesRepository>().fetchItemBy(id: id).then((Item? item) {
|
locator.get<StoriesRepository>().fetchItemBy(id: id).then((Item? item) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
goToItemScreen(args: ItemScreenArgs(item: item));
|
goToItemScreen(
|
||||||
|
args: ItemScreenArgs(item: item),
|
||||||
|
forceNewScreen: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
// ignore_for_file: comment_references
|
||||||
|
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:feature_discovery/feature_discovery.dart';
|
import 'package:feature_discovery/feature_discovery.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -38,6 +40,7 @@ class ItemScreenArgs extends Equatable {
|
|||||||
const ItemScreenArgs({
|
const ItemScreenArgs({
|
||||||
required this.item,
|
required this.item,
|
||||||
this.onlyShowTargetComment = false,
|
this.onlyShowTargetComment = false,
|
||||||
|
this.useCommentCache = false,
|
||||||
this.targetComments,
|
this.targetComments,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -45,11 +48,17 @@ class ItemScreenArgs extends Equatable {
|
|||||||
final bool onlyShowTargetComment;
|
final bool onlyShowTargetComment;
|
||||||
final List<Comment>? targetComments;
|
final List<Comment>? targetComments;
|
||||||
|
|
||||||
|
/// when a user is trying to view a sub-thread from a main thread, we don't
|
||||||
|
/// need to fetch comments from [StoryRepository] since we have some, if not
|
||||||
|
/// all, comments cached in [CommentCache].
|
||||||
|
final bool useCommentCache;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
item,
|
item,
|
||||||
onlyShowTargetComment,
|
onlyShowTargetComment,
|
||||||
targetComments,
|
targetComments,
|
||||||
|
useCommentCache,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,25 +75,36 @@ class ItemScreen extends StatefulWidget {
|
|||||||
static Route<dynamic> route(ItemScreenArgs args) {
|
static Route<dynamic> route(ItemScreenArgs args) {
|
||||||
return MaterialPageRoute<ItemScreen>(
|
return MaterialPageRoute<ItemScreen>(
|
||||||
settings: const RouteSettings(name: routeName),
|
settings: const RouteSettings(name: routeName),
|
||||||
builder: (BuildContext context) => MultiBlocProvider(
|
builder: (BuildContext context) => RepositoryProvider<CollapseCache>(
|
||||||
providers: <BlocProvider<dynamic>>[
|
create: (BuildContext context) => CollapseCache(),
|
||||||
BlocProvider<CommentsCubit>(
|
lazy: false,
|
||||||
create: (_) => CommentsCubit(
|
child: MultiBlocProvider(
|
||||||
offlineReading: context.read<StoriesBloc>().state.offlineReading,
|
providers: <BlocProvider<dynamic>>[
|
||||||
item: args.item,
|
BlocProvider<CommentsCubit>(
|
||||||
)..init(
|
create: (BuildContext context) => CommentsCubit(
|
||||||
onlyShowTargetComment: args.onlyShowTargetComment,
|
offlineReading:
|
||||||
targetParents: args.targetComments,
|
context.read<StoriesBloc>().state.offlineReading,
|
||||||
),
|
item: args.item,
|
||||||
|
collapseCache: context.read<CollapseCache>(),
|
||||||
|
defaultFetchMode:
|
||||||
|
context.read<PreferenceCubit>().state.fetchMode,
|
||||||
|
defaultCommentsOrder:
|
||||||
|
context.read<PreferenceCubit>().state.order,
|
||||||
|
)..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 +120,37 @@ class ItemScreen extends StatefulWidget {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: MultiBlocProvider(
|
child: RepositoryProvider<CollapseCache>(
|
||||||
key: ValueKey<ItemScreenArgs>(args),
|
create: (BuildContext context) => CollapseCache(),
|
||||||
providers: <BlocProvider<dynamic>>[
|
lazy: false,
|
||||||
BlocProvider<CommentsCubit>(
|
child: MultiBlocProvider(
|
||||||
create: (BuildContext context) => CommentsCubit(
|
key: ValueKey<ItemScreenArgs>(args),
|
||||||
offlineReading: context.read<StoriesBloc>().state.offlineReading,
|
providers: <BlocProvider<dynamic>>[
|
||||||
item: args.item,
|
BlocProvider<CommentsCubit>(
|
||||||
)..init(
|
create: (BuildContext context) => CommentsCubit(
|
||||||
onlyShowTargetComment: args.onlyShowTargetComment,
|
offlineReading:
|
||||||
targetParents: args.targetComments,
|
context.read<StoriesBloc>().state.offlineReading,
|
||||||
),
|
item: args.item,
|
||||||
|
collapseCache: context.read<CollapseCache>(),
|
||||||
|
defaultFetchMode:
|
||||||
|
context.read<PreferenceCubit>().state.fetchMode,
|
||||||
|
defaultCommentsOrder:
|
||||||
|
context.read<PreferenceCubit>().state.order,
|
||||||
|
)..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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -160,14 +190,16 @@ class _ItemScreenState extends State<ItemScreen> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
FeatureDiscovery.discoverFeatures(
|
if (!isTesting) {
|
||||||
context,
|
FeatureDiscovery.discoverFeatures(
|
||||||
const <String>{
|
context,
|
||||||
Constants.featurePinToTop,
|
const <String>{
|
||||||
Constants.featureAddStoryToFavList,
|
Constants.featurePinToTop,
|
||||||
Constants.featureOpenStoryInWebView,
|
Constants.featureAddStoryToFavList,
|
||||||
},
|
Constants.featureOpenStoryInWebView,
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
scrollController.addListener(() {
|
scrollController.addListener(() {
|
||||||
@ -180,7 +212,6 @@ class _ItemScreenState extends State<ItemScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
locator.get<CacheService>().resetComments();
|
|
||||||
refreshController.dispose();
|
refreshController.dispose();
|
||||||
commentEditingController.dispose();
|
commentEditingController.dispose();
|
||||||
scrollController.dispose();
|
scrollController.dispose();
|
||||||
@ -284,309 +315,370 @@ class _ItemScreenState extends State<ItemScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLoading: context.read<CommentsCubit>().loadMore,
|
onLoading: () {
|
||||||
child: ListView(
|
if (state.fetchMode == FetchMode.eager) {
|
||||||
|
context.read<CommentsCubit>().loadMore();
|
||||||
|
} else {
|
||||||
|
refreshController.loadComplete();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: ListView.builder(
|
||||||
primary: false,
|
primary: false,
|
||||||
children: <Widget>[
|
itemCount: state.comments.length + 2,
|
||||||
SizedBox(
|
itemBuilder: (BuildContext context, int index) {
|
||||||
height: topPadding,
|
if (index == 0) {
|
||||||
),
|
return Column(
|
||||||
if (!widget.splitViewEnabled)
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: Dimens.pt6),
|
|
||||||
child: OfflineBanner(),
|
|
||||||
),
|
|
||||||
Slidable(
|
|
||||||
startActionPane: ActionPane(
|
|
||||||
motion: const BehindMotion(),
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
SlidableAction(
|
SizedBox(
|
||||||
onPressed: (_) {
|
height: topPadding,
|
||||||
HapticFeedback.lightImpact();
|
|
||||||
|
|
||||||
if (state.item.id !=
|
|
||||||
context
|
|
||||||
.read<EditCubit>()
|
|
||||||
.state
|
|
||||||
.replyingTo
|
|
||||||
?.id) {
|
|
||||||
commentEditingController.clear();
|
|
||||||
}
|
|
||||||
context
|
|
||||||
.read<EditCubit>()
|
|
||||||
.onReplyTapped(state.item);
|
|
||||||
focusNode.requestFocus();
|
|
||||||
},
|
|
||||||
backgroundColor: Palette.orange,
|
|
||||||
foregroundColor: Palette.white,
|
|
||||||
icon: Icons.message,
|
|
||||||
),
|
),
|
||||||
SlidableAction(
|
if (!widget.splitViewEnabled)
|
||||||
onPressed: (_) => onMoreTapped(state.item),
|
const Padding(
|
||||||
backgroundColor: Palette.orange,
|
padding: EdgeInsets.only(bottom: Dimens.pt6),
|
||||||
foregroundColor: Palette.white,
|
child: OfflineBanner(),
|
||||||
icon: Icons.more_horiz,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: Dimens.pt6,
|
|
||||||
right: Dimens.pt6,
|
|
||||||
),
|
),
|
||||||
child: Row(
|
Slidable(
|
||||||
|
startActionPane: ActionPane(
|
||||||
|
motion: const BehindMotion(),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
SlidableAction(
|
||||||
state.item.by,
|
onPressed: (_) {
|
||||||
style: const TextStyle(
|
HapticFeedback.lightImpact();
|
||||||
color: Palette.orange,
|
|
||||||
),
|
if (state.item.id !=
|
||||||
|
context
|
||||||
|
.read<EditCubit>()
|
||||||
|
.state
|
||||||
|
.replyingTo
|
||||||
|
?.id) {
|
||||||
|
commentEditingController.clear();
|
||||||
|
}
|
||||||
|
context
|
||||||
|
.read<EditCubit>()
|
||||||
|
.onReplyTapped(state.item);
|
||||||
|
focusNode.requestFocus();
|
||||||
|
},
|
||||||
|
backgroundColor: Palette.orange,
|
||||||
|
foregroundColor: Palette.white,
|
||||||
|
icon: Icons.message,
|
||||||
),
|
),
|
||||||
const Spacer(),
|
SlidableAction(
|
||||||
Text(
|
onPressed: (BuildContext context) =>
|
||||||
state.item.postedDate,
|
onMoreTapped(state.item, context.rect),
|
||||||
style: const TextStyle(
|
backgroundColor: Palette.orange,
|
||||||
color: Palette.grey,
|
foregroundColor: Palette.white,
|
||||||
),
|
icon: Icons.more_horiz,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
child: Column(
|
||||||
if (state.item is Story)
|
children: <Widget>[
|
||||||
InkWell(
|
Padding(
|
||||||
onTap: () => LinkUtil.launch(
|
padding: const EdgeInsets.only(
|
||||||
state.item.url,
|
left: Dimens.pt6,
|
||||||
useReader: context
|
right: Dimens.pt6,
|
||||||
.read<PreferenceCubit>()
|
),
|
||||||
.state
|
child: Row(
|
||||||
.useReader,
|
children: <Widget>[
|
||||||
offlineReading: context
|
Text(
|
||||||
.read<StoriesBloc>()
|
state.item.by,
|
||||||
.state
|
style: const TextStyle(
|
||||||
.offlineReading,
|
color: Palette.orange,
|
||||||
),
|
),
|
||||||
child: Padding(
|
),
|
||||||
padding: const EdgeInsets.only(
|
const Spacer(),
|
||||||
left: Dimens.pt6,
|
Text(
|
||||||
right: Dimens.pt6,
|
state.item.postedDate,
|
||||||
bottom: Dimens.pt12,
|
style: const TextStyle(
|
||||||
top: Dimens.pt12,
|
color: Palette.grey,
|
||||||
),
|
),
|
||||||
child: Text(
|
),
|
||||||
state.item.title,
|
],
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: state.item.url.isNotEmpty
|
|
||||||
? Palette.orange
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (state.item is Story)
|
||||||
)
|
InkWell(
|
||||||
else
|
onTap: () => LinkUtil.launch(
|
||||||
const SizedBox(
|
state.item.url,
|
||||||
height: Dimens.pt6,
|
useReader: context
|
||||||
|
.read<PreferenceCubit>()
|
||||||
|
.state
|
||||||
|
.useReader,
|
||||||
|
offlineReading: context
|
||||||
|
.read<StoriesBloc>()
|
||||||
|
.state
|
||||||
|
.offlineReading,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: Dimens.pt6,
|
||||||
|
right: Dimens.pt6,
|
||||||
|
bottom: Dimens.pt12,
|
||||||
|
top: Dimens.pt12,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
state.item.title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: state.item.url.isNotEmpty
|
||||||
|
? Palette.orange
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SizedBox(
|
||||||
|
height: Dimens.pt6,
|
||||||
|
),
|
||||||
|
if (state.item.text.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Dimens.pt10,
|
||||||
|
),
|
||||||
|
child: SelectableLinkify(
|
||||||
|
text: state.item.text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: MediaQuery.of(context)
|
||||||
|
.textScaleFactor *
|
||||||
|
TextDimens.pt15,
|
||||||
|
),
|
||||||
|
linkStyle: TextStyle(
|
||||||
|
fontSize: MediaQuery.of(context)
|
||||||
|
.textScaleFactor *
|
||||||
|
TextDimens.pt15,
|
||||||
|
color: Palette.orange,
|
||||||
|
),
|
||||||
|
onOpen: (LinkableElement link) {
|
||||||
|
if (link.url.contains(
|
||||||
|
'news.ycombinator.com/item',
|
||||||
|
)) {
|
||||||
|
onStoryLinkTapped(link.url);
|
||||||
|
} else {
|
||||||
|
LinkUtil.launch(link.url);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (state.item.isPoll)
|
||||||
|
BlocProvider<PollCubit>(
|
||||||
|
create: (BuildContext context) =>
|
||||||
|
PollCubit(story: state.item as Story)
|
||||||
|
..init(),
|
||||||
|
child: PollView(
|
||||||
|
onLoginTapped: onLoginTapped,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
if (state.item.text.isNotEmpty)
|
if (state.item.text.isNotEmpty)
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: Dimens.pt10,
|
|
||||||
),
|
|
||||||
child: SelectableLinkify(
|
|
||||||
text: state.item.text,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize:
|
|
||||||
MediaQuery.of(context).textScaleFactor *
|
|
||||||
TextDimens.pt15,
|
|
||||||
),
|
|
||||||
linkStyle: TextStyle(
|
|
||||||
fontSize:
|
|
||||||
MediaQuery.of(context).textScaleFactor *
|
|
||||||
TextDimens.pt15,
|
|
||||||
color: Palette.orange,
|
|
||||||
),
|
|
||||||
onOpen: (LinkableElement link) {
|
|
||||||
if (link.url.contains(
|
|
||||||
'news.ycombinator.com/item',
|
|
||||||
)) {
|
|
||||||
onStoryLinkTapped(link.url);
|
|
||||||
} else {
|
|
||||||
LinkUtil.launch(link.url);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (state.item.isPoll)
|
|
||||||
BlocProvider<PollCubit>(
|
|
||||||
create: (BuildContext context) =>
|
|
||||||
PollCubit(story: state.item as Story)..init(),
|
|
||||||
child: PollView(
|
|
||||||
onLoginTapped: onLoginTapped,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (state.item.text.isNotEmpty)
|
|
||||||
const SizedBox(
|
|
||||||
height: Dimens.pt8,
|
|
||||||
),
|
|
||||||
const Divider(
|
|
||||||
height: Dimens.zero,
|
|
||||||
),
|
|
||||||
if (state.onlyShowTargetComment) ...<Widget>[
|
|
||||||
Center(
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () => context
|
|
||||||
.read<CommentsCubit>()
|
|
||||||
.loadAll(state.item as Story),
|
|
||||||
child: const Text('View all comments'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(
|
|
||||||
height: Dimens.zero,
|
|
||||||
),
|
|
||||||
] else ...<Widget>[
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
if (state.item is Story) ...<Widget>[
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: Dimens.pt12,
|
height: Dimens.pt8,
|
||||||
),
|
),
|
||||||
Text(
|
const Divider(
|
||||||
'''${state.item.score} karma, ${state.item.descendants} comment${state.item.descendants > 1 ? 's' : ''}''',
|
height: Dimens.zero,
|
||||||
|
),
|
||||||
|
if (state.onlyShowTargetComment) ...<Widget>[
|
||||||
|
Center(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => context
|
||||||
|
.read<CommentsCubit>()
|
||||||
|
.loadAll(state.item as Story),
|
||||||
|
child: const Text('View all comments'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(
|
||||||
|
height: Dimens.zero,
|
||||||
),
|
),
|
||||||
] else ...<Widget>[
|
] else ...<Widget>[
|
||||||
const SizedBox(
|
Row(
|
||||||
width: Dimens.pt4,
|
children: <Widget>[
|
||||||
),
|
if (state.item is Story) ...<Widget>[
|
||||||
TextButton(
|
const SizedBox(
|
||||||
onPressed: context
|
width: Dimens.pt12,
|
||||||
.read<CommentsCubit>()
|
),
|
||||||
.loadParentThread,
|
Text(
|
||||||
child: state.fetchParentStatus ==
|
'''${state.item.score} karma, ${state.item.descendants} comment${state.item.descendants > 1 ? 's' : ''}''',
|
||||||
CommentsStatus.loading
|
style: const TextStyle(
|
||||||
? const SizedBox(
|
fontSize: TextDimens.pt12,
|
||||||
height: Dimens.pt12,
|
),
|
||||||
width: Dimens.pt12,
|
),
|
||||||
child: CustomCircularProgressIndicator(
|
] else ...<Widget>[
|
||||||
strokeWidth: Dimens.pt2,
|
const SizedBox(
|
||||||
|
width: Dimens.pt4,
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: context
|
||||||
|
.read<CommentsCubit>()
|
||||||
|
.loadParentThread,
|
||||||
|
child: state.fetchParentStatus ==
|
||||||
|
CommentsStatus.loading
|
||||||
|
? const SizedBox(
|
||||||
|
height: Dimens.pt12,
|
||||||
|
width: Dimens.pt12,
|
||||||
|
child:
|
||||||
|
CustomCircularProgressIndicator(
|
||||||
|
strokeWidth: Dimens.pt2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Text(
|
||||||
|
'View parent thread',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const Spacer(),
|
||||||
|
if (!state.offlineReading)
|
||||||
|
DropdownButton<FetchMode>(
|
||||||
|
value: state.fetchMode,
|
||||||
|
underline: const SizedBox.shrink(),
|
||||||
|
items: const <DropdownMenuItem<FetchMode>>[
|
||||||
|
DropdownMenuItem<FetchMode>(
|
||||||
|
value: FetchMode.lazy,
|
||||||
|
child: Text(
|
||||||
|
'Lazy',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt12,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
DropdownMenuItem<FetchMode>(
|
||||||
: const Text('View parent thread'),
|
value: FetchMode.eager,
|
||||||
|
child: Text(
|
||||||
|
'Eager',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: context
|
||||||
|
.read<CommentsCubit>()
|
||||||
|
.onFetchModeChanged,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt6,
|
||||||
|
),
|
||||||
|
DropdownButton<CommentsOrder>(
|
||||||
|
value: state.order,
|
||||||
|
underline: const SizedBox.shrink(),
|
||||||
|
items: const <
|
||||||
|
DropdownMenuItem<CommentsOrder>>[
|
||||||
|
DropdownMenuItem<CommentsOrder>(
|
||||||
|
value: CommentsOrder.natural,
|
||||||
|
child: Text(
|
||||||
|
'Natural',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<CommentsOrder>(
|
||||||
|
value: CommentsOrder.newestFirst,
|
||||||
|
child: Text(
|
||||||
|
'Newest first',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<CommentsOrder>(
|
||||||
|
value: CommentsOrder.oldestFirst,
|
||||||
|
child: Text(
|
||||||
|
'Oldest first',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: context
|
||||||
|
.read<CommentsCubit>()
|
||||||
|
.onOrderChanged,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt4,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(
|
||||||
|
height: Dimens.zero,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const Spacer(),
|
if (state.comments.isEmpty &&
|
||||||
DropdownButton<CommentsOrder>(
|
state.status ==
|
||||||
value: state.order,
|
CommentsStatus.allLoaded) ...<Widget>[
|
||||||
underline: const SizedBox.shrink(),
|
const SizedBox(
|
||||||
items: const <DropdownMenuItem<CommentsOrder>>[
|
height: 240,
|
||||||
DropdownMenuItem<CommentsOrder>(
|
),
|
||||||
value: CommentsOrder.natural,
|
const Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Natural',
|
'Nothing yet',
|
||||||
style: TextStyle(
|
style: TextStyle(color: Palette.grey),
|
||||||
fontSize: TextDimens.pt14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
DropdownMenuItem<CommentsOrder>(
|
),
|
||||||
value: CommentsOrder.newestFirst,
|
],
|
||||||
child: Text(
|
|
||||||
'Newest first',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: TextDimens.pt14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DropdownMenuItem<CommentsOrder>(
|
|
||||||
value: CommentsOrder.oldestFirst,
|
|
||||||
child: Text(
|
|
||||||
'Oldest first',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: TextDimens.pt14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onChanged:
|
|
||||||
context.read<CommentsCubit>().onOrderChanged,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: Dimens.pt4,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
const Divider(
|
} else if (index == state.comments.length + 1) {
|
||||||
height: Dimens.zero,
|
if ((state.status == CommentsStatus.allLoaded &&
|
||||||
),
|
state.comments.isNotEmpty) ||
|
||||||
],
|
state.onlyShowTargetComment) {
|
||||||
if (state.comments.isEmpty &&
|
return SizedBox(
|
||||||
state.status == CommentsStatus.allLoaded) ...<Widget>[
|
height: 240,
|
||||||
const SizedBox(
|
child: Center(
|
||||||
height: 240,
|
child: Text(happyFace),
|
||||||
),
|
),
|
||||||
const Center(
|
);
|
||||||
child: Text(
|
} else {
|
||||||
'Nothing yet',
|
return const SizedBox.shrink();
|
||||||
style: TextStyle(color: Palette.grey),
|
}
|
||||||
),
|
}
|
||||||
),
|
|
||||||
],
|
|
||||||
for (final Comment comment in state.comments)
|
|
||||||
FadeIn(
|
|
||||||
key: ValueKey<String>('${comment.id}-FadeIn'),
|
|
||||||
child: CommentTile(
|
|
||||||
comment: comment,
|
|
||||||
level: comment.level,
|
|
||||||
myUsername:
|
|
||||||
authState.isLoggedIn ? authState.username : null,
|
|
||||||
opUsername: state.item.by,
|
|
||||||
onReplyTapped: (Comment cmt) {
|
|
||||||
HapticFeedback.lightImpact();
|
|
||||||
if (cmt.deleted || cmt.dead) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmt.id !=
|
index = index - 1;
|
||||||
context
|
final Comment comment = state.comments.elementAt(index);
|
||||||
.read<EditCubit>()
|
return FadeIn(
|
||||||
.state
|
key: ValueKey<String>('${comment.id}-FadeIn'),
|
||||||
.replyingTo
|
child: CommentTile(
|
||||||
?.id) {
|
comment: comment,
|
||||||
commentEditingController.clear();
|
level: comment.level,
|
||||||
}
|
myUsername:
|
||||||
|
authState.isLoggedIn ? authState.username : null,
|
||||||
|
opUsername: state.item.by,
|
||||||
|
fetchMode: state.fetchMode,
|
||||||
|
onReplyTapped: (Comment cmt) {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
if (cmt.deleted || cmt.dead) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
context.read<EditCubit>().onReplyTapped(cmt);
|
if (cmt.id !=
|
||||||
focusNode.requestFocus();
|
context.read<EditCubit>().state.replyingTo?.id) {
|
||||||
},
|
|
||||||
onEditTapped: (Comment cmt) {
|
|
||||||
HapticFeedback.lightImpact();
|
|
||||||
if (cmt.deleted || cmt.dead) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
commentEditingController.clear();
|
commentEditingController.clear();
|
||||||
context.read<EditCubit>().onEditTapped(cmt);
|
}
|
||||||
focusNode.requestFocus();
|
|
||||||
},
|
context.read<EditCubit>().onReplyTapped(cmt);
|
||||||
onMoreTapped: onMoreTapped,
|
focusNode.requestFocus();
|
||||||
onStoryLinkTapped: onStoryLinkTapped,
|
},
|
||||||
onRightMoreTapped: onRightMoreTapped,
|
onEditTapped: (Comment cmt) {
|
||||||
),
|
HapticFeedback.lightImpact();
|
||||||
|
if (cmt.deleted || cmt.dead) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
commentEditingController.clear();
|
||||||
|
context.read<EditCubit>().onEditTapped(cmt);
|
||||||
|
focusNode.requestFocus();
|
||||||
|
},
|
||||||
|
onMoreTapped: onMoreTapped,
|
||||||
|
onStoryLinkTapped: onStoryLinkTapped,
|
||||||
|
onRightMoreTapped: onRightMoreTapped,
|
||||||
),
|
),
|
||||||
if ((state.status == CommentsStatus.allLoaded &&
|
);
|
||||||
state.comments.isNotEmpty) ||
|
},
|
||||||
state.onlyShowTargetComment)
|
|
||||||
SizedBox(
|
|
||||||
height: 240,
|
|
||||||
child: Center(
|
|
||||||
child: Text(happyFace),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -742,7 +834,10 @@ class _ItemScreenState extends State<ItemScreen> {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
goToItemScreen(
|
goToItemScreen(
|
||||||
args: ItemScreenArgs(item: comment),
|
args: ItemScreenArgs(
|
||||||
|
item: comment,
|
||||||
|
useCommentCache: true,
|
||||||
|
),
|
||||||
forceNewScreen: true,
|
forceNewScreen: true,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -814,6 +909,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
|||||||
context.read<AuthBloc>().state.username,
|
context.read<AuthBloc>().state.username,
|
||||||
onStoryLinkTapped: onStoryLinkTapped,
|
onStoryLinkTapped: onStoryLinkTapped,
|
||||||
actionable: false,
|
actionable: false,
|
||||||
|
fetchMode: FetchMode.eager,
|
||||||
),
|
),
|
||||||
const Divider(
|
const Divider(
|
||||||
height: Dimens.zero,
|
height: Dimens.zero,
|
||||||
@ -855,7 +951,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onMoreTapped(Item item) {
|
void onMoreTapped(Item item, Rect? rect) {
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
|
|
||||||
if (item.dead || item.deleted) {
|
if (item.dead || item.deleted) {
|
||||||
@ -1064,7 +1160,7 @@ class _ItemScreenState extends State<ItemScreen> {
|
|||||||
case _MenuAction.downvote:
|
case _MenuAction.downvote:
|
||||||
break;
|
break;
|
||||||
case _MenuAction.share:
|
case _MenuAction.share:
|
||||||
onShareTapped(item);
|
onShareTapped(item, rect);
|
||||||
break;
|
break;
|
||||||
case _MenuAction.flag:
|
case _MenuAction.flag:
|
||||||
onFlagTapped(item);
|
onFlagTapped(item);
|
||||||
@ -1079,8 +1175,12 @@ class _ItemScreenState extends State<ItemScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void onShareTapped(Item item) =>
|
void onShareTapped(Item item, Rect? rect) {
|
||||||
Share.share('https://news.ycombinator.com/item?id=${item.id}');
|
Share.share(
|
||||||
|
'https://news.ycombinator.com/item?id=${item.id}',
|
||||||
|
sharePositionOrigin: rect,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void onFlagTapped(Item item) {
|
void onFlagTapped(Item item) {
|
||||||
showDialog<bool>(
|
showDialog<bool>(
|
||||||
|
@ -283,6 +283,122 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
},
|
},
|
||||||
activeColor: Palette.orange,
|
activeColor: Palette.orange,
|
||||||
),
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: Dimens.pt8,
|
||||||
|
),
|
||||||
|
Flex(
|
||||||
|
direction: Axis.horizontal,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Flexible(
|
||||||
|
child: Row(
|
||||||
|
children: const <Widget>[
|
||||||
|
SizedBox(
|
||||||
|
width: Dimens.pt16,
|
||||||
|
),
|
||||||
|
Text('Default fetch mode'),
|
||||||
|
Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Row(
|
||||||
|
children: const <Widget>[
|
||||||
|
Text('Default comments order'),
|
||||||
|
Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Flex(
|
||||||
|
direction: Axis.horizontal,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Flexible(
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt16,
|
||||||
|
),
|
||||||
|
DropdownButton<FetchMode>(
|
||||||
|
value: preferenceState.fetchMode,
|
||||||
|
underline: const SizedBox.shrink(),
|
||||||
|
items: const <
|
||||||
|
DropdownMenuItem<FetchMode>>[
|
||||||
|
DropdownMenuItem<FetchMode>(
|
||||||
|
value: FetchMode.lazy,
|
||||||
|
child: Text(
|
||||||
|
'Lazy',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<FetchMode>(
|
||||||
|
value: FetchMode.eager,
|
||||||
|
child: Text(
|
||||||
|
'Eager',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: context
|
||||||
|
.read<PreferenceCubit>()
|
||||||
|
.selectFetchMode,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
DropdownButton<CommentsOrder>(
|
||||||
|
value: preferenceState.order,
|
||||||
|
underline: const SizedBox.shrink(),
|
||||||
|
items: const <
|
||||||
|
DropdownMenuItem<CommentsOrder>>[
|
||||||
|
DropdownMenuItem<CommentsOrder>(
|
||||||
|
value: CommentsOrder.natural,
|
||||||
|
child: Text(
|
||||||
|
'Natural',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<CommentsOrder>(
|
||||||
|
value: CommentsOrder.newestFirst,
|
||||||
|
child: Text(
|
||||||
|
'Newest first',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<CommentsOrder>(
|
||||||
|
value: CommentsOrder.oldestFirst,
|
||||||
|
child: Text(
|
||||||
|
'Oldest first',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: context
|
||||||
|
.read<PreferenceCubit>()
|
||||||
|
.selectCommentsOrder,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: const Text('Complex Story Tile'),
|
title: const Text('Complex Story Tile'),
|
||||||
subtitle: const Text(
|
subtitle: const Text(
|
||||||
@ -410,7 +526,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
showAboutDialog(
|
showAboutDialog(
|
||||||
context: context,
|
context: context,
|
||||||
applicationName: 'Hacki',
|
applicationName: 'Hacki',
|
||||||
applicationVersion: 'v0.2.23',
|
applicationVersion: 'v0.2.28',
|
||||||
applicationIcon: ClipRRect(
|
applicationIcon: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(
|
borderRadius: const BorderRadius.all(
|
||||||
Radius.circular(
|
Radius.circular(
|
||||||
@ -680,7 +796,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
.get<SembastRepository>()
|
.get<SembastRepository>()
|
||||||
.deleteAllCachedComments()
|
.deleteAllCachedComments()
|
||||||
.whenComplete(
|
.whenComplete(
|
||||||
locator.get<CacheRepository>().deleteAll,
|
locator.get<OfflineRepository>().deleteAll,
|
||||||
)
|
)
|
||||||
.whenComplete(
|
.whenComplete(
|
||||||
locator.get<PreferenceRepository>().clearAllReadStories,
|
locator.get<PreferenceRepository>().clearAllReadStories,
|
||||||
|
@ -34,7 +34,7 @@ class _WebViewScreenState extends State<WebViewScreen> {
|
|||||||
),
|
),
|
||||||
body: WebView(
|
body: WebView(
|
||||||
onWebViewCreated: (WebViewController controller) async {
|
onWebViewCreated: (WebViewController controller) async {
|
||||||
final String? html = await locator.get<CacheRepository>().getHtml(
|
final String? html = await locator.get<OfflineRepository>().getHtml(
|
||||||
url: widget.url,
|
url: widget.url,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import 'package:hacki/blocs/blocs.dart';
|
|||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/extensions/extensions.dart';
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
|
import 'package:hacki/services/services.dart';
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
import 'package:hacki/utils/utils.dart';
|
import 'package:hacki/utils/utils.dart';
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
required this.myUsername,
|
required this.myUsername,
|
||||||
required this.comment,
|
required this.comment,
|
||||||
required this.onStoryLinkTapped,
|
required this.onStoryLinkTapped,
|
||||||
|
required this.fetchMode,
|
||||||
this.onReplyTapped,
|
this.onReplyTapped,
|
||||||
this.onMoreTapped,
|
this.onMoreTapped,
|
||||||
this.onEditTapped,
|
this.onEditTapped,
|
||||||
@ -31,10 +33,11 @@ class CommentTile extends StatelessWidget {
|
|||||||
final int level;
|
final int level;
|
||||||
final bool actionable;
|
final bool actionable;
|
||||||
final Function(Comment)? onReplyTapped;
|
final Function(Comment)? onReplyTapped;
|
||||||
final Function(Comment)? onMoreTapped;
|
final Function(Comment, Rect?)? onMoreTapped;
|
||||||
final Function(Comment)? onEditTapped;
|
final Function(Comment)? onEditTapped;
|
||||||
final Function(Comment)? onRightMoreTapped;
|
final Function(Comment)? onRightMoreTapped;
|
||||||
final Function(String) onStoryLinkTapped;
|
final Function(String) onStoryLinkTapped;
|
||||||
|
final FetchMode fetchMode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -43,6 +46,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
lazy: false,
|
lazy: false,
|
||||||
create: (_) => CollapseCubit(
|
create: (_) => CollapseCubit(
|
||||||
commentId: comment.id,
|
commentId: comment.id,
|
||||||
|
collapseCache: context.tryRead<CollapseCache>() ?? CollapseCache(),
|
||||||
)..init(),
|
)..init(),
|
||||||
child: BlocBuilder<CollapseCubit, CollapseState>(
|
child: BlocBuilder<CollapseCubit, CollapseState>(
|
||||||
builder: (BuildContext context, CollapseState state) {
|
builder: (BuildContext context, CollapseState state) {
|
||||||
@ -86,8 +90,11 @@ class CommentTile extends StatelessWidget {
|
|||||||
icon: Icons.edit,
|
icon: Icons.edit,
|
||||||
),
|
),
|
||||||
SlidableAction(
|
SlidableAction(
|
||||||
onPressed: (_) =>
|
onPressed: (BuildContext context) =>
|
||||||
onMoreTapped?.call(comment),
|
onMoreTapped?.call(
|
||||||
|
comment,
|
||||||
|
context.rect,
|
||||||
|
),
|
||||||
backgroundColor: Palette.orange,
|
backgroundColor: Palette.orange,
|
||||||
foregroundColor: Palette.white,
|
foregroundColor: Palette.white,
|
||||||
icon: Icons.more_horiz,
|
icon: Icons.more_horiz,
|
||||||
@ -275,6 +282,30 @@ class CommentTile extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (!state.collapsed &&
|
||||||
|
fetchMode == FetchMode.lazy &&
|
||||||
|
comment.kids.isNotEmpty &&
|
||||||
|
!context
|
||||||
|
.read<CommentsCubit>()
|
||||||
|
.state
|
||||||
|
.commentIds
|
||||||
|
.contains(comment.kids.first))
|
||||||
|
Center(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
context
|
||||||
|
.read<CommentsCubit>()
|
||||||
|
.loadMore(comment: comment);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'''Load ${comment.kids.length} ${comment.kids.length > 1 ? 'replies' : 'reply'}''',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: TextDimens.pt12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
const Divider(
|
const Divider(
|
||||||
height: Dimens.zero,
|
height: Dimens.zero,
|
||||||
),
|
),
|
||||||
@ -296,7 +327,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
: Palette.transparent;
|
: Palette.transparent;
|
||||||
final bool isMyComment = myUsername == comment.by;
|
final bool isMyComment = myUsername == comment.by;
|
||||||
|
|
||||||
Widget? wrapper = child;
|
Widget wrapper = child;
|
||||||
|
|
||||||
if (isMyComment && level == 0) {
|
if (isMyComment && level == 0) {
|
||||||
return Container(
|
return Container(
|
||||||
@ -328,7 +359,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return wrapper!;
|
return wrapper;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -338,7 +369,12 @@ class CommentTile extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static final Map<int, Color> _colors = <int, Color>{};
|
||||||
|
|
||||||
Color _getColor(int level) {
|
Color _getColor(int level) {
|
||||||
|
final int initialLevel = level;
|
||||||
|
if (_colors[initialLevel] != null) return _colors[initialLevel]!;
|
||||||
|
|
||||||
while (level >= 10) {
|
while (level >= 10) {
|
||||||
level = level - 10;
|
level = level - 10;
|
||||||
}
|
}
|
||||||
@ -359,6 +395,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_colors[initialLevel] = color;
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hacki/config/constants.dart';
|
import 'package:hacki/config/constants.dart';
|
||||||
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/screens/widgets/link_preview/link_view.dart';
|
import 'package:hacki/screens/widgets/link_preview/link_view.dart';
|
||||||
import 'package:hacki/screens/widgets/link_preview/web_analyzer.dart';
|
import 'package:hacki/screens/widgets/link_preview/web_analyzer.dart';
|
||||||
@ -199,23 +200,9 @@ class _LinkPreviewState extends State<LinkPreview> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const double screenWidthLowerBound = 428,
|
|
||||||
screenWidthUpperBound = 850,
|
|
||||||
picHeightLowerBound = 118,
|
|
||||||
picHeightUpperBound = 140,
|
|
||||||
smallPicHeight = 100,
|
|
||||||
picHeightFactor = 0.14;
|
|
||||||
final double screenWidth = MediaQuery.of(context).size.width;
|
|
||||||
final bool showSmallerPreviewPic = screenWidth > screenWidthLowerBound &&
|
|
||||||
screenWidth < screenWidthUpperBound;
|
|
||||||
final double height = showSmallerPreviewPic
|
|
||||||
? smallPicHeight
|
|
||||||
: (MediaQuery.of(context).size.height * picHeightFactor)
|
|
||||||
.clamp(picHeightLowerBound, picHeightUpperBound);
|
|
||||||
|
|
||||||
final Widget loadingWidget = widget.placeholderWidget ??
|
final Widget loadingWidget = widget.placeholderWidget ??
|
||||||
Container(
|
Container(
|
||||||
height: height,
|
height: context.storyTileHeight,
|
||||||
width: MediaQuery.of(context).size.width,
|
width: MediaQuery.of(context).size.width,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
@ -232,13 +219,13 @@ class _LinkPreviewState extends State<LinkPreview> {
|
|||||||
final WebInfo? info = _info as WebInfo?;
|
final WebInfo? info = _info as WebInfo?;
|
||||||
loadedWidget = _info == null
|
loadedWidget = _info == null
|
||||||
? _buildLinkContainer(
|
? _buildLinkContainer(
|
||||||
height,
|
context.storyTileHeight,
|
||||||
title: _errorTitle,
|
title: _errorTitle,
|
||||||
desc: _errorBody,
|
desc: _errorBody,
|
||||||
imageUri: null,
|
imageUri: null,
|
||||||
)
|
)
|
||||||
: _buildLinkContainer(
|
: _buildLinkContainer(
|
||||||
height,
|
context.storyTileHeight,
|
||||||
title: _errorTitle,
|
title: _errorTitle,
|
||||||
desc: WebAnalyzer.isNotEmpty(info!.description)
|
desc: WebAnalyzer.isNotEmpty(info!.description)
|
||||||
? info.description
|
? info.description
|
||||||
|
@ -147,7 +147,7 @@ class LinkView extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildTitleContainer(TextStyle _titleTS, int _maxLines) {
|
Widget _buildTitleContainer(TextStyle _titleTS, int _maxLines) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(4, 2, 3, 1),
|
padding: const EdgeInsets.fromLTRB(4, 2, 3, 0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
@ -168,7 +168,7 @@ class LinkView extends StatelessWidget {
|
|||||||
return Expanded(
|
return Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(5, 3, 5, 0),
|
padding: const EdgeInsets.fromLTRB(5, 2, 5, 0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
if (showMetadata)
|
if (showMetadata)
|
||||||
|
@ -140,7 +140,7 @@ class WebAnalyzer {
|
|||||||
|
|
||||||
while (comment == null && index < story.kids.length) {
|
while (comment == null && index < story.kids.length) {
|
||||||
comment = await locator
|
comment = await locator
|
||||||
.get<CacheRepository>()
|
.get<OfflineRepository>()
|
||||||
.getCachedComment(id: story.kids.elementAt(index));
|
.getCachedComment(id: story.kids.elementAt(index));
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter_fadein/flutter_fadein.dart';
|
import 'package:flutter_fadein/flutter_fadein.dart';
|
||||||
import 'package:hacki/blocs/blocs.dart';
|
import 'package:hacki/blocs/blocs.dart';
|
||||||
import 'package:hacki/config/constants.dart';
|
import 'package:hacki/config/constants.dart';
|
||||||
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
@ -29,20 +30,7 @@ class StoryTile extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (showWebPreview) {
|
if (showWebPreview) {
|
||||||
const double screenWidthLowerBound = 428,
|
final double height = context.storyTileHeight;
|
||||||
screenWidthUpperBound = 850,
|
|
||||||
picHeightLowerBound = 118,
|
|
||||||
picHeightUpperBound = 140,
|
|
||||||
smallPicHeight = 100,
|
|
||||||
picHeightFactor = 0.14;
|
|
||||||
final double screenWidth = MediaQuery.of(context).size.width;
|
|
||||||
final bool showSmallerPreviewPic = screenWidth > screenWidthLowerBound &&
|
|
||||||
screenWidth < screenWidthUpperBound;
|
|
||||||
final double height = showSmallerPreviewPic
|
|
||||||
? smallPicHeight
|
|
||||||
: (MediaQuery.of(context).size.height * picHeightFactor)
|
|
||||||
.clamp(picHeightLowerBound, picHeightUpperBound);
|
|
||||||
|
|
||||||
return TapDownWrapper(
|
return TapDownWrapper(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -143,7 +131,7 @@ class StoryTile extends StatelessWidget {
|
|||||||
backgroundColor: Palette.transparent,
|
backgroundColor: Palette.transparent,
|
||||||
borderRadius: Dimens.zero,
|
borderRadius: Dimens.zero,
|
||||||
removeElevation: true,
|
removeElevation: true,
|
||||||
bodyMaxLines: height == smallPicHeight ? 3 : 4,
|
bodyMaxLines: context.storyTileMaxLines,
|
||||||
errorTitle: story.title,
|
errorTitle: story.title,
|
||||||
titleStyle: TextStyle(
|
titleStyle: TextStyle(
|
||||||
color: hasRead
|
color: hasRead
|
||||||
|
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';
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
class CacheService {
|
class CollapseCache {
|
||||||
static final Map<int, Comment> _comments = <int, Comment>{};
|
final Map<int, Set<int>> _kids = <int, Set<int>>{};
|
||||||
static final Map<int, String> _drafts = <int, String>{};
|
final Set<int> _collapsed = <int>{};
|
||||||
static final Map<int, Set<int>> _kids = <int, Set<int>>{};
|
final Map<int, Set<int>> _hidden = <int, Set<int>>{};
|
||||||
static final Set<int> _collapsed = <int>{};
|
|
||||||
static final Map<int, Set<int>> _hidden = <int, Set<int>>{};
|
|
||||||
final PublishSubject<Map<int, Set<int>>> _hiddenCommentsSubject =
|
final PublishSubject<Map<int, Set<int>>> _hiddenCommentsSubject =
|
||||||
PublishSubject<Map<int, Set<int>>>();
|
PublishSubject<Map<int, Set<int>>>();
|
||||||
|
|
||||||
@ -76,19 +73,4 @@ class CacheService {
|
|||||||
bool isCollapsed(int commentId) => _collapsed.contains(commentId);
|
bool isCollapsed(int commentId) => _collapsed.contains(commentId);
|
||||||
|
|
||||||
int totalHidden(int commentId) => _hidden[commentId]?.length ?? 0;
|
int totalHidden(int commentId) => _hidden[commentId]?.length ?? 0;
|
||||||
|
|
||||||
void cacheComment(Comment comment) => _comments[comment.id] = comment;
|
|
||||||
|
|
||||||
Comment? getComment(int id) => _comments[id];
|
|
||||||
|
|
||||||
void resetComments() {
|
|
||||||
_comments.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeDraft({required int replyingTo}) => _drafts.remove(replyingTo);
|
|
||||||
|
|
||||||
void cacheDraft({required String text, required int replyingTo}) =>
|
|
||||||
_drafts[replyingTo] = text;
|
|
||||||
|
|
||||||
String? getDraft({required int replyingTo}) => _drafts[replyingTo];
|
|
||||||
}
|
}
|
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
@ -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];
|
||||||
|
}
|
@ -3,15 +3,30 @@ import 'package:hacki/config/locator.dart';
|
|||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
class CustomBlocObserver extends BlocObserver {
|
class CustomBlocObserver extends BlocObserver {
|
||||||
|
@override
|
||||||
|
void onCreate(BlocBase<dynamic> bloc) {
|
||||||
|
locator.get<Logger>().v('$bloc created');
|
||||||
|
super.onCreate(bloc);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onEvent(
|
void onEvent(
|
||||||
Bloc<dynamic, dynamic> bloc,
|
Bloc<dynamic, dynamic> bloc,
|
||||||
Object? event,
|
Object? event,
|
||||||
) {
|
) {
|
||||||
locator.get<Logger>().d(event);
|
locator.get<Logger>().v(event);
|
||||||
super.onEvent(bloc, event);
|
super.onEvent(bloc, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onTransition(
|
||||||
|
Bloc<dynamic, dynamic> bloc,
|
||||||
|
Transition<dynamic, dynamic> transition,
|
||||||
|
) {
|
||||||
|
locator.get<Logger>().v(transition);
|
||||||
|
super.onTransition(bloc, transition);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onError(
|
void onError(
|
||||||
BlocBase<dynamic> bloc,
|
BlocBase<dynamic> bloc,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export 'cache_service.dart';
|
export 'caches/caches.dart';
|
||||||
export 'custom_bloc_observer.dart';
|
export 'custom_bloc_observer.dart';
|
||||||
export 'fetcher.dart';
|
export 'fetcher.dart';
|
||||||
export 'firebase_client.dart';
|
export 'firebase_client.dart';
|
||||||
|
@ -19,7 +19,7 @@ abstract class LinkUtil {
|
|||||||
}) {
|
}) {
|
||||||
if (offlineReading) {
|
if (offlineReading) {
|
||||||
locator
|
locator
|
||||||
.get<CacheRepository>()
|
.get<OfflineRepository>()
|
||||||
.hasCachedWebPage(url: link)
|
.hasCachedWebPage(url: link)
|
||||||
.then((bool cached) {
|
.then((bool cached) {
|
||||||
if (cached) {
|
if (cached) {
|
||||||
|
54
pubspec.lock
@ -14,7 +14,7 @@ packages:
|
|||||||
name: adaptive_theme
|
name: adaptive_theme
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.1.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -22,6 +22,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.0"
|
version: "4.1.0"
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.11"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -175,14 +182,14 @@ packages:
|
|||||||
name: coverage
|
name: coverage
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.2"
|
version: "1.2.0"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.1"
|
||||||
csslib:
|
csslib:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -196,7 +203,7 @@ packages:
|
|||||||
name: dbus
|
name: dbus
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.5"
|
version: "0.7.6"
|
||||||
diff_match_patch:
|
diff_match_patch:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -281,6 +288,11 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.3.0"
|
version: "3.3.0"
|
||||||
|
flutter_driver:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
flutter_fadein:
|
flutter_fadein:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -410,6 +422,11 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.3"
|
||||||
|
fuchsia_remote_debug_protocol:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
gbk_codec:
|
gbk_codec:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -473,6 +490,11 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.1"
|
version: "4.0.1"
|
||||||
|
integration_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -605,7 +627,7 @@ packages:
|
|||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.15"
|
version: "2.0.16"
|
||||||
path_provider_ios:
|
path_provider_ios:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -747,7 +769,7 @@ packages:
|
|||||||
name: share_plus
|
name: share_plus
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.9"
|
version: "4.0.10"
|
||||||
share_plus_linux:
|
share_plus_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -935,6 +957,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
sync_http:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sync_http
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.0"
|
||||||
synced_shared_preferences:
|
synced_shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -997,7 +1026,7 @@ packages:
|
|||||||
name: typed_data
|
name: typed_data
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.0"
|
||||||
universal_platform:
|
universal_platform:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1088,7 +1117,7 @@ packages:
|
|||||||
name: vm_service
|
name: vm_service
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.3.0"
|
version: "8.2.2"
|
||||||
wakelock:
|
wakelock:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1138,6 +1167,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
|
webdriver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webdriver
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
webkit_inspection_protocol:
|
webkit_inspection_protocol:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1210,4 +1246,4 @@ packages:
|
|||||||
version: "3.1.1"
|
version: "3.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.17.0 <3.0.0"
|
dart: ">=2.17.0 <3.0.0"
|
||||||
flutter: ">=3.0.0"
|
flutter: ">=3.0.3"
|
||||||
|
11
pubspec.yaml
@ -1,10 +1,11 @@
|
|||||||
name: hacki
|
name: hacki
|
||||||
description: A Hacker News reader.
|
description: A Hacker News reader.
|
||||||
version: 0.2.23+65
|
version: 0.2.28+70
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.0 <3.0.0"
|
sdk: ">=2.17.0 <3.0.0"
|
||||||
|
flutter: "3.0.3"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
adaptive_theme: ^3.0.0
|
adaptive_theme: ^3.0.0
|
||||||
@ -17,7 +18,7 @@ dependencies:
|
|||||||
dio: ^4.0.4
|
dio: ^4.0.4
|
||||||
equatable: 2.0.3
|
equatable: 2.0.3
|
||||||
fast_gbk: ^1.0.0
|
fast_gbk: ^1.0.0
|
||||||
# feature_discovery: ^0.14.0
|
# feature_discovery: ^0.14.0
|
||||||
feature_discovery:
|
feature_discovery:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/livinglist/feature_discovery
|
url: https://github.com/livinglist/feature_discovery
|
||||||
@ -47,7 +48,7 @@ dependencies:
|
|||||||
path_provider: ^2.0.8
|
path_provider: ^2.0.8
|
||||||
path_provider_android: ^2.0.8
|
path_provider_android: ^2.0.8
|
||||||
path_provider_ios: ^2.0.8
|
path_provider_ios: ^2.0.8
|
||||||
# pull_to_refresh: ^2.0.0
|
# pull_to_refresh: ^2.0.0
|
||||||
pull_to_refresh:
|
pull_to_refresh:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/livinglist/flutter_pulltorefresh
|
url: https://github.com/livinglist/flutter_pulltorefresh
|
||||||
@ -74,8 +75,10 @@ dev_dependencies:
|
|||||||
bloc_test: ^9.0.3
|
bloc_test: ^9.0.3
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
integration_test:
|
||||||
|
sdk: flutter
|
||||||
mocktail: ^0.3.0
|
mocktail: ^0.3.0
|
||||||
very_good_analysis: ^2.3.0
|
very_good_analysis: ^2.4.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|