Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
19f2107d95 | |||
c9b2d82dfc | |||
56e442b09f | |||
9069efcced | |||
bf6a5667dc | |||
cff73a010b | |||
f0d6cac3fd | |||
a90d52f348 | |||
cff4a3c5c4 | |||
502faaf188 |
4
.github/workflows/github-actions.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
JAVA_VERSION: "11.0"
|
||||
FLUTTER_VERSION: "3.0.0"
|
||||
FLUTTER_VERSION: "3.0.3"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v2
|
||||
@ -20,7 +20,7 @@ jobs:
|
||||
java-version: '17'
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.0.0'
|
||||
flutter-version: '3.0.3'
|
||||
channel: 'stable'
|
||||
- run: flutter pub get
|
||||
- run: flutter analyze
|
||||
|
@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
|
||||
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
compileSdkVersion 32
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
@ -49,10 +49,9 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.jiaqifeng.hacki"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 30
|
||||
targetSdkVersion 32
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
@ -78,5 +77,5 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
}
|
||||
|
@ -16,8 +16,10 @@
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:label="hacki"
|
||||
android:label="Hacki"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
@ -25,6 +27,7 @@
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
|
4
android/app/src/main/res/xml/backup_rules.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<exclude domain="sharedpref" path="FlutterSecureStorage"/>
|
||||
</full-backup-content>
|
@ -1,12 +1,12 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.6.10'
|
||||
ext.kotlin_version = '1.7.0'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.1.1'
|
||||
classpath 'com.android.tools.build:gradle:7.1.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
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/64.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/65.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/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/standard (= 2.7.5)
|
||||
- FMDB/standard (2.7.5)
|
||||
- integration_test (0.0.1):
|
||||
- Flutter
|
||||
- OrderedSet (5.0.0)
|
||||
- path_provider_ios (0.0.1):
|
||||
- Flutter
|
||||
@ -50,6 +52,7 @@ DEPENDENCIES:
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/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`)
|
||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
@ -80,6 +83,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
flutter_siri_suggestions:
|
||||
:path: ".symlinks/plugins/flutter_siri_suggestions/ios"
|
||||
integration_test:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
path_provider_ios:
|
||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||
receive_sharing_intent:
|
||||
@ -109,6 +114,7 @@ SPEC CHECKSUMS:
|
||||
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
|
||||
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
|
||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
|
@ -568,7 +568,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -577,7 +577,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.21;
|
||||
MARKETING_VERSION = 0.2.28;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -705,7 +705,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -714,7 +714,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.21;
|
||||
MARKETING_VERSION = 0.2.28;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -736,7 +736,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -745,7 +745,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.21;
|
||||
MARKETING_VERSION = 0.2.28;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -7,7 +7,9 @@ import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
part 'stories_event.dart';
|
||||
part 'stories_state.dart';
|
||||
@ -15,15 +17,18 @@ part 'stories_state.dart';
|
||||
class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
StoriesBloc({
|
||||
required PreferenceCubit preferenceCubit,
|
||||
CacheRepository? cacheRepository,
|
||||
OfflineRepository? offlineRepository,
|
||||
StoriesRepository? storiesRepository,
|
||||
PreferenceRepository? preferenceRepository,
|
||||
Logger? logger,
|
||||
}) : _preferenceCubit = preferenceCubit,
|
||||
_cacheRepository = cacheRepository ?? locator.get<CacheRepository>(),
|
||||
_offlineRepository =
|
||||
offlineRepository ?? locator.get<OfflineRepository>(),
|
||||
_storiesRepository =
|
||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||
_preferenceRepository =
|
||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(const StoriesState.init()) {
|
||||
on<StoriesInitialize>(onInitialize);
|
||||
on<StoriesRefresh>(onRefresh);
|
||||
@ -39,9 +44,10 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
}
|
||||
|
||||
final PreferenceCubit _preferenceCubit;
|
||||
final CacheRepository _cacheRepository;
|
||||
final OfflineRepository _offlineRepository;
|
||||
final StoriesRepository _storiesRepository;
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
final Logger _logger;
|
||||
DeviceScreenType? deviceScreenType;
|
||||
StreamSubscription<PreferenceState>? _streamSubscription;
|
||||
static const int _smallPageSize = 10;
|
||||
@ -71,7 +77,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
add(StoriesPageSizeChanged(pageSize: pageSize));
|
||||
}
|
||||
});
|
||||
final bool hasCachedStories = await _cacheRepository.hasCachedStories;
|
||||
final bool hasCachedStories = await _offlineRepository.hasCachedStories;
|
||||
final bool isComplexTile = _preferenceCubit.state.showComplexStoryTile;
|
||||
final int pageSize = _getPageSize(isComplexTile: isComplexTile);
|
||||
emit(
|
||||
@ -90,13 +96,13 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
required Emitter<StoriesState> emit,
|
||||
}) async {
|
||||
if (state.offlineReading) {
|
||||
final List<int> ids = await _cacheRepository.getCachedStoryIds(of: of);
|
||||
final List<int> ids = await _offlineRepository.getCachedStoryIds(of: of);
|
||||
emit(
|
||||
state
|
||||
.copyWithStoryIdsUpdated(of: of, to: ids)
|
||||
.copyWithCurrentPageUpdated(of: of, to: 0),
|
||||
);
|
||||
_cacheRepository
|
||||
_offlineRepository
|
||||
.getCachedStoriesStream(
|
||||
ids: ids.sublist(0, min(ids.length, state.currentPageSize)),
|
||||
)
|
||||
@ -167,7 +173,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
}
|
||||
|
||||
if (state.offlineReading) {
|
||||
_cacheRepository
|
||||
_offlineRepository
|
||||
.getCachedStoriesStream(
|
||||
ids: state.storyIdsByType[event.type]!.sublist(
|
||||
lower,
|
||||
@ -241,9 +247,9 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
),
|
||||
);
|
||||
|
||||
await _cacheRepository.deleteAllStoryIds();
|
||||
await _cacheRepository.deleteAllStories();
|
||||
await _cacheRepository.deleteAllComments();
|
||||
await _offlineRepository.deleteAllStoryIds();
|
||||
await _offlineRepository.deleteAllStories();
|
||||
await _offlineRepository.deleteAllComments();
|
||||
|
||||
final Set<int> prioritizedIds = <int>{};
|
||||
final List<StoryType> prioritizedTypes = <StoryType>[...types]
|
||||
@ -251,7 +257,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
|
||||
for (final StoryType type in prioritizedTypes) {
|
||||
final List<int> ids = await _storiesRepository.fetchStoryIds(of: type);
|
||||
await _cacheRepository.cacheStoryIds(of: type, ids: ids);
|
||||
await _offlineRepository.cacheStoryIds(of: type, ids: ids);
|
||||
prioritizedIds.addAll(ids);
|
||||
}
|
||||
|
||||
@ -273,7 +279,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
final List<int> ids = await _storiesRepository.fetchStoryIds(
|
||||
of: StoryType.latest,
|
||||
);
|
||||
await _cacheRepository.cacheStoryIds(of: StoryType.latest, ids: ids);
|
||||
await _offlineRepository.cacheStoryIds(of: StoryType.latest, ids: ids);
|
||||
latestIds.addAll(ids);
|
||||
|
||||
await fetchAndCacheStories(
|
||||
@ -281,12 +287,6 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
includingWebPage: event.includingWebPage,
|
||||
isPrioritized: false,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
downloadStatus: StoriesDownloadStatus.finished,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -318,44 +318,53 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
continue;
|
||||
}
|
||||
|
||||
await _cacheRepository.cacheStory(story: story);
|
||||
await _offlineRepository.cacheStory(story: story);
|
||||
|
||||
if (story.url.isNotEmpty && includingWebPage) {
|
||||
await _cacheRepository.cacheUrl(url: story.url);
|
||||
_logger.i('downloading ${story.url}');
|
||||
await _offlineRepository.cacheUrl(url: story.url);
|
||||
}
|
||||
|
||||
final Completer<void> completer = Completer<void>();
|
||||
_storiesRepository
|
||||
.fetchAllChildrenComments(ids: story.kids)
|
||||
.listen((Comment? comment) async {
|
||||
if (comment != null) {
|
||||
await _cacheRepository.cacheComment(comment: comment);
|
||||
}
|
||||
}).onDone(() {
|
||||
completer.complete();
|
||||
add(StoryDownloaded(skipped: false));
|
||||
});
|
||||
|
||||
await completer.future;
|
||||
.whereType<Comment>()
|
||||
.listen(
|
||||
(Comment comment) => unawaited(
|
||||
_offlineRepository.cacheComment(comment: comment),
|
||||
),
|
||||
)
|
||||
.onDone(() => add(StoryDownloaded(skipped: false)));
|
||||
}
|
||||
}
|
||||
|
||||
void onStoryDownloaded(StoryDownloaded event, Emitter<StoriesState> emit) {
|
||||
if (event.skipped) {
|
||||
final int updatedStoriesToBeDownloaded = state.storiesToBeDownloaded - 1;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
storiesToBeDownloaded: state.storiesToBeDownloaded - 1,
|
||||
storiesToBeDownloaded: updatedStoriesToBeDownloaded,
|
||||
downloadStatus:
|
||||
state.storiesDownloaded == updatedStoriesToBeDownloaded
|
||||
? StoriesDownloadStatus.finished
|
||||
: null,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final int updatedStoriesDownloaded = state.storiesDownloaded + 1;
|
||||
final int updatedStoriesToBeDownloaded =
|
||||
updatedStoriesDownloaded > state.storiesToBeDownloaded
|
||||
? state.storiesToBeDownloaded + 1
|
||||
: state.storiesToBeDownloaded;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
storiesDownloaded: updatedStoriesDownloaded,
|
||||
storiesToBeDownloaded:
|
||||
updatedStoriesDownloaded > state.storiesToBeDownloaded
|
||||
? state.storiesToBeDownloaded + 1
|
||||
: state.storiesToBeDownloaded,
|
||||
storiesToBeDownloaded: updatedStoriesToBeDownloaded,
|
||||
downloadStatus:
|
||||
updatedStoriesDownloaded == updatedStoriesToBeDownloaded
|
||||
? StoriesDownloadStatus.finished
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -373,10 +382,10 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
StoriesExitOffline event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
await _cacheRepository.deleteAllStoryIds();
|
||||
await _cacheRepository.deleteAllStories();
|
||||
await _cacheRepository.deleteAllComments();
|
||||
await _cacheRepository.deleteAllWebPages();
|
||||
await _offlineRepository.deleteAllStoryIds();
|
||||
await _offlineRepository.deleteAllStories();
|
||||
await _offlineRepository.deleteAllComments();
|
||||
await _offlineRepository.deleteAllWebPages();
|
||||
emit(state.copyWith(offlineReading: false));
|
||||
add(StoriesInitialize());
|
||||
}
|
||||
|
31
lib/config/custom_log_filter.dart
Normal file
@ -0,0 +1,31 @@
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
class CustomLogFilter extends LogFilter {
|
||||
@override
|
||||
// ignore: overridden_fields
|
||||
Level? level = Level.verbose;
|
||||
|
||||
/// The minimal level allowed in production.
|
||||
static const Level _minimalLevel = Level.info;
|
||||
|
||||
@override
|
||||
bool shouldLog(LogEvent event) {
|
||||
bool shouldLog = false;
|
||||
|
||||
if (event.level.index >= _minimalLevel.index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
assert(
|
||||
() {
|
||||
if (event.level.index >= level!.index) {
|
||||
shouldLog = true;
|
||||
}
|
||||
return true;
|
||||
}(),
|
||||
'',
|
||||
);
|
||||
|
||||
return shouldLog;
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:hacki/config/custom_log_filter.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
@ -10,16 +11,17 @@ final GetIt locator = GetIt.instance;
|
||||
/// Set up [GetIt] locator.
|
||||
Future<void> setUpLocator() async {
|
||||
locator
|
||||
..registerSingleton<Logger>(Logger(filter: CustomLogFilter()))
|
||||
..registerSingleton<StoriesRepository>(StoriesRepository())
|
||||
..registerSingleton<PreferenceRepository>(PreferenceRepository())
|
||||
..registerSingleton<SearchRepository>(SearchRepository())
|
||||
..registerSingleton<AuthRepository>(AuthRepository())
|
||||
..registerSingleton<PostRepository>(PostRepository())
|
||||
..registerSingleton<SembastRepository>(SembastRepository())
|
||||
..registerSingleton<CacheRepository>(CacheRepository())
|
||||
..registerSingleton<CacheService>(CacheService())
|
||||
..registerSingleton<OfflineRepository>(OfflineRepository())
|
||||
..registerSingleton<DraftCache>(DraftCache())
|
||||
..registerSingleton<CommentCache>(CommentCache())
|
||||
..registerSingleton<LocalNotification>(LocalNotification())
|
||||
..registerSingleton<Logger>(Logger())
|
||||
..registerSingleton<RouteObserver<ModalRoute<dynamic>>>(
|
||||
RouteObserver<ModalRoute<dynamic>>(),
|
||||
);
|
||||
|
@ -10,31 +10,31 @@ part 'collapse_state.dart';
|
||||
class CollapseCubit extends Cubit<CollapseState> {
|
||||
CollapseCubit({
|
||||
required int commentId,
|
||||
CacheService? cacheService,
|
||||
CollapseCache? collapseCache,
|
||||
}) : _commentId = commentId,
|
||||
_cacheService = cacheService ?? locator.get<CacheService>(),
|
||||
_collapseCache = collapseCache ?? locator.get<CollapseCache>(),
|
||||
super(const CollapseState.init());
|
||||
|
||||
final int _commentId;
|
||||
final CacheService _cacheService;
|
||||
final CollapseCache _collapseCache;
|
||||
late final StreamSubscription<Map<int, Set<int>>> _streamSubscription;
|
||||
|
||||
void init() {
|
||||
_streamSubscription =
|
||||
_cacheService.hiddenComments.listen(hiddenCommentsStreamListener);
|
||||
_collapseCache.hiddenComments.listen(hiddenCommentsStreamListener);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
collapsedCount: _cacheService.totalHidden(_commentId),
|
||||
collapsed: _cacheService.isCollapsed(_commentId),
|
||||
hidden: _cacheService.isHidden(_commentId),
|
||||
collapsedCount: _collapseCache.totalHidden(_commentId),
|
||||
collapsed: _collapseCache.isCollapsed(_commentId),
|
||||
hidden: _collapseCache.isHidden(_commentId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void collapse() {
|
||||
if (state.collapsed) {
|
||||
_cacheService.uncollapse(_commentId);
|
||||
_collapseCache.uncollapse(_commentId);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -43,7 +43,7 @@ class CollapseCubit extends Cubit<CollapseState> {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final int count = _cacheService.collapse(_commentId);
|
||||
final int count = _collapseCache.collapse(_commentId);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
|
@ -11,31 +11,56 @@ import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
part 'comments_state.dart';
|
||||
|
||||
class CommentsCubit extends Cubit<CommentsState> {
|
||||
CommentsCubit({
|
||||
CacheService? cacheService,
|
||||
CacheRepository? cacheRepository,
|
||||
required CollapseCache collapseCache,
|
||||
CommentCache? commentCache,
|
||||
OfflineRepository? offlineRepository,
|
||||
StoriesRepository? storiesRepository,
|
||||
SembastRepository? sembastRepository,
|
||||
Logger? logger,
|
||||
required bool offlineReading,
|
||||
required Item item,
|
||||
}) : _cacheService = cacheService ?? locator.get<CacheService>(),
|
||||
_cacheRepository = cacheRepository ?? locator.get<CacheRepository>(),
|
||||
required FetchMode defaultFetchMode,
|
||||
required CommentsOrder defaultCommentsOrder,
|
||||
}) : _collapseCache = collapseCache,
|
||||
_commentCache = commentCache ?? locator.get<CommentCache>(),
|
||||
_offlineRepository =
|
||||
offlineRepository ?? locator.get<OfflineRepository>(),
|
||||
_storiesRepository =
|
||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||
_sembastRepository =
|
||||
sembastRepository ?? locator.get<SembastRepository>(),
|
||||
super(CommentsState.init(offlineReading: offlineReading, item: item));
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(
|
||||
CommentsState.init(
|
||||
offlineReading: offlineReading,
|
||||
item: item,
|
||||
fetchMode: defaultFetchMode,
|
||||
order: defaultCommentsOrder,
|
||||
),
|
||||
);
|
||||
|
||||
final CacheService _cacheService;
|
||||
final CacheRepository _cacheRepository;
|
||||
final CollapseCache _collapseCache;
|
||||
final CommentCache _commentCache;
|
||||
final OfflineRepository _offlineRepository;
|
||||
final StoriesRepository _storiesRepository;
|
||||
final SembastRepository _sembastRepository;
|
||||
final Logger _logger;
|
||||
|
||||
/// The [StreamSubscription] for stream (both lazy or eager)
|
||||
/// fetching comments posted directly to the story.
|
||||
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;
|
||||
|
||||
@override
|
||||
@ -47,6 +72,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
|
||||
Future<void> init({
|
||||
bool onlyShowTargetComment = false,
|
||||
bool useCommentCache = false,
|
||||
List<Comment>? targetParents,
|
||||
}) async {
|
||||
if (onlyShowTargetComment && (targetParents?.isNotEmpty ?? false)) {
|
||||
@ -59,7 +85,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
);
|
||||
|
||||
_streamSubscription = _storiesRepository
|
||||
.fetchCommentsStream(
|
||||
.fetchAllCommentsRecursivelyStream(
|
||||
ids: targetParents!.last.kids,
|
||||
level: targetParents.last.level + 1,
|
||||
)
|
||||
@ -69,35 +95,48 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(status: CommentsStatus.loading));
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.loading,
|
||||
comments: <Comment>[],
|
||||
currentPage: 0,
|
||||
),
|
||||
);
|
||||
|
||||
final Item item = state.item;
|
||||
final Item updatedItem = state.offlineReading
|
||||
? item
|
||||
: await _storiesRepository.fetchItemBy(id: item.id) ?? item;
|
||||
final List<int> kids = () {
|
||||
switch (state.order) {
|
||||
case CommentsOrder.natural:
|
||||
return updatedItem.kids;
|
||||
case CommentsOrder.newestFirst:
|
||||
return updatedItem.kids.sorted((int a, int b) => b.compareTo(a));
|
||||
case CommentsOrder.oldestFirst:
|
||||
return updatedItem.kids.sorted((int a, int b) => a.compareTo(b));
|
||||
}
|
||||
}();
|
||||
final List<int> kids = sortKids(updatedItem.kids);
|
||||
|
||||
emit(state.copyWith(item: updatedItem));
|
||||
|
||||
if (state.offlineReading) {
|
||||
_streamSubscription = _cacheRepository
|
||||
_streamSubscription = _offlineRepository
|
||||
.getCachedCommentsStream(ids: kids)
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
} else {
|
||||
_streamSubscription = _storiesRepository
|
||||
.fetchCommentsStream(ids: kids)
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
switch (state.fetchMode) {
|
||||
case FetchMode.lazy:
|
||||
_streamSubscription = _storiesRepository
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,37 +150,47 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
return;
|
||||
}
|
||||
|
||||
_cacheService
|
||||
..resetComments()
|
||||
..resetCollapsedComments();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.loading,
|
||||
comments: <Comment>[],
|
||||
),
|
||||
);
|
||||
|
||||
_collapseCache.resetCollapsedComments();
|
||||
|
||||
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 updatedItem =
|
||||
await _storiesRepository.fetchItemBy(id: item.id) ?? item;
|
||||
final List<int> kids = () {
|
||||
switch (state.order) {
|
||||
case CommentsOrder.natural:
|
||||
return updatedItem.kids;
|
||||
case CommentsOrder.newestFirst:
|
||||
return updatedItem.kids.sorted((int a, int b) => b.compareTo(a));
|
||||
case CommentsOrder.oldestFirst:
|
||||
return updatedItem.kids.sorted((int a, int b) => a.compareTo(b));
|
||||
}
|
||||
}();
|
||||
final List<int> kids = sortKids(updatedItem.kids);
|
||||
|
||||
_streamSubscription = _storiesRepository
|
||||
.fetchCommentsStream(ids: kids)
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
if (state.fetchMode == FetchMode.lazy) {
|
||||
_streamSubscription = _storiesRepository
|
||||
.fetchCommentsStream(
|
||||
ids: kids,
|
||||
)
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
} else {
|
||||
_streamSubscription = _storiesRepository
|
||||
.fetchAllCommentsRecursivelyStream(
|
||||
ids: kids,
|
||||
)
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -156,17 +205,67 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
onlyShowTargetComment: false,
|
||||
comments: <Comment>[],
|
||||
item: story,
|
||||
),
|
||||
);
|
||||
init();
|
||||
}
|
||||
|
||||
void loadMore() {
|
||||
if (_streamSubscription != null) {
|
||||
emit(state.copyWith(status: CommentsStatus.loading));
|
||||
_streamSubscription?.resume();
|
||||
/// [comment] is only used for lazy fetching.
|
||||
void loadMore({Comment? comment}) {
|
||||
switch (state.fetchMode) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -193,11 +292,41 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
}
|
||||
|
||||
void onOrderChanged(CommentsOrder? order) {
|
||||
HapticFeedback.selectionClick();
|
||||
if (order == null) return;
|
||||
if (state.order == order) return;
|
||||
HapticFeedback.selectionClick();
|
||||
_streamSubscription?.cancel();
|
||||
emit(state.copyWith(order: order, comments: <Comment>[]));
|
||||
init();
|
||||
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
|
||||
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) {
|
||||
switch (state.order) {
|
||||
case CommentsOrder.natural:
|
||||
return kids;
|
||||
case CommentsOrder.newestFirst:
|
||||
return kids.sorted((int a, int b) => b.compareTo(a));
|
||||
case CommentsOrder.oldestFirst:
|
||||
return kids.sorted((int a, int b) => a.compareTo(b));
|
||||
}
|
||||
}
|
||||
|
||||
void _onDone() {
|
||||
@ -212,9 +341,8 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
|
||||
void _onCommentFetched(Comment? comment) {
|
||||
if (comment != null) {
|
||||
_cacheService
|
||||
..addKid(comment.id, to: comment.parent)
|
||||
..cacheComment(comment);
|
||||
_collapseCache.addKid(comment.id, to: comment.parent);
|
||||
_commentCache.cacheComment(comment);
|
||||
_sembastRepository.cacheComment(comment);
|
||||
|
||||
final List<LinkifyElement> elements = _linkify(
|
||||
@ -231,21 +359,24 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
|
||||
emit(state.copyWith(comments: updatedComments));
|
||||
|
||||
if (updatedComments.length >= _pageSize + _pageSize * state.currentPage &&
|
||||
updatedComments.length <=
|
||||
_pageSize * 2 + _pageSize * state.currentPage) {
|
||||
final bool isHidden = _cacheService.isHidden(comment.id);
|
||||
if (state.fetchMode == FetchMode.eager) {
|
||||
if (updatedComments.length >=
|
||||
_pageSize + _pageSize * state.currentPage &&
|
||||
updatedComments.length <=
|
||||
_pageSize * 2 + _pageSize * state.currentPage) {
|
||||
final bool isHidden = _collapseCache.isHidden(comment.id);
|
||||
|
||||
if (!isHidden) {
|
||||
_streamSubscription?.pause();
|
||||
if (!isHidden) {
|
||||
_streamSubscription?.pause();
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentPage: state.currentPage + 1,
|
||||
status: CommentsStatus.loaded,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentPage: state.currentPage + 1,
|
||||
status: CommentsStatus.loaded,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -278,6 +409,9 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _streamSubscription?.cancel();
|
||||
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
|
||||
await s.cancel();
|
||||
}
|
||||
await super.close();
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,11 @@ enum CommentsOrder {
|
||||
oldestFirst,
|
||||
}
|
||||
|
||||
enum FetchMode {
|
||||
lazy,
|
||||
eager,
|
||||
}
|
||||
|
||||
class CommentsState extends Equatable {
|
||||
const CommentsState({
|
||||
required this.item,
|
||||
@ -21,6 +26,7 @@ class CommentsState extends Equatable {
|
||||
required this.status,
|
||||
required this.fetchParentStatus,
|
||||
required this.order,
|
||||
required this.fetchMode,
|
||||
required this.onlyShowTargetComment,
|
||||
required this.offlineReading,
|
||||
required this.currentPage,
|
||||
@ -29,10 +35,11 @@ class CommentsState extends Equatable {
|
||||
CommentsState.init({
|
||||
required this.offlineReading,
|
||||
required this.item,
|
||||
required this.fetchMode,
|
||||
required this.order,
|
||||
}) : comments = <Comment>[],
|
||||
status = CommentsStatus.init,
|
||||
fetchParentStatus = CommentsStatus.init,
|
||||
order = CommentsOrder.natural,
|
||||
onlyShowTargetComment = false,
|
||||
currentPage = 0;
|
||||
|
||||
@ -41,6 +48,7 @@ class CommentsState extends Equatable {
|
||||
final CommentsStatus status;
|
||||
final CommentsStatus fetchParentStatus;
|
||||
final CommentsOrder order;
|
||||
final FetchMode fetchMode;
|
||||
final bool onlyShowTargetComment;
|
||||
final bool offlineReading;
|
||||
final int currentPage;
|
||||
@ -51,6 +59,7 @@ class CommentsState extends Equatable {
|
||||
CommentsStatus? status,
|
||||
CommentsStatus? fetchParentStatus,
|
||||
CommentsOrder? order,
|
||||
FetchMode? fetchMode,
|
||||
bool? onlyShowTargetComment,
|
||||
bool? offlineReading,
|
||||
int? currentPage,
|
||||
@ -61,6 +70,7 @@ class CommentsState extends Equatable {
|
||||
fetchParentStatus: fetchParentStatus ?? this.fetchParentStatus,
|
||||
status: status ?? this.status,
|
||||
order: order ?? this.order,
|
||||
fetchMode: fetchMode ?? this.fetchMode,
|
||||
onlyShowTargetComment:
|
||||
onlyShowTargetComment ?? this.onlyShowTargetComment,
|
||||
offlineReading: offlineReading ?? this.offlineReading,
|
||||
@ -68,6 +78,8 @@ class CommentsState extends Equatable {
|
||||
);
|
||||
}
|
||||
|
||||
Set<int> get commentIds => comments.map((Comment e) => e.id).toSet();
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
item,
|
||||
@ -75,6 +87,7 @@ class CommentsState extends Equatable {
|
||||
status,
|
||||
fetchParentStatus,
|
||||
order,
|
||||
fetchMode,
|
||||
onlyShowTargetComment,
|
||||
offlineReading,
|
||||
currentPage,
|
||||
|
@ -8,19 +8,19 @@ import 'package:hacki/utils/debouncer.dart';
|
||||
part 'edit_state.dart';
|
||||
|
||||
class EditCubit extends Cubit<EditState> {
|
||||
EditCubit({CacheService? cacheService})
|
||||
: _cacheService = cacheService ?? locator.get<CacheService>(),
|
||||
EditCubit({DraftCache? draftCache})
|
||||
: _draftCache = draftCache ?? locator.get<DraftCache>(),
|
||||
_debouncer = Debouncer(delay: const Duration(seconds: 1)),
|
||||
super(const EditState.init());
|
||||
|
||||
final CacheService _cacheService;
|
||||
final DraftCache _draftCache;
|
||||
final Debouncer _debouncer;
|
||||
|
||||
void onReplyTapped(Item item) {
|
||||
emit(
|
||||
EditState(
|
||||
replyingTo: item,
|
||||
text: _cacheService.getDraft(replyingTo: item.id),
|
||||
text: _draftCache.getDraft(replyingTo: item.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -44,7 +44,7 @@ class EditCubit extends Cubit<EditState> {
|
||||
|
||||
void onReplySubmittedSuccessfully() {
|
||||
if (state.replyingTo != null) {
|
||||
_cacheService.removeDraft(replyingTo: state.replyingTo!.id);
|
||||
_draftCache.removeDraft(replyingTo: state.replyingTo!.id);
|
||||
}
|
||||
emit(const EditState.init());
|
||||
}
|
||||
@ -54,7 +54,7 @@ class EditCubit extends Cubit<EditState> {
|
||||
if (state.replyingTo != null) {
|
||||
final int? id = state.replyingTo?.id;
|
||||
_debouncer.run(() {
|
||||
_cacheService.cacheDraft(
|
||||
_draftCache.cacheDraft(
|
||||
text: text,
|
||||
replyingTo: id!,
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/comments/comments_cubit.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
|
||||
part 'preference_state.dart';
|
||||
@ -33,6 +34,10 @@ class PreferenceCubit extends Cubit<PreferenceState> {
|
||||
.then((bool value) => emit(state.copyWith(markReadStories: value)));
|
||||
_preferenceRepository.shouldShowMetadata
|
||||
.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() {
|
||||
@ -74,4 +79,16 @@ class PreferenceCubit extends Cubit<PreferenceState> {
|
||||
emit(state.copyWith(showMetadata: !state.showMetadata));
|
||||
_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.markReadStories,
|
||||
required this.showMetadata,
|
||||
required this.fetchMode,
|
||||
required this.order,
|
||||
});
|
||||
|
||||
const PreferenceState.init()
|
||||
@ -20,7 +22,9 @@ class PreferenceState extends Equatable {
|
||||
useTrueDark = false,
|
||||
useReader = false,
|
||||
markReadStories = false,
|
||||
showMetadata = false;
|
||||
showMetadata = false,
|
||||
fetchMode = FetchMode.eager,
|
||||
order = CommentsOrder.natural;
|
||||
|
||||
final bool showNotification;
|
||||
final bool showComplexStoryTile;
|
||||
@ -30,6 +34,8 @@ class PreferenceState extends Equatable {
|
||||
final bool useReader;
|
||||
final bool markReadStories;
|
||||
final bool showMetadata;
|
||||
final FetchMode fetchMode;
|
||||
final CommentsOrder order;
|
||||
|
||||
PreferenceState copyWith({
|
||||
bool? showNotification,
|
||||
@ -40,6 +46,8 @@ class PreferenceState extends Equatable {
|
||||
bool? useReader,
|
||||
bool? markReadStories,
|
||||
bool? showMetadata,
|
||||
FetchMode? fetchMode,
|
||||
CommentsOrder? order,
|
||||
}) {
|
||||
return PreferenceState(
|
||||
showNotification: showNotification ?? this.showNotification,
|
||||
@ -50,6 +58,8 @@ class PreferenceState extends Equatable {
|
||||
useReader: useReader ?? this.useReader,
|
||||
markReadStories: markReadStories ?? this.markReadStories,
|
||||
showMetadata: showMetadata ?? this.showMetadata,
|
||||
fetchMode: fetchMode ?? this.fetchMode,
|
||||
order: order ?? this.order,
|
||||
);
|
||||
}
|
||||
|
||||
@ -63,5 +73,7 @@ class PreferenceState extends Equatable {
|
||||
useReader,
|
||||
markReadStories,
|
||||
showMetadata,
|
||||
fetchMode,
|
||||
order,
|
||||
];
|
||||
}
|
||||
|
@ -3,18 +3,24 @@ import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
part 'split_view_state.dart';
|
||||
|
||||
class SplitViewCubit extends Cubit<SplitViewState> {
|
||||
SplitViewCubit({CacheService? cacheService})
|
||||
: _cacheService = cacheService ?? locator.get<CacheService>(),
|
||||
SplitViewCubit({
|
||||
CommentCache? commentCache,
|
||||
Logger? logger,
|
||||
}) : _commentCache = commentCache ?? locator.get<CommentCache>(),
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(const SplitViewState.init());
|
||||
|
||||
final CacheService _cacheService;
|
||||
final Logger _logger;
|
||||
final CommentCache _commentCache;
|
||||
|
||||
void updateItemScreenArgs(ItemScreenArgs args) {
|
||||
_cacheService.resetCollapsedComments();
|
||||
_logger.i('resetting comments in CommentCache');
|
||||
_commentCache.resetComments();
|
||||
emit(state.copyWith(itemScreenArgs: args));
|
||||
}
|
||||
|
||||
|
@ -3,34 +3,34 @@ import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/models/models.dart' show Comment;
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/services/cache_service.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
|
||||
part 'time_machine_state.dart';
|
||||
|
||||
class TimeMachineCubit extends Cubit<TimeMachineState> {
|
||||
TimeMachineCubit({
|
||||
SembastRepository? sembastRepository,
|
||||
CacheService? cacheService,
|
||||
CommentCache? commentCache,
|
||||
}) : _sembastRepository =
|
||||
sembastRepository ?? locator.get<SembastRepository>(),
|
||||
_cacheService = cacheService ?? locator.get<CacheService>(),
|
||||
_commentCache = commentCache ?? locator.get<CommentCache>(),
|
||||
super(TimeMachineState.init());
|
||||
|
||||
final SembastRepository _sembastRepository;
|
||||
final CacheService _cacheService;
|
||||
final CommentCache _commentCache;
|
||||
|
||||
Future<void> activateTimeMachine(Comment comment) async {
|
||||
emit(state.copyWith(parents: <Comment>[]));
|
||||
|
||||
final List<Comment> parents = <Comment>[];
|
||||
Comment? parent = _cacheService.getComment(comment.parent);
|
||||
Comment? parent = _commentCache.getComment(comment.parent);
|
||||
parent ??= await _sembastRepository.getCachedComment(id: comment.parent);
|
||||
|
||||
while (parent != null) {
|
||||
parents.insert(0, parent);
|
||||
|
||||
final int parentId = parent.parent;
|
||||
parent = _cacheService.getComment(parentId);
|
||||
parent = _commentCache.getComment(parentId);
|
||||
parent ??= await _sembastRepository.getCachedComment(id: parentId);
|
||||
}
|
||||
|
||||
|
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 'int_extension.dart';
|
||||
export 'list_extension.dart';
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/main.dart';
|
||||
import 'package:hacki/screens/screens.dart' show ItemScreen, ItemScreenArgs;
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
extension StateExtension on State {
|
||||
void showSnackBar({
|
||||
@ -12,7 +13,7 @@ extension StateExtension on State {
|
||||
}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.deepOrange,
|
||||
backgroundColor: Palette.deepOrange,
|
||||
content: Text(content),
|
||||
action: action != null && label != null
|
||||
? SnackBarAction(
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension WidgetModifier on Widget {
|
||||
Widget padding([EdgeInsetsGeometry value = const EdgeInsets.all(12)]) {
|
||||
Widget padded([EdgeInsetsGeometry value = const EdgeInsets.all(12)]) {
|
||||
return Padding(
|
||||
padding: value,
|
||||
child: this,
|
||||
|
@ -3,6 +3,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:adaptive_theme/adaptive_theme.dart';
|
||||
import 'package:feature_discovery/feature_discovery.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@ -14,7 +15,9 @@ import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/repositories/repositories.dart' show PreferenceRepository;
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/services/custom_bloc_observer.dart';
|
||||
import 'package:hacki/services/fetcher.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
|
||||
@ -29,9 +32,13 @@ final BehaviorSubject<String?> selectNotificationSubject =
|
||||
final BehaviorSubject<String?> siriSuggestionSubject =
|
||||
BehaviorSubject<String?>();
|
||||
|
||||
Future<void> main() async {
|
||||
late final bool isTesting;
|
||||
|
||||
Future<void> main({bool testing = false}) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
isTesting = testing;
|
||||
|
||||
if (Platform.isIOS) {
|
||||
unawaited(
|
||||
Workmanager().initialize(
|
||||
@ -85,25 +92,26 @@ Future<void> main() async {
|
||||
final bool trueDarkMode =
|
||||
prefs.getBool(PreferenceRepository.trueDarkModeKey) ?? false;
|
||||
|
||||
// Uncomment code below for running with logging.
|
||||
// BlocOverrides.runZoned(
|
||||
// () {
|
||||
// runApp(
|
||||
// HackiApp(
|
||||
// savedThemeMode: savedThemeMode,
|
||||
// trueDarkMode: trueDarkMode,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// blocObserver: CustomBlocObserver(),
|
||||
// );
|
||||
|
||||
runApp(
|
||||
HackiApp(
|
||||
savedThemeMode: savedThemeMode,
|
||||
trueDarkMode: trueDarkMode,
|
||||
),
|
||||
);
|
||||
if (kReleaseMode) {
|
||||
runApp(
|
||||
HackiApp(
|
||||
savedThemeMode: savedThemeMode,
|
||||
trueDarkMode: trueDarkMode,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
BlocOverrides.runZoned(
|
||||
() {
|
||||
runApp(
|
||||
HackiApp(
|
||||
savedThemeMode: savedThemeMode,
|
||||
trueDarkMode: trueDarkMode,
|
||||
),
|
||||
);
|
||||
},
|
||||
blocObserver: CustomBlocObserver(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HackiApp extends StatelessWidget {
|
||||
@ -182,19 +190,19 @@ class HackiApp extends StatelessWidget {
|
||||
],
|
||||
child: AdaptiveTheme(
|
||||
light: ThemeData(
|
||||
primarySwatch: Colors.orange,
|
||||
primarySwatch: Palette.orange,
|
||||
),
|
||||
dark: ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
primarySwatch: Colors.orange,
|
||||
canvasColor: trueDarkMode ? Colors.black : null,
|
||||
primarySwatch: Palette.orange,
|
||||
canvasColor: trueDarkMode ? Palette.black : null,
|
||||
),
|
||||
initial: savedThemeMode ?? AdaptiveThemeMode.system,
|
||||
builder: (ThemeData theme, ThemeData darkTheme) {
|
||||
final ThemeData trueDarkTheme = ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
primarySwatch: Colors.orange,
|
||||
canvasColor: Colors.black,
|
||||
primarySwatch: Palette.orange,
|
||||
canvasColor: Palette.black,
|
||||
);
|
||||
return FutureBuilder<AdaptiveThemeMode?>(
|
||||
future: AdaptiveTheme.getThemeMode(),
|
||||
|
@ -1,5 +1,3 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hacki/models/item.dart';
|
||||
|
||||
enum StoryType {
|
||||
@ -113,9 +111,10 @@ class Story extends Item {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final String prettyString =
|
||||
const JsonEncoder.withIndent(' ').convert(this);
|
||||
return 'Story $prettyString';
|
||||
// final String prettyString =
|
||||
// const JsonEncoder.withIndent(' ').convert(this);
|
||||
// return 'Story $prettyString';
|
||||
return 'Story $id';
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -5,16 +5,20 @@ import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/postable_repository.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
class AuthRepository extends PostableRepository {
|
||||
AuthRepository({
|
||||
Dio? dio,
|
||||
PreferenceRepository? preferenceRepository,
|
||||
Logger? logger,
|
||||
}) : _preferenceRepository =
|
||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(dio: dio);
|
||||
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
final Logger _logger;
|
||||
|
||||
static const String _authority = 'news.ycombinator.com';
|
||||
|
||||
@ -38,10 +42,15 @@ class AuthRepository extends PostableRepository {
|
||||
final bool success = await performDefaultPost(uri, data);
|
||||
|
||||
if (success) {
|
||||
await _preferenceRepository.setAuth(
|
||||
username: username,
|
||||
password: password,
|
||||
);
|
||||
try {
|
||||
await _preferenceRepository.setAuth(
|
||||
username: username,
|
||||
password: password,
|
||||
);
|
||||
} catch (_) {
|
||||
_logger.e(_);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
|
@ -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 '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:synced_shared_preferences/synced_shared_preferences.dart';
|
||||
|
||||
@ -10,9 +13,11 @@ class PreferenceRepository {
|
||||
SyncedSharedPreferences? syncedPrefs,
|
||||
Future<SharedPreferences>? prefs,
|
||||
FlutterSecureStorage? secureStorage,
|
||||
Logger? logger,
|
||||
}) : _syncedPrefs = syncedPrefs ?? SyncedSharedPreferences.instance,
|
||||
_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 _passwordKey = 'password';
|
||||
@ -38,20 +43,26 @@ class PreferenceRepository {
|
||||
static const String _navigationModeKey = 'navigationMode';
|
||||
static const String _eyeCandyModeKey = 'eyeCandyMode';
|
||||
static const String _markReadStoriesModeKey = 'markReadStoriesMode';
|
||||
static const String _fetchModeKey = 'fetchMode';
|
||||
static const String _commentsOrderKey = 'commentsOrder';
|
||||
|
||||
static const bool _notificationModeDefaultValue = true;
|
||||
static const bool _displayModeDefaultValue = true;
|
||||
static const bool _navigationModeDefaultValue = true;
|
||||
static const bool _navigationModeDefaultValueIOS = true;
|
||||
static const bool _navigationModeDefaultValueAndroid = false;
|
||||
static const bool _eyeCandyModeDefaultValue = false;
|
||||
static const bool _trueDarkModeDefaultValue = false;
|
||||
static const bool _readerModeDefaultValue = true;
|
||||
static const bool _markReadStoriesModeDefaultValue = true;
|
||||
static const bool _isFirstLaunchKeyDefaultValue = true;
|
||||
static const bool _metadataModeDefaultValue = true;
|
||||
static final int _fetchModeDefaultValue = FetchMode.eager.index;
|
||||
static final int _commentsOrderDefaultValue = CommentsOrder.natural.index;
|
||||
|
||||
final SyncedSharedPreferences _syncedPrefs;
|
||||
final Future<SharedPreferences> _prefs;
|
||||
final FlutterSecureStorage _secureStorage;
|
||||
final Logger _logger;
|
||||
|
||||
Future<bool> get loggedIn async => await username != null;
|
||||
|
||||
@ -82,7 +93,10 @@ class PreferenceRepository {
|
||||
|
||||
Future<bool> get shouldShowWebFirst async => _prefs.then(
|
||||
(SharedPreferences prefs) =>
|
||||
prefs.getBool(_navigationModeKey) ?? _navigationModeDefaultValue,
|
||||
prefs.getBool(_navigationModeKey) ??
|
||||
(Platform.isAndroid
|
||||
? _navigationModeDefaultValueAndroid
|
||||
: _navigationModeDefaultValueIOS),
|
||||
);
|
||||
|
||||
Future<bool> get shouldShowEyeCandy async => _prefs.then(
|
||||
@ -111,6 +125,17 @@ class PreferenceRepository {
|
||||
_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 =>
|
||||
_prefs.then((SharedPreferences prefs) {
|
||||
final bool? val = prefs.getBool(_getPushNotificationKey(commentId));
|
||||
@ -140,8 +165,29 @@ class PreferenceRepository {
|
||||
required String username,
|
||||
required String password,
|
||||
}) async {
|
||||
await _secureStorage.write(key: _usernameKey, value: username);
|
||||
await _secureStorage.write(key: _passwordKey, value: password);
|
||||
const AndroidOptions androidOptions = AndroidOptions(resetOnError: true);
|
||||
try {
|
||||
await _secureStorage.write(
|
||||
key: _usernameKey,
|
||||
value: username,
|
||||
aOptions: androidOptions,
|
||||
);
|
||||
await _secureStorage.write(
|
||||
key: _passwordKey,
|
||||
value: password,
|
||||
aOptions: androidOptions,
|
||||
);
|
||||
} catch (_) {
|
||||
try {
|
||||
await _secureStorage.deleteAll(
|
||||
aOptions: androidOptions,
|
||||
);
|
||||
} catch (_) {
|
||||
_logger.e(_);
|
||||
}
|
||||
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeAuth() async {
|
||||
@ -165,8 +211,10 @@ class PreferenceRepository {
|
||||
|
||||
Future<void> toggleNavigationMode() async {
|
||||
final SharedPreferences prefs = await _prefs;
|
||||
final bool currentMode =
|
||||
prefs.getBool(_navigationModeKey) ?? _navigationModeDefaultValue;
|
||||
final bool currentMode = prefs.getBool(_navigationModeKey) ??
|
||||
(Platform.isAndroid
|
||||
? _navigationModeDefaultValueAndroid
|
||||
: _navigationModeDefaultValueIOS);
|
||||
await prefs.setBool(_navigationModeKey, !currentMode);
|
||||
}
|
||||
|
||||
@ -205,6 +253,18 @@ class PreferenceRepository {
|
||||
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
|
||||
|
||||
Future<List<int>> favList({required String of}) async {
|
||||
|
@ -1,5 +1,5 @@
|
||||
export 'auth_repository.dart';
|
||||
export 'cache_repository.dart';
|
||||
export 'offline_repository.dart';
|
||||
export 'post_repository.dart';
|
||||
export 'preference_repository.dart';
|
||||
export 'search_repository.dart';
|
||||
|
@ -51,9 +51,37 @@ class StoriesRepository {
|
||||
Stream<Comment> fetchCommentsStream({
|
||||
required List<int> ids,
|
||||
int level = 0,
|
||||
Comment? Function(int)? getFromCache,
|
||||
}) async* {
|
||||
for (final int id in ids) {
|
||||
final Comment? comment = await _firebaseClient
|
||||
Comment? comment = getFromCache?.call(id)?.copyWith(level: level);
|
||||
|
||||
comment ??= await _firebaseClient
|
||||
.get('${_baseUrl}item/$id.json')
|
||||
.then((dynamic json) => _parseJson(json as Map<String, dynamic>?))
|
||||
.then((Map<String, dynamic>? json) async {
|
||||
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')
|
||||
.then((dynamic json) => _parseJson(json as Map<String, dynamic>?))
|
||||
.then((Map<String, dynamic>? json) async {
|
||||
@ -66,9 +94,10 @@ class StoriesRepository {
|
||||
if (comment != null) {
|
||||
yield comment;
|
||||
|
||||
yield* fetchCommentsStream(
|
||||
yield* fetchAllCommentsRecursivelyStream(
|
||||
ids: comment.kids,
|
||||
level: level + 1,
|
||||
getFromCache: getFromCache,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,9 @@ import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
|
||||
@ -46,7 +48,6 @@ class HomeScreen extends StatefulWidget {
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen>
|
||||
with SingleTickerProviderStateMixin, RouteAware {
|
||||
final CacheService cacheService = locator.get<CacheService>();
|
||||
final Throttle featureDiscoveryDismissThrottle = Throttle(
|
||||
delay: _throttleDelay,
|
||||
);
|
||||
@ -65,9 +66,10 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
super.didPopNext();
|
||||
if (context.read<StoriesBloc>().deviceScreenType ==
|
||||
DeviceScreenType.mobile) {
|
||||
locator.get<Logger>().i('resetting comments in CommentCache');
|
||||
Future<void>.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
cacheService.resetCollapsedComments,
|
||||
locator.get<CommentCache>().resetComments,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -101,12 +103,14 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
|
||||
SchedulerBinding.instance
|
||||
..addPostFrameCallback((_) {
|
||||
FeatureDiscovery.discoverFeatures(
|
||||
context,
|
||||
const <String>{
|
||||
Constants.featureLogIn,
|
||||
},
|
||||
);
|
||||
if (!isTesting) {
|
||||
FeatureDiscovery.discoverFeatures(
|
||||
context,
|
||||
const <String>{
|
||||
Constants.featureLogIn,
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
..addPostFrameCallback((_) {
|
||||
final ModalRoute<dynamic>? route = ModalRoute.of(context);
|
||||
@ -171,8 +175,8 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
HapticFeedback.lightImpact();
|
||||
context.read<PinCubit>().unpinStory(story);
|
||||
},
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Palette.red,
|
||||
foregroundColor: Palette.white,
|
||||
icon: preferenceState.showComplexStoryTile
|
||||
? Icons.close
|
||||
: null,
|
||||
@ -181,7 +185,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
color: Colors.orangeAccent.withOpacity(0.2),
|
||||
color: Palette.orangeAccent.withOpacity(0.2),
|
||||
child: StoryTile(
|
||||
key: ValueKey<String>('${story.id}-PinnedStoryTile'),
|
||||
story: story,
|
||||
@ -194,9 +198,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
if (state.pinnedStories.isNotEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: EdgeInsets.symmetric(horizontal: Dimens.pt12),
|
||||
child: Divider(
|
||||
color: Colors.orangeAccent,
|
||||
color: Palette.orangeAccent,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -209,27 +213,32 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size(0, 40),
|
||||
preferredSize: const Size(
|
||||
Dimens.zero,
|
||||
Dimens.pt40,
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).padding.top - 8,
|
||||
height: MediaQuery.of(context).padding.top - Dimens.pt8,
|
||||
),
|
||||
Theme(
|
||||
data: ThemeData(
|
||||
highlightColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Palette.transparent,
|
||||
splashColor: Palette.transparent,
|
||||
primaryColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
child: TabBar(
|
||||
isScrollable: true,
|
||||
controller: tabController,
|
||||
indicatorColor: Colors.orange,
|
||||
indicatorColor: Palette.orange,
|
||||
indicator: CircleTabIndicator(
|
||||
color: Colors.orange,
|
||||
radius: 2,
|
||||
color: Palette.orange,
|
||||
radius: Dimens.pt2,
|
||||
),
|
||||
indicatorPadding: const EdgeInsets.only(
|
||||
bottom: Dimens.pt8,
|
||||
),
|
||||
indicatorPadding: const EdgeInsets.only(bottom: 8),
|
||||
onTap: (_) {
|
||||
HapticFeedback.selectionClick();
|
||||
},
|
||||
@ -242,10 +251,12 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
child: Text(
|
||||
StoriesBloc.types.elementAt(i).label,
|
||||
style: TextStyle(
|
||||
fontSize: currentIndex == i ? 14 : 10,
|
||||
fontSize: currentIndex == i
|
||||
? TextDimens.pt14
|
||||
: TextDimens.pt10,
|
||||
color: currentIndex == i
|
||||
? Colors.orange
|
||||
: Colors.grey,
|
||||
? Palette.orange
|
||||
: Palette.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -263,8 +274,8 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
targetColor: Theme.of(context).primaryColor,
|
||||
tapTarget: const Icon(
|
||||
Icons.person,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
size: TextDimens.pt16,
|
||||
color: Palette.white,
|
||||
),
|
||||
featureId: Constants.featureLogIn,
|
||||
title: const Text('Log in for more'),
|
||||
@ -274,7 +285,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
'posted in the past, and get in-app '
|
||||
'notification when there is new reply to '
|
||||
'your comments or stories.',
|
||||
style: TextStyle(fontSize: 16),
|
||||
style: TextStyle(fontSize: TextDimens.pt16),
|
||||
),
|
||||
child: BlocBuilder<NotificationCubit,
|
||||
NotificationState>(
|
||||
@ -292,19 +303,21 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
showBadge: state.unreadCommentsIds.isNotEmpty,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
badgeContent: Container(
|
||||
height: 3,
|
||||
width: 3,
|
||||
height: Dimens.pt3,
|
||||
width: Dimens.pt3,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
color: Palette.white,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
size: currentIndex == 5 ? 16 : 12,
|
||||
size: currentIndex == 5
|
||||
? TextDimens.pt16
|
||||
: TextDimens.pt12,
|
||||
color: currentIndex == 5
|
||||
? Colors.orange
|
||||
: Colors.grey,
|
||||
? Palette.orange
|
||||
: Palette.grey,
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -439,7 +452,10 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
locator.get<StoriesRepository>().fetchItemBy(id: id).then((Item? item) {
|
||||
if (mounted) {
|
||||
if (item != null) {
|
||||
goToItemScreen(args: ItemScreenArgs(item: item));
|
||||
goToItemScreen(
|
||||
args: ItemScreenArgs(item: item),
|
||||
forceNewScreen: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -506,10 +522,10 @@ class _MobileHomeScreen extends StatelessWidget {
|
||||
Positioned.fill(child: homeScreen),
|
||||
if (!context.read<ReminderCubit>().state.hasShown)
|
||||
const Positioned(
|
||||
left: 24,
|
||||
right: 24,
|
||||
bottom: 36,
|
||||
height: 40,
|
||||
left: Dimens.pt24,
|
||||
right: Dimens.pt24,
|
||||
bottom: Dimens.pt36,
|
||||
height: Dimens.pt40,
|
||||
child: CountdownReminder(),
|
||||
),
|
||||
],
|
||||
@ -533,7 +549,7 @@ class _TabletHomeScreen extends StatelessWidget {
|
||||
double homeScreenWidth = 428;
|
||||
|
||||
if (sizeInfo.screenSize.width < homeScreenWidth * 2) {
|
||||
homeScreenWidth = 345.0;
|
||||
homeScreenWidth = 345;
|
||||
}
|
||||
|
||||
return BlocBuilder<SplitViewCubit, SplitViewState>(
|
||||
@ -543,26 +559,26 @@ class _TabletHomeScreen extends StatelessWidget {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
AnimatedPositioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: state.expanded ? 0 : homeScreenWidth,
|
||||
left: Dimens.zero,
|
||||
top: Dimens.zero,
|
||||
bottom: Dimens.zero,
|
||||
width: state.expanded ? Dimens.zero : homeScreenWidth,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.elasticOut,
|
||||
child: homeScreen,
|
||||
),
|
||||
Positioned(
|
||||
left: 24,
|
||||
bottom: 36,
|
||||
height: 40,
|
||||
width: homeScreenWidth - 24,
|
||||
left: Dimens.pt24,
|
||||
bottom: Dimens.pt36,
|
||||
height: Dimens.pt40,
|
||||
width: homeScreenWidth - Dimens.pt24,
|
||||
child: const CountdownReminder(),
|
||||
),
|
||||
AnimatedPositioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: state.expanded ? 0 : homeScreenWidth,
|
||||
right: Dimens.zero,
|
||||
top: Dimens.zero,
|
||||
bottom: Dimens.zero,
|
||||
left: state.expanded ? Dimens.zero : homeScreenWidth,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.elasticOut,
|
||||
child: const _TabletStoryView(),
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/item/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class CustomAppBar extends AppBar {
|
||||
CustomAppBar({
|
||||
@ -18,7 +19,7 @@ class CustomAppBar extends AppBar {
|
||||
}) : super(
|
||||
key: key,
|
||||
backgroundColor: backgroundColor,
|
||||
elevation: 0,
|
||||
elevation: Dimens.zero,
|
||||
actions: <Widget>[
|
||||
if (splitViewEnabled) ...<Widget>[
|
||||
IconButton(
|
||||
@ -26,7 +27,7 @@ class CustomAppBar extends AppBar {
|
||||
expanded ?? false
|
||||
? FeatherIcons.minimize2
|
||||
: FeatherIcons.maximize2,
|
||||
size: 20,
|
||||
size: TextDimens.pt20,
|
||||
),
|
||||
onPressed: () {
|
||||
HapticFeedback.lightImpact();
|
||||
|
@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class FavIconButton extends StatelessWidget {
|
||||
const FavIconButton({
|
||||
@ -37,17 +38,17 @@ class FavIconButton extends StatelessWidget {
|
||||
targetColor: Theme.of(context).primaryColor,
|
||||
tapTarget: Icon(
|
||||
isFav ? Icons.favorite : Icons.favorite_border,
|
||||
color: Colors.white,
|
||||
color: Palette.white,
|
||||
),
|
||||
featureId: Constants.featureAddStoryToFavList,
|
||||
title: const Text('Fav a Story'),
|
||||
description: const Text(
|
||||
'Add it to your favorites.',
|
||||
style: TextStyle(fontSize: 16),
|
||||
style: TextStyle(fontSize: TextDimens.pt16),
|
||||
),
|
||||
child: Icon(
|
||||
isFav ? Icons.favorite : Icons.favorite_border,
|
||||
color: isFav ? Colors.orange : Theme.of(context).iconTheme.color,
|
||||
color: isFav ? Palette.orange : Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
|
@ -4,6 +4,7 @@ import 'package:feature_discovery/feature_discovery.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
|
||||
class LinkIconButton extends StatelessWidget {
|
||||
@ -33,7 +34,7 @@ class LinkIconButton extends StatelessWidget {
|
||||
targetColor: Theme.of(context).primaryColor,
|
||||
tapTarget: const Icon(
|
||||
Icons.stream,
|
||||
color: Colors.white,
|
||||
color: Palette.white,
|
||||
),
|
||||
featureId: Constants.featureOpenStoryInWebView,
|
||||
title: const Text('Open in Browser'),
|
||||
@ -41,7 +42,7 @@ class LinkIconButton extends StatelessWidget {
|
||||
'Want more than just reading and replying? '
|
||||
'You can tap here to open this story in a '
|
||||
'browser.',
|
||||
style: TextStyle(fontSize: 16),
|
||||
style: TextStyle(fontSize: TextDimens.pt16),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.stream,
|
||||
|
@ -8,6 +8,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class PinIconButton extends StatelessWidget {
|
||||
const PinIconButton({
|
||||
@ -43,7 +44,7 @@ class PinIconButton extends StatelessWidget {
|
||||
targetColor: Theme.of(context).primaryColor,
|
||||
tapTarget: Icon(
|
||||
pinned ? Icons.push_pin : Icons.push_pin_outlined,
|
||||
color: Colors.white,
|
||||
color: Palette.white,
|
||||
),
|
||||
featureId: Constants.featurePinToTop,
|
||||
title: const Text('Pin a Story'),
|
||||
@ -51,12 +52,12 @@ class PinIconButton extends StatelessWidget {
|
||||
'Pin this story to the top of your '
|
||||
'home screen so that you can come'
|
||||
' back later.',
|
||||
style: TextStyle(fontSize: 16),
|
||||
style: TextStyle(fontSize: TextDimens.pt16),
|
||||
),
|
||||
child: Icon(
|
||||
pinned ? Icons.push_pin : Icons.push_pin_outlined,
|
||||
color: pinned
|
||||
? Colors.orange
|
||||
? Palette.orange
|
||||
: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
|
@ -5,6 +5,7 @@ import 'package:flutter_fadein/flutter_fadein.dart';
|
||||
import 'package:hacki/blocs/auth/auth_bloc.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class PollView extends StatelessWidget {
|
||||
const PollView({
|
||||
@ -21,29 +22,29 @@ class PollView extends StatelessWidget {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
height: Dimens.pt24,
|
||||
),
|
||||
if (state.status == PollStatus.loading) ...<Widget>[
|
||||
const LinearProgressIndicator(),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
height: Dimens.pt24,
|
||||
),
|
||||
] else ...<Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
const SizedBox(
|
||||
width: 24,
|
||||
width: Dimens.pt24,
|
||||
),
|
||||
Text(
|
||||
'Total votes: ${state.totalVotes}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontSize: TextDimens.pt14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
height: Dimens.pt12,
|
||||
),
|
||||
],
|
||||
for (final PollOption option in state.pollOptions)
|
||||
@ -97,9 +98,9 @@ class PollView extends StatelessWidget {
|
||||
builder: (BuildContext context, VoteState voteState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
right: 24,
|
||||
bottom: 4,
|
||||
left: Dimens.pt12,
|
||||
right: Dimens.pt24,
|
||||
bottom: Dimens.pt4,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
@ -111,9 +112,9 @@ class PollView extends StatelessWidget {
|
||||
icon: Icon(
|
||||
Icons.arrow_drop_up,
|
||||
color: voteState.vote == Vote.up
|
||||
? Colors.orange
|
||||
: Colors.grey,
|
||||
size: 36,
|
||||
? Palette.orange
|
||||
: Palette.grey,
|
||||
size: TextDimens.pt36,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@ -126,16 +127,16 @@ class PollView extends StatelessWidget {
|
||||
Text(
|
||||
'''${option.score} vote${option.score > 1 ? 's' : ''}''',
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
color: Palette.grey,
|
||||
fontSize: TextDimens.pt12,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
height: Dimens.pt4,
|
||||
),
|
||||
LinearProgressIndicator(
|
||||
value: option.ratio,
|
||||
color: Colors.deepOrange,
|
||||
color: Palette.deepOrange,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -161,7 +162,7 @@ class PollView extends StatelessWidget {
|
||||
}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.deepOrange,
|
||||
backgroundColor: Palette.deepOrange,
|
||||
content: Text(content),
|
||||
action: action != null && label != null
|
||||
? SnackBarAction(
|
||||
|
@ -6,6 +6,7 @@ import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/models/item.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/link_util.dart';
|
||||
|
||||
class ReplyBox extends StatefulWidget {
|
||||
@ -34,6 +35,8 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
bool expanded = false;
|
||||
double? expandedHeight;
|
||||
|
||||
static const double _collapsedHeight = 100;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
expandedHeight ??= MediaQuery.of(context).size.height;
|
||||
@ -53,20 +56,21 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: expanded
|
||||
? 0
|
||||
? Dimens.zero
|
||||
: widget.splitViewEnabled
|
||||
? MediaQuery.of(context).viewInsets.bottom
|
||||
: 0,
|
||||
: Dimens.zero,
|
||||
),
|
||||
child: AnimatedContainer(
|
||||
height: expanded ? expandedHeight : 100,
|
||||
height: expanded ? expandedHeight : _collapsedHeight,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: <BoxShadow>[
|
||||
if (!context.read<SplitViewCubit>().state.enabled)
|
||||
BoxShadow(
|
||||
color: expanded ? Colors.transparent : Colors.black26,
|
||||
blurRadius: 40,
|
||||
color:
|
||||
expanded ? Palette.transparent : Palette.black26,
|
||||
blurRadius: Dimens.pt40,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -75,10 +79,10 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
children: <Widget>[
|
||||
if (context.read<SplitViewCubit>().state.enabled)
|
||||
const Divider(
|
||||
height: 0,
|
||||
height: Dimens.zero,
|
||||
),
|
||||
AnimatedContainer(
|
||||
height: expanded ? 36 : 0,
|
||||
height: expanded ? Dimens.pt36 : Dimens.zero,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
),
|
||||
Row(
|
||||
@ -86,16 +90,16 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
left: Dimens.pt12,
|
||||
top: Dimens.pt8,
|
||||
bottom: Dimens.pt8,
|
||||
),
|
||||
child: Text(
|
||||
replyingTo == null
|
||||
? 'Editing'
|
||||
: 'Replying '
|
||||
'${replyingTo.by}',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
style: const TextStyle(color: Palette.grey),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@ -111,8 +115,8 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
key: const Key('quote'),
|
||||
icon: const Icon(
|
||||
FeatherIcons.code,
|
||||
color: Colors.orange,
|
||||
size: 18,
|
||||
color: Palette.orange,
|
||||
size: TextDimens.pt18,
|
||||
),
|
||||
onPressed:
|
||||
expanded ? showTextPopup : null,
|
||||
@ -124,8 +128,8 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
expanded
|
||||
? FeatherIcons.minimize2
|
||||
: FeatherIcons.maximize2,
|
||||
color: Colors.orange,
|
||||
size: 18,
|
||||
color: Palette.orange,
|
||||
size: TextDimens.pt18,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
@ -138,7 +142,7 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
key: const Key('close'),
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
color: Colors.orange,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onPressed: () {
|
||||
widget.onCloseTapped();
|
||||
@ -149,15 +153,15 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
if (isLoading)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 16,
|
||||
vertical: Dimens.pt12,
|
||||
horizontal: Dimens.pt16,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
height: Dimens.pt24,
|
||||
width: Dimens.pt24,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.orange,
|
||||
strokeWidth: 2,
|
||||
color: Palette.orange,
|
||||
strokeWidth: Dimens.pt2,
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -166,7 +170,7 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
key: const Key('send'),
|
||||
icon: const Icon(
|
||||
Icons.send,
|
||||
color: Colors.orange,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onPressed: () {
|
||||
widget.onSendTapped();
|
||||
@ -177,7 +181,9 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt16,
|
||||
),
|
||||
child: TextField(
|
||||
focusNode: widget.focusNode,
|
||||
controller: widget.textEditingController,
|
||||
@ -187,7 +193,7 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintText: '...',
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.grey,
|
||||
color: Palette.grey,
|
||||
),
|
||||
focusedBorder: InputBorder.none,
|
||||
border: InputBorder.none,
|
||||
@ -220,8 +226,8 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
builder: (_) {
|
||||
return AlertDialog(
|
||||
insetPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 24,
|
||||
horizontal: Dimens.pt12,
|
||||
vertical: Dimens.pt24,
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: ConstrainedBox(
|
||||
@ -233,14 +239,14 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
top: 6,
|
||||
left: Dimens.pt12,
|
||||
top: Dimens.pt6,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
replyingTo?.by ?? '',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
style: const TextStyle(color: Palette.grey),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
@ -252,8 +258,8 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
color: Colors.orange,
|
||||
size: 18,
|
||||
color: Palette.orange,
|
||||
size: TextDimens.pt18,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
@ -265,17 +271,17 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
thumbVisibility: true,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
right: 6,
|
||||
top: 6,
|
||||
left: Dimens.pt12,
|
||||
right: Dimens.pt6,
|
||||
top: Dimens.pt6,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: SelectableLinkify(
|
||||
scrollPhysics: const NeverScrollableScrollPhysics(),
|
||||
linkStyle: TextStyle(
|
||||
fontSize:
|
||||
MediaQuery.of(context).textScaleFactor * 15,
|
||||
color: Colors.orange,
|
||||
fontSize: MediaQuery.of(context).textScaleFactor *
|
||||
TextDimens.pt15,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) =>
|
||||
LinkUtil.launch(link.url),
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class ScrollUpIconButton extends StatefulWidget {
|
||||
const ScrollUpIconButton({
|
||||
@ -35,8 +36,8 @@ class _ScrollUpIconButtonState extends State<ScrollUpIconButton> {
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
FeatherIcons.chevronsUp,
|
||||
color: Colors.orange,
|
||||
size: 26,
|
||||
color: Palette.orange,
|
||||
size: TextDimens.pt26,
|
||||
),
|
||||
onPressed: () {
|
||||
final double curPos = widget.scrollController.offset;
|
||||
|
@ -18,6 +18,7 @@ import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/screens/profile/widgets/widgets.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
@ -91,7 +92,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
Positioned.fill(
|
||||
top: 50,
|
||||
top: Dimens.pt50,
|
||||
child: Visibility(
|
||||
visible: pageType == _PageType.history,
|
||||
child: BlocConsumer<HistoryCubit, HistoryState>(
|
||||
@ -148,7 +149,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
top: 50,
|
||||
top: Dimens.pt50,
|
||||
child: Visibility(
|
||||
visible: pageType == _PageType.fav,
|
||||
child: BlocConsumer<FavCubit, FavState>(
|
||||
@ -192,7 +193,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
top: 50,
|
||||
top: Dimens.pt50,
|
||||
child: Visibility(
|
||||
visible: pageType == _PageType.search,
|
||||
maintainState: true,
|
||||
@ -200,7 +201,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
top: 50,
|
||||
top: Dimens.pt50,
|
||||
child: Visibility(
|
||||
visible: pageType == _PageType.notification,
|
||||
child: notificationState.comments.isEmpty
|
||||
@ -241,7 +242,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
top: 50,
|
||||
top: Dimens.pt50,
|
||||
child: Visibility(
|
||||
visible: pageType == _PageType.settings,
|
||||
child: SingleChildScrollView(
|
||||
@ -280,7 +281,123 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
.read<PreferenceCubit>()
|
||||
.toggleNotificationMode();
|
||||
},
|
||||
activeColor: Colors.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(
|
||||
title: const Text('Complex Story Tile'),
|
||||
@ -294,7 +411,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
.read<PreferenceCubit>()
|
||||
.toggleDisplayMode();
|
||||
},
|
||||
activeColor: Colors.orange,
|
||||
activeColor: Palette.orange,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Show Metadata'),
|
||||
@ -309,7 +426,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
.read<PreferenceCubit>()
|
||||
.toggleMetadataMode();
|
||||
},
|
||||
activeColor: Colors.orange,
|
||||
activeColor: Palette.orange,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Show Web Page First'),
|
||||
@ -324,7 +441,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
.read<PreferenceCubit>()
|
||||
.toggleNavigationMode();
|
||||
},
|
||||
activeColor: Colors.orange,
|
||||
activeColor: Palette.orange,
|
||||
),
|
||||
if (Platform.isIOS)
|
||||
SwitchListTile(
|
||||
@ -340,7 +457,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
.read<PreferenceCubit>()
|
||||
.toggleReaderMode();
|
||||
},
|
||||
activeColor: Colors.orange,
|
||||
activeColor: Palette.orange,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Mark Read Stories'),
|
||||
@ -361,7 +478,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
.read<PreferenceCubit>()
|
||||
.toggleMarkReadStoriesMode();
|
||||
},
|
||||
activeColor: Colors.orange,
|
||||
activeColor: Palette.orange,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Eye Candy'),
|
||||
@ -373,7 +490,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
.read<PreferenceCubit>()
|
||||
.toggleEyeCandyMode();
|
||||
},
|
||||
activeColor: Colors.orange,
|
||||
activeColor: Palette.orange,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('True Dark Mode'),
|
||||
@ -387,7 +504,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
.read<PreferenceCubit>()
|
||||
.toggleTrueDarkMode();
|
||||
},
|
||||
activeColor: Colors.orange,
|
||||
activeColor: Palette.orange,
|
||||
),
|
||||
ListTile(
|
||||
title: const Text(
|
||||
@ -409,15 +526,17 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationName: 'Hacki',
|
||||
applicationVersion: 'v0.2.21',
|
||||
applicationVersion: 'v0.2.28',
|
||||
applicationIcon: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
Radius.circular(
|
||||
Dimens.pt12,
|
||||
),
|
||||
),
|
||||
child: Image.asset(
|
||||
Constants.hackiIconPath,
|
||||
height: 50,
|
||||
width: 50,
|
||||
height: Dimens.pt50,
|
||||
width: Dimens.pt50,
|
||||
),
|
||||
),
|
||||
children: <Widget>[
|
||||
@ -431,7 +550,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
FontAwesomeIcons.addressCard,
|
||||
),
|
||||
SizedBox(
|
||||
width: 12,
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
Text('Developer'),
|
||||
],
|
||||
@ -447,7 +566,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
FontAwesomeIcons.github,
|
||||
),
|
||||
SizedBox(
|
||||
width: 12,
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
Text('Source code'),
|
||||
],
|
||||
@ -465,7 +584,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
Icons.thumb_up,
|
||||
),
|
||||
SizedBox(
|
||||
width: 12,
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
Text('Like the app?'),
|
||||
],
|
||||
@ -481,7 +600,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
FeatherIcons.coffee,
|
||||
),
|
||||
SizedBox(
|
||||
width: 12,
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
Text('Buy me a coffee'),
|
||||
],
|
||||
@ -492,7 +611,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
height: Dimens.pt48,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -507,7 +626,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
CustomChip(
|
||||
label: 'Submit',
|
||||
@ -526,7 +645,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
CustomChip(
|
||||
label: 'Inbox : '
|
||||
@ -542,7 +661,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
CustomChip(
|
||||
label: 'Favorite',
|
||||
@ -556,7 +675,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
CustomChip(
|
||||
label: 'Submitted',
|
||||
@ -570,7 +689,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
CustomChip(
|
||||
label: 'Search',
|
||||
@ -584,7 +703,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
CustomChip(
|
||||
label: 'Settings',
|
||||
@ -598,7 +717,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -666,7 +785,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
style: TextStyle(
|
||||
color: Colors.orange,
|
||||
color: Palette.orange,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -677,7 +796,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
.get<SembastRepository>()
|
||||
.deleteAllCachedComments()
|
||||
.whenComplete(
|
||||
locator.get<CacheRepository>().deleteAll,
|
||||
locator.get<OfflineRepository>().deleteAll,
|
||||
)
|
||||
.whenComplete(
|
||||
locator.get<PreferenceRepository>().clearAllReadStories,
|
||||
@ -743,64 +862,64 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
children: <Widget>[
|
||||
if (state.status == AuthStatus.loading)
|
||||
const SizedBox(
|
||||
height: 36,
|
||||
width: 36,
|
||||
height: Dimens.pt36,
|
||||
width: Dimens.pt36,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.orange,
|
||||
color: Palette.orange,
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (!state.isLoggedIn) ...<Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 18,
|
||||
horizontal: Dimens.pt18,
|
||||
),
|
||||
child: TextField(
|
||||
controller: usernameController,
|
||||
cursorColor: Colors.orange,
|
||||
cursorColor: Palette.orange,
|
||||
autocorrect: false,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Username',
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.orange),
|
||||
borderSide: BorderSide(color: Palette.orange),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
height: Dimens.pt16,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 18,
|
||||
horizontal: Dimens.pt18,
|
||||
),
|
||||
child: TextField(
|
||||
controller: passwordController,
|
||||
cursorColor: Colors.orange,
|
||||
cursorColor: Palette.orange,
|
||||
obscureText: true,
|
||||
autocorrect: false,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Password',
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.orange),
|
||||
borderSide: BorderSide(color: Palette.orange),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
height: Dimens.pt16,
|
||||
),
|
||||
if (state.status == AuthStatus.failure)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 18,
|
||||
left: Dimens.pt18,
|
||||
),
|
||||
child: Text(
|
||||
'Something went wrong...',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
color: Palette.grey,
|
||||
fontSize: TextDimens.pt12,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -814,8 +933,8 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
? Icons.check_box
|
||||
: Icons.check_box_outline_blank,
|
||||
color: state.agreedToEULA
|
||||
? Colors.deepOrange
|
||||
: Colors.grey,
|
||||
? Palette.deepOrange
|
||||
: Palette.grey,
|
||||
),
|
||||
onPressed: () => context
|
||||
.read<AuthBloc>()
|
||||
@ -840,7 +959,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
child: const Text(
|
||||
'End User Agreement',
|
||||
style: TextStyle(
|
||||
color: Colors.deepOrange,
|
||||
color: Palette.deepOrange,
|
||||
decoration: TextDecoration.underline,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@ -855,7 +974,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 12,
|
||||
right: Dimens.pt12,
|
||||
),
|
||||
child: ButtonBar(
|
||||
children: <Widget>[
|
||||
@ -886,15 +1005,15 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
state.agreedToEULA
|
||||
? Colors.deepOrange
|
||||
: Colors.grey,
|
||||
? Palette.deepOrange
|
||||
: Palette.grey,
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Log in',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
color: Palette.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -921,7 +1040,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
content: Text(
|
||||
'Log out as ${authBloc.state.username}?',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontSize: TextDimens.pt16,
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class CenteredMessageView extends StatelessWidget {
|
||||
const CenteredMessageView({
|
||||
@ -12,14 +13,14 @@ class CenteredMessageView extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 120,
|
||||
left: 40,
|
||||
right: 40,
|
||||
top: Dimens.pt120,
|
||||
left: Dimens.pt40,
|
||||
right: Dimens.pt40,
|
||||
),
|
||||
child: Text(
|
||||
content,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
style: const TextStyle(color: Palette.grey),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter_fadein/flutter_fadein.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/link_util.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
@ -29,8 +30,8 @@ class InboxView extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color textColor = Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black;
|
||||
? Palette.white
|
||||
: Palette.black;
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
if (unreadCommentsIds.isNotEmpty)
|
||||
@ -42,12 +43,14 @@ class InboxView extends StatelessWidget {
|
||||
child: SmartRefresher(
|
||||
enablePullUp: true,
|
||||
header: const WaterDropMaterialHeader(
|
||||
backgroundColor: Colors.orange,
|
||||
backgroundColor: Palette.orange,
|
||||
),
|
||||
footer: CustomFooter(
|
||||
loadStyle: LoadStyle.ShowWhenLoading,
|
||||
builder: (BuildContext context, LoadStatus? mode) {
|
||||
Widget body;
|
||||
const double height = 55;
|
||||
late final Widget body;
|
||||
|
||||
if (mode == LoadStatus.loading) {
|
||||
body = const CustomCircularProgressIndicator();
|
||||
} else if (mode == LoadStatus.failed) {
|
||||
@ -58,7 +61,7 @@ class InboxView extends StatelessWidget {
|
||||
body = const SizedBox.shrink();
|
||||
}
|
||||
return SizedBox(
|
||||
height: 55,
|
||||
height: height,
|
||||
child: Center(child: body),
|
||||
);
|
||||
},
|
||||
@ -72,7 +75,9 @@ class InboxView extends StatelessWidget {
|
||||
return <Widget>[
|
||||
FadeIn(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 6),
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () => onCommentTapped(e),
|
||||
child: Padding(
|
||||
@ -87,8 +92,8 @@ class InboxView extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 6,
|
||||
vertical: Dimens.pt8,
|
||||
horizontal: Dimens.pt6,
|
||||
),
|
||||
child: Linkify(
|
||||
text: '${e.by} : ${e.text}',
|
||||
@ -96,13 +101,13 @@ class InboxView extends StatelessWidget {
|
||||
color:
|
||||
unreadCommentsIds.contains(e.id)
|
||||
? textColor
|
||||
: Colors.grey,
|
||||
: Palette.grey,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
color:
|
||||
unreadCommentsIds.contains(e.id)
|
||||
? Colors.orange
|
||||
: Colors.orange
|
||||
? Palette.orange
|
||||
: Palette.orange
|
||||
.withOpacity(0.6),
|
||||
),
|
||||
maxLines: 4,
|
||||
@ -116,18 +121,18 @@ class InboxView extends StatelessWidget {
|
||||
Text(
|
||||
e.postedDate,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
height: 0,
|
||||
height: Dimens.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -136,12 +141,12 @@ class InboxView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
height: 0,
|
||||
height: Dimens.zero,
|
||||
),
|
||||
];
|
||||
}).expand((List<Widget> element) => element),
|
||||
const SizedBox(
|
||||
height: 40,
|
||||
height: Dimens.pt40,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
|
||||
class OfflineListTile extends StatelessWidget {
|
||||
@ -32,8 +33,8 @@ class OfflineListTile extends StatelessWidget {
|
||||
final Widget trailingWidget = () {
|
||||
if (downloading) {
|
||||
return const SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
height: Dimens.pt24,
|
||||
width: Dimens.pt24,
|
||||
child: CustomCircularProgressIndicator(),
|
||||
);
|
||||
} else if (downloaded) {
|
||||
|
@ -7,6 +7,7 @@ import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/screens/search/widgets/widgets.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
@ -38,14 +39,16 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt12,
|
||||
),
|
||||
child: TextField(
|
||||
cursorColor: Colors.orange,
|
||||
cursorColor: Palette.orange,
|
||||
autocorrect: false,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Search Hacker News',
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.orange),
|
||||
borderSide: BorderSide(color: Palette.orange),
|
||||
),
|
||||
),
|
||||
onChanged: (String val) {
|
||||
@ -58,7 +61,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 6,
|
||||
height: Dimens.pt6,
|
||||
),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
@ -83,7 +86,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
.removeFilter<DateTimeRangeFilter>,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
width: Dimens.pt8,
|
||||
),
|
||||
CustomChip(
|
||||
onSelected: (_) =>
|
||||
@ -92,7 +95,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
label: '''newest first''',
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
width: Dimens.pt8,
|
||||
),
|
||||
for (final CustomDateTimeRange range
|
||||
in CustomDateTimeRange.values) ...<Widget>[
|
||||
@ -107,7 +110,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
width: Dimens.pt8,
|
||||
),
|
||||
],
|
||||
],
|
||||
@ -116,7 +119,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
if (state.status == SearchStatus.loading &&
|
||||
state.results.isEmpty) ...<Widget>[
|
||||
const SizedBox(
|
||||
height: 100,
|
||||
height: Dimens.pt100,
|
||||
),
|
||||
const CustomCircularProgressIndicator(),
|
||||
],
|
||||
@ -125,12 +128,14 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
enablePullDown: false,
|
||||
enablePullUp: true,
|
||||
header: const WaterDropMaterialHeader(
|
||||
backgroundColor: Colors.orange,
|
||||
backgroundColor: Palette.orange,
|
||||
),
|
||||
footer: CustomFooter(
|
||||
loadStyle: LoadStyle.ShowWhenLoading,
|
||||
builder: (BuildContext context, LoadStatus? mode) {
|
||||
Widget body;
|
||||
const double height = 55;
|
||||
late final Widget body;
|
||||
|
||||
if (mode == LoadStatus.loading) {
|
||||
body = const CustomCircularProgressIndicator();
|
||||
} else if (mode == LoadStatus.failed) {
|
||||
@ -140,8 +145,9 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
} else {
|
||||
body = const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: 55,
|
||||
height: height,
|
||||
child: Center(child: body),
|
||||
);
|
||||
},
|
||||
@ -169,14 +175,14 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
),
|
||||
if (!prefState.showComplexStoryTile)
|
||||
const Divider(
|
||||
height: 0,
|
||||
height: Dimens.zero,
|
||||
),
|
||||
],
|
||||
)
|
||||
.expand((List<Widget> e) => e)
|
||||
.toList(),
|
||||
const SizedBox(
|
||||
height: 40,
|
||||
height: Dimens.pt40,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class SubmitScreen extends StatefulWidget {
|
||||
const SubmitScreen({super.key});
|
||||
@ -58,7 +59,7 @@ class _SubmitScreenState extends State<SubmitScreen> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).canvasColor,
|
||||
elevation: 0,
|
||||
elevation: Dimens.zero,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
@ -78,7 +79,7 @@ class _SubmitScreenState extends State<SubmitScreen> {
|
||||
child: const Text(
|
||||
'Yes',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
color: Palette.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -99,14 +100,14 @@ class _SubmitScreenState extends State<SubmitScreen> {
|
||||
if (state.status == SubmitStatus.submitting)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 18,
|
||||
horizontal: 16,
|
||||
vertical: Dimens.pt18,
|
||||
horizontal: Dimens.pt16,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
height: Dimens.pt20,
|
||||
width: Dimens.pt20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.orange,
|
||||
color: Palette.orange,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
@ -115,7 +116,7 @@ class _SubmitScreenState extends State<SubmitScreen> {
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.send,
|
||||
color: Colors.orange,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onPressed: context.read<SubmitCubit>().onSubmitTapped,
|
||||
)
|
||||
@ -123,7 +124,7 @@ class _SubmitScreenState extends State<SubmitScreen> {
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.send,
|
||||
color: Colors.grey,
|
||||
color: Palette.grey,
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
@ -132,50 +133,58 @@ class _SubmitScreenState extends State<SubmitScreen> {
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt12,
|
||||
),
|
||||
child: TextField(
|
||||
controller: titleEditingController,
|
||||
cursorColor: Colors.orange,
|
||||
cursorColor: Palette.orange,
|
||||
autocorrect: false,
|
||||
maxLength: 80,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Title',
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.orange),
|
||||
borderSide: BorderSide(color: Palette.orange),
|
||||
),
|
||||
),
|
||||
onChanged: context.read<SubmitCubit>().onTitleChanged,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt12,
|
||||
),
|
||||
child: TextField(
|
||||
enabled: textEditingController.text.isEmpty,
|
||||
controller: urlEditingController,
|
||||
cursorColor: Colors.orange,
|
||||
cursorColor: Palette.orange,
|
||||
autocorrect: false,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Url',
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.orange),
|
||||
borderSide: BorderSide(color: Palette.orange),
|
||||
),
|
||||
),
|
||||
onChanged: context.read<SubmitCubit>().onUrlChanged,
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: Dimens.pt12,
|
||||
),
|
||||
child: Center(
|
||||
child: Text('or'),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt12,
|
||||
),
|
||||
child: TextField(
|
||||
enabled: urlEditingController.text.isEmpty,
|
||||
controller: textEditingController,
|
||||
cursorColor: Colors.orange,
|
||||
cursorColor: Palette.orange,
|
||||
maxLines: 200,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Text',
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class WebViewScreen extends StatefulWidget {
|
||||
@ -24,7 +25,7 @@ class _WebViewScreenState extends State<WebViewScreen> {
|
||||
title: Text(
|
||||
humanize(widget.url),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontSize: TextDimens.pt12,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
@ -33,7 +34,7 @@ class _WebViewScreenState extends State<WebViewScreen> {
|
||||
),
|
||||
body: WebView(
|
||||
onWebViewCreated: (WebViewController controller) async {
|
||||
final String? html = await locator.get<CacheRepository>().getHtml(
|
||||
final String? html = await locator.get<OfflineRepository>().getHtml(
|
||||
url: widget.url,
|
||||
);
|
||||
|
||||
|
@ -7,6 +7,8 @@ import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
|
||||
class CommentTile extends StatelessWidget {
|
||||
@ -15,6 +17,7 @@ class CommentTile extends StatelessWidget {
|
||||
required this.myUsername,
|
||||
required this.comment,
|
||||
required this.onStoryLinkTapped,
|
||||
required this.fetchMode,
|
||||
this.onReplyTapped,
|
||||
this.onMoreTapped,
|
||||
this.onEditTapped,
|
||||
@ -30,10 +33,11 @@ class CommentTile extends StatelessWidget {
|
||||
final int level;
|
||||
final bool actionable;
|
||||
final Function(Comment)? onReplyTapped;
|
||||
final Function(Comment)? onMoreTapped;
|
||||
final Function(Comment, Rect?)? onMoreTapped;
|
||||
final Function(Comment)? onEditTapped;
|
||||
final Function(Comment)? onRightMoreTapped;
|
||||
final Function(String) onStoryLinkTapped;
|
||||
final FetchMode fetchMode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -42,6 +46,7 @@ class CommentTile extends StatelessWidget {
|
||||
lazy: false,
|
||||
create: (_) => CollapseCubit(
|
||||
commentId: comment.id,
|
||||
collapseCache: context.tryRead<CollapseCache>() ?? CollapseCache(),
|
||||
)..init(),
|
||||
child: BlocBuilder<CollapseCubit, CollapseState>(
|
||||
builder: (BuildContext context, CollapseState state) {
|
||||
@ -67,8 +72,8 @@ class CommentTile extends StatelessWidget {
|
||||
SlidableAction(
|
||||
onPressed: (_) =>
|
||||
onReplyTapped?.call(comment),
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.message,
|
||||
),
|
||||
if (context
|
||||
@ -80,15 +85,18 @@ class CommentTile extends StatelessWidget {
|
||||
SlidableAction(
|
||||
onPressed: (_) =>
|
||||
onEditTapped?.call(comment),
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.edit,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (_) =>
|
||||
onMoreTapped?.call(comment),
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
onPressed: (BuildContext context) =>
|
||||
onMoreTapped?.call(
|
||||
comment,
|
||||
context.rect,
|
||||
),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.more_horiz,
|
||||
),
|
||||
],
|
||||
@ -101,8 +109,8 @@ class CommentTile extends StatelessWidget {
|
||||
SlidableAction(
|
||||
onPressed: (_) =>
|
||||
onRightMoreTapped?.call(comment),
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.av_timer,
|
||||
),
|
||||
],
|
||||
@ -111,7 +119,7 @@ class CommentTile extends StatelessWidget {
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (actionable) {
|
||||
HapticFeedback.lightImpact();
|
||||
HapticFeedback.selectionClick();
|
||||
context.read<CollapseCubit>().collapse();
|
||||
}
|
||||
},
|
||||
@ -120,9 +128,9 @@ class CommentTile extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6,
|
||||
right: 6,
|
||||
top: 6,
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
top: Dimens.pt6,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
@ -145,7 +153,7 @@ class CommentTile extends StatelessWidget {
|
||||
Text(
|
||||
comment.postedDate,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -154,13 +162,14 @@ class CommentTile extends StatelessWidget {
|
||||
if (actionable && state.collapsed)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
'collapsed '
|
||||
'(${state.collapsedCount + 1})',
|
||||
style: const TextStyle(
|
||||
color: Colors.orangeAccent,
|
||||
color: Palette.orangeAccent,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -168,11 +177,13 @@ class CommentTile extends StatelessWidget {
|
||||
else if (comment.deleted)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 12),
|
||||
padding: EdgeInsets.only(
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
'deleted',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -180,11 +191,13 @@ class CommentTile extends StatelessWidget {
|
||||
else if (comment.dead)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 12),
|
||||
padding: EdgeInsets.only(
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
'dead',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -193,11 +206,13 @@ class CommentTile extends StatelessWidget {
|
||||
.contains(comment.by))
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 12),
|
||||
padding: EdgeInsets.only(
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
'blocked',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -205,10 +220,10 @@ class CommentTile extends StatelessWidget {
|
||||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 6,
|
||||
bottom: 12,
|
||||
left: Dimens.pt8,
|
||||
right: Dimens.pt8,
|
||||
top: Dimens.pt6,
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: comment is BuildableComment
|
||||
? SelectableText.rich(
|
||||
@ -219,15 +234,15 @@ class CommentTile extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
fontSize: MediaQuery.of(context)
|
||||
.textScaleFactor *
|
||||
15,
|
||||
TextDimens.pt15,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: MediaQuery.of(context)
|
||||
.textScaleFactor *
|
||||
15,
|
||||
TextDimens.pt15,
|
||||
decoration:
|
||||
TextDecoration.underline,
|
||||
color: Colors.orange,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) {
|
||||
if (link.url.contains(
|
||||
@ -247,13 +262,13 @@ class CommentTile extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
fontSize: MediaQuery.of(context)
|
||||
.textScaleFactor *
|
||||
15,
|
||||
TextDimens.pt15,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: MediaQuery.of(context)
|
||||
.textScaleFactor *
|
||||
15,
|
||||
color: Colors.orange,
|
||||
TextDimens.pt15,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) {
|
||||
if (link.url.contains(
|
||||
@ -267,8 +282,32 @@ 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(
|
||||
height: 0,
|
||||
height: Dimens.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -285,14 +324,14 @@ class CommentTile extends StatelessWidget {
|
||||
|
||||
final Color commentColor = prefState.showEyeCandy
|
||||
? color.withOpacity(commentBackgroundColorOpacity)
|
||||
: Colors.transparent;
|
||||
: Palette.transparent;
|
||||
final bool isMyComment = myUsername == comment.by;
|
||||
|
||||
Widget? wrapper = child;
|
||||
Widget wrapper = child;
|
||||
|
||||
if (isMyComment && level == 0) {
|
||||
return Container(
|
||||
color: Colors.orange.withOpacity(0.2),
|
||||
color: Palette.orange.withOpacity(0.2),
|
||||
child: wrapper,
|
||||
);
|
||||
}
|
||||
@ -301,7 +340,9 @@ class CommentTile extends StatelessWidget {
|
||||
final Color wrapperBorderColor = _getColor(i);
|
||||
final bool shouldHighlight = isMyComment && i == level;
|
||||
wrapper = Container(
|
||||
margin: const EdgeInsets.only(left: 12),
|
||||
margin: const EdgeInsets.only(
|
||||
left: Dimens.pt12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: i != 0
|
||||
? Border(
|
||||
@ -311,14 +352,14 @@ class CommentTile extends StatelessWidget {
|
||||
)
|
||||
: null,
|
||||
color: shouldHighlight
|
||||
? Colors.orange.withOpacity(0.2)
|
||||
? Palette.orange.withOpacity(0.2)
|
||||
: commentColor,
|
||||
),
|
||||
child: wrapper,
|
||||
);
|
||||
}
|
||||
|
||||
return wrapper!;
|
||||
return wrapper;
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -328,7 +369,12 @@ class CommentTile extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
static final Map<int, Color> _colors = <int, Color>{};
|
||||
|
||||
Color _getColor(int level) {
|
||||
final int initialLevel = level;
|
||||
if (_colors[initialLevel] != null) return _colors[initialLevel]!;
|
||||
|
||||
while (level >= 10) {
|
||||
level = level - 10;
|
||||
}
|
||||
@ -349,6 +395,7 @@ class CommentTile extends StatelessWidget {
|
||||
1,
|
||||
);
|
||||
|
||||
_colors[initialLevel] = color;
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/story.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class CountdownReminder extends StatefulWidget {
|
||||
const CountdownReminder({super.key});
|
||||
@ -95,9 +96,13 @@ class _CountDownReminderState extends State<CountdownReminder>
|
||||
animation: animationController,
|
||||
child: FadeIn(
|
||||
child: Material(
|
||||
color: Colors.deepOrange,
|
||||
color: Palette.deepOrange,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(
|
||||
Dimens.pt4,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (state.storyId != null) {
|
||||
@ -122,24 +127,24 @@ class _CountDownReminderState extends State<CountdownReminder>
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
top: 10,
|
||||
right: 10,
|
||||
left: Dimens.pt12,
|
||||
top: Dimens.pt10,
|
||||
right: Dimens.pt10,
|
||||
),
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
Text(
|
||||
'Pick up where you left off',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
fontSize: TextDimens.pt12,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Colors.white,
|
||||
size: TextDimens.pt12,
|
||||
color: Palette.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class CustomChip extends StatelessWidget {
|
||||
CustomChip({
|
||||
@ -15,16 +16,16 @@ class CustomChip extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FilterChip(
|
||||
shadowColor: Colors.transparent,
|
||||
selectedShadowColor: Colors.transparent,
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Palette.transparent,
|
||||
selectedShadowColor: Palette.transparent,
|
||||
backgroundColor: Palette.transparent,
|
||||
shape: const StadiumBorder(
|
||||
side: BorderSide(color: Colors.orange),
|
||||
side: BorderSide(color: Palette.orange),
|
||||
),
|
||||
label: Text(label),
|
||||
selected: selected,
|
||||
onSelected: onSelected,
|
||||
selectedColor: Colors.orange,
|
||||
selectedColor: Palette.orange,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
/// Circular progress indicator with color.
|
||||
/// Changing `colorScheme`'s `primary` color doesn't work because it changes
|
||||
@ -17,7 +18,7 @@ class CustomCircularProgressIndicator extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return CircularProgressIndicator(
|
||||
strokeWidth: strokeWidth,
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(Colors.orange),
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(Palette.orange),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
@ -80,8 +81,8 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
HapticFeedback.lightImpact();
|
||||
onPinned?.call(e);
|
||||
},
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: showWebPreview
|
||||
? Icons.push_pin_outlined
|
||||
: null,
|
||||
@ -97,13 +98,15 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
showWebPreview: showWebPreview,
|
||||
showMetadata: showMetadata,
|
||||
hasRead: markReadStories && hasRead,
|
||||
simpleTileFontSize: useConsistentFontSize ? 14 : 16,
|
||||
simpleTileFontSize: useConsistentFontSize
|
||||
? TextDimens.pt14
|
||||
: TextDimens.pt16,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!showWebPreview)
|
||||
const Divider(
|
||||
height: 0,
|
||||
height: Dimens.zero,
|
||||
),
|
||||
];
|
||||
} else if (e is Comment) {
|
||||
@ -111,22 +114,22 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
return <Widget>[
|
||||
if (showWebPreview)
|
||||
const Divider(
|
||||
height: 0,
|
||||
height: Dimens.zero,
|
||||
),
|
||||
_CommentTile(
|
||||
comment: e,
|
||||
onTap: () => onTap(e),
|
||||
fontSize: showWebPreview ? 14 : 16,
|
||||
fontSize: showWebPreview ? TextDimens.pt14 : TextDimens.pt16,
|
||||
),
|
||||
const Divider(
|
||||
height: 0,
|
||||
height: Dimens.zero,
|
||||
),
|
||||
];
|
||||
}
|
||||
return <Widget>[
|
||||
FadeIn(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 6),
|
||||
padding: const EdgeInsets.only(left: Dimens.pt6),
|
||||
child: InkWell(
|
||||
onTap: () => onTap(e),
|
||||
child: Padding(
|
||||
@ -137,10 +140,12 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
if (e.deleted)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 6),
|
||||
padding: EdgeInsets.only(
|
||||
top: Dimens.pt6,
|
||||
),
|
||||
child: Text(
|
||||
'deleted',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
style: TextStyle(color: Palette.grey),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -151,15 +156,15 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 6,
|
||||
vertical: Dimens.pt8,
|
||||
horizontal: Dimens.pt6,
|
||||
),
|
||||
child: Linkify(
|
||||
text:
|
||||
'''${showCommentBy ? '${e.by}: ' : ''}${e.text}''',
|
||||
maxLines: 4,
|
||||
linkStyle: const TextStyle(
|
||||
color: Colors.orange,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) =>
|
||||
LinkUtil.launch(link.url),
|
||||
@ -171,18 +176,18 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
Text(
|
||||
e.postedDate,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
height: 0,
|
||||
height: Dimens.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -191,7 +196,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
height: 0,
|
||||
height: Dimens.zero,
|
||||
),
|
||||
];
|
||||
}
|
||||
@ -199,7 +204,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
return <Widget>[Container()];
|
||||
}).expand((List<Widget> element) => element),
|
||||
const SizedBox(
|
||||
height: 40,
|
||||
height: Dimens.pt40,
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -208,12 +213,14 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
enablePullUp: true,
|
||||
enablePullDown: enablePullDown,
|
||||
header: const WaterDropMaterialHeader(
|
||||
backgroundColor: Colors.orange,
|
||||
backgroundColor: Palette.orange,
|
||||
),
|
||||
footer: CustomFooter(
|
||||
loadStyle: LoadStyle.ShowWhenLoading,
|
||||
builder: (BuildContext context, LoadStatus? mode) {
|
||||
Widget body;
|
||||
const double height = 55;
|
||||
late final Widget body;
|
||||
|
||||
if (mode == LoadStatus.loading) {
|
||||
body = const CustomCircularProgressIndicator();
|
||||
} else if (mode == LoadStatus.failed) {
|
||||
@ -224,7 +231,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
body = const SizedBox.shrink();
|
||||
}
|
||||
return SizedBox(
|
||||
height: 55,
|
||||
height: height,
|
||||
child: Center(child: body),
|
||||
);
|
||||
},
|
||||
@ -254,12 +261,14 @@ class _CommentTile extends StatelessWidget {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt12,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
@ -281,7 +290,7 @@ class _CommentTile extends StatelessWidget {
|
||||
child: Text(
|
||||
comment.metadata,
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
color: Palette.grey,
|
||||
fontSize: fontSize - 2,
|
||||
),
|
||||
maxLines: 1,
|
||||
@ -290,7 +299,7 @@ class _CommentTile extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -2,9 +2,11 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/link_preview/link_view.dart';
|
||||
import 'package:hacki/screens/widgets/link_preview/web_analyzer.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class LinkPreview extends StatefulWidget {
|
||||
@ -97,7 +99,7 @@ class LinkPreview extends StatefulWidget {
|
||||
final bool removeElevation;
|
||||
|
||||
/// Box shadow for the card. Defaults to
|
||||
/// `[BoxShadow(blurRadius: 3, color: Colors.grey)]`
|
||||
/// `[BoxShadow(blurRadius: 3, color: Palette.grey)]`
|
||||
final List<BoxShadow>? boxShadow;
|
||||
|
||||
final bool showMetadata;
|
||||
@ -163,11 +165,15 @@ class _LinkPreviewState extends State<LinkPreview> {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: widget.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(widget.borderRadius ?? 12),
|
||||
borderRadius: BorderRadius.circular(
|
||||
widget.borderRadius ?? Dimens.pt12,
|
||||
),
|
||||
boxShadow: widget.removeElevation
|
||||
? <BoxShadow>[]
|
||||
: widget.boxShadow ??
|
||||
<BoxShadow>[const BoxShadow(blurRadius: 3, color: Colors.grey)],
|
||||
<BoxShadow>[
|
||||
const BoxShadow(blurRadius: 3, color: Palette.grey),
|
||||
],
|
||||
),
|
||||
height: _height,
|
||||
child: LinkView(
|
||||
@ -194,19 +200,15 @@ class _LinkPreviewState extends State<LinkPreview> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double screenWidth = MediaQuery.of(context).size.width;
|
||||
final bool showSmallerPreviewPic = screenWidth > 428.0 && screenWidth < 850;
|
||||
final double _height = showSmallerPreviewPic
|
||||
? 100.0
|
||||
: (MediaQuery.of(context).size.height * 0.14).clamp(118.0, 140.0);
|
||||
|
||||
final Widget loadingWidget = widget.placeholderWidget ??
|
||||
Container(
|
||||
height: _height,
|
||||
height: context.storyTileHeight,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(widget.borderRadius ?? 12),
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(
|
||||
widget.borderRadius ?? Dimens.pt12,
|
||||
),
|
||||
color: Palette.grey[200],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Text('Fetching data...'),
|
||||
@ -217,13 +219,13 @@ class _LinkPreviewState extends State<LinkPreview> {
|
||||
final WebInfo? info = _info as WebInfo?;
|
||||
loadedWidget = _info == null
|
||||
? _buildLinkContainer(
|
||||
_height,
|
||||
context.storyTileHeight,
|
||||
title: _errorTitle,
|
||||
desc: _errorBody,
|
||||
imageUri: null,
|
||||
)
|
||||
: _buildLinkContainer(
|
||||
_height,
|
||||
context.storyTileHeight,
|
||||
title: _errorTitle,
|
||||
desc: WebAnalyzer.isNotEmpty(info!.description)
|
||||
? info.description
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class LinkView extends StatelessWidget {
|
||||
const LinkView({
|
||||
@ -74,13 +75,13 @@ class LinkView extends StatelessWidget {
|
||||
final TextStyle _titleFontSize = titleTextStyle ??
|
||||
TextStyle(
|
||||
fontSize: computeTitleFontSize(layoutWidth),
|
||||
color: Colors.black,
|
||||
color: Palette.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
final TextStyle _bodyFontSize = bodyTextStyle ??
|
||||
TextStyle(
|
||||
fontSize: computeTitleFontSize(layoutWidth) - 1,
|
||||
color: Colors.grey,
|
||||
color: Palette.grey,
|
||||
fontWeight: FontWeight.w400,
|
||||
);
|
||||
|
||||
@ -146,7 +147,7 @@ class LinkView extends StatelessWidget {
|
||||
|
||||
Widget _buildTitleContainer(TextStyle _titleTS, int _maxLines) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4, 2, 3, 1),
|
||||
padding: const EdgeInsets.fromLTRB(4, 2, 3, 0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
@ -167,7 +168,7 @@ class LinkView extends StatelessWidget {
|
||||
return Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(5, 3, 5, 0),
|
||||
padding: const EdgeInsets.fromLTRB(5, 2, 5, 0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
if (showMetadata)
|
||||
|
@ -140,7 +140,7 @@ class WebAnalyzer {
|
||||
|
||||
while (comment == null && index < story.kids.length) {
|
||||
comment = await locator
|
||||
.get<CacheRepository>()
|
||||
.get<OfflineRepository>()
|
||||
.getCachedComment(id: story.kids.elementAt(index));
|
||||
index++;
|
||||
}
|
||||
|