Compare commits

...

10 Commits

Author SHA1 Message Date
19f2107d95 v0.2.28 (#65)
* bumped version.

* fixed comments cubit and story tile.

* cancel subscription on error.
2022-07-02 01:14:40 -07:00
c9b2d82dfc v0.2.27 (#64)
* bumped version.

* fixed comment cubit.

* fixed share button.

* fixed share dialog.

* added lazy loading.

* bumped version.

* fixed lazy loading.

* bumped version.

* updated screenshots.

* added customization of fetch mode and comments order.

* updated screenshots.

* added haptic feedback.
2022-06-30 18:32:11 -07:00
56e442b09f v0.2.26 (#63)
* added integration test.

* replace listview with listview.builder

* added cache.

* bumped version.

* bumped version.

* updated github action.

* bumped version.

* fixed time machine cubit.

* fixed time machine cubit.

* reverted changes.

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

* improved cache.

* improved comment cache.

* updated default val for navigationMode.
2022-06-28 00:08:07 -07:00
f0d6cac3fd v0.2.24 (#61)
* bumped version.

* improved collapse.

* improved download speed.

* improved err handling.

* improved error handling.

* improved error handling.

* improved logging.

* improved logging.

* bumped version.
2022-06-27 00:47:22 -07:00
a90d52f348 v0.2.23 (#60)
* fixed #58

* bumped version.
2022-06-23 19:38:14 -07:00
cff4a3c5c4 v0.2.22 (#59)
* bumped version.

* cleaned up code.

* bumped version.

* fixed lint.

* updated android config.

* prevent backup of secure storage.

* small fix.

* cleaned up code.

* cleaned up.

* small fix
2022-06-22 23:44:49 -07:00
502faaf188 corrected spelling. 2022-06-22 10:28:27 -07:00
116 changed files with 2266 additions and 1326 deletions

View File

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

View File

@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
android { android {
compileSdkVersion 31 compileSdkVersion 32
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -49,10 +49,9 @@ android {
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.jiaqifeng.hacki" applicationId "com.jiaqifeng.hacki"
minSdkVersion 26 minSdkVersion 26
targetSdkVersion 30 targetSdkVersion 32
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }
@ -78,5 +77,5 @@ flutter {
} }
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
} }

View File

@ -16,8 +16,10 @@
</queries> </queries>
<application <application
android:label="hacki" android:label="Hacki"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
@ -25,6 +27,7 @@
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:exported="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<exclude domain="sharedpref" path="FlutterSecureStorage"/>
</full-backup-content>

View File

@ -1,12 +1,12 @@
buildscript { buildscript {
ext.kotlin_version = '1.6.10' ext.kotlin_version = '1.7.0'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { 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" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 935 KiB

After

Width:  |  Height:  |  Size: 820 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

After

Width:  |  Height:  |  Size: 406 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 935 KiB

After

Width:  |  Height:  |  Size: 820 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

After

Width:  |  Height:  |  Size: 406 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,31 +11,56 @@ import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/screens.dart'; import 'package:hacki/screens/screens.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
import 'package:logger/logger.dart';
part 'comments_state.dart'; part 'comments_state.dart';
class CommentsCubit extends Cubit<CommentsState> { class CommentsCubit extends Cubit<CommentsState> {
CommentsCubit({ CommentsCubit({
CacheService? cacheService, required CollapseCache collapseCache,
CacheRepository? cacheRepository, CommentCache? commentCache,
OfflineRepository? offlineRepository,
StoriesRepository? storiesRepository, StoriesRepository? storiesRepository,
SembastRepository? sembastRepository, SembastRepository? sembastRepository,
Logger? logger,
required bool offlineReading, required bool offlineReading,
required Item item, required Item item,
}) : _cacheService = cacheService ?? locator.get<CacheService>(), required FetchMode defaultFetchMode,
_cacheRepository = cacheRepository ?? locator.get<CacheRepository>(), required CommentsOrder defaultCommentsOrder,
}) : _collapseCache = collapseCache,
_commentCache = commentCache ?? locator.get<CommentCache>(),
_offlineRepository =
offlineRepository ?? locator.get<OfflineRepository>(),
_storiesRepository = _storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(), storiesRepository ?? locator.get<StoriesRepository>(),
_sembastRepository = _sembastRepository =
sembastRepository ?? locator.get<SembastRepository>(), sembastRepository ?? locator.get<SembastRepository>(),
super(CommentsState.init(offlineReading: offlineReading, item: item)); _logger = logger ?? locator.get<Logger>(),
super(
CommentsState.init(
offlineReading: offlineReading,
item: item,
fetchMode: defaultFetchMode,
order: defaultCommentsOrder,
),
);
final CacheService _cacheService; final CollapseCache _collapseCache;
final CacheRepository _cacheRepository; final CommentCache _commentCache;
final OfflineRepository _offlineRepository;
final StoriesRepository _storiesRepository; final StoriesRepository _storiesRepository;
final SembastRepository _sembastRepository; final SembastRepository _sembastRepository;
final Logger _logger;
/// The [StreamSubscription] for stream (both lazy or eager)
/// fetching comments posted directly to the story.
StreamSubscription<Comment>? _streamSubscription; StreamSubscription<Comment>? _streamSubscription;
/// The map of [StreamSubscription] for streams
/// fetching comments lazily. [int] is the id of parent comment.
final Map<int, StreamSubscription<Comment>> _streamSubscriptions =
<int, StreamSubscription<Comment>>{};
static const int _pageSize = 20; static const int _pageSize = 20;
@override @override
@ -47,6 +72,7 @@ class CommentsCubit extends Cubit<CommentsState> {
Future<void> init({ Future<void> init({
bool onlyShowTargetComment = false, bool onlyShowTargetComment = false,
bool useCommentCache = false,
List<Comment>? targetParents, List<Comment>? targetParents,
}) async { }) async {
if (onlyShowTargetComment && (targetParents?.isNotEmpty ?? false)) { if (onlyShowTargetComment && (targetParents?.isNotEmpty ?? false)) {
@ -59,7 +85,7 @@ class CommentsCubit extends Cubit<CommentsState> {
); );
_streamSubscription = _storiesRepository _streamSubscription = _storiesRepository
.fetchCommentsStream( .fetchAllCommentsRecursivelyStream(
ids: targetParents!.last.kids, ids: targetParents!.last.kids,
level: targetParents.last.level + 1, level: targetParents.last.level + 1,
) )
@ -69,35 +95,48 @@ class CommentsCubit extends Cubit<CommentsState> {
return; return;
} }
emit(state.copyWith(status: CommentsStatus.loading)); emit(
state.copyWith(
status: CommentsStatus.loading,
comments: <Comment>[],
currentPage: 0,
),
);
final Item item = state.item; final Item item = state.item;
final Item updatedItem = state.offlineReading final Item updatedItem = state.offlineReading
? item ? item
: await _storiesRepository.fetchItemBy(id: item.id) ?? item; : await _storiesRepository.fetchItemBy(id: item.id) ?? item;
final List<int> kids = () { final List<int> kids = sortKids(updatedItem.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));
}
}();
emit(state.copyWith(item: updatedItem)); emit(state.copyWith(item: updatedItem));
if (state.offlineReading) { if (state.offlineReading) {
_streamSubscription = _cacheRepository _streamSubscription = _offlineRepository
.getCachedCommentsStream(ids: kids) .getCachedCommentsStream(ids: kids)
.listen(_onCommentFetched) .listen(_onCommentFetched)
..onDone(_onDone); ..onDone(_onDone);
} else { } else {
_streamSubscription = _storiesRepository switch (state.fetchMode) {
.fetchCommentsStream(ids: kids) case FetchMode.lazy:
.listen(_onCommentFetched) _streamSubscription = _storiesRepository
..onDone(_onDone); .fetchCommentsStream(
ids: kids,
getFromCache: useCommentCache ? _commentCache.getComment : null,
)
.listen(_onCommentFetched)
..onDone(_onDone);
break;
case FetchMode.eager:
_streamSubscription = _storiesRepository
.fetchAllCommentsRecursivelyStream(
ids: kids,
getFromCache: useCommentCache ? _commentCache.getComment : null,
)
.listen(_onCommentFetched)
..onDone(_onDone);
break;
}
} }
} }
@ -111,37 +150,47 @@ class CommentsCubit extends Cubit<CommentsState> {
return; return;
} }
_cacheService
..resetComments()
..resetCollapsedComments();
emit( emit(
state.copyWith( state.copyWith(
status: CommentsStatus.loading, status: CommentsStatus.loading,
comments: <Comment>[],
), ),
); );
_collapseCache.resetCollapsedComments();
await _streamSubscription?.cancel(); await _streamSubscription?.cancel();
for (final int id in _streamSubscriptions.keys) {
await _streamSubscriptions[id]?.cancel();
}
_streamSubscriptions.clear();
emit(
state.copyWith(
comments: <Comment>[],
currentPage: 0,
),
);
final Item item = state.item; final Item item = state.item;
final Item updatedItem = final Item updatedItem =
await _storiesRepository.fetchItemBy(id: item.id) ?? item; await _storiesRepository.fetchItemBy(id: item.id) ?? item;
final List<int> kids = () { final List<int> kids = sortKids(updatedItem.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));
}
}();
_streamSubscription = _storiesRepository if (state.fetchMode == FetchMode.lazy) {
.fetchCommentsStream(ids: kids) _streamSubscription = _storiesRepository
.listen(_onCommentFetched) .fetchCommentsStream(
..onDone(_onDone); ids: kids,
)
.listen(_onCommentFetched)
..onDone(_onDone);
} else {
_streamSubscription = _storiesRepository
.fetchAllCommentsRecursivelyStream(
ids: kids,
)
.listen(_onCommentFetched)
..onDone(_onDone);
}
emit( emit(
state.copyWith( state.copyWith(
@ -156,17 +205,67 @@ class CommentsCubit extends Cubit<CommentsState> {
emit( emit(
state.copyWith( state.copyWith(
onlyShowTargetComment: false, onlyShowTargetComment: false,
comments: <Comment>[],
item: story, item: story,
), ),
); );
init(); init();
} }
void loadMore() { /// [comment] is only used for lazy fetching.
if (_streamSubscription != null) { void loadMore({Comment? comment}) {
emit(state.copyWith(status: CommentsStatus.loading)); switch (state.fetchMode) {
_streamSubscription?.resume(); case FetchMode.lazy:
if (comment == null) return;
if (_streamSubscriptions.containsKey(comment.id)) return;
final int level = comment.level + 1;
int offset = 0;
/// Ignoring because the subscription will be cancelled in close()
// ignore: cancel_subscriptions
final StreamSubscription<Comment> streamSubscription =
_storiesRepository
.fetchCommentsStream(ids: comment.kids)
.listen((Comment cmt) {
_collapseCache.addKid(cmt.id, to: cmt.parent);
_commentCache.cacheComment(cmt);
_sembastRepository.cacheComment(cmt);
final List<LinkifyElement> elements = _linkify(
cmt.text,
);
final BuildableComment buildableComment =
BuildableComment.fromComment(cmt, elements: elements);
emit(
state.copyWith(
comments: <Comment>[...state.comments]..insert(
state.comments.indexOf(comment) + offset + 1,
buildableComment.copyWith(level: level),
),
),
);
offset++;
})
..onDone(() {
_streamSubscriptions[comment.id]?.cancel();
_streamSubscriptions.remove(comment.id);
})
..onError((dynamic error) {
_logger.e(error);
_streamSubscriptions[comment.id]?.cancel();
_streamSubscriptions.remove(comment.id);
});
_streamSubscriptions[comment.id] = streamSubscription;
break;
case FetchMode.eager:
if (_streamSubscription != null) {
emit(state.copyWith(status: CommentsStatus.loading));
_streamSubscription?.resume();
}
break;
} }
} }
@ -193,11 +292,41 @@ class CommentsCubit extends Cubit<CommentsState> {
} }
void onOrderChanged(CommentsOrder? order) { void onOrderChanged(CommentsOrder? order) {
HapticFeedback.selectionClick();
if (order == null) return; if (order == null) return;
if (state.order == order) return;
HapticFeedback.selectionClick();
_streamSubscription?.cancel(); _streamSubscription?.cancel();
emit(state.copyWith(order: order, comments: <Comment>[])); for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
init(); s.cancel();
}
_streamSubscriptions.clear();
emit(state.copyWith(order: order));
init(useCommentCache: true);
}
void onFetchModeChanged(FetchMode? fetchMode) {
if (fetchMode == null) return;
if (state.fetchMode == fetchMode) return;
_collapseCache.resetCollapsedComments();
HapticFeedback.selectionClick();
_streamSubscription?.cancel();
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
s.cancel();
}
_streamSubscriptions.clear();
emit(state.copyWith(fetchMode: fetchMode));
init(useCommentCache: true);
}
List<int> sortKids(List<int> kids) {
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() { void _onDone() {
@ -212,9 +341,8 @@ class CommentsCubit extends Cubit<CommentsState> {
void _onCommentFetched(Comment? comment) { void _onCommentFetched(Comment? comment) {
if (comment != null) { if (comment != null) {
_cacheService _collapseCache.addKid(comment.id, to: comment.parent);
..addKid(comment.id, to: comment.parent) _commentCache.cacheComment(comment);
..cacheComment(comment);
_sembastRepository.cacheComment(comment); _sembastRepository.cacheComment(comment);
final List<LinkifyElement> elements = _linkify( final List<LinkifyElement> elements = _linkify(
@ -231,21 +359,24 @@ class CommentsCubit extends Cubit<CommentsState> {
emit(state.copyWith(comments: updatedComments)); emit(state.copyWith(comments: updatedComments));
if (updatedComments.length >= _pageSize + _pageSize * state.currentPage && if (state.fetchMode == FetchMode.eager) {
updatedComments.length <= if (updatedComments.length >=
_pageSize * 2 + _pageSize * state.currentPage) { _pageSize + _pageSize * state.currentPage &&
final bool isHidden = _cacheService.isHidden(comment.id); updatedComments.length <=
_pageSize * 2 + _pageSize * state.currentPage) {
final bool isHidden = _collapseCache.isHidden(comment.id);
if (!isHidden) { if (!isHidden) {
_streamSubscription?.pause(); _streamSubscription?.pause();
}
emit(
state.copyWith(
currentPage: state.currentPage + 1,
status: CommentsStatus.loaded,
),
);
} }
emit(
state.copyWith(
currentPage: state.currentPage + 1,
status: CommentsStatus.loaded,
),
);
} }
} }
} }
@ -278,6 +409,9 @@ class CommentsCubit extends Cubit<CommentsState> {
@override @override
Future<void> close() async { Future<void> close() async {
await _streamSubscription?.cancel(); await _streamSubscription?.cancel();
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
await s.cancel();
}
await super.close(); await super.close();
} }
} }

View File

@ -14,6 +14,11 @@ enum CommentsOrder {
oldestFirst, oldestFirst,
} }
enum FetchMode {
lazy,
eager,
}
class CommentsState extends Equatable { class CommentsState extends Equatable {
const CommentsState({ const CommentsState({
required this.item, required this.item,
@ -21,6 +26,7 @@ class CommentsState extends Equatable {
required this.status, required this.status,
required this.fetchParentStatus, required this.fetchParentStatus,
required this.order, required this.order,
required this.fetchMode,
required this.onlyShowTargetComment, required this.onlyShowTargetComment,
required this.offlineReading, required this.offlineReading,
required this.currentPage, required this.currentPage,
@ -29,10 +35,11 @@ class CommentsState extends Equatable {
CommentsState.init({ CommentsState.init({
required this.offlineReading, required this.offlineReading,
required this.item, required this.item,
required this.fetchMode,
required this.order,
}) : comments = <Comment>[], }) : comments = <Comment>[],
status = CommentsStatus.init, status = CommentsStatus.init,
fetchParentStatus = CommentsStatus.init, fetchParentStatus = CommentsStatus.init,
order = CommentsOrder.natural,
onlyShowTargetComment = false, onlyShowTargetComment = false,
currentPage = 0; currentPage = 0;
@ -41,6 +48,7 @@ class CommentsState extends Equatable {
final CommentsStatus status; final CommentsStatus status;
final CommentsStatus fetchParentStatus; final CommentsStatus fetchParentStatus;
final CommentsOrder order; final CommentsOrder order;
final FetchMode fetchMode;
final bool onlyShowTargetComment; final bool onlyShowTargetComment;
final bool offlineReading; final bool offlineReading;
final int currentPage; final int currentPage;
@ -51,6 +59,7 @@ class CommentsState extends Equatable {
CommentsStatus? status, CommentsStatus? status,
CommentsStatus? fetchParentStatus, CommentsStatus? fetchParentStatus,
CommentsOrder? order, CommentsOrder? order,
FetchMode? fetchMode,
bool? onlyShowTargetComment, bool? onlyShowTargetComment,
bool? offlineReading, bool? offlineReading,
int? currentPage, int? currentPage,
@ -61,6 +70,7 @@ class CommentsState extends Equatable {
fetchParentStatus: fetchParentStatus ?? this.fetchParentStatus, fetchParentStatus: fetchParentStatus ?? this.fetchParentStatus,
status: status ?? this.status, status: status ?? this.status,
order: order ?? this.order, order: order ?? this.order,
fetchMode: fetchMode ?? this.fetchMode,
onlyShowTargetComment: onlyShowTargetComment:
onlyShowTargetComment ?? this.onlyShowTargetComment, onlyShowTargetComment ?? this.onlyShowTargetComment,
offlineReading: offlineReading ?? this.offlineReading, offlineReading: offlineReading ?? this.offlineReading,
@ -68,6 +78,8 @@ class CommentsState extends Equatable {
); );
} }
Set<int> get commentIds => comments.map((Comment e) => e.id).toSet();
@override @override
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
item, item,
@ -75,6 +87,7 @@ class CommentsState extends Equatable {
status, status,
fetchParentStatus, fetchParentStatus,
order, order,
fetchMode,
onlyShowTargetComment, onlyShowTargetComment,
offlineReading, offlineReading,
currentPage, currentPage,

View File

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

View File

@ -1,6 +1,7 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/comments/comments_cubit.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
part 'preference_state.dart'; part 'preference_state.dart';
@ -33,6 +34,10 @@ class PreferenceCubit extends Cubit<PreferenceState> {
.then((bool value) => emit(state.copyWith(markReadStories: value))); .then((bool value) => emit(state.copyWith(markReadStories: value)));
_preferenceRepository.shouldShowMetadata _preferenceRepository.shouldShowMetadata
.then((bool value) => emit(state.copyWith(showMetadata: value))); .then((bool value) => emit(state.copyWith(showMetadata: value)));
_preferenceRepository.fetchMode
.then((FetchMode value) => emit(state.copyWith(fetchMode: value)));
_preferenceRepository.commentsOrder
.then((CommentsOrder value) => emit(state.copyWith(order: value)));
} }
void toggleNotificationMode() { void toggleNotificationMode() {
@ -74,4 +79,16 @@ class PreferenceCubit extends Cubit<PreferenceState> {
emit(state.copyWith(showMetadata: !state.showMetadata)); emit(state.copyWith(showMetadata: !state.showMetadata));
_preferenceRepository.toggleMetadataMode(); _preferenceRepository.toggleMetadataMode();
} }
void selectFetchMode(FetchMode? fetchMode) {
if (fetchMode == null || state.fetchMode == fetchMode) return;
emit(state.copyWith(fetchMode: fetchMode));
_preferenceRepository.selectFetchMode(fetchMode);
}
void selectCommentsOrder(CommentsOrder? order) {
if (order == null || state.order == order) return;
emit(state.copyWith(order: order));
_preferenceRepository.selectCommentsOrder(order);
}
} }

View File

@ -10,6 +10,8 @@ class PreferenceState extends Equatable {
required this.useReader, required this.useReader,
required this.markReadStories, required this.markReadStories,
required this.showMetadata, required this.showMetadata,
required this.fetchMode,
required this.order,
}); });
const PreferenceState.init() const PreferenceState.init()
@ -20,7 +22,9 @@ class PreferenceState extends Equatable {
useTrueDark = false, useTrueDark = false,
useReader = false, useReader = false,
markReadStories = false, markReadStories = false,
showMetadata = false; showMetadata = false,
fetchMode = FetchMode.eager,
order = CommentsOrder.natural;
final bool showNotification; final bool showNotification;
final bool showComplexStoryTile; final bool showComplexStoryTile;
@ -30,6 +34,8 @@ class PreferenceState extends Equatable {
final bool useReader; final bool useReader;
final bool markReadStories; final bool markReadStories;
final bool showMetadata; final bool showMetadata;
final FetchMode fetchMode;
final CommentsOrder order;
PreferenceState copyWith({ PreferenceState copyWith({
bool? showNotification, bool? showNotification,
@ -40,6 +46,8 @@ class PreferenceState extends Equatable {
bool? useReader, bool? useReader,
bool? markReadStories, bool? markReadStories,
bool? showMetadata, bool? showMetadata,
FetchMode? fetchMode,
CommentsOrder? order,
}) { }) {
return PreferenceState( return PreferenceState(
showNotification: showNotification ?? this.showNotification, showNotification: showNotification ?? this.showNotification,
@ -50,6 +58,8 @@ class PreferenceState extends Equatable {
useReader: useReader ?? this.useReader, useReader: useReader ?? this.useReader,
markReadStories: markReadStories ?? this.markReadStories, markReadStories: markReadStories ?? this.markReadStories,
showMetadata: showMetadata ?? this.showMetadata, showMetadata: showMetadata ?? this.showMetadata,
fetchMode: fetchMode ?? this.fetchMode,
order: order ?? this.order,
); );
} }
@ -63,5 +73,7 @@ class PreferenceState extends Equatable {
useReader, useReader,
markReadStories, markReadStories,
showMetadata, showMetadata,
fetchMode,
order,
]; ];
} }

View File

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

View File

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

View File

@ -0,0 +1,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;
}
}

View File

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

View File

@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/main.dart'; import 'package:hacki/main.dart';
import 'package:hacki/screens/screens.dart' show ItemScreen, ItemScreenArgs; import 'package:hacki/screens/screens.dart' show ItemScreen, ItemScreenArgs;
import 'package:hacki/styles/styles.dart';
extension StateExtension on State { extension StateExtension on State {
void showSnackBar({ void showSnackBar({
@ -12,7 +13,7 @@ extension StateExtension on State {
}) { }) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
backgroundColor: Colors.deepOrange, backgroundColor: Palette.deepOrange,
content: Text(content), content: Text(content),
action: action != null && label != null action: action != null && label != null
? SnackBarAction( ? SnackBarAction(

View File

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

View File

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:feature_discovery/feature_discovery.dart'; import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -14,7 +15,9 @@ import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/repositories/repositories.dart' show PreferenceRepository; import 'package:hacki/repositories/repositories.dart' show PreferenceRepository;
import 'package:hacki/screens/screens.dart'; import 'package:hacki/screens/screens.dart';
import 'package:hacki/services/custom_bloc_observer.dart';
import 'package:hacki/services/fetcher.dart'; import 'package:hacki/services/fetcher.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:rxdart/rxdart.dart' show BehaviorSubject; import 'package:rxdart/rxdart.dart' show BehaviorSubject;
@ -29,9 +32,13 @@ final BehaviorSubject<String?> selectNotificationSubject =
final BehaviorSubject<String?> siriSuggestionSubject = final BehaviorSubject<String?> siriSuggestionSubject =
BehaviorSubject<String?>(); BehaviorSubject<String?>();
Future<void> main() async { late final bool isTesting;
Future<void> main({bool testing = false}) async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
isTesting = testing;
if (Platform.isIOS) { if (Platform.isIOS) {
unawaited( unawaited(
Workmanager().initialize( Workmanager().initialize(
@ -85,25 +92,26 @@ Future<void> main() async {
final bool trueDarkMode = final bool trueDarkMode =
prefs.getBool(PreferenceRepository.trueDarkModeKey) ?? false; prefs.getBool(PreferenceRepository.trueDarkModeKey) ?? false;
// Uncomment code below for running with logging. if (kReleaseMode) {
// BlocOverrides.runZoned( runApp(
// () { HackiApp(
// runApp( savedThemeMode: savedThemeMode,
// HackiApp( trueDarkMode: trueDarkMode,
// savedThemeMode: savedThemeMode, ),
// trueDarkMode: trueDarkMode, );
// ), } else {
// ); BlocOverrides.runZoned(
// }, () {
// blocObserver: CustomBlocObserver(), runApp(
// ); HackiApp(
savedThemeMode: savedThemeMode,
runApp( trueDarkMode: trueDarkMode,
HackiApp( ),
savedThemeMode: savedThemeMode, );
trueDarkMode: trueDarkMode, },
), blocObserver: CustomBlocObserver(),
); );
}
} }
class HackiApp extends StatelessWidget { class HackiApp extends StatelessWidget {
@ -182,19 +190,19 @@ class HackiApp extends StatelessWidget {
], ],
child: AdaptiveTheme( child: AdaptiveTheme(
light: ThemeData( light: ThemeData(
primarySwatch: Colors.orange, primarySwatch: Palette.orange,
), ),
dark: ThemeData( dark: ThemeData(
brightness: Brightness.dark, brightness: Brightness.dark,
primarySwatch: Colors.orange, primarySwatch: Palette.orange,
canvasColor: trueDarkMode ? Colors.black : null, canvasColor: trueDarkMode ? Palette.black : null,
), ),
initial: savedThemeMode ?? AdaptiveThemeMode.system, initial: savedThemeMode ?? AdaptiveThemeMode.system,
builder: (ThemeData theme, ThemeData darkTheme) { builder: (ThemeData theme, ThemeData darkTheme) {
final ThemeData trueDarkTheme = ThemeData( final ThemeData trueDarkTheme = ThemeData(
brightness: Brightness.dark, brightness: Brightness.dark,
primarySwatch: Colors.orange, primarySwatch: Palette.orange,
canvasColor: Colors.black, canvasColor: Palette.black,
); );
return FutureBuilder<AdaptiveThemeMode?>( return FutureBuilder<AdaptiveThemeMode?>(
future: AdaptiveTheme.getThemeMode(), future: AdaptiveTheme.getThemeMode(),

View File

@ -1,5 +1,3 @@
import 'dart:convert';
import 'package:hacki/models/item.dart'; import 'package:hacki/models/item.dart';
enum StoryType { enum StoryType {
@ -113,9 +111,10 @@ class Story extends Item {
@override @override
String toString() { String toString() {
final String prettyString = // final String prettyString =
const JsonEncoder.withIndent(' ').convert(this); // const JsonEncoder.withIndent(' ').convert(this);
return 'Story $prettyString'; // return 'Story $prettyString';
return 'Story $id';
} }
@override @override

View File

@ -5,16 +5,20 @@ import 'package:hacki/config/locator.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/postable_repository.dart'; import 'package:hacki/repositories/postable_repository.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
import 'package:logger/logger.dart';
class AuthRepository extends PostableRepository { class AuthRepository extends PostableRepository {
AuthRepository({ AuthRepository({
Dio? dio, Dio? dio,
PreferenceRepository? preferenceRepository, PreferenceRepository? preferenceRepository,
Logger? logger,
}) : _preferenceRepository = }) : _preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>(), preferenceRepository ?? locator.get<PreferenceRepository>(),
_logger = logger ?? locator.get<Logger>(),
super(dio: dio); super(dio: dio);
final PreferenceRepository _preferenceRepository; final PreferenceRepository _preferenceRepository;
final Logger _logger;
static const String _authority = 'news.ycombinator.com'; static const String _authority = 'news.ycombinator.com';
@ -38,10 +42,15 @@ class AuthRepository extends PostableRepository {
final bool success = await performDefaultPost(uri, data); final bool success = await performDefaultPost(uri, data);
if (success) { if (success) {
await _preferenceRepository.setAuth( try {
username: username, await _preferenceRepository.setAuth(
password: password, username: username,
); password: password,
);
} catch (_) {
_logger.e(_);
return false;
}
} }
return success; return success;

View File

@ -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.''';
}
}
}

View 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.''';
}
}
}

View File

@ -2,6 +2,9 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/comments/comments_cubit.dart';
import 'package:logger/logger.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:synced_shared_preferences/synced_shared_preferences.dart'; import 'package:synced_shared_preferences/synced_shared_preferences.dart';
@ -10,9 +13,11 @@ class PreferenceRepository {
SyncedSharedPreferences? syncedPrefs, SyncedSharedPreferences? syncedPrefs,
Future<SharedPreferences>? prefs, Future<SharedPreferences>? prefs,
FlutterSecureStorage? secureStorage, FlutterSecureStorage? secureStorage,
Logger? logger,
}) : _syncedPrefs = syncedPrefs ?? SyncedSharedPreferences.instance, }) : _syncedPrefs = syncedPrefs ?? SyncedSharedPreferences.instance,
_prefs = prefs ?? SharedPreferences.getInstance(), _prefs = prefs ?? SharedPreferences.getInstance(),
_secureStorage = secureStorage ?? const FlutterSecureStorage(); _secureStorage = secureStorage ?? const FlutterSecureStorage(),
_logger = logger ?? locator.get<Logger>();
static const String _usernameKey = 'username'; static const String _usernameKey = 'username';
static const String _passwordKey = 'password'; static const String _passwordKey = 'password';
@ -38,20 +43,26 @@ class PreferenceRepository {
static const String _navigationModeKey = 'navigationMode'; static const String _navigationModeKey = 'navigationMode';
static const String _eyeCandyModeKey = 'eyeCandyMode'; static const String _eyeCandyModeKey = 'eyeCandyMode';
static const String _markReadStoriesModeKey = 'markReadStoriesMode'; static const String _markReadStoriesModeKey = 'markReadStoriesMode';
static const String _fetchModeKey = 'fetchMode';
static const String _commentsOrderKey = 'commentsOrder';
static const bool _notificationModeDefaultValue = true; static const bool _notificationModeDefaultValue = true;
static const bool _displayModeDefaultValue = true; static const bool _displayModeDefaultValue = true;
static const bool _navigationModeDefaultValue = true; static const bool _navigationModeDefaultValueIOS = true;
static const bool _navigationModeDefaultValueAndroid = false;
static const bool _eyeCandyModeDefaultValue = false; static const bool _eyeCandyModeDefaultValue = false;
static const bool _trueDarkModeDefaultValue = false; static const bool _trueDarkModeDefaultValue = false;
static const bool _readerModeDefaultValue = true; static const bool _readerModeDefaultValue = true;
static const bool _markReadStoriesModeDefaultValue = true; static const bool _markReadStoriesModeDefaultValue = true;
static const bool _isFirstLaunchKeyDefaultValue = true; static const bool _isFirstLaunchKeyDefaultValue = true;
static const bool _metadataModeDefaultValue = true; static const bool _metadataModeDefaultValue = true;
static final int _fetchModeDefaultValue = FetchMode.eager.index;
static final int _commentsOrderDefaultValue = CommentsOrder.natural.index;
final SyncedSharedPreferences _syncedPrefs; final SyncedSharedPreferences _syncedPrefs;
final Future<SharedPreferences> _prefs; final Future<SharedPreferences> _prefs;
final FlutterSecureStorage _secureStorage; final FlutterSecureStorage _secureStorage;
final Logger _logger;
Future<bool> get loggedIn async => await username != null; Future<bool> get loggedIn async => await username != null;
@ -82,7 +93,10 @@ class PreferenceRepository {
Future<bool> get shouldShowWebFirst async => _prefs.then( Future<bool> get shouldShowWebFirst async => _prefs.then(
(SharedPreferences prefs) => (SharedPreferences prefs) =>
prefs.getBool(_navigationModeKey) ?? _navigationModeDefaultValue, prefs.getBool(_navigationModeKey) ??
(Platform.isAndroid
? _navigationModeDefaultValueAndroid
: _navigationModeDefaultValueIOS),
); );
Future<bool> get shouldShowEyeCandy async => _prefs.then( Future<bool> get shouldShowEyeCandy async => _prefs.then(
@ -111,6 +125,17 @@ class PreferenceRepository {
_markReadStoriesModeDefaultValue, _markReadStoriesModeDefaultValue,
); );
Future<FetchMode> get fetchMode async => _prefs.then(
(SharedPreferences prefs) => FetchMode.values
.elementAt(prefs.getInt(_fetchModeKey) ?? _fetchModeDefaultValue),
);
Future<CommentsOrder> get commentsOrder async => _prefs.then(
(SharedPreferences prefs) => CommentsOrder.values.elementAt(
prefs.getInt(_commentsOrderKey) ?? _commentsOrderDefaultValue,
),
);
Future<bool> hasPushed(int commentId) async => Future<bool> hasPushed(int commentId) async =>
_prefs.then((SharedPreferences prefs) { _prefs.then((SharedPreferences prefs) {
final bool? val = prefs.getBool(_getPushNotificationKey(commentId)); final bool? val = prefs.getBool(_getPushNotificationKey(commentId));
@ -140,8 +165,29 @@ class PreferenceRepository {
required String username, required String username,
required String password, required String password,
}) async { }) async {
await _secureStorage.write(key: _usernameKey, value: username); const AndroidOptions androidOptions = AndroidOptions(resetOnError: true);
await _secureStorage.write(key: _passwordKey, value: password); 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 { Future<void> removeAuth() async {
@ -165,8 +211,10 @@ class PreferenceRepository {
Future<void> toggleNavigationMode() async { Future<void> toggleNavigationMode() async {
final SharedPreferences prefs = await _prefs; final SharedPreferences prefs = await _prefs;
final bool currentMode = final bool currentMode = prefs.getBool(_navigationModeKey) ??
prefs.getBool(_navigationModeKey) ?? _navigationModeDefaultValue; (Platform.isAndroid
? _navigationModeDefaultValueAndroid
: _navigationModeDefaultValueIOS);
await prefs.setBool(_navigationModeKey, !currentMode); await prefs.setBool(_navigationModeKey, !currentMode);
} }
@ -205,6 +253,18 @@ class PreferenceRepository {
await prefs.setBool(_metadataModeKey, !currentMode); await prefs.setBool(_metadataModeKey, !currentMode);
} }
Future<void> selectFetchMode(FetchMode fetchMode) async {
final SharedPreferences prefs = await _prefs;
final int index = fetchMode.index;
await prefs.setInt(_fetchModeKey, index);
}
Future<void> selectCommentsOrder(CommentsOrder order) async {
final SharedPreferences prefs = await _prefs;
final int index = order.index;
await prefs.setInt(_commentsOrderKey, index);
}
//#region fav //#region fav
Future<List<int>> favList({required String of}) async { Future<List<int>> favList({required String of}) async {

View File

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

View File

@ -51,9 +51,37 @@ class StoriesRepository {
Stream<Comment> fetchCommentsStream({ Stream<Comment> fetchCommentsStream({
required List<int> ids, required List<int> ids,
int level = 0, int level = 0,
Comment? Function(int)? getFromCache,
}) async* { }) async* {
for (final int id in ids) { for (final int id in ids) {
final Comment? comment = await _firebaseClient Comment? comment = getFromCache?.call(id)?.copyWith(level: level);
comment ??= await _firebaseClient
.get('${_baseUrl}item/$id.json')
.then((dynamic json) => _parseJson(json as Map<String, dynamic>?))
.then((Map<String, dynamic>? json) async {
if (json == null) return null;
final Comment comment = Comment.fromJson(json, level: level);
return comment;
});
if (comment != null) {
yield comment;
}
}
return;
}
Stream<Comment> fetchAllCommentsRecursivelyStream({
required List<int> ids,
int level = 0,
Comment? Function(int)? getFromCache,
}) async* {
for (final int id in ids) {
Comment? comment = getFromCache?.call(id)?.copyWith(level: level);
comment ??= await _firebaseClient
.get('${_baseUrl}item/$id.json') .get('${_baseUrl}item/$id.json')
.then((dynamic json) => _parseJson(json as Map<String, dynamic>?)) .then((dynamic json) => _parseJson(json as Map<String, dynamic>?))
.then((Map<String, dynamic>? json) async { .then((Map<String, dynamic>? json) async {
@ -66,9 +94,10 @@ class StoriesRepository {
if (comment != null) { if (comment != null) {
yield comment; yield comment;
yield* fetchCommentsStream( yield* fetchAllCommentsRecursivelyStream(
ids: comment.kids, ids: comment.kids,
level: level + 1, level: level + 1,
getFromCache: getFromCache,
); );
} }
} }

View File

@ -24,7 +24,9 @@ import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/screens.dart'; import 'package:hacki/screens/screens.dart';
import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart'; import 'package:hacki/utils/utils.dart';
import 'package:logger/logger.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:responsive_builder/responsive_builder.dart'; import 'package:responsive_builder/responsive_builder.dart';
@ -46,7 +48,6 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen> class _HomeScreenState extends State<HomeScreen>
with SingleTickerProviderStateMixin, RouteAware { with SingleTickerProviderStateMixin, RouteAware {
final CacheService cacheService = locator.get<CacheService>();
final Throttle featureDiscoveryDismissThrottle = Throttle( final Throttle featureDiscoveryDismissThrottle = Throttle(
delay: _throttleDelay, delay: _throttleDelay,
); );
@ -65,9 +66,10 @@ class _HomeScreenState extends State<HomeScreen>
super.didPopNext(); super.didPopNext();
if (context.read<StoriesBloc>().deviceScreenType == if (context.read<StoriesBloc>().deviceScreenType ==
DeviceScreenType.mobile) { DeviceScreenType.mobile) {
locator.get<Logger>().i('resetting comments in CommentCache');
Future<void>.delayed( Future<void>.delayed(
const Duration(milliseconds: 500), const Duration(milliseconds: 500),
cacheService.resetCollapsedComments, locator.get<CommentCache>().resetComments,
); );
} }
} }
@ -101,12 +103,14 @@ class _HomeScreenState extends State<HomeScreen>
SchedulerBinding.instance SchedulerBinding.instance
..addPostFrameCallback((_) { ..addPostFrameCallback((_) {
FeatureDiscovery.discoverFeatures( if (!isTesting) {
context, FeatureDiscovery.discoverFeatures(
const <String>{ context,
Constants.featureLogIn, const <String>{
}, Constants.featureLogIn,
); },
);
}
}) })
..addPostFrameCallback((_) { ..addPostFrameCallback((_) {
final ModalRoute<dynamic>? route = ModalRoute.of(context); final ModalRoute<dynamic>? route = ModalRoute.of(context);
@ -171,8 +175,8 @@ class _HomeScreenState extends State<HomeScreen>
HapticFeedback.lightImpact(); HapticFeedback.lightImpact();
context.read<PinCubit>().unpinStory(story); context.read<PinCubit>().unpinStory(story);
}, },
backgroundColor: Colors.red, backgroundColor: Palette.red,
foregroundColor: Colors.white, foregroundColor: Palette.white,
icon: preferenceState.showComplexStoryTile icon: preferenceState.showComplexStoryTile
? Icons.close ? Icons.close
: null, : null,
@ -181,7 +185,7 @@ class _HomeScreenState extends State<HomeScreen>
], ],
), ),
child: Container( child: Container(
color: Colors.orangeAccent.withOpacity(0.2), color: Palette.orangeAccent.withOpacity(0.2),
child: StoryTile( child: StoryTile(
key: ValueKey<String>('${story.id}-PinnedStoryTile'), key: ValueKey<String>('${story.id}-PinnedStoryTile'),
story: story, story: story,
@ -194,9 +198,9 @@ class _HomeScreenState extends State<HomeScreen>
), ),
if (state.pinnedStories.isNotEmpty) if (state.pinnedStories.isNotEmpty)
const Padding( const Padding(
padding: EdgeInsets.symmetric(horizontal: 12), padding: EdgeInsets.symmetric(horizontal: Dimens.pt12),
child: Divider( child: Divider(
color: Colors.orangeAccent, color: Palette.orangeAccent,
), ),
), ),
], ],
@ -209,27 +213,32 @@ class _HomeScreenState extends State<HomeScreen>
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: PreferredSize( appBar: PreferredSize(
preferredSize: const Size(0, 40), preferredSize: const Size(
Dimens.zero,
Dimens.pt40,
),
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
SizedBox( SizedBox(
height: MediaQuery.of(context).padding.top - 8, height: MediaQuery.of(context).padding.top - Dimens.pt8,
), ),
Theme( Theme(
data: ThemeData( data: ThemeData(
highlightColor: Colors.transparent, highlightColor: Palette.transparent,
splashColor: Colors.transparent, splashColor: Palette.transparent,
primaryColor: Theme.of(context).primaryColor, primaryColor: Theme.of(context).primaryColor,
), ),
child: TabBar( child: TabBar(
isScrollable: true, isScrollable: true,
controller: tabController, controller: tabController,
indicatorColor: Colors.orange, indicatorColor: Palette.orange,
indicator: CircleTabIndicator( indicator: CircleTabIndicator(
color: Colors.orange, color: Palette.orange,
radius: 2, radius: Dimens.pt2,
),
indicatorPadding: const EdgeInsets.only(
bottom: Dimens.pt8,
), ),
indicatorPadding: const EdgeInsets.only(bottom: 8),
onTap: (_) { onTap: (_) {
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
}, },
@ -242,10 +251,12 @@ class _HomeScreenState extends State<HomeScreen>
child: Text( child: Text(
StoriesBloc.types.elementAt(i).label, StoriesBloc.types.elementAt(i).label,
style: TextStyle( style: TextStyle(
fontSize: currentIndex == i ? 14 : 10, fontSize: currentIndex == i
? TextDimens.pt14
: TextDimens.pt10,
color: currentIndex == i color: currentIndex == i
? Colors.orange ? Palette.orange
: Colors.grey, : Palette.grey,
), ),
), ),
), ),
@ -263,8 +274,8 @@ class _HomeScreenState extends State<HomeScreen>
targetColor: Theme.of(context).primaryColor, targetColor: Theme.of(context).primaryColor,
tapTarget: const Icon( tapTarget: const Icon(
Icons.person, Icons.person,
size: 16, size: TextDimens.pt16,
color: Colors.white, color: Palette.white,
), ),
featureId: Constants.featureLogIn, featureId: Constants.featureLogIn,
title: const Text('Log in for more'), title: const Text('Log in for more'),
@ -274,7 +285,7 @@ class _HomeScreenState extends State<HomeScreen>
'posted in the past, and get in-app ' 'posted in the past, and get in-app '
'notification when there is new reply to ' 'notification when there is new reply to '
'your comments or stories.', 'your comments or stories.',
style: TextStyle(fontSize: 16), style: TextStyle(fontSize: TextDimens.pt16),
), ),
child: BlocBuilder<NotificationCubit, child: BlocBuilder<NotificationCubit,
NotificationState>( NotificationState>(
@ -292,19 +303,21 @@ class _HomeScreenState extends State<HomeScreen>
showBadge: state.unreadCommentsIds.isNotEmpty, showBadge: state.unreadCommentsIds.isNotEmpty,
borderRadius: BorderRadius.circular(100), borderRadius: BorderRadius.circular(100),
badgeContent: Container( badgeContent: Container(
height: 3, height: Dimens.pt3,
width: 3, width: Dimens.pt3,
decoration: const BoxDecoration( decoration: const BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: Colors.white, color: Palette.white,
), ),
), ),
child: Icon( child: Icon(
Icons.person, Icons.person,
size: currentIndex == 5 ? 16 : 12, size: currentIndex == 5
? TextDimens.pt16
: TextDimens.pt12,
color: currentIndex == 5 color: currentIndex == 5
? Colors.orange ? Palette.orange
: Colors.grey, : Palette.grey,
), ),
); );
}, },
@ -439,7 +452,10 @@ class _HomeScreenState extends State<HomeScreen>
locator.get<StoriesRepository>().fetchItemBy(id: id).then((Item? item) { locator.get<StoriesRepository>().fetchItemBy(id: id).then((Item? item) {
if (mounted) { if (mounted) {
if (item != null) { if (item != null) {
goToItemScreen(args: ItemScreenArgs(item: item)); goToItemScreen(
args: ItemScreenArgs(item: item),
forceNewScreen: true,
);
} }
} }
}); });
@ -506,10 +522,10 @@ class _MobileHomeScreen extends StatelessWidget {
Positioned.fill(child: homeScreen), Positioned.fill(child: homeScreen),
if (!context.read<ReminderCubit>().state.hasShown) if (!context.read<ReminderCubit>().state.hasShown)
const Positioned( const Positioned(
left: 24, left: Dimens.pt24,
right: 24, right: Dimens.pt24,
bottom: 36, bottom: Dimens.pt36,
height: 40, height: Dimens.pt40,
child: CountdownReminder(), child: CountdownReminder(),
), ),
], ],
@ -533,7 +549,7 @@ class _TabletHomeScreen extends StatelessWidget {
double homeScreenWidth = 428; double homeScreenWidth = 428;
if (sizeInfo.screenSize.width < homeScreenWidth * 2) { if (sizeInfo.screenSize.width < homeScreenWidth * 2) {
homeScreenWidth = 345.0; homeScreenWidth = 345;
} }
return BlocBuilder<SplitViewCubit, SplitViewState>( return BlocBuilder<SplitViewCubit, SplitViewState>(
@ -543,26 +559,26 @@ class _TabletHomeScreen extends StatelessWidget {
return Stack( return Stack(
children: <Widget>[ children: <Widget>[
AnimatedPositioned( AnimatedPositioned(
left: 0, left: Dimens.zero,
top: 0, top: Dimens.zero,
bottom: 0, bottom: Dimens.zero,
width: state.expanded ? 0 : homeScreenWidth, width: state.expanded ? Dimens.zero : homeScreenWidth,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.elasticOut, curve: Curves.elasticOut,
child: homeScreen, child: homeScreen,
), ),
Positioned( Positioned(
left: 24, left: Dimens.pt24,
bottom: 36, bottom: Dimens.pt36,
height: 40, height: Dimens.pt40,
width: homeScreenWidth - 24, width: homeScreenWidth - Dimens.pt24,
child: const CountdownReminder(), child: const CountdownReminder(),
), ),
AnimatedPositioned( AnimatedPositioned(
right: 0, right: Dimens.zero,
top: 0, top: Dimens.zero,
bottom: 0, bottom: Dimens.zero,
left: state.expanded ? 0 : homeScreenWidth, left: state.expanded ? Dimens.zero : homeScreenWidth,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.elasticOut, curve: Curves.elasticOut,
child: const _TabletStoryView(), child: const _TabletStoryView(),

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/screens/item/widgets/widgets.dart'; import 'package:hacki/screens/item/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
class CustomAppBar extends AppBar { class CustomAppBar extends AppBar {
CustomAppBar({ CustomAppBar({
@ -18,7 +19,7 @@ class CustomAppBar extends AppBar {
}) : super( }) : super(
key: key, key: key,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
elevation: 0, elevation: Dimens.zero,
actions: <Widget>[ actions: <Widget>[
if (splitViewEnabled) ...<Widget>[ if (splitViewEnabled) ...<Widget>[
IconButton( IconButton(
@ -26,7 +27,7 @@ class CustomAppBar extends AppBar {
expanded ?? false expanded ?? false
? FeatherIcons.minimize2 ? FeatherIcons.minimize2
: FeatherIcons.maximize2, : FeatherIcons.maximize2,
size: 20, size: TextDimens.pt20,
), ),
onPressed: () { onPressed: () {
HapticFeedback.lightImpact(); HapticFeedback.lightImpact();

View File

@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/config/constants.dart'; import 'package:hacki/config/constants.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/styles/styles.dart';
class FavIconButton extends StatelessWidget { class FavIconButton extends StatelessWidget {
const FavIconButton({ const FavIconButton({
@ -37,17 +38,17 @@ class FavIconButton extends StatelessWidget {
targetColor: Theme.of(context).primaryColor, targetColor: Theme.of(context).primaryColor,
tapTarget: Icon( tapTarget: Icon(
isFav ? Icons.favorite : Icons.favorite_border, isFav ? Icons.favorite : Icons.favorite_border,
color: Colors.white, color: Palette.white,
), ),
featureId: Constants.featureAddStoryToFavList, featureId: Constants.featureAddStoryToFavList,
title: const Text('Fav a Story'), title: const Text('Fav a Story'),
description: const Text( description: const Text(
'Add it to your favorites.', 'Add it to your favorites.',
style: TextStyle(fontSize: 16), style: TextStyle(fontSize: TextDimens.pt16),
), ),
child: Icon( child: Icon(
isFav ? Icons.favorite : Icons.favorite_border, 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: () { onPressed: () {

View File

@ -4,6 +4,7 @@ import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hacki/config/constants.dart'; import 'package:hacki/config/constants.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart'; import 'package:hacki/utils/utils.dart';
class LinkIconButton extends StatelessWidget { class LinkIconButton extends StatelessWidget {
@ -33,7 +34,7 @@ class LinkIconButton extends StatelessWidget {
targetColor: Theme.of(context).primaryColor, targetColor: Theme.of(context).primaryColor,
tapTarget: const Icon( tapTarget: const Icon(
Icons.stream, Icons.stream,
color: Colors.white, color: Palette.white,
), ),
featureId: Constants.featureOpenStoryInWebView, featureId: Constants.featureOpenStoryInWebView,
title: const Text('Open in Browser'), title: const Text('Open in Browser'),
@ -41,7 +42,7 @@ class LinkIconButton extends StatelessWidget {
'Want more than just reading and replying? ' 'Want more than just reading and replying? '
'You can tap here to open this story in a ' 'You can tap here to open this story in a '
'browser.', 'browser.',
style: TextStyle(fontSize: 16), style: TextStyle(fontSize: TextDimens.pt16),
), ),
child: const Icon( child: const Icon(
Icons.stream, Icons.stream,

View File

@ -8,6 +8,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/config/constants.dart'; import 'package:hacki/config/constants.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/styles/styles.dart';
class PinIconButton extends StatelessWidget { class PinIconButton extends StatelessWidget {
const PinIconButton({ const PinIconButton({
@ -43,7 +44,7 @@ class PinIconButton extends StatelessWidget {
targetColor: Theme.of(context).primaryColor, targetColor: Theme.of(context).primaryColor,
tapTarget: Icon( tapTarget: Icon(
pinned ? Icons.push_pin : Icons.push_pin_outlined, pinned ? Icons.push_pin : Icons.push_pin_outlined,
color: Colors.white, color: Palette.white,
), ),
featureId: Constants.featurePinToTop, featureId: Constants.featurePinToTop,
title: const Text('Pin a Story'), title: const Text('Pin a Story'),
@ -51,12 +52,12 @@ class PinIconButton extends StatelessWidget {
'Pin this story to the top of your ' 'Pin this story to the top of your '
'home screen so that you can come' 'home screen so that you can come'
' back later.', ' back later.',
style: TextStyle(fontSize: 16), style: TextStyle(fontSize: TextDimens.pt16),
), ),
child: Icon( child: Icon(
pinned ? Icons.push_pin : Icons.push_pin_outlined, pinned ? Icons.push_pin : Icons.push_pin_outlined,
color: pinned color: pinned
? Colors.orange ? Palette.orange
: Theme.of(context).iconTheme.color, : Theme.of(context).iconTheme.color,
), ),
), ),

View File

@ -5,6 +5,7 @@ import 'package:flutter_fadein/flutter_fadein.dart';
import 'package:hacki/blocs/auth/auth_bloc.dart'; import 'package:hacki/blocs/auth/auth_bloc.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/styles/styles.dart';
class PollView extends StatelessWidget { class PollView extends StatelessWidget {
const PollView({ const PollView({
@ -21,29 +22,29 @@ class PollView extends StatelessWidget {
return Column( return Column(
children: <Widget>[ children: <Widget>[
const SizedBox( const SizedBox(
height: 24, height: Dimens.pt24,
), ),
if (state.status == PollStatus.loading) ...<Widget>[ if (state.status == PollStatus.loading) ...<Widget>[
const LinearProgressIndicator(), const LinearProgressIndicator(),
const SizedBox( const SizedBox(
height: 24, height: Dimens.pt24,
), ),
] else ...<Widget>[ ] else ...<Widget>[
Row( Row(
children: <Widget>[ children: <Widget>[
const SizedBox( const SizedBox(
width: 24, width: Dimens.pt24,
), ),
Text( Text(
'Total votes: ${state.totalVotes}', 'Total votes: ${state.totalVotes}',
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: TextDimens.pt14,
), ),
), ),
], ],
), ),
const SizedBox( const SizedBox(
height: 12, height: Dimens.pt12,
), ),
], ],
for (final PollOption option in state.pollOptions) for (final PollOption option in state.pollOptions)
@ -97,9 +98,9 @@ class PollView extends StatelessWidget {
builder: (BuildContext context, VoteState voteState) { builder: (BuildContext context, VoteState voteState) {
return Padding( return Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 12, left: Dimens.pt12,
right: 24, right: Dimens.pt24,
bottom: 4, bottom: Dimens.pt4,
), ),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
@ -111,9 +112,9 @@ class PollView extends StatelessWidget {
icon: Icon( icon: Icon(
Icons.arrow_drop_up, Icons.arrow_drop_up,
color: voteState.vote == Vote.up color: voteState.vote == Vote.up
? Colors.orange ? Palette.orange
: Colors.grey, : Palette.grey,
size: 36, size: TextDimens.pt36,
), ),
), ),
Expanded( Expanded(
@ -126,16 +127,16 @@ class PollView extends StatelessWidget {
Text( Text(
'''${option.score} vote${option.score > 1 ? 's' : ''}''', '''${option.score} vote${option.score > 1 ? 's' : ''}''',
style: const TextStyle( style: const TextStyle(
color: Colors.grey, color: Palette.grey,
fontSize: 12, fontSize: TextDimens.pt12,
), ),
), ),
const SizedBox( const SizedBox(
height: 4, height: Dimens.pt4,
), ),
LinearProgressIndicator( LinearProgressIndicator(
value: option.ratio, value: option.ratio,
color: Colors.deepOrange, color: Palette.deepOrange,
), ),
], ],
), ),
@ -161,7 +162,7 @@ class PollView extends StatelessWidget {
}) { }) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
backgroundColor: Colors.deepOrange, backgroundColor: Palette.deepOrange,
content: Text(content), content: Text(content),
action: action != null && label != null action: action != null && label != null
? SnackBarAction( ? SnackBarAction(

View File

@ -6,6 +6,7 @@ import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/item.dart'; import 'package:hacki/models/item.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/link_util.dart'; import 'package:hacki/utils/link_util.dart';
class ReplyBox extends StatefulWidget { class ReplyBox extends StatefulWidget {
@ -34,6 +35,8 @@ class _ReplyBoxState extends State<ReplyBox> {
bool expanded = false; bool expanded = false;
double? expandedHeight; double? expandedHeight;
static const double _collapsedHeight = 100;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
expandedHeight ??= MediaQuery.of(context).size.height; expandedHeight ??= MediaQuery.of(context).size.height;
@ -53,20 +56,21 @@ class _ReplyBoxState extends State<ReplyBox> {
return Padding( return Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: expanded bottom: expanded
? 0 ? Dimens.zero
: widget.splitViewEnabled : widget.splitViewEnabled
? MediaQuery.of(context).viewInsets.bottom ? MediaQuery.of(context).viewInsets.bottom
: 0, : Dimens.zero,
), ),
child: AnimatedContainer( child: AnimatedContainer(
height: expanded ? expandedHeight : 100, height: expanded ? expandedHeight : _collapsedHeight,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
decoration: BoxDecoration( decoration: BoxDecoration(
boxShadow: <BoxShadow>[ boxShadow: <BoxShadow>[
if (!context.read<SplitViewCubit>().state.enabled) if (!context.read<SplitViewCubit>().state.enabled)
BoxShadow( BoxShadow(
color: expanded ? Colors.transparent : Colors.black26, color:
blurRadius: 40, expanded ? Palette.transparent : Palette.black26,
blurRadius: Dimens.pt40,
), ),
], ],
), ),
@ -75,10 +79,10 @@ class _ReplyBoxState extends State<ReplyBox> {
children: <Widget>[ children: <Widget>[
if (context.read<SplitViewCubit>().state.enabled) if (context.read<SplitViewCubit>().state.enabled)
const Divider( const Divider(
height: 0, height: Dimens.zero,
), ),
AnimatedContainer( AnimatedContainer(
height: expanded ? 36 : 0, height: expanded ? Dimens.pt36 : Dimens.zero,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
), ),
Row( Row(
@ -86,16 +90,16 @@ class _ReplyBoxState extends State<ReplyBox> {
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 12, left: Dimens.pt12,
top: 8, top: Dimens.pt8,
bottom: 8, bottom: Dimens.pt8,
), ),
child: Text( child: Text(
replyingTo == null replyingTo == null
? 'Editing' ? 'Editing'
: 'Replying ' : 'Replying '
'${replyingTo.by}', '${replyingTo.by}',
style: const TextStyle(color: Colors.grey), style: const TextStyle(color: Palette.grey),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -111,8 +115,8 @@ class _ReplyBoxState extends State<ReplyBox> {
key: const Key('quote'), key: const Key('quote'),
icon: const Icon( icon: const Icon(
FeatherIcons.code, FeatherIcons.code,
color: Colors.orange, color: Palette.orange,
size: 18, size: TextDimens.pt18,
), ),
onPressed: onPressed:
expanded ? showTextPopup : null, expanded ? showTextPopup : null,
@ -124,8 +128,8 @@ class _ReplyBoxState extends State<ReplyBox> {
expanded expanded
? FeatherIcons.minimize2 ? FeatherIcons.minimize2
: FeatherIcons.maximize2, : FeatherIcons.maximize2,
color: Colors.orange, color: Palette.orange,
size: 18, size: TextDimens.pt18,
), ),
onPressed: () { onPressed: () {
setState(() { setState(() {
@ -138,7 +142,7 @@ class _ReplyBoxState extends State<ReplyBox> {
key: const Key('close'), key: const Key('close'),
icon: const Icon( icon: const Icon(
Icons.close, Icons.close,
color: Colors.orange, color: Palette.orange,
), ),
onPressed: () { onPressed: () {
widget.onCloseTapped(); widget.onCloseTapped();
@ -149,15 +153,15 @@ class _ReplyBoxState extends State<ReplyBox> {
if (isLoading) if (isLoading)
const Padding( const Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
vertical: 12, vertical: Dimens.pt12,
horizontal: 16, horizontal: Dimens.pt16,
), ),
child: SizedBox( child: SizedBox(
height: 24, height: Dimens.pt24,
width: 24, width: Dimens.pt24,
child: CircularProgressIndicator( child: CircularProgressIndicator(
color: Colors.orange, color: Palette.orange,
strokeWidth: 2, strokeWidth: Dimens.pt2,
), ),
), ),
) )
@ -166,7 +170,7 @@ class _ReplyBoxState extends State<ReplyBox> {
key: const Key('send'), key: const Key('send'),
icon: const Icon( icon: const Icon(
Icons.send, Icons.send,
color: Colors.orange, color: Palette.orange,
), ),
onPressed: () { onPressed: () {
widget.onSendTapped(); widget.onSendTapped();
@ -177,7 +181,9 @@ class _ReplyBoxState extends State<ReplyBox> {
), ),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt16,
),
child: TextField( child: TextField(
focusNode: widget.focusNode, focusNode: widget.focusNode,
controller: widget.textEditingController, controller: widget.textEditingController,
@ -187,7 +193,7 @@ class _ReplyBoxState extends State<ReplyBox> {
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
hintText: '...', hintText: '...',
hintStyle: TextStyle( hintStyle: TextStyle(
color: Colors.grey, color: Palette.grey,
), ),
focusedBorder: InputBorder.none, focusedBorder: InputBorder.none,
border: InputBorder.none, border: InputBorder.none,
@ -220,8 +226,8 @@ class _ReplyBoxState extends State<ReplyBox> {
builder: (_) { builder: (_) {
return AlertDialog( return AlertDialog(
insetPadding: const EdgeInsets.symmetric( insetPadding: const EdgeInsets.symmetric(
horizontal: 12, horizontal: Dimens.pt12,
vertical: 24, vertical: Dimens.pt24,
), ),
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
content: ConstrainedBox( content: ConstrainedBox(
@ -233,14 +239,14 @@ class _ReplyBoxState extends State<ReplyBox> {
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 12, left: Dimens.pt12,
top: 6, top: Dimens.pt6,
), ),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Text( Text(
replyingTo?.by ?? '', replyingTo?.by ?? '',
style: const TextStyle(color: Colors.grey), style: const TextStyle(color: Palette.grey),
), ),
const Spacer(), const Spacer(),
TextButton( TextButton(
@ -252,8 +258,8 @@ class _ReplyBoxState extends State<ReplyBox> {
IconButton( IconButton(
icon: const Icon( icon: const Icon(
Icons.close, Icons.close,
color: Colors.orange, color: Palette.orange,
size: 18, size: TextDimens.pt18,
), ),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
@ -265,17 +271,17 @@ class _ReplyBoxState extends State<ReplyBox> {
thumbVisibility: true, thumbVisibility: true,
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 12, left: Dimens.pt12,
right: 6, right: Dimens.pt6,
top: 6, top: Dimens.pt6,
), ),
child: SingleChildScrollView( child: SingleChildScrollView(
child: SelectableLinkify( child: SelectableLinkify(
scrollPhysics: const NeverScrollableScrollPhysics(), scrollPhysics: const NeverScrollableScrollPhysics(),
linkStyle: TextStyle( linkStyle: TextStyle(
fontSize: fontSize: MediaQuery.of(context).textScaleFactor *
MediaQuery.of(context).textScaleFactor * 15, TextDimens.pt15,
color: Colors.orange, color: Palette.orange,
), ),
onOpen: (LinkableElement link) => onOpen: (LinkableElement link) =>
LinkUtil.launch(link.url), LinkUtil.launch(link.url),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:hacki/styles/styles.dart';
class ScrollUpIconButton extends StatefulWidget { class ScrollUpIconButton extends StatefulWidget {
const ScrollUpIconButton({ const ScrollUpIconButton({
@ -35,8 +36,8 @@ class _ScrollUpIconButtonState extends State<ScrollUpIconButton> {
child: IconButton( child: IconButton(
icon: const Icon( icon: const Icon(
FeatherIcons.chevronsUp, FeatherIcons.chevronsUp,
color: Colors.orange, color: Palette.orange,
size: 26, size: TextDimens.pt26,
), ),
onPressed: () { onPressed: () {
final double curPos = widget.scrollController.offset; final double curPos = widget.scrollController.offset;

View File

@ -18,6 +18,7 @@ import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/profile/widgets/widgets.dart'; import 'package:hacki/screens/profile/widgets/widgets.dart';
import 'package:hacki/screens/screens.dart'; import 'package:hacki/screens/screens.dart';
import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart'; import 'package:hacki/utils/utils.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -91,7 +92,7 @@ class _ProfileScreenState extends State<ProfileScreen>
return Stack( return Stack(
children: <Widget>[ children: <Widget>[
Positioned.fill( Positioned.fill(
top: 50, top: Dimens.pt50,
child: Visibility( child: Visibility(
visible: pageType == _PageType.history, visible: pageType == _PageType.history,
child: BlocConsumer<HistoryCubit, HistoryState>( child: BlocConsumer<HistoryCubit, HistoryState>(
@ -148,7 +149,7 @@ class _ProfileScreenState extends State<ProfileScreen>
), ),
), ),
Positioned.fill( Positioned.fill(
top: 50, top: Dimens.pt50,
child: Visibility( child: Visibility(
visible: pageType == _PageType.fav, visible: pageType == _PageType.fav,
child: BlocConsumer<FavCubit, FavState>( child: BlocConsumer<FavCubit, FavState>(
@ -192,7 +193,7 @@ class _ProfileScreenState extends State<ProfileScreen>
), ),
), ),
Positioned.fill( Positioned.fill(
top: 50, top: Dimens.pt50,
child: Visibility( child: Visibility(
visible: pageType == _PageType.search, visible: pageType == _PageType.search,
maintainState: true, maintainState: true,
@ -200,7 +201,7 @@ class _ProfileScreenState extends State<ProfileScreen>
), ),
), ),
Positioned.fill( Positioned.fill(
top: 50, top: Dimens.pt50,
child: Visibility( child: Visibility(
visible: pageType == _PageType.notification, visible: pageType == _PageType.notification,
child: notificationState.comments.isEmpty child: notificationState.comments.isEmpty
@ -241,7 +242,7 @@ class _ProfileScreenState extends State<ProfileScreen>
), ),
), ),
Positioned.fill( Positioned.fill(
top: 50, top: Dimens.pt50,
child: Visibility( child: Visibility(
visible: pageType == _PageType.settings, visible: pageType == _PageType.settings,
child: SingleChildScrollView( child: SingleChildScrollView(
@ -280,7 +281,123 @@ class _ProfileScreenState extends State<ProfileScreen>
.read<PreferenceCubit>() .read<PreferenceCubit>()
.toggleNotificationMode(); .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( SwitchListTile(
title: const Text('Complex Story Tile'), title: const Text('Complex Story Tile'),
@ -294,7 +411,7 @@ class _ProfileScreenState extends State<ProfileScreen>
.read<PreferenceCubit>() .read<PreferenceCubit>()
.toggleDisplayMode(); .toggleDisplayMode();
}, },
activeColor: Colors.orange, activeColor: Palette.orange,
), ),
SwitchListTile( SwitchListTile(
title: const Text('Show Metadata'), title: const Text('Show Metadata'),
@ -309,7 +426,7 @@ class _ProfileScreenState extends State<ProfileScreen>
.read<PreferenceCubit>() .read<PreferenceCubit>()
.toggleMetadataMode(); .toggleMetadataMode();
}, },
activeColor: Colors.orange, activeColor: Palette.orange,
), ),
SwitchListTile( SwitchListTile(
title: const Text('Show Web Page First'), title: const Text('Show Web Page First'),
@ -324,7 +441,7 @@ class _ProfileScreenState extends State<ProfileScreen>
.read<PreferenceCubit>() .read<PreferenceCubit>()
.toggleNavigationMode(); .toggleNavigationMode();
}, },
activeColor: Colors.orange, activeColor: Palette.orange,
), ),
if (Platform.isIOS) if (Platform.isIOS)
SwitchListTile( SwitchListTile(
@ -340,7 +457,7 @@ class _ProfileScreenState extends State<ProfileScreen>
.read<PreferenceCubit>() .read<PreferenceCubit>()
.toggleReaderMode(); .toggleReaderMode();
}, },
activeColor: Colors.orange, activeColor: Palette.orange,
), ),
SwitchListTile( SwitchListTile(
title: const Text('Mark Read Stories'), title: const Text('Mark Read Stories'),
@ -361,7 +478,7 @@ class _ProfileScreenState extends State<ProfileScreen>
.read<PreferenceCubit>() .read<PreferenceCubit>()
.toggleMarkReadStoriesMode(); .toggleMarkReadStoriesMode();
}, },
activeColor: Colors.orange, activeColor: Palette.orange,
), ),
SwitchListTile( SwitchListTile(
title: const Text('Eye Candy'), title: const Text('Eye Candy'),
@ -373,7 +490,7 @@ class _ProfileScreenState extends State<ProfileScreen>
.read<PreferenceCubit>() .read<PreferenceCubit>()
.toggleEyeCandyMode(); .toggleEyeCandyMode();
}, },
activeColor: Colors.orange, activeColor: Palette.orange,
), ),
SwitchListTile( SwitchListTile(
title: const Text('True Dark Mode'), title: const Text('True Dark Mode'),
@ -387,7 +504,7 @@ class _ProfileScreenState extends State<ProfileScreen>
.read<PreferenceCubit>() .read<PreferenceCubit>()
.toggleTrueDarkMode(); .toggleTrueDarkMode();
}, },
activeColor: Colors.orange, activeColor: Palette.orange,
), ),
ListTile( ListTile(
title: const Text( title: const Text(
@ -409,15 +526,17 @@ class _ProfileScreenState extends State<ProfileScreen>
showAboutDialog( showAboutDialog(
context: context, context: context,
applicationName: 'Hacki', applicationName: 'Hacki',
applicationVersion: 'v0.2.21', applicationVersion: 'v0.2.28',
applicationIcon: ClipRRect( applicationIcon: ClipRRect(
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular(12), Radius.circular(
Dimens.pt12,
),
), ),
child: Image.asset( child: Image.asset(
Constants.hackiIconPath, Constants.hackiIconPath,
height: 50, height: Dimens.pt50,
width: 50, width: Dimens.pt50,
), ),
), ),
children: <Widget>[ children: <Widget>[
@ -431,7 +550,7 @@ class _ProfileScreenState extends State<ProfileScreen>
FontAwesomeIcons.addressCard, FontAwesomeIcons.addressCard,
), ),
SizedBox( SizedBox(
width: 12, width: Dimens.pt12,
), ),
Text('Developer'), Text('Developer'),
], ],
@ -447,7 +566,7 @@ class _ProfileScreenState extends State<ProfileScreen>
FontAwesomeIcons.github, FontAwesomeIcons.github,
), ),
SizedBox( SizedBox(
width: 12, width: Dimens.pt12,
), ),
Text('Source code'), Text('Source code'),
], ],
@ -465,7 +584,7 @@ class _ProfileScreenState extends State<ProfileScreen>
Icons.thumb_up, Icons.thumb_up,
), ),
SizedBox( SizedBox(
width: 12, width: Dimens.pt12,
), ),
Text('Like the app?'), Text('Like the app?'),
], ],
@ -481,7 +600,7 @@ class _ProfileScreenState extends State<ProfileScreen>
FeatherIcons.coffee, FeatherIcons.coffee,
), ),
SizedBox( SizedBox(
width: 12, width: Dimens.pt12,
), ),
Text('Buy me a coffee'), Text('Buy me a coffee'),
], ],
@ -492,7 +611,7 @@ class _ProfileScreenState extends State<ProfileScreen>
}, },
), ),
const SizedBox( const SizedBox(
height: 48, height: Dimens.pt48,
), ),
], ],
), ),
@ -507,7 +626,7 @@ class _ProfileScreenState extends State<ProfileScreen>
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
const SizedBox( const SizedBox(
width: 12, width: Dimens.pt12,
), ),
CustomChip( CustomChip(
label: 'Submit', label: 'Submit',
@ -526,7 +645,7 @@ class _ProfileScreenState extends State<ProfileScreen>
}, },
), ),
const SizedBox( const SizedBox(
width: 12, width: Dimens.pt12,
), ),
CustomChip( CustomChip(
label: 'Inbox : ' label: 'Inbox : '
@ -542,7 +661,7 @@ class _ProfileScreenState extends State<ProfileScreen>
}, },
), ),
const SizedBox( const SizedBox(
width: 12, width: Dimens.pt12,
), ),
CustomChip( CustomChip(
label: 'Favorite', label: 'Favorite',
@ -556,7 +675,7 @@ class _ProfileScreenState extends State<ProfileScreen>
}, },
), ),
const SizedBox( const SizedBox(
width: 12, width: Dimens.pt12,
), ),
CustomChip( CustomChip(
label: 'Submitted', label: 'Submitted',
@ -570,7 +689,7 @@ class _ProfileScreenState extends State<ProfileScreen>
}, },
), ),
const SizedBox( const SizedBox(
width: 12, width: Dimens.pt12,
), ),
CustomChip( CustomChip(
label: 'Search', label: 'Search',
@ -584,7 +703,7 @@ class _ProfileScreenState extends State<ProfileScreen>
}, },
), ),
const SizedBox( const SizedBox(
width: 12, width: Dimens.pt12,
), ),
CustomChip( CustomChip(
label: 'Settings', label: 'Settings',
@ -598,7 +717,7 @@ class _ProfileScreenState extends State<ProfileScreen>
}, },
), ),
const SizedBox( const SizedBox(
width: 12, width: Dimens.pt12,
), ),
], ],
), ),
@ -666,7 +785,7 @@ class _ProfileScreenState extends State<ProfileScreen>
child: const Text( child: const Text(
'Cancel', 'Cancel',
style: TextStyle( style: TextStyle(
color: Colors.orange, color: Palette.orange,
), ),
), ),
), ),
@ -677,7 +796,7 @@ class _ProfileScreenState extends State<ProfileScreen>
.get<SembastRepository>() .get<SembastRepository>()
.deleteAllCachedComments() .deleteAllCachedComments()
.whenComplete( .whenComplete(
locator.get<CacheRepository>().deleteAll, locator.get<OfflineRepository>().deleteAll,
) )
.whenComplete( .whenComplete(
locator.get<PreferenceRepository>().clearAllReadStories, locator.get<PreferenceRepository>().clearAllReadStories,
@ -743,64 +862,64 @@ class _ProfileScreenState extends State<ProfileScreen>
children: <Widget>[ children: <Widget>[
if (state.status == AuthStatus.loading) if (state.status == AuthStatus.loading)
const SizedBox( const SizedBox(
height: 36, height: Dimens.pt36,
width: 36, width: Dimens.pt36,
child: Center( child: Center(
child: CircularProgressIndicator( child: CircularProgressIndicator(
color: Colors.orange, color: Palette.orange,
), ),
), ),
) )
else if (!state.isLoggedIn) ...<Widget>[ else if (!state.isLoggedIn) ...<Widget>[
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 18, horizontal: Dimens.pt18,
), ),
child: TextField( child: TextField(
controller: usernameController, controller: usernameController,
cursorColor: Colors.orange, cursorColor: Palette.orange,
autocorrect: false, autocorrect: false,
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: 'Username', hintText: 'Username',
focusedBorder: UnderlineInputBorder( focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.orange), borderSide: BorderSide(color: Palette.orange),
), ),
), ),
), ),
), ),
const SizedBox( const SizedBox(
height: 16, height: Dimens.pt16,
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 18, horizontal: Dimens.pt18,
), ),
child: TextField( child: TextField(
controller: passwordController, controller: passwordController,
cursorColor: Colors.orange, cursorColor: Palette.orange,
obscureText: true, obscureText: true,
autocorrect: false, autocorrect: false,
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: 'Password', hintText: 'Password',
focusedBorder: UnderlineInputBorder( focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.orange), borderSide: BorderSide(color: Palette.orange),
), ),
), ),
), ),
), ),
const SizedBox( const SizedBox(
height: 16, height: Dimens.pt16,
), ),
if (state.status == AuthStatus.failure) if (state.status == AuthStatus.failure)
const Padding( const Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: 18, left: Dimens.pt18,
), ),
child: Text( child: Text(
'Something went wrong...', 'Something went wrong...',
style: TextStyle( style: TextStyle(
color: Colors.grey, color: Palette.grey,
fontSize: 12, fontSize: TextDimens.pt12,
), ),
), ),
), ),
@ -814,8 +933,8 @@ class _ProfileScreenState extends State<ProfileScreen>
? Icons.check_box ? Icons.check_box
: Icons.check_box_outline_blank, : Icons.check_box_outline_blank,
color: state.agreedToEULA color: state.agreedToEULA
? Colors.deepOrange ? Palette.deepOrange
: Colors.grey, : Palette.grey,
), ),
onPressed: () => context onPressed: () => context
.read<AuthBloc>() .read<AuthBloc>()
@ -840,7 +959,7 @@ class _ProfileScreenState extends State<ProfileScreen>
child: const Text( child: const Text(
'End User Agreement', 'End User Agreement',
style: TextStyle( style: TextStyle(
color: Colors.deepOrange, color: Palette.deepOrange,
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
@ -855,7 +974,7 @@ class _ProfileScreenState extends State<ProfileScreen>
), ),
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
right: 12, right: Dimens.pt12,
), ),
child: ButtonBar( child: ButtonBar(
children: <Widget>[ children: <Widget>[
@ -886,15 +1005,15 @@ class _ProfileScreenState extends State<ProfileScreen>
style: ButtonStyle( style: ButtonStyle(
backgroundColor: MaterialStateProperty.all( backgroundColor: MaterialStateProperty.all(
state.agreedToEULA state.agreedToEULA
? Colors.deepOrange ? Palette.deepOrange
: Colors.grey, : Palette.grey,
), ),
), ),
child: const Text( child: const Text(
'Log in', 'Log in',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: Palette.white,
), ),
), ),
), ),
@ -921,7 +1040,7 @@ class _ProfileScreenState extends State<ProfileScreen>
content: Text( content: Text(
'Log out as ${authBloc.state.username}?', 'Log out as ${authBloc.state.username}?',
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: TextDimens.pt16,
), ),
), ),
actions: <Widget>[ actions: <Widget>[

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hacki/styles/styles.dart';
class CenteredMessageView extends StatelessWidget { class CenteredMessageView extends StatelessWidget {
const CenteredMessageView({ const CenteredMessageView({
@ -12,14 +13,14 @@ class CenteredMessageView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
top: 120, top: Dimens.pt120,
left: 40, left: Dimens.pt40,
right: 40, right: Dimens.pt40,
), ),
child: Text( child: Text(
content, content,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle(color: Colors.grey), style: const TextStyle(color: Palette.grey),
), ),
); );
} }

View File

@ -3,6 +3,7 @@ import 'package:flutter_fadein/flutter_fadein.dart';
import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/link_util.dart'; import 'package:hacki/utils/link_util.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
@ -29,8 +30,8 @@ class InboxView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color textColor = Theme.of(context).brightness == Brightness.dark final Color textColor = Theme.of(context).brightness == Brightness.dark
? Colors.white ? Palette.white
: Colors.black; : Palette.black;
return Column( return Column(
children: <Widget>[ children: <Widget>[
if (unreadCommentsIds.isNotEmpty) if (unreadCommentsIds.isNotEmpty)
@ -42,12 +43,14 @@ class InboxView extends StatelessWidget {
child: SmartRefresher( child: SmartRefresher(
enablePullUp: true, enablePullUp: true,
header: const WaterDropMaterialHeader( header: const WaterDropMaterialHeader(
backgroundColor: Colors.orange, backgroundColor: Palette.orange,
), ),
footer: CustomFooter( footer: CustomFooter(
loadStyle: LoadStyle.ShowWhenLoading, loadStyle: LoadStyle.ShowWhenLoading,
builder: (BuildContext context, LoadStatus? mode) { builder: (BuildContext context, LoadStatus? mode) {
Widget body; const double height = 55;
late final Widget body;
if (mode == LoadStatus.loading) { if (mode == LoadStatus.loading) {
body = const CustomCircularProgressIndicator(); body = const CustomCircularProgressIndicator();
} else if (mode == LoadStatus.failed) { } else if (mode == LoadStatus.failed) {
@ -58,7 +61,7 @@ class InboxView extends StatelessWidget {
body = const SizedBox.shrink(); body = const SizedBox.shrink();
} }
return SizedBox( return SizedBox(
height: 55, height: height,
child: Center(child: body), child: Center(child: body),
); );
}, },
@ -72,7 +75,9 @@ class InboxView extends StatelessWidget {
return <Widget>[ return <Widget>[
FadeIn( FadeIn(
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 6), padding: const EdgeInsets.only(
left: Dimens.pt6,
),
child: InkWell( child: InkWell(
onTap: () => onCommentTapped(e), onTap: () => onCommentTapped(e),
child: Padding( child: Padding(
@ -87,8 +92,8 @@ class InboxView extends StatelessWidget {
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 8, vertical: Dimens.pt8,
horizontal: 6, horizontal: Dimens.pt6,
), ),
child: Linkify( child: Linkify(
text: '${e.by} : ${e.text}', text: '${e.by} : ${e.text}',
@ -96,13 +101,13 @@ class InboxView extends StatelessWidget {
color: color:
unreadCommentsIds.contains(e.id) unreadCommentsIds.contains(e.id)
? textColor ? textColor
: Colors.grey, : Palette.grey,
), ),
linkStyle: TextStyle( linkStyle: TextStyle(
color: color:
unreadCommentsIds.contains(e.id) unreadCommentsIds.contains(e.id)
? Colors.orange ? Palette.orange
: Colors.orange : Palette.orange
.withOpacity(0.6), .withOpacity(0.6),
), ),
maxLines: 4, maxLines: 4,
@ -116,18 +121,18 @@ class InboxView extends StatelessWidget {
Text( Text(
e.postedDate, e.postedDate,
style: const TextStyle( style: const TextStyle(
color: Colors.grey, color: Palette.grey,
), ),
), ),
const SizedBox( const SizedBox(
width: 12, width: Dimens.pt12,
), ),
], ],
), ),
], ],
), ),
const Divider( const Divider(
height: 0, height: Dimens.zero,
), ),
], ],
), ),
@ -136,12 +141,12 @@ class InboxView extends StatelessWidget {
), ),
), ),
const Divider( const Divider(
height: 0, height: Dimens.zero,
), ),
]; ];
}).expand((List<Widget> element) => element), }).expand((List<Widget> element) => element),
const SizedBox( const SizedBox(
height: 40, height: Dimens.pt40,
), ),
], ],
), ),

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/blocs/blocs.dart'; import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:wakelock/wakelock.dart'; import 'package:wakelock/wakelock.dart';
class OfflineListTile extends StatelessWidget { class OfflineListTile extends StatelessWidget {
@ -32,8 +33,8 @@ class OfflineListTile extends StatelessWidget {
final Widget trailingWidget = () { final Widget trailingWidget = () {
if (downloading) { if (downloading) {
return const SizedBox( return const SizedBox(
height: 24, height: Dimens.pt24,
width: 24, width: Dimens.pt24,
child: CustomCircularProgressIndicator(), child: CustomCircularProgressIndicator(),
); );
} else if (downloaded) { } else if (downloaded) {

View File

@ -7,6 +7,7 @@ import 'package:hacki/models/models.dart';
import 'package:hacki/screens/screens.dart'; import 'package:hacki/screens/screens.dart';
import 'package:hacki/screens/search/widgets/widgets.dart'; import 'package:hacki/screens/search/widgets/widgets.dart';
import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart'; import 'package:hacki/utils/utils.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
@ -38,14 +39,16 @@ class _SearchScreenState extends State<SearchScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt12,
),
child: TextField( child: TextField(
cursorColor: Colors.orange, cursorColor: Palette.orange,
autocorrect: false, autocorrect: false,
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: 'Search Hacker News', hintText: 'Search Hacker News',
focusedBorder: UnderlineInputBorder( focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.orange), borderSide: BorderSide(color: Palette.orange),
), ),
), ),
onChanged: (String val) { onChanged: (String val) {
@ -58,7 +61,7 @@ class _SearchScreenState extends State<SearchScreen> {
), ),
), ),
const SizedBox( const SizedBox(
height: 6, height: Dimens.pt6,
), ),
SingleChildScrollView( SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@ -83,7 +86,7 @@ class _SearchScreenState extends State<SearchScreen> {
.removeFilter<DateTimeRangeFilter>, .removeFilter<DateTimeRangeFilter>,
), ),
const SizedBox( const SizedBox(
width: 8, width: Dimens.pt8,
), ),
CustomChip( CustomChip(
onSelected: (_) => onSelected: (_) =>
@ -92,7 +95,7 @@ class _SearchScreenState extends State<SearchScreen> {
label: '''newest first''', label: '''newest first''',
), ),
const SizedBox( const SizedBox(
width: 8, width: Dimens.pt8,
), ),
for (final CustomDateTimeRange range for (final CustomDateTimeRange range
in CustomDateTimeRange.values) ...<Widget>[ in CustomDateTimeRange.values) ...<Widget>[
@ -107,7 +110,7 @@ class _SearchScreenState extends State<SearchScreen> {
), ),
), ),
const SizedBox( const SizedBox(
width: 8, width: Dimens.pt8,
), ),
], ],
], ],
@ -116,7 +119,7 @@ class _SearchScreenState extends State<SearchScreen> {
if (state.status == SearchStatus.loading && if (state.status == SearchStatus.loading &&
state.results.isEmpty) ...<Widget>[ state.results.isEmpty) ...<Widget>[
const SizedBox( const SizedBox(
height: 100, height: Dimens.pt100,
), ),
const CustomCircularProgressIndicator(), const CustomCircularProgressIndicator(),
], ],
@ -125,12 +128,14 @@ class _SearchScreenState extends State<SearchScreen> {
enablePullDown: false, enablePullDown: false,
enablePullUp: true, enablePullUp: true,
header: const WaterDropMaterialHeader( header: const WaterDropMaterialHeader(
backgroundColor: Colors.orange, backgroundColor: Palette.orange,
), ),
footer: CustomFooter( footer: CustomFooter(
loadStyle: LoadStyle.ShowWhenLoading, loadStyle: LoadStyle.ShowWhenLoading,
builder: (BuildContext context, LoadStatus? mode) { builder: (BuildContext context, LoadStatus? mode) {
Widget body; const double height = 55;
late final Widget body;
if (mode == LoadStatus.loading) { if (mode == LoadStatus.loading) {
body = const CustomCircularProgressIndicator(); body = const CustomCircularProgressIndicator();
} else if (mode == LoadStatus.failed) { } else if (mode == LoadStatus.failed) {
@ -140,8 +145,9 @@ class _SearchScreenState extends State<SearchScreen> {
} else { } else {
body = const SizedBox.shrink(); body = const SizedBox.shrink();
} }
return SizedBox( return SizedBox(
height: 55, height: height,
child: Center(child: body), child: Center(child: body),
); );
}, },
@ -169,14 +175,14 @@ class _SearchScreenState extends State<SearchScreen> {
), ),
if (!prefState.showComplexStoryTile) if (!prefState.showComplexStoryTile)
const Divider( const Divider(
height: 0, height: Dimens.zero,
), ),
], ],
) )
.expand((List<Widget> e) => e) .expand((List<Widget> e) => e)
.toList(), .toList(),
const SizedBox( const SizedBox(
height: 40, height: Dimens.pt40,
), ),
], ],
), ),

View File

@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/styles/styles.dart';
class SubmitScreen extends StatefulWidget { class SubmitScreen extends StatefulWidget {
const SubmitScreen({super.key}); const SubmitScreen({super.key});
@ -58,7 +59,7 @@ class _SubmitScreenState extends State<SubmitScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: Theme.of(context).canvasColor, backgroundColor: Theme.of(context).canvasColor,
elevation: 0, elevation: Dimens.zero,
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () { onPressed: () {
@ -78,7 +79,7 @@ class _SubmitScreenState extends State<SubmitScreen> {
child: const Text( child: const Text(
'Yes', 'Yes',
style: TextStyle( style: TextStyle(
color: Colors.red, color: Palette.red,
), ),
), ),
), ),
@ -99,14 +100,14 @@ class _SubmitScreenState extends State<SubmitScreen> {
if (state.status == SubmitStatus.submitting) if (state.status == SubmitStatus.submitting)
const Padding( const Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
vertical: 18, vertical: Dimens.pt18,
horizontal: 16, horizontal: Dimens.pt16,
), ),
child: SizedBox( child: SizedBox(
height: 20, height: Dimens.pt20,
width: 20, width: Dimens.pt20,
child: CircularProgressIndicator( child: CircularProgressIndicator(
color: Colors.orange, color: Palette.orange,
strokeWidth: 2, strokeWidth: 2,
), ),
), ),
@ -115,7 +116,7 @@ class _SubmitScreenState extends State<SubmitScreen> {
IconButton( IconButton(
icon: const Icon( icon: const Icon(
Icons.send, Icons.send,
color: Colors.orange, color: Palette.orange,
), ),
onPressed: context.read<SubmitCubit>().onSubmitTapped, onPressed: context.read<SubmitCubit>().onSubmitTapped,
) )
@ -123,7 +124,7 @@ class _SubmitScreenState extends State<SubmitScreen> {
IconButton( IconButton(
icon: const Icon( icon: const Icon(
Icons.send, Icons.send,
color: Colors.grey, color: Palette.grey,
), ),
onPressed: () {}, onPressed: () {},
), ),
@ -132,50 +133,58 @@ class _SubmitScreenState extends State<SubmitScreen> {
body: Column( body: Column(
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt12,
),
child: TextField( child: TextField(
controller: titleEditingController, controller: titleEditingController,
cursorColor: Colors.orange, cursorColor: Palette.orange,
autocorrect: false, autocorrect: false,
maxLength: 80, maxLength: 80,
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: 'Title', hintText: 'Title',
focusedBorder: UnderlineInputBorder( focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.orange), borderSide: BorderSide(color: Palette.orange),
), ),
), ),
onChanged: context.read<SubmitCubit>().onTitleChanged, onChanged: context.read<SubmitCubit>().onTitleChanged,
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt12,
),
child: TextField( child: TextField(
enabled: textEditingController.text.isEmpty, enabled: textEditingController.text.isEmpty,
controller: urlEditingController, controller: urlEditingController,
cursorColor: Colors.orange, cursorColor: Palette.orange,
autocorrect: false, autocorrect: false,
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: 'Url', hintText: 'Url',
focusedBorder: UnderlineInputBorder( focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.orange), borderSide: BorderSide(color: Palette.orange),
), ),
), ),
onChanged: context.read<SubmitCubit>().onUrlChanged, onChanged: context.read<SubmitCubit>().onUrlChanged,
), ),
), ),
const Padding( const Padding(
padding: EdgeInsets.symmetric(vertical: 12), padding: EdgeInsets.symmetric(
vertical: Dimens.pt12,
),
child: Center( child: Center(
child: Text('or'), child: Text('or'),
), ),
), ),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt12,
),
child: TextField( child: TextField(
enabled: urlEditingController.text.isEmpty, enabled: urlEditingController.text.isEmpty,
controller: textEditingController, controller: textEditingController,
cursorColor: Colors.orange, cursorColor: Palette.orange,
maxLines: 200, maxLines: 200,
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: 'Text', hintText: 'Text',

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/styles/styles.dart';
import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter/webview_flutter.dart';
class WebViewScreen extends StatefulWidget { class WebViewScreen extends StatefulWidget {
@ -24,7 +25,7 @@ class _WebViewScreenState extends State<WebViewScreen> {
title: Text( title: Text(
humanize(widget.url), humanize(widget.url),
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: TextDimens.pt12,
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -33,7 +34,7 @@ class _WebViewScreenState extends State<WebViewScreen> {
), ),
body: WebView( body: WebView(
onWebViewCreated: (WebViewController controller) async { onWebViewCreated: (WebViewController controller) async {
final String? html = await locator.get<CacheRepository>().getHtml( final String? html = await locator.get<OfflineRepository>().getHtml(
url: widget.url, url: widget.url,
); );

View File

@ -7,6 +7,8 @@ import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/services/services.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart'; import 'package:hacki/utils/utils.dart';
class CommentTile extends StatelessWidget { class CommentTile extends StatelessWidget {
@ -15,6 +17,7 @@ class CommentTile extends StatelessWidget {
required this.myUsername, required this.myUsername,
required this.comment, required this.comment,
required this.onStoryLinkTapped, required this.onStoryLinkTapped,
required this.fetchMode,
this.onReplyTapped, this.onReplyTapped,
this.onMoreTapped, this.onMoreTapped,
this.onEditTapped, this.onEditTapped,
@ -30,10 +33,11 @@ class CommentTile extends StatelessWidget {
final int level; final int level;
final bool actionable; final bool actionable;
final Function(Comment)? onReplyTapped; final Function(Comment)? onReplyTapped;
final Function(Comment)? onMoreTapped; final Function(Comment, Rect?)? onMoreTapped;
final Function(Comment)? onEditTapped; final Function(Comment)? onEditTapped;
final Function(Comment)? onRightMoreTapped; final Function(Comment)? onRightMoreTapped;
final Function(String) onStoryLinkTapped; final Function(String) onStoryLinkTapped;
final FetchMode fetchMode;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -42,6 +46,7 @@ class CommentTile extends StatelessWidget {
lazy: false, lazy: false,
create: (_) => CollapseCubit( create: (_) => CollapseCubit(
commentId: comment.id, commentId: comment.id,
collapseCache: context.tryRead<CollapseCache>() ?? CollapseCache(),
)..init(), )..init(),
child: BlocBuilder<CollapseCubit, CollapseState>( child: BlocBuilder<CollapseCubit, CollapseState>(
builder: (BuildContext context, CollapseState state) { builder: (BuildContext context, CollapseState state) {
@ -67,8 +72,8 @@ class CommentTile extends StatelessWidget {
SlidableAction( SlidableAction(
onPressed: (_) => onPressed: (_) =>
onReplyTapped?.call(comment), onReplyTapped?.call(comment),
backgroundColor: Colors.orange, backgroundColor: Palette.orange,
foregroundColor: Colors.white, foregroundColor: Palette.white,
icon: Icons.message, icon: Icons.message,
), ),
if (context if (context
@ -80,15 +85,18 @@ class CommentTile extends StatelessWidget {
SlidableAction( SlidableAction(
onPressed: (_) => onPressed: (_) =>
onEditTapped?.call(comment), onEditTapped?.call(comment),
backgroundColor: Colors.orange, backgroundColor: Palette.orange,
foregroundColor: Colors.white, foregroundColor: Palette.white,
icon: Icons.edit, icon: Icons.edit,
), ),
SlidableAction( SlidableAction(
onPressed: (_) => onPressed: (BuildContext context) =>
onMoreTapped?.call(comment), onMoreTapped?.call(
backgroundColor: Colors.orange, comment,
foregroundColor: Colors.white, context.rect,
),
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
icon: Icons.more_horiz, icon: Icons.more_horiz,
), ),
], ],
@ -101,8 +109,8 @@ class CommentTile extends StatelessWidget {
SlidableAction( SlidableAction(
onPressed: (_) => onPressed: (_) =>
onRightMoreTapped?.call(comment), onRightMoreTapped?.call(comment),
backgroundColor: Colors.orange, backgroundColor: Palette.orange,
foregroundColor: Colors.white, foregroundColor: Palette.white,
icon: Icons.av_timer, icon: Icons.av_timer,
), ),
], ],
@ -111,7 +119,7 @@ class CommentTile extends StatelessWidget {
child: InkWell( child: InkWell(
onTap: () { onTap: () {
if (actionable) { if (actionable) {
HapticFeedback.lightImpact(); HapticFeedback.selectionClick();
context.read<CollapseCubit>().collapse(); context.read<CollapseCubit>().collapse();
} }
}, },
@ -120,9 +128,9 @@ class CommentTile extends StatelessWidget {
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 6, left: Dimens.pt6,
right: 6, right: Dimens.pt6,
top: 6, top: Dimens.pt6,
), ),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
@ -145,7 +153,7 @@ class CommentTile extends StatelessWidget {
Text( Text(
comment.postedDate, comment.postedDate,
style: const TextStyle( style: const TextStyle(
color: Colors.grey, color: Palette.grey,
), ),
), ),
], ],
@ -154,13 +162,14 @@ class CommentTile extends StatelessWidget {
if (actionable && state.collapsed) if (actionable && state.collapsed)
Center( Center(
child: Padding( child: Padding(
padding: padding: const EdgeInsets.only(
const EdgeInsets.only(bottom: 12), bottom: Dimens.pt12,
),
child: Text( child: Text(
'collapsed ' 'collapsed '
'(${state.collapsedCount + 1})', '(${state.collapsedCount + 1})',
style: const TextStyle( style: const TextStyle(
color: Colors.orangeAccent, color: Palette.orangeAccent,
), ),
), ),
), ),
@ -168,11 +177,13 @@ class CommentTile extends StatelessWidget {
else if (comment.deleted) else if (comment.deleted)
const Center( const Center(
child: Padding( child: Padding(
padding: EdgeInsets.only(bottom: 12), padding: EdgeInsets.only(
bottom: Dimens.pt12,
),
child: Text( child: Text(
'deleted', 'deleted',
style: TextStyle( style: TextStyle(
color: Colors.grey, color: Palette.grey,
), ),
), ),
), ),
@ -180,11 +191,13 @@ class CommentTile extends StatelessWidget {
else if (comment.dead) else if (comment.dead)
const Center( const Center(
child: Padding( child: Padding(
padding: EdgeInsets.only(bottom: 12), padding: EdgeInsets.only(
bottom: Dimens.pt12,
),
child: Text( child: Text(
'dead', 'dead',
style: TextStyle( style: TextStyle(
color: Colors.grey, color: Palette.grey,
), ),
), ),
), ),
@ -193,11 +206,13 @@ class CommentTile extends StatelessWidget {
.contains(comment.by)) .contains(comment.by))
const Center( const Center(
child: Padding( child: Padding(
padding: EdgeInsets.only(bottom: 12), padding: EdgeInsets.only(
bottom: Dimens.pt12,
),
child: Text( child: Text(
'blocked', 'blocked',
style: TextStyle( style: TextStyle(
color: Colors.grey, color: Palette.grey,
), ),
), ),
), ),
@ -205,10 +220,10 @@ class CommentTile extends StatelessWidget {
else else
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 8, left: Dimens.pt8,
right: 8, right: Dimens.pt8,
top: 6, top: Dimens.pt6,
bottom: 12, bottom: Dimens.pt12,
), ),
child: comment is BuildableComment child: comment is BuildableComment
? SelectableText.rich( ? SelectableText.rich(
@ -219,15 +234,15 @@ class CommentTile extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: MediaQuery.of(context) fontSize: MediaQuery.of(context)
.textScaleFactor * .textScaleFactor *
15, TextDimens.pt15,
), ),
linkStyle: TextStyle( linkStyle: TextStyle(
fontSize: MediaQuery.of(context) fontSize: MediaQuery.of(context)
.textScaleFactor * .textScaleFactor *
15, TextDimens.pt15,
decoration: decoration:
TextDecoration.underline, TextDecoration.underline,
color: Colors.orange, color: Palette.orange,
), ),
onOpen: (LinkableElement link) { onOpen: (LinkableElement link) {
if (link.url.contains( if (link.url.contains(
@ -247,13 +262,13 @@ class CommentTile extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: MediaQuery.of(context) fontSize: MediaQuery.of(context)
.textScaleFactor * .textScaleFactor *
15, TextDimens.pt15,
), ),
linkStyle: TextStyle( linkStyle: TextStyle(
fontSize: MediaQuery.of(context) fontSize: MediaQuery.of(context)
.textScaleFactor * .textScaleFactor *
15, TextDimens.pt15,
color: Colors.orange, color: Palette.orange,
), ),
onOpen: (LinkableElement link) { onOpen: (LinkableElement link) {
if (link.url.contains( 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( const Divider(
height: 0, height: Dimens.zero,
), ),
], ],
), ),
@ -285,14 +324,14 @@ class CommentTile extends StatelessWidget {
final Color commentColor = prefState.showEyeCandy final Color commentColor = prefState.showEyeCandy
? color.withOpacity(commentBackgroundColorOpacity) ? color.withOpacity(commentBackgroundColorOpacity)
: Colors.transparent; : Palette.transparent;
final bool isMyComment = myUsername == comment.by; final bool isMyComment = myUsername == comment.by;
Widget? wrapper = child; Widget wrapper = child;
if (isMyComment && level == 0) { if (isMyComment && level == 0) {
return Container( return Container(
color: Colors.orange.withOpacity(0.2), color: Palette.orange.withOpacity(0.2),
child: wrapper, child: wrapper,
); );
} }
@ -301,7 +340,9 @@ class CommentTile extends StatelessWidget {
final Color wrapperBorderColor = _getColor(i); final Color wrapperBorderColor = _getColor(i);
final bool shouldHighlight = isMyComment && i == level; final bool shouldHighlight = isMyComment && i == level;
wrapper = Container( wrapper = Container(
margin: const EdgeInsets.only(left: 12), margin: const EdgeInsets.only(
left: Dimens.pt12,
),
decoration: BoxDecoration( decoration: BoxDecoration(
border: i != 0 border: i != 0
? Border( ? Border(
@ -311,14 +352,14 @@ class CommentTile extends StatelessWidget {
) )
: null, : null,
color: shouldHighlight color: shouldHighlight
? Colors.orange.withOpacity(0.2) ? Palette.orange.withOpacity(0.2)
: commentColor, : commentColor,
), ),
child: wrapper, 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) { Color _getColor(int level) {
final int initialLevel = level;
if (_colors[initialLevel] != null) return _colors[initialLevel]!;
while (level >= 10) { while (level >= 10) {
level = level - 10; level = level - 10;
} }
@ -349,6 +395,7 @@ class CommentTile extends StatelessWidget {
1, 1,
); );
_colors[initialLevel] = color;
return color; return color;
} }
} }

View File

@ -7,6 +7,7 @@ import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/story.dart'; import 'package:hacki/models/story.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/screens.dart'; import 'package:hacki/screens/screens.dart';
import 'package:hacki/styles/styles.dart';
class CountdownReminder extends StatefulWidget { class CountdownReminder extends StatefulWidget {
const CountdownReminder({super.key}); const CountdownReminder({super.key});
@ -95,9 +96,13 @@ class _CountDownReminderState extends State<CountdownReminder>
animation: animationController, animation: animationController,
child: FadeIn( child: FadeIn(
child: Material( child: Material(
color: Colors.deepOrange, color: Palette.deepOrange,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
borderRadius: const BorderRadius.all(Radius.circular(4)), borderRadius: const BorderRadius.all(
Radius.circular(
Dimens.pt4,
),
),
child: InkWell( child: InkWell(
onTap: () { onTap: () {
if (state.storyId != null) { if (state.storyId != null) {
@ -122,24 +127,24 @@ class _CountDownReminderState extends State<CountdownReminder>
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 12, left: Dimens.pt12,
top: 10, top: Dimens.pt10,
right: 10, right: Dimens.pt10,
), ),
child: Row( child: Row(
children: const <Widget>[ children: const <Widget>[
Text( Text(
'Pick up where you left off', 'Pick up where you left off',
style: TextStyle( style: TextStyle(
color: Colors.white, color: Palette.white,
fontSize: 12, fontSize: TextDimens.pt12,
), ),
), ),
Spacer(), Spacer(),
Icon( Icon(
Icons.arrow_forward_ios, Icons.arrow_forward_ios,
size: 12, size: TextDimens.pt12,
color: Colors.white, color: Palette.white,
), ),
], ],
), ),

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hacki/styles/styles.dart';
class CustomChip extends StatelessWidget { class CustomChip extends StatelessWidget {
CustomChip({ CustomChip({
@ -15,16 +16,16 @@ class CustomChip extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FilterChip( return FilterChip(
shadowColor: Colors.transparent, shadowColor: Palette.transparent,
selectedShadowColor: Colors.transparent, selectedShadowColor: Palette.transparent,
backgroundColor: Colors.transparent, backgroundColor: Palette.transparent,
shape: const StadiumBorder( shape: const StadiumBorder(
side: BorderSide(color: Colors.orange), side: BorderSide(color: Palette.orange),
), ),
label: Text(label), label: Text(label),
selected: selected, selected: selected,
onSelected: onSelected, onSelected: onSelected,
selectedColor: Colors.orange, selectedColor: Palette.orange,
); );
} }
} }

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hacki/styles/styles.dart';
/// Circular progress indicator with color. /// Circular progress indicator with color.
/// Changing `colorScheme`'s `primary` color doesn't work because it changes /// Changing `colorScheme`'s `primary` color doesn't work because it changes
@ -17,7 +18,7 @@ class CustomCircularProgressIndicator extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CircularProgressIndicator( return CircularProgressIndicator(
strokeWidth: strokeWidth, strokeWidth: strokeWidth,
valueColor: const AlwaysStoppedAnimation<Color>(Colors.orange), valueColor: const AlwaysStoppedAnimation<Color>(Palette.orange),
); );
} }
} }

View File

@ -7,6 +7,7 @@ import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:hacki/blocs/blocs.dart'; import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart'; import 'package:hacki/utils/utils.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
@ -80,8 +81,8 @@ class ItemsListView<T extends Item> extends StatelessWidget {
HapticFeedback.lightImpact(); HapticFeedback.lightImpact();
onPinned?.call(e); onPinned?.call(e);
}, },
backgroundColor: Colors.orange, backgroundColor: Palette.orange,
foregroundColor: Colors.white, foregroundColor: Palette.white,
icon: showWebPreview icon: showWebPreview
? Icons.push_pin_outlined ? Icons.push_pin_outlined
: null, : null,
@ -97,13 +98,15 @@ class ItemsListView<T extends Item> extends StatelessWidget {
showWebPreview: showWebPreview, showWebPreview: showWebPreview,
showMetadata: showMetadata, showMetadata: showMetadata,
hasRead: markReadStories && hasRead, hasRead: markReadStories && hasRead,
simpleTileFontSize: useConsistentFontSize ? 14 : 16, simpleTileFontSize: useConsistentFontSize
? TextDimens.pt14
: TextDimens.pt16,
), ),
), ),
), ),
if (!showWebPreview) if (!showWebPreview)
const Divider( const Divider(
height: 0, height: Dimens.zero,
), ),
]; ];
} else if (e is Comment) { } else if (e is Comment) {
@ -111,22 +114,22 @@ class ItemsListView<T extends Item> extends StatelessWidget {
return <Widget>[ return <Widget>[
if (showWebPreview) if (showWebPreview)
const Divider( const Divider(
height: 0, height: Dimens.zero,
), ),
_CommentTile( _CommentTile(
comment: e, comment: e,
onTap: () => onTap(e), onTap: () => onTap(e),
fontSize: showWebPreview ? 14 : 16, fontSize: showWebPreview ? TextDimens.pt14 : TextDimens.pt16,
), ),
const Divider( const Divider(
height: 0, height: Dimens.zero,
), ),
]; ];
} }
return <Widget>[ return <Widget>[
FadeIn( FadeIn(
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 6), padding: const EdgeInsets.only(left: Dimens.pt6),
child: InkWell( child: InkWell(
onTap: () => onTap(e), onTap: () => onTap(e),
child: Padding( child: Padding(
@ -137,10 +140,12 @@ class ItemsListView<T extends Item> extends StatelessWidget {
if (e.deleted) if (e.deleted)
const Center( const Center(
child: Padding( child: Padding(
padding: EdgeInsets.only(top: 6), padding: EdgeInsets.only(
top: Dimens.pt6,
),
child: Text( child: Text(
'deleted', 'deleted',
style: TextStyle(color: Colors.grey), style: TextStyle(color: Palette.grey),
), ),
), ),
), ),
@ -151,15 +156,15 @@ class ItemsListView<T extends Item> extends StatelessWidget {
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 8, vertical: Dimens.pt8,
horizontal: 6, horizontal: Dimens.pt6,
), ),
child: Linkify( child: Linkify(
text: text:
'''${showCommentBy ? '${e.by}: ' : ''}${e.text}''', '''${showCommentBy ? '${e.by}: ' : ''}${e.text}''',
maxLines: 4, maxLines: 4,
linkStyle: const TextStyle( linkStyle: const TextStyle(
color: Colors.orange, color: Palette.orange,
), ),
onOpen: (LinkableElement link) => onOpen: (LinkableElement link) =>
LinkUtil.launch(link.url), LinkUtil.launch(link.url),
@ -171,18 +176,18 @@ class ItemsListView<T extends Item> extends StatelessWidget {
Text( Text(
e.postedDate, e.postedDate,
style: const TextStyle( style: const TextStyle(
color: Colors.grey, color: Palette.grey,
), ),
), ),
const SizedBox( const SizedBox(
width: 12, width: Dimens.pt12,
), ),
], ],
), ),
], ],
), ),
const Divider( const Divider(
height: 0, height: Dimens.zero,
), ),
], ],
), ),
@ -191,7 +196,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
), ),
), ),
const Divider( const Divider(
height: 0, height: Dimens.zero,
), ),
]; ];
} }
@ -199,7 +204,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
return <Widget>[Container()]; return <Widget>[Container()];
}).expand((List<Widget> element) => element), }).expand((List<Widget> element) => element),
const SizedBox( const SizedBox(
height: 40, height: Dimens.pt40,
), ),
], ],
); );
@ -208,12 +213,14 @@ class ItemsListView<T extends Item> extends StatelessWidget {
enablePullUp: true, enablePullUp: true,
enablePullDown: enablePullDown, enablePullDown: enablePullDown,
header: const WaterDropMaterialHeader( header: const WaterDropMaterialHeader(
backgroundColor: Colors.orange, backgroundColor: Palette.orange,
), ),
footer: CustomFooter( footer: CustomFooter(
loadStyle: LoadStyle.ShowWhenLoading, loadStyle: LoadStyle.ShowWhenLoading,
builder: (BuildContext context, LoadStatus? mode) { builder: (BuildContext context, LoadStatus? mode) {
Widget body; const double height = 55;
late final Widget body;
if (mode == LoadStatus.loading) { if (mode == LoadStatus.loading) {
body = const CustomCircularProgressIndicator(); body = const CustomCircularProgressIndicator();
} else if (mode == LoadStatus.failed) { } else if (mode == LoadStatus.failed) {
@ -224,7 +231,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
body = const SizedBox.shrink(); body = const SizedBox.shrink();
} }
return SizedBox( return SizedBox(
height: 55, height: height,
child: Center(child: body), child: Center(child: body),
); );
}, },
@ -254,12 +261,14 @@ class _CommentTile extends StatelessWidget {
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 12), padding: const EdgeInsets.only(
left: Dimens.pt12,
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
const SizedBox( const SizedBox(
height: 8, height: Dimens.pt8,
), ),
Row( Row(
children: <Widget>[ children: <Widget>[
@ -281,7 +290,7 @@ class _CommentTile extends StatelessWidget {
child: Text( child: Text(
comment.metadata, comment.metadata,
style: TextStyle( style: TextStyle(
color: Colors.grey, color: Palette.grey,
fontSize: fontSize - 2, fontSize: fontSize - 2,
), ),
maxLines: 1, maxLines: 1,
@ -290,7 +299,7 @@ class _CommentTile extends StatelessWidget {
], ],
), ),
const SizedBox( const SizedBox(
height: 8, height: Dimens.pt8,
), ),
], ],
), ),

View File

@ -2,9 +2,11 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hacki/config/constants.dart'; import 'package:hacki/config/constants.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/link_preview/link_view.dart'; import 'package:hacki/screens/widgets/link_preview/link_view.dart';
import 'package:hacki/screens/widgets/link_preview/web_analyzer.dart'; import 'package:hacki/screens/widgets/link_preview/web_analyzer.dart';
import 'package:hacki/styles/styles.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class LinkPreview extends StatefulWidget { class LinkPreview extends StatefulWidget {
@ -97,7 +99,7 @@ class LinkPreview extends StatefulWidget {
final bool removeElevation; final bool removeElevation;
/// Box shadow for the card. Defaults to /// Box shadow for the card. Defaults to
/// `[BoxShadow(blurRadius: 3, color: Colors.grey)]` /// `[BoxShadow(blurRadius: 3, color: Palette.grey)]`
final List<BoxShadow>? boxShadow; final List<BoxShadow>? boxShadow;
final bool showMetadata; final bool showMetadata;
@ -163,11 +165,15 @@ class _LinkPreviewState extends State<LinkPreview> {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: widget.backgroundColor, color: widget.backgroundColor,
borderRadius: BorderRadius.circular(widget.borderRadius ?? 12), borderRadius: BorderRadius.circular(
widget.borderRadius ?? Dimens.pt12,
),
boxShadow: widget.removeElevation boxShadow: widget.removeElevation
? <BoxShadow>[] ? <BoxShadow>[]
: widget.boxShadow ?? : widget.boxShadow ??
<BoxShadow>[const BoxShadow(blurRadius: 3, color: Colors.grey)], <BoxShadow>[
const BoxShadow(blurRadius: 3, color: Palette.grey),
],
), ),
height: _height, height: _height,
child: LinkView( child: LinkView(
@ -194,19 +200,15 @@ class _LinkPreviewState extends State<LinkPreview> {
@override @override
Widget build(BuildContext context) { 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 ?? final Widget loadingWidget = widget.placeholderWidget ??
Container( Container(
height: _height, height: context.storyTileHeight,
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(widget.borderRadius ?? 12), borderRadius: BorderRadius.circular(
color: Colors.grey[200], widget.borderRadius ?? Dimens.pt12,
),
color: Palette.grey[200],
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: const Text('Fetching data...'), child: const Text('Fetching data...'),
@ -217,13 +219,13 @@ class _LinkPreviewState extends State<LinkPreview> {
final WebInfo? info = _info as WebInfo?; final WebInfo? info = _info as WebInfo?;
loadedWidget = _info == null loadedWidget = _info == null
? _buildLinkContainer( ? _buildLinkContainer(
_height, context.storyTileHeight,
title: _errorTitle, title: _errorTitle,
desc: _errorBody, desc: _errorBody,
imageUri: null, imageUri: null,
) )
: _buildLinkContainer( : _buildLinkContainer(
_height, context.storyTileHeight,
title: _errorTitle, title: _errorTitle,
desc: WebAnalyzer.isNotEmpty(info!.description) desc: WebAnalyzer.isNotEmpty(info!.description)
? info.description ? info.description

View File

@ -1,6 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hacki/config/constants.dart'; import 'package:hacki/config/constants.dart';
import 'package:hacki/styles/styles.dart';
class LinkView extends StatelessWidget { class LinkView extends StatelessWidget {
const LinkView({ const LinkView({
@ -74,13 +75,13 @@ class LinkView extends StatelessWidget {
final TextStyle _titleFontSize = titleTextStyle ?? final TextStyle _titleFontSize = titleTextStyle ??
TextStyle( TextStyle(
fontSize: computeTitleFontSize(layoutWidth), fontSize: computeTitleFontSize(layoutWidth),
color: Colors.black, color: Palette.black,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
); );
final TextStyle _bodyFontSize = bodyTextStyle ?? final TextStyle _bodyFontSize = bodyTextStyle ??
TextStyle( TextStyle(
fontSize: computeTitleFontSize(layoutWidth) - 1, fontSize: computeTitleFontSize(layoutWidth) - 1,
color: Colors.grey, color: Palette.grey,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
); );
@ -146,7 +147,7 @@ class LinkView extends StatelessWidget {
Widget _buildTitleContainer(TextStyle _titleTS, int _maxLines) { Widget _buildTitleContainer(TextStyle _titleTS, int _maxLines) {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(4, 2, 3, 1), padding: const EdgeInsets.fromLTRB(4, 2, 3, 0),
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
Container( Container(
@ -167,7 +168,7 @@ class LinkView extends StatelessWidget {
return Expanded( return Expanded(
flex: 2, flex: 2,
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(5, 3, 5, 0), padding: const EdgeInsets.fromLTRB(5, 2, 5, 0),
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
if (showMetadata) if (showMetadata)

View File

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

Some files were not shown because too many files have changed in this diff Show More