Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
a2c66a0075 | |||
5f43fd6968 | |||
d83381a7fd | |||
764ff09345 | |||
ab449adce2 | |||
2ec41b26f2 | |||
19f2107d95 | |||
c9b2d82dfc | |||
56e442b09f | |||
9069efcced | |||
bf6a5667dc | |||
cff73a010b | |||
f0d6cac3fd | |||
a90d52f348 | |||
cff4a3c5c4 | |||
502faaf188 | |||
b952f349fc | |||
9cefffa518 | |||
fe630ea7a9 | |||
459ab961d1 | |||
362d7005df | |||
6b7c1d42de | |||
2a889bca56 | |||
e25026f129 | |||
fefc86275d | |||
1a73a6991e | |||
36d7f4606e | |||
9312c56dd0 | |||
6e71de5913 |
13
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: livinglist
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: jfeng_for_open_source
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
otechie: # Replace with a single Otechie username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
4
.github/workflows/github-actions.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
JAVA_VERSION: "11.0"
|
JAVA_VERSION: "11.0"
|
||||||
FLUTTER_VERSION: "3.0.0"
|
FLUTTER_VERSION: "3.3.2"
|
||||||
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.3.2'
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
- run: flutter pub get
|
- run: flutter pub get
|
||||||
- run: flutter analyze
|
- run: flutter analyze
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
|
|
||||||
# <img width="64" src="https://user-images.githubusercontent.com/7277662/167775086-0b234f28-dee4-44f6-aae4-14a28ed4bbb6.png"> Hacki for Hacker News
|
# <img width="64" src="https://user-images.githubusercontent.com/7277662/167775086-0b234f28-dee4-44f6-aae4-14a28ed4bbb6.png"> Hacki for Hacker News
|
||||||
|
|
||||||
A simple noiseless [Hacker News](https://news.ycombinator.com/) client made with Flutter that is just enough.
|
A [Hacker News](https://news.ycombinator.com/) client made with Flutter that is just enough.
|
||||||
|
|
||||||
[](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone)
|
[](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone)
|
||||||
[](https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US)
|
|
||||||
[](https://f-droid.org/en/packages/com.jiaqifeng.hacki/)
|
[](https://f-droid.org/en/packages/com.jiaqifeng.hacki/)
|
||||||
[](https://github.com/Livinglist/Hacki/releases/latest)
|
[](https://github.com/Livinglist/Hacki/releases/latest)
|
||||||
[](https://badges.pufler.dev)
|
|
||||||
[](https://img.shields.io/github/stars/livinglist/Hacki?style=social)
|
|
||||||
[](https://pub.dev/packages/effective_dart)
|
[](https://pub.dev/packages/effective_dart)
|
||||||
|
[](https://img.shields.io/github/stars/livinglist/Hacki?style=social)
|
||||||
<noscript><a href="https://liberapay.com/jfeng_for_open_source/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a></noscript>
|
|
||||||
|
|
||||||
[<img src="assets/images/app_store_badge.png" height="50">](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone) [<img src="assets/images/google_play_badge.png" height="50">](https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US) [<img src="assets/images/f_droid_badge.png" height="50">](https://f-droid.org/en/packages/com.jiaqifeng.hacki/)
|
[<img src="assets/images/app_store_badge.png" height="50">](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone) [<img src="assets/images/google_play_badge.png" height="50">](https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US) [<img src="assets/images/f_droid_badge.png" height="50">](https://f-droid.org/en/packages/com.jiaqifeng.hacki/)
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
|
|||||||
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 31
|
compileSdkVersion 33
|
||||||
|
|
||||||
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 33
|
||||||
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"
|
||||||
}
|
}
|
||||||
|
@ -16,14 +16,18 @@
|
|||||||
</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">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
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
|
||||||
@ -52,6 +56,21 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="*/*" />
|
<data android:mimeType="*/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:scheme="http"
|
||||||
|
android:host="news.ycombinator.com"
|
||||||
|
android:pathPrefix="/item" />
|
||||||
|
<data
|
||||||
|
android:scheme="https"
|
||||||
|
android:host="news.ycombinator.com"
|
||||||
|
android:pathPrefix="/item" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
4
android/app/src/main/res/xml/backup_rules.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<full-backup-content>
|
||||||
|
<exclude domain="sharedpref" path="FlutterSecureStorage"/>
|
||||||
|
</full-backup-content>
|
@ -1,12 +1,12 @@
|
|||||||
buildscript {
|
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 698 KiB After Width: | Height: | Size: 820 KiB |
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 121 KiB |
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 375 KiB |
Before Width: | Height: | Size: 313 KiB After Width: | Height: | Size: 406 KiB |
@ -1 +0,0 @@
|
|||||||
- Bugfixes.
|
|
@ -1 +0,0 @@
|
|||||||
- Updates to UI.
|
|
@ -1 +0,0 @@
|
|||||||
- Updates to UI.
|
|
@ -1 +0,0 @@
|
|||||||
- Updates to UI.
|
|
@ -1 +0,0 @@
|
|||||||
- Tapping on comments in notification and history screen will lead you directly to the comment.
|
|
@ -1,3 +0,0 @@
|
|||||||
- Tapping on comment in notification or history screen will now lead you directly to the comment.
|
|
||||||
- Fixed the bug where reply box cannot be expanded in editing mode.
|
|
||||||
- Fixed inconsistent font size in history screen.
|
|
@ -1 +0,0 @@
|
|||||||
- Added offline mode.
|
|
@ -1,2 +0,0 @@
|
|||||||
- Added offline mode.
|
|
||||||
- Bugfixes.
|
|
@ -1,2 +0,0 @@
|
|||||||
- Added offline mode.
|
|
||||||
- Bugfixes.
|
|
@ -1,2 +0,0 @@
|
|||||||
- Added offline mode.
|
|
||||||
- Bugfixes.
|
|
@ -1,3 +0,0 @@
|
|||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
@ -1,3 +0,0 @@
|
|||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
@ -1,3 +0,0 @@
|
|||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
@ -1,4 +0,0 @@
|
|||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
@ -1,4 +0,0 @@
|
|||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
@ -1,4 +0,0 @@
|
|||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
@ -1,4 +0,0 @@
|
|||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
@ -1,4 +0,0 @@
|
|||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
@ -1,4 +0,0 @@
|
|||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
@ -1,4 +0,0 @@
|
|||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
@ -1,4 +0,0 @@
|
|||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
@ -1,4 +0,0 @@
|
|||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
@ -1,4 +0,0 @@
|
|||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
@ -1,4 +0,0 @@
|
|||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
@ -1,4 +0,0 @@
|
|||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
@ -1,4 +0,0 @@
|
|||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
@ -1,4 +0,0 @@
|
|||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
@ -1,4 +0,0 @@
|
|||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
@ -1,5 +0,0 @@
|
|||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
||||||
- Bugfixes.
|
|
@ -1,6 +0,0 @@
|
|||||||
- You can now add filters for searching.
|
|
||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
||||||
- Bugfixes.
|
|
@ -1,6 +0,0 @@
|
|||||||
- You can now add filters for searching.
|
|
||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
||||||
- Bugfixes.
|
|
@ -1,6 +0,0 @@
|
|||||||
- You can now add filters for searching.
|
|
||||||
- You can now participate in polls.
|
|
||||||
- Pick up where you left off.
|
|
||||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
|
||||||
- Huge performance boost.
|
|
||||||
- Bugfixes.
|
|
1
fastlane/metadata/android/en-US/changelogs/58.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
- Offline mode now includes web pages.
|
1
fastlane/metadata/android/en-US/changelogs/59.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
- Offline mode now includes web pages.
|
1
fastlane/metadata/android/en-US/changelogs/60.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
- Offline mode now includes web pages.
|
1
fastlane/metadata/android/en-US/changelogs/61.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
- Offline mode now includes web pages.
|
2
fastlane/metadata/android/en-US/changelogs/62.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
- Offline mode now includes web pages.
|
||||||
|
- You can now sort comments in story screen.
|
2
fastlane/metadata/android/en-US/changelogs/63.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
- Offline mode now includes web pages.
|
||||||
|
- You can now sort comments in story screen.
|
2
fastlane/metadata/android/en-US/changelogs/64.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
- Offline mode now includes web pages.
|
||||||
|
- You can now sort comments in story screen.
|
2
fastlane/metadata/android/en-US/changelogs/65.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
- Offline mode now includes web pages.
|
||||||
|
- You can now sort comments in story screen.
|
2
fastlane/metadata/android/en-US/changelogs/66.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
- Offline mode now includes web pages.
|
||||||
|
- You can now sort comments in story screen.
|
2
fastlane/metadata/android/en-US/changelogs/67.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
- Offline mode now includes web pages.
|
||||||
|
- You can now sort comments in story screen.
|
2
fastlane/metadata/android/en-US/changelogs/68.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
- Offline mode now includes web pages.
|
||||||
|
- You can now sort comments in story screen.
|
3
fastlane/metadata/android/en-US/changelogs/69.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
- Lazy loading.
|
||||||
|
- Offline mode now includes web pages.
|
||||||
|
- You can now sort comments in story screen.
|
3
fastlane/metadata/android/en-US/changelogs/70.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
- Lazy loading.
|
||||||
|
- Offline mode now includes web pages.
|
||||||
|
- You can now sort comments in story screen.
|
3
fastlane/metadata/android/en-US/changelogs/71.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
- Lazy loading.
|
||||||
|
- Offline mode now includes web pages.
|
||||||
|
- You can now sort comments in story screen.
|
3
fastlane/metadata/android/en-US/changelogs/72.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
- Lazy loading.
|
||||||
|
- Offline mode now includes web pages.
|
||||||
|
- You can now sort comments in story screen.
|
3
fastlane/metadata/android/en-US/changelogs/73.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
- Lazy loading.
|
||||||
|
- Offline mode now includes web pages.
|
||||||
|
- You can now sort comments in story screen.
|
2
fastlane/metadata/android/en-US/changelogs/74.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
- Bumped Flutter version.
|
||||||
|
- Updated navigation bar background color.
|
Before Width: | Height: | Size: 698 KiB After Width: | Height: | Size: 820 KiB |
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 121 KiB |
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 375 KiB |
Before Width: | Height: | Size: 313 KiB After Width: | Height: | Size: 406 KiB |
68
integration_test/scrolling_test.dart
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:hacki/main.dart' as app;
|
||||||
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final IntegrationTestWidgetsFlutterBinding binding =
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
testWidgets('Scrolling test', (WidgetTester tester) async {
|
||||||
|
await app.main(testing: true);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final Finder bestTabFinder = find.widgetWithText(Tab, 'BEST');
|
||||||
|
|
||||||
|
expect(bestTabFinder, findsOneWidget);
|
||||||
|
|
||||||
|
Future<void> scrollDown(WidgetTester tester) async {
|
||||||
|
await tester.timedDragFrom(
|
||||||
|
const Offset(100, 200),
|
||||||
|
const Offset(100, -700),
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> scrollUp(WidgetTester tester) async {
|
||||||
|
await tester.timedDragFrom(
|
||||||
|
const Offset(100, 200),
|
||||||
|
const Offset(100, 700),
|
||||||
|
const Duration(seconds: 1),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
}
|
||||||
|
|
||||||
|
await binding.traceAction(
|
||||||
|
() async {
|
||||||
|
await tester.tap(bestTabFinder);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
const int count = 10;
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
await scrollDown(tester);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < count - 3; i++) {
|
||||||
|
await scrollUp(tester);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 2));
|
||||||
|
|
||||||
|
final Finder storyFinder = find.byType(StoryTile);
|
||||||
|
|
||||||
|
expect(storyFinder, findsWidgets);
|
||||||
|
|
||||||
|
final Finder firstStoryFinder = storyFinder.first;
|
||||||
|
|
||||||
|
expect(firstStoryFinder, findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(firstStoryFinder);
|
||||||
|
await tester.pump(const Duration(seconds: 4));
|
||||||
|
},
|
||||||
|
reportKey: 'scrolling_timeline',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
@ -21,6 +21,6 @@
|
|||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>MinimumOSVersion</key>
|
<key>MinimumOSVersion</key>
|
||||||
<string>9.0</string>
|
<string>11.0</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Uncomment this line to define a global platform for your project
|
# Uncomment this line to define a global platform for your project
|
||||||
platform :ios, '13.0'
|
# platform :ios, '11.0'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
@ -37,8 +37,5 @@ end
|
|||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
installer.pods_project.targets.each do |target|
|
installer.pods_project.targets.each do |target|
|
||||||
flutter_additional_ios_build_settings(target)
|
flutter_additional_ios_build_settings(target)
|
||||||
target.build_configurations.each do |config|
|
|
||||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -14,15 +14,21 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- flutter_secure_storage (3.3.1):
|
- flutter_secure_storage (3.3.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- flutter_siri_suggestions (0.0.1):
|
||||||
|
- Flutter
|
||||||
- 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
|
||||||
- ReachabilitySwift (5.0.0)
|
- ReachabilitySwift (5.0.0)
|
||||||
- receive_sharing_intent (0.0.1):
|
- receive_sharing_intent (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- share_plus (0.0.1):
|
||||||
|
- Flutter
|
||||||
- shared_preferences_ios (0.0.1):
|
- shared_preferences_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- sqflite (0.0.2):
|
- sqflite (0.0.2):
|
||||||
@ -32,8 +38,6 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- video_player_avfoundation (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- wakelock (0.0.1):
|
- wakelock (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- webview_flutter_wkwebview (0.0.1):
|
- webview_flutter_wkwebview (0.0.1):
|
||||||
@ -47,13 +51,15 @@ DEPENDENCIES:
|
|||||||
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
|
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
|
||||||
- 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`)
|
||||||
|
- 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`)
|
||||||
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
||||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||||
- synced_shared_preferences (from `.symlinks/plugins/synced_shared_preferences/ios`)
|
- synced_shared_preferences (from `.symlinks/plugins/synced_shared_preferences/ios`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
|
|
||||||
- wakelock (from `.symlinks/plugins/wakelock/ios`)
|
- wakelock (from `.symlinks/plugins/wakelock/ios`)
|
||||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
||||||
- workmanager (from `.symlinks/plugins/workmanager/ios`)
|
- workmanager (from `.symlinks/plugins/workmanager/ios`)
|
||||||
@ -75,10 +81,16 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||||
|
flutter_siri_suggestions:
|
||||||
|
:path: ".symlinks/plugins/flutter_siri_suggestions/ios"
|
||||||
|
integration_test:
|
||||||
|
:path: ".symlinks/plugins/integration_test/ios"
|
||||||
path_provider_ios:
|
path_provider_ios:
|
||||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||||
receive_sharing_intent:
|
receive_sharing_intent:
|
||||||
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
||||||
|
share_plus:
|
||||||
|
:path: ".symlinks/plugins/share_plus/ios"
|
||||||
shared_preferences_ios:
|
shared_preferences_ios:
|
||||||
:path: ".symlinks/plugins/shared_preferences_ios/ios"
|
:path: ".symlinks/plugins/shared_preferences_ios/ios"
|
||||||
sqflite:
|
sqflite:
|
||||||
@ -87,8 +99,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/synced_shared_preferences/ios"
|
:path: ".symlinks/plugins/synced_shared_preferences/ios"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
video_player_avfoundation:
|
|
||||||
:path: ".symlinks/plugins/video_player_avfoundation/ios"
|
|
||||||
wakelock:
|
wakelock:
|
||||||
:path: ".symlinks/plugins/wakelock/ios"
|
:path: ".symlinks/plugins/wakelock/ios"
|
||||||
webview_flutter_wkwebview:
|
webview_flutter_wkwebview:
|
||||||
@ -98,24 +108,26 @@ EXTERNAL SOURCES:
|
|||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
|
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
|
||||||
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||||
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
|
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
|
||||||
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
|
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
|
||||||
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
|
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
|
||||||
|
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
|
||||||
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
|
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
|
||||||
|
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||||
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
||||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||||
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
|
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
|
||||||
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||||
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
|
|
||||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||||
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
|
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
|
||||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||||
|
|
||||||
PODFILE CHECKSUM: e4c97c7a9aacaeda4b952f7ef9ea29e47660f622
|
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
|
||||||
|
|
||||||
COCOAPODS: 1.11.2
|
COCOAPODS: 1.11.2
|
||||||
|
@ -549,7 +549,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
@ -568,16 +568,16 @@
|
|||||||
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 = 1;
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.2.16;
|
MARKETING_VERSION = 0.2.31;
|
||||||
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 = "";
|
||||||
@ -635,7 +635,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@ -684,7 +684,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
@ -705,16 +705,16 @@
|
|||||||
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 = 1;
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.2.16;
|
MARKETING_VERSION = 0.2.31;
|
||||||
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,16 +736,16 @@
|
|||||||
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 = 1;
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.2.16;
|
MARKETING_VERSION = 0.2.31;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
@ -7,7 +7,9 @@ import 'package:hacki/config/locator.dart';
|
|||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
import 'package:responsive_builder/responsive_builder.dart';
|
import 'package:responsive_builder/responsive_builder.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
part 'stories_event.dart';
|
part 'stories_event.dart';
|
||||||
part 'stories_state.dart';
|
part 'stories_state.dart';
|
||||||
@ -15,15 +17,18 @@ part 'stories_state.dart';
|
|||||||
class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||||
StoriesBloc({
|
StoriesBloc({
|
||||||
required PreferenceCubit preferenceCubit,
|
required PreferenceCubit preferenceCubit,
|
||||||
CacheRepository? cacheRepository,
|
OfflineRepository? offlineRepository,
|
||||||
StoriesRepository? storiesRepository,
|
StoriesRepository? storiesRepository,
|
||||||
PreferenceRepository? preferenceRepository,
|
PreferenceRepository? preferenceRepository,
|
||||||
|
Logger? logger,
|
||||||
}) : _preferenceCubit = preferenceCubit,
|
}) : _preferenceCubit = preferenceCubit,
|
||||||
_cacheRepository = cacheRepository ?? locator.get<CacheRepository>(),
|
_offlineRepository =
|
||||||
|
offlineRepository ?? locator.get<OfflineRepository>(),
|
||||||
_storiesRepository =
|
_storiesRepository =
|
||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||||
_preferenceRepository =
|
_preferenceRepository =
|
||||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||||
|
_logger = logger ?? locator.get<Logger>(),
|
||||||
super(const StoriesState.init()) {
|
super(const StoriesState.init()) {
|
||||||
on<StoriesInitialize>(onInitialize);
|
on<StoriesInitialize>(onInitialize);
|
||||||
on<StoriesRefresh>(onRefresh);
|
on<StoriesRefresh>(onRefresh);
|
||||||
@ -32,15 +37,17 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
on<StoryRead>(onStoryRead);
|
on<StoryRead>(onStoryRead);
|
||||||
on<StoriesLoaded>(onStoriesLoaded);
|
on<StoriesLoaded>(onStoriesLoaded);
|
||||||
on<StoriesDownload>(onDownload);
|
on<StoriesDownload>(onDownload);
|
||||||
|
on<StoryDownloaded>(onStoryDownloaded);
|
||||||
on<StoriesExitOffline>(onExitOffline);
|
on<StoriesExitOffline>(onExitOffline);
|
||||||
on<StoriesPageSizeChanged>(onPageSizeChanged);
|
on<StoriesPageSizeChanged>(onPageSizeChanged);
|
||||||
on<ClearAllReadStories>(onClearAllReadStories);
|
on<ClearAllReadStories>(onClearAllReadStories);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@ -70,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(
|
||||||
@ -89,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)),
|
||||||
)
|
)
|
||||||
@ -166,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,
|
||||||
@ -240,39 +247,46 @@ 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> allIds = <int>{};
|
final Set<int> prioritizedIds = <int>{};
|
||||||
|
final List<StoryType> prioritizedTypes = <StoryType>[...types]
|
||||||
|
..remove(StoryType.latest);
|
||||||
|
|
||||||
for (final StoryType type in types) {
|
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);
|
||||||
allIds.addAll(ids);
|
prioritizedIds.addAll(ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
_storiesRepository
|
|
||||||
.fetchStoriesStream(ids: allIds.toList())
|
|
||||||
.listen((Story story) async {
|
|
||||||
if (story.kids.isNotEmpty) {
|
|
||||||
await _cacheRepository.cacheStory(story: story);
|
|
||||||
_storiesRepository
|
|
||||||
.fetchAllChildrenComments(ids: story.kids)
|
|
||||||
.listen((Comment? comment) async {
|
|
||||||
if (comment != null) {
|
|
||||||
await _cacheRepository.cacheComment(comment: comment);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).onDone(() {
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
downloadStatus: StoriesDownloadStatus.finished,
|
storiesDownloaded: 0,
|
||||||
|
storiesToBeDownloaded: prioritizedIds.length,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
try {
|
||||||
|
await fetchAndCacheStories(
|
||||||
|
prioritizedIds,
|
||||||
|
includingWebPage: event.includingWebPage,
|
||||||
|
isPrioritized: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final Set<int> latestIds = <int>{};
|
||||||
|
final List<int> ids = await _storiesRepository.fetchStoryIds(
|
||||||
|
of: StoryType.latest,
|
||||||
|
);
|
||||||
|
await _offlineRepository.cacheStoryIds(of: StoryType.latest, ids: ids);
|
||||||
|
latestIds.addAll(ids);
|
||||||
|
|
||||||
|
await fetchAndCacheStories(
|
||||||
|
latestIds,
|
||||||
|
includingWebPage: event.includingWebPage,
|
||||||
|
isPrioritized: false,
|
||||||
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -282,6 +296,80 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> fetchAndCacheStories(
|
||||||
|
Iterable<int> ids, {
|
||||||
|
required bool includingWebPage,
|
||||||
|
required bool isPrioritized,
|
||||||
|
}) async {
|
||||||
|
for (final int id in ids) {
|
||||||
|
final Story? story = await _storiesRepository.fetchStoryBy(id);
|
||||||
|
|
||||||
|
if (story == null) {
|
||||||
|
if (isPrioritized) {
|
||||||
|
add(StoryDownloaded(skipped: true));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (story.kids.isEmpty) {
|
||||||
|
if (isPrioritized) {
|
||||||
|
add(StoryDownloaded(skipped: true));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _offlineRepository.cacheStory(story: story);
|
||||||
|
|
||||||
|
if (story.url.isNotEmpty && includingWebPage) {
|
||||||
|
_logger.i('downloading ${story.url}');
|
||||||
|
await _offlineRepository.cacheUrl(url: story.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
_storiesRepository
|
||||||
|
.fetchAllChildrenComments(ids: story.kids)
|
||||||
|
.whereType<Comment>()
|
||||||
|
.listen(
|
||||||
|
(Comment comment) => unawaited(
|
||||||
|
_offlineRepository.cacheComment(comment: comment),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.onDone(() => add(StoryDownloaded(skipped: false)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onStoryDownloaded(StoryDownloaded event, Emitter<StoriesState> emit) {
|
||||||
|
if (event.skipped) {
|
||||||
|
final int updatedStoriesToBeDownloaded = state.storiesToBeDownloaded - 1;
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
storiesToBeDownloaded: updatedStoriesToBeDownloaded,
|
||||||
|
downloadStatus:
|
||||||
|
state.storiesDownloaded == updatedStoriesToBeDownloaded
|
||||||
|
? StoriesDownloadStatus.finished
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final int updatedStoriesDownloaded = state.storiesDownloaded + 1;
|
||||||
|
final int updatedStoriesToBeDownloaded =
|
||||||
|
updatedStoriesDownloaded > state.storiesToBeDownloaded
|
||||||
|
? state.storiesToBeDownloaded + 1
|
||||||
|
: state.storiesToBeDownloaded;
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
storiesDownloaded: updatedStoriesDownloaded,
|
||||||
|
storiesToBeDownloaded: updatedStoriesToBeDownloaded,
|
||||||
|
downloadStatus:
|
||||||
|
updatedStoriesDownloaded == updatedStoriesToBeDownloaded
|
||||||
|
? StoriesDownloadStatus.finished
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> onPageSizeChanged(
|
Future<void> onPageSizeChanged(
|
||||||
StoriesPageSizeChanged event,
|
StoriesPageSizeChanged event,
|
||||||
Emitter<StoriesState> emit,
|
Emitter<StoriesState> emit,
|
||||||
@ -294,9 +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 _offlineRepository.deleteAllWebPages();
|
||||||
emit(state.copyWith(offlineReading: false));
|
emit(state.copyWith(offlineReading: false));
|
||||||
add(StoriesInitialize());
|
add(StoriesInitialize());
|
||||||
}
|
}
|
||||||
|
@ -38,8 +38,21 @@ class StoriesLoadMore extends StoriesEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class StoriesDownload extends StoriesEvent {
|
class StoriesDownload extends StoriesEvent {
|
||||||
|
StoriesDownload({required this.includingWebPage});
|
||||||
|
|
||||||
|
final bool includingWebPage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[];
|
List<Object?> get props => <Object?>[includingWebPage];
|
||||||
|
}
|
||||||
|
|
||||||
|
class StoryDownloaded extends StoriesEvent {
|
||||||
|
StoryDownloaded({required this.skipped});
|
||||||
|
|
||||||
|
final bool skipped;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[skipped];
|
||||||
}
|
}
|
||||||
|
|
||||||
class StoriesExitOffline extends StoriesEvent {
|
class StoriesExitOffline extends StoriesEvent {
|
||||||
|
@ -23,6 +23,8 @@ class StoriesState extends Equatable {
|
|||||||
required this.offlineReading,
|
required this.offlineReading,
|
||||||
required this.downloadStatus,
|
required this.downloadStatus,
|
||||||
required this.currentPageSize,
|
required this.currentPageSize,
|
||||||
|
required this.storiesDownloaded,
|
||||||
|
required this.storiesToBeDownloaded,
|
||||||
});
|
});
|
||||||
|
|
||||||
const StoriesState.init({
|
const StoriesState.init({
|
||||||
@ -61,7 +63,9 @@ class StoriesState extends Equatable {
|
|||||||
}) : offlineReading = false,
|
}) : offlineReading = false,
|
||||||
downloadStatus = StoriesDownloadStatus.initial,
|
downloadStatus = StoriesDownloadStatus.initial,
|
||||||
currentPageSize = 0,
|
currentPageSize = 0,
|
||||||
readStoriesIds = const <int>{};
|
readStoriesIds = const <int>{},
|
||||||
|
storiesDownloaded = 0,
|
||||||
|
storiesToBeDownloaded = 0;
|
||||||
|
|
||||||
final Map<StoryType, List<Story>> storiesByType;
|
final Map<StoryType, List<Story>> storiesByType;
|
||||||
final Map<StoryType, List<int>> storyIdsByType;
|
final Map<StoryType, List<int>> storyIdsByType;
|
||||||
@ -71,6 +75,8 @@ class StoriesState extends Equatable {
|
|||||||
final StoriesDownloadStatus downloadStatus;
|
final StoriesDownloadStatus downloadStatus;
|
||||||
final bool offlineReading;
|
final bool offlineReading;
|
||||||
final int currentPageSize;
|
final int currentPageSize;
|
||||||
|
final int storiesDownloaded;
|
||||||
|
final int storiesToBeDownloaded;
|
||||||
|
|
||||||
StoriesState copyWith({
|
StoriesState copyWith({
|
||||||
Map<StoryType, List<Story>>? storiesByType,
|
Map<StoryType, List<Story>>? storiesByType,
|
||||||
@ -81,6 +87,8 @@ class StoriesState extends Equatable {
|
|||||||
StoriesDownloadStatus? downloadStatus,
|
StoriesDownloadStatus? downloadStatus,
|
||||||
bool? offlineReading,
|
bool? offlineReading,
|
||||||
int? currentPageSize,
|
int? currentPageSize,
|
||||||
|
int? storiesDownloaded,
|
||||||
|
int? storiesToBeDownloaded,
|
||||||
}) {
|
}) {
|
||||||
return StoriesState(
|
return StoriesState(
|
||||||
storiesByType: storiesByType ?? this.storiesByType,
|
storiesByType: storiesByType ?? this.storiesByType,
|
||||||
@ -91,6 +99,9 @@ class StoriesState extends Equatable {
|
|||||||
offlineReading: offlineReading ?? this.offlineReading,
|
offlineReading: offlineReading ?? this.offlineReading,
|
||||||
downloadStatus: downloadStatus ?? this.downloadStatus,
|
downloadStatus: downloadStatus ?? this.downloadStatus,
|
||||||
currentPageSize: currentPageSize ?? this.currentPageSize,
|
currentPageSize: currentPageSize ?? this.currentPageSize,
|
||||||
|
storiesDownloaded: storiesDownloaded ?? this.storiesDownloaded,
|
||||||
|
storiesToBeDownloaded:
|
||||||
|
storiesToBeDownloaded ?? this.storiesToBeDownloaded,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,18 +113,12 @@ class StoriesState extends Equatable {
|
|||||||
final Map<StoryType, List<Story>> newMap =
|
final Map<StoryType, List<Story>> newMap =
|
||||||
Map<StoryType, List<Story>>.from(storiesByType);
|
Map<StoryType, List<Story>>.from(storiesByType);
|
||||||
newMap[of] = List<Story>.from(newMap[of]!)..add(story);
|
newMap[of] = List<Story>.from(newMap[of]!)..add(story);
|
||||||
return StoriesState(
|
return copyWith(
|
||||||
storiesByType: newMap,
|
storiesByType: newMap,
|
||||||
storyIdsByType: storyIdsByType,
|
|
||||||
statusByType: statusByType,
|
|
||||||
currentPageByType: currentPageByType,
|
|
||||||
readStoriesIds: <int>{
|
readStoriesIds: <int>{
|
||||||
...readStoriesIds,
|
...readStoriesIds,
|
||||||
if (hasRead) story.id,
|
if (hasRead) story.id,
|
||||||
},
|
},
|
||||||
offlineReading: offlineReading,
|
|
||||||
downloadStatus: downloadStatus,
|
|
||||||
currentPageSize: currentPageSize,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,15 +129,8 @@ class StoriesState extends Equatable {
|
|||||||
final Map<StoryType, List<int>> newMap =
|
final Map<StoryType, List<int>> newMap =
|
||||||
Map<StoryType, List<int>>.from(storyIdsByType);
|
Map<StoryType, List<int>>.from(storyIdsByType);
|
||||||
newMap[of] = to;
|
newMap[of] = to;
|
||||||
return StoriesState(
|
return copyWith(
|
||||||
storiesByType: storiesByType,
|
|
||||||
storyIdsByType: newMap,
|
storyIdsByType: newMap,
|
||||||
statusByType: statusByType,
|
|
||||||
currentPageByType: currentPageByType,
|
|
||||||
readStoriesIds: readStoriesIds,
|
|
||||||
offlineReading: offlineReading,
|
|
||||||
downloadStatus: downloadStatus,
|
|
||||||
currentPageSize: currentPageSize,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,15 +141,8 @@ class StoriesState extends Equatable {
|
|||||||
final Map<StoryType, StoriesStatus> newMap =
|
final Map<StoryType, StoriesStatus> newMap =
|
||||||
Map<StoryType, StoriesStatus>.from(statusByType);
|
Map<StoryType, StoriesStatus>.from(statusByType);
|
||||||
newMap[of] = to;
|
newMap[of] = to;
|
||||||
return StoriesState(
|
return copyWith(
|
||||||
storiesByType: storiesByType,
|
|
||||||
storyIdsByType: storyIdsByType,
|
|
||||||
statusByType: newMap,
|
statusByType: newMap,
|
||||||
currentPageByType: currentPageByType,
|
|
||||||
readStoriesIds: readStoriesIds,
|
|
||||||
offlineReading: offlineReading,
|
|
||||||
downloadStatus: downloadStatus,
|
|
||||||
currentPageSize: currentPageSize,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,15 +153,8 @@ class StoriesState extends Equatable {
|
|||||||
final Map<StoryType, int> newMap =
|
final Map<StoryType, int> newMap =
|
||||||
Map<StoryType, int>.from(currentPageByType);
|
Map<StoryType, int>.from(currentPageByType);
|
||||||
newMap[of] = to;
|
newMap[of] = to;
|
||||||
return StoriesState(
|
return copyWith(
|
||||||
storiesByType: storiesByType,
|
|
||||||
storyIdsByType: storyIdsByType,
|
|
||||||
statusByType: statusByType,
|
|
||||||
currentPageByType: newMap,
|
currentPageByType: newMap,
|
||||||
readStoriesIds: readStoriesIds,
|
|
||||||
offlineReading: offlineReading,
|
|
||||||
downloadStatus: downloadStatus,
|
|
||||||
currentPageSize: currentPageSize,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,15 +171,11 @@ class StoriesState extends Equatable {
|
|||||||
final Map<StoryType, int> newCurrentPageMap =
|
final Map<StoryType, int> newCurrentPageMap =
|
||||||
Map<StoryType, int>.from(currentPageByType);
|
Map<StoryType, int>.from(currentPageByType);
|
||||||
newCurrentPageMap[of] = 0;
|
newCurrentPageMap[of] = 0;
|
||||||
return StoriesState(
|
return copyWith(
|
||||||
storiesByType: newStoriesMap,
|
storiesByType: newStoriesMap,
|
||||||
storyIdsByType: newStoryIdsMap,
|
storyIdsByType: newStoryIdsMap,
|
||||||
statusByType: newStatusMap,
|
statusByType: newStatusMap,
|
||||||
currentPageByType: newCurrentPageMap,
|
currentPageByType: newCurrentPageMap,
|
||||||
readStoriesIds: readStoriesIds,
|
|
||||||
offlineReading: offlineReading,
|
|
||||||
downloadStatus: downloadStatus,
|
|
||||||
currentPageSize: currentPageSize,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,5 +189,7 @@ class StoriesState extends Equatable {
|
|||||||
offlineReading,
|
offlineReading,
|
||||||
downloadStatus,
|
downloadStatus,
|
||||||
currentPageSize,
|
currentPageSize,
|
||||||
|
storiesDownloaded,
|
||||||
|
storiesToBeDownloaded,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ abstract class Constants {
|
|||||||
'https://apps.apple.com/us/app/hacki/id1602043763?action=write-review';
|
'https://apps.apple.com/us/app/hacki/id1602043763?action=write-review';
|
||||||
static const String googlePlayLink =
|
static const String googlePlayLink =
|
||||||
'https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US';
|
'https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US';
|
||||||
|
static const String sponsorLink = 'https://github.com/sponsors/Livinglist';
|
||||||
|
|
||||||
static const String _imagePath = 'assets/images';
|
static const String _imagePath = 'assets/images';
|
||||||
static const String hackerNewsLogoPath = '$_imagePath/hacker_news_logo.png';
|
static const String hackerNewsLogoPath = '$_imagePath/hacker_news_logo.png';
|
||||||
|
31
lib/config/custom_log_filter.dart
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
|
class CustomLogFilter extends LogFilter {
|
||||||
|
@override
|
||||||
|
// ignore: overridden_fields
|
||||||
|
Level? level = Level.verbose;
|
||||||
|
|
||||||
|
/// The minimal level allowed in production.
|
||||||
|
static const Level _minimalLevel = Level.info;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldLog(LogEvent event) {
|
||||||
|
bool shouldLog = false;
|
||||||
|
|
||||||
|
if (event.level.index >= _minimalLevel.index) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(
|
||||||
|
() {
|
||||||
|
if (event.level.index >= level!.index) {
|
||||||
|
shouldLog = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}(),
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
|
||||||
|
return shouldLog;
|
||||||
|
}
|
||||||
|
}
|
@ -10,8 +10,8 @@ class CustomRouter {
|
|||||||
switch (settings.name) {
|
switch (settings.name) {
|
||||||
case HomeScreen.routeName:
|
case HomeScreen.routeName:
|
||||||
return HomeScreen.route();
|
return HomeScreen.route();
|
||||||
case StoryScreen.routeName:
|
case ItemScreen.routeName:
|
||||||
return StoryScreen.route(settings.arguments! as StoryScreenArgs);
|
return ItemScreen.route(settings.arguments! as ItemScreenArgs);
|
||||||
case SubmitScreen.routeName:
|
case SubmitScreen.routeName:
|
||||||
return SubmitScreen.route();
|
return SubmitScreen.route();
|
||||||
default:
|
default:
|
||||||
@ -22,8 +22,8 @@ class CustomRouter {
|
|||||||
/// Nested routing for bottom navigation bar.
|
/// Nested routing for bottom navigation bar.
|
||||||
static Route<dynamic> onGenerateNestedRoute(RouteSettings settings) {
|
static Route<dynamic> onGenerateNestedRoute(RouteSettings settings) {
|
||||||
switch (settings.name) {
|
switch (settings.name) {
|
||||||
case StoryScreen.routeName:
|
case ItemScreen.routeName:
|
||||||
return StoryScreen.route(settings.arguments! as StoryScreenArgs);
|
return ItemScreen.route(settings.arguments! as ItemScreenArgs);
|
||||||
case SubmitScreen.routeName:
|
case SubmitScreen.routeName:
|
||||||
return SubmitScreen.route();
|
return SubmitScreen.route();
|
||||||
default:
|
default:
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:hacki/config/custom_log_filter.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
import 'package:hacki/services/services.dart';
|
import 'package:hacki/services/services.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
@ -10,16 +11,17 @@ final GetIt locator = GetIt.instance;
|
|||||||
/// Set up [GetIt] locator.
|
/// Set up [GetIt] locator.
|
||||||
Future<void> setUpLocator() async {
|
Future<void> setUpLocator() async {
|
||||||
locator
|
locator
|
||||||
|
..registerSingleton<Logger>(Logger(filter: CustomLogFilter()))
|
||||||
..registerSingleton<StoriesRepository>(StoriesRepository())
|
..registerSingleton<StoriesRepository>(StoriesRepository())
|
||||||
..registerSingleton<PreferenceRepository>(PreferenceRepository())
|
..registerSingleton<PreferenceRepository>(PreferenceRepository())
|
||||||
..registerSingleton<SearchRepository>(SearchRepository())
|
..registerSingleton<SearchRepository>(SearchRepository())
|
||||||
..registerSingleton<AuthRepository>(AuthRepository())
|
..registerSingleton<AuthRepository>(AuthRepository())
|
||||||
..registerSingleton<PostRepository>(PostRepository())
|
..registerSingleton<PostRepository>(PostRepository())
|
||||||
..registerSingleton<SembastRepository>(SembastRepository())
|
..registerSingleton<SembastRepository>(SembastRepository())
|
||||||
..registerSingleton<CacheRepository>(CacheRepository())
|
..registerSingleton<OfflineRepository>(OfflineRepository())
|
||||||
..registerSingleton<CacheService>(CacheService())
|
..registerSingleton<DraftCache>(DraftCache())
|
||||||
|
..registerSingleton<CommentCache>(CommentCache())
|
||||||
..registerSingleton<LocalNotification>(LocalNotification())
|
..registerSingleton<LocalNotification>(LocalNotification())
|
||||||
..registerSingleton<Logger>(Logger())
|
|
||||||
..registerSingleton<RouteObserver<ModalRoute<dynamic>>>(
|
..registerSingleton<RouteObserver<ModalRoute<dynamic>>>(
|
||||||
RouteObserver<ModalRoute<dynamic>>(),
|
RouteObserver<ModalRoute<dynamic>>(),
|
||||||
);
|
);
|
||||||
|
@ -10,31 +10,31 @@ part 'collapse_state.dart';
|
|||||||
class CollapseCubit extends Cubit<CollapseState> {
|
class CollapseCubit extends Cubit<CollapseState> {
|
||||||
CollapseCubit({
|
CollapseCubit({
|
||||||
required int commentId,
|
required int commentId,
|
||||||
CacheService? cacheService,
|
CollapseCache? collapseCache,
|
||||||
}) : _commentId = commentId,
|
}) : _commentId = commentId,
|
||||||
_cacheService = cacheService ?? locator.get<CacheService>(),
|
_collapseCache = collapseCache ?? locator.get<CollapseCache>(),
|
||||||
super(const CollapseState.init());
|
super(const CollapseState.init());
|
||||||
|
|
||||||
final int _commentId;
|
final int _commentId;
|
||||||
final CacheService _cacheService;
|
final CollapseCache _collapseCache;
|
||||||
late final StreamSubscription<Map<int, Set<int>>> _streamSubscription;
|
late final StreamSubscription<Map<int, Set<int>>> _streamSubscription;
|
||||||
|
|
||||||
void init() {
|
void init() {
|
||||||
_streamSubscription =
|
_streamSubscription =
|
||||||
_cacheService.hiddenComments.listen(hiddenCommentsStreamListener);
|
_collapseCache.hiddenComments.listen(hiddenCommentsStreamListener);
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
collapsedCount: _cacheService.totalHidden(_commentId),
|
collapsedCount: _collapseCache.totalHidden(_commentId),
|
||||||
collapsed: _cacheService.isCollapsed(_commentId),
|
collapsed: _collapseCache.isCollapsed(_commentId),
|
||||||
hidden: _cacheService.isHidden(_commentId),
|
hidden: _collapseCache.isHidden(_commentId),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void collapse() {
|
void collapse() {
|
||||||
if (state.collapsed) {
|
if (state.collapsed) {
|
||||||
_cacheService.uncollapse(_commentId);
|
_collapseCache.uncollapse(_commentId);
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -43,7 +43,7 @@ class CollapseCubit extends Cubit<CollapseState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final int count = _cacheService.collapse(_commentId);
|
final int count = _collapseCache.collapse(_commentId);
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
|
@ -1,36 +1,66 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
|
import 'package:hacki/main.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: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 Story story,
|
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, story: story));
|
_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
|
||||||
@ -42,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)) {
|
||||||
@ -54,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,
|
||||||
)
|
)
|
||||||
@ -64,34 +95,53 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(state.copyWith(status: CommentsStatus.loading));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: CommentsStatus.loading,
|
||||||
|
comments: <Comment>[],
|
||||||
|
currentPage: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final Story story = state.story;
|
final Item item = state.item;
|
||||||
final Story updatedStory = state.offlineReading
|
final Item updatedItem = state.offlineReading
|
||||||
? story
|
? item
|
||||||
: await _storiesRepository.fetchStoryBy(story.id) ?? story;
|
: await _storiesRepository.fetchItemBy(id: item.id) ?? item;
|
||||||
|
final List<int> kids = sortKids(updatedItem.kids);
|
||||||
|
|
||||||
emit(state.copyWith(story: updatedStory));
|
emit(state.copyWith(item: updatedItem));
|
||||||
|
|
||||||
if (state.offlineReading) {
|
if (state.offlineReading) {
|
||||||
_streamSubscription = _cacheRepository
|
_streamSubscription = _offlineRepository
|
||||||
.getCachedCommentsStream(ids: updatedStory.kids)
|
.getCachedCommentsStream(ids: kids)
|
||||||
.listen(_onCommentFetched)
|
.listen(_onCommentFetched)
|
||||||
..onDone(_onDone);
|
..onDone(_onDone);
|
||||||
} else {
|
} else {
|
||||||
|
switch (state.fetchMode) {
|
||||||
|
case FetchMode.lazy:
|
||||||
_streamSubscription = _storiesRepository
|
_streamSubscription = _storiesRepository
|
||||||
.fetchCommentsStream(ids: updatedStory.kids)
|
.fetchCommentsStream(
|
||||||
|
ids: kids,
|
||||||
|
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
||||||
|
)
|
||||||
.listen(_onCommentFetched)
|
.listen(_onCommentFetched)
|
||||||
..onDone(_onDone);
|
..onDone(_onDone);
|
||||||
|
break;
|
||||||
|
case FetchMode.eager:
|
||||||
|
_streamSubscription = _storiesRepository
|
||||||
|
.fetchAllCommentsRecursivelyStream(
|
||||||
|
ids: kids,
|
||||||
|
getFromCache: useCommentCache ? _commentCache.getComment : null,
|
||||||
|
)
|
||||||
|
.listen(_onCommentFetched)
|
||||||
|
..onDone(_onDone);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refresh() async {
|
Future<void> refresh() async {
|
||||||
final bool offlineReading = await _cacheRepository.hasCachedStories;
|
if (state.offlineReading) {
|
||||||
|
|
||||||
_cacheService.resetCollapsedComments();
|
|
||||||
|
|
||||||
if (offlineReading) {
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: CommentsStatus.loaded,
|
status: CommentsStatus.loaded,
|
||||||
@ -103,44 +153,180 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: CommentsStatus.loading,
|
status: CommentsStatus.loading,
|
||||||
comments: <Comment>[],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await _streamSubscription?.cancel();
|
_collapseCache.resetCollapsedComments();
|
||||||
|
|
||||||
final Story story = state.story;
|
await _streamSubscription?.cancel();
|
||||||
final Story updatedStory =
|
for (final int id in _streamSubscriptions.keys) {
|
||||||
await _storiesRepository.fetchStoryBy(story.id) ?? story;
|
await _streamSubscriptions[id]?.cancel();
|
||||||
_streamSubscription = _storiesRepository
|
}
|
||||||
.fetchCommentsStream(ids: updatedStory.kids)
|
_streamSubscriptions.clear();
|
||||||
.listen(_onCommentFetched)
|
|
||||||
..onDone(_onDone);
|
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
story: updatedStory,
|
comments: <Comment>[],
|
||||||
|
currentPage: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Item item = state.item;
|
||||||
|
final Item updatedItem =
|
||||||
|
await _storiesRepository.fetchItemBy(id: item.id) ?? item;
|
||||||
|
final List<int> kids = sortKids(updatedItem.kids);
|
||||||
|
|
||||||
|
if (state.fetchMode == FetchMode.lazy) {
|
||||||
|
_streamSubscription = _storiesRepository
|
||||||
|
.fetchCommentsStream(
|
||||||
|
ids: kids,
|
||||||
|
)
|
||||||
|
.listen(_onCommentFetched)
|
||||||
|
..onDone(_onDone);
|
||||||
|
} else {
|
||||||
|
_streamSubscription = _storiesRepository
|
||||||
|
.fetchAllCommentsRecursivelyStream(
|
||||||
|
ids: kids,
|
||||||
|
)
|
||||||
|
.listen(_onCommentFetched)
|
||||||
|
..onDone(_onDone);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
item: updatedItem,
|
||||||
status: CommentsStatus.loaded,
|
status: CommentsStatus.loaded,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadAll(Story story) {
|
void loadAll(Story story) {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
onlyShowTargetComment: false,
|
onlyShowTargetComment: false,
|
||||||
comments: <Comment>[],
|
item: story,
|
||||||
story: story,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadMore() {
|
/// [comment] is only used for lazy fetching.
|
||||||
|
void loadMore({Comment? comment}) {
|
||||||
|
switch (state.fetchMode) {
|
||||||
|
case FetchMode.lazy:
|
||||||
|
if (comment == null) return;
|
||||||
|
if (_streamSubscriptions.containsKey(comment.id)) return;
|
||||||
|
|
||||||
|
final int level = comment.level + 1;
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
|
/// Ignoring because the subscription will be cancelled in close()
|
||||||
|
// ignore: cancel_subscriptions
|
||||||
|
final StreamSubscription<Comment> streamSubscription =
|
||||||
|
_storiesRepository
|
||||||
|
.fetchCommentsStream(ids: comment.kids)
|
||||||
|
.listen((Comment cmt) {
|
||||||
|
_collapseCache.addKid(cmt.id, to: cmt.parent);
|
||||||
|
_commentCache.cacheComment(cmt);
|
||||||
|
_sembastRepository.cacheComment(cmt);
|
||||||
|
|
||||||
|
final List<LinkifyElement> elements = _linkify(
|
||||||
|
cmt.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
final BuildableComment buildableComment =
|
||||||
|
BuildableComment.fromComment(cmt, elements: elements);
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
comments: <Comment>[...state.comments]..insert(
|
||||||
|
state.comments.indexOf(comment) + offset + 1,
|
||||||
|
buildableComment.copyWith(level: level),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
offset++;
|
||||||
|
})
|
||||||
|
..onDone(() {
|
||||||
|
_streamSubscriptions[comment.id]?.cancel();
|
||||||
|
_streamSubscriptions.remove(comment.id);
|
||||||
|
})
|
||||||
|
..onError((dynamic error) {
|
||||||
|
_logger.e(error);
|
||||||
|
_streamSubscriptions[comment.id]?.cancel();
|
||||||
|
_streamSubscriptions.remove(comment.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
_streamSubscriptions[comment.id] = streamSubscription;
|
||||||
|
break;
|
||||||
|
case FetchMode.eager:
|
||||||
if (_streamSubscription != null) {
|
if (_streamSubscription != null) {
|
||||||
emit(state.copyWith(status: CommentsStatus.loading));
|
emit(state.copyWith(status: CommentsStatus.loading));
|
||||||
_streamSubscription?.resume();
|
_streamSubscription?.resume();
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadParentThread() async {
|
||||||
|
unawaited(HapticFeedback.lightImpact());
|
||||||
|
emit(state.copyWith(fetchParentStatus: CommentsStatus.loading));
|
||||||
|
final Story? parent =
|
||||||
|
await _storiesRepository.fetchParentStory(id: state.item.id);
|
||||||
|
|
||||||
|
if (parent == null) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
await HackiApp.navigatorKey.currentState?.pushNamed(
|
||||||
|
ItemScreen.routeName,
|
||||||
|
arguments: ItemScreenArgs(item: parent),
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
fetchParentStatus: CommentsStatus.loaded,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onOrderChanged(CommentsOrder? order) {
|
||||||
|
if (order == null) return;
|
||||||
|
if (state.order == order) return;
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
_streamSubscription?.cancel();
|
||||||
|
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
|
||||||
|
s.cancel();
|
||||||
|
}
|
||||||
|
_streamSubscriptions.clear();
|
||||||
|
emit(state.copyWith(order: order));
|
||||||
|
init(useCommentCache: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onFetchModeChanged(FetchMode? fetchMode) {
|
||||||
|
if (fetchMode == null) return;
|
||||||
|
if (state.fetchMode == fetchMode) return;
|
||||||
|
_collapseCache.resetCollapsedComments();
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
_streamSubscription?.cancel();
|
||||||
|
for (final StreamSubscription<Comment> s in _streamSubscriptions.values) {
|
||||||
|
s.cancel();
|
||||||
|
}
|
||||||
|
_streamSubscriptions.clear();
|
||||||
|
emit(state.copyWith(fetchMode: fetchMode));
|
||||||
|
init(useCommentCache: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> sortKids(List<int> kids) {
|
||||||
|
switch (state.order) {
|
||||||
|
case CommentsOrder.natural:
|
||||||
|
return kids;
|
||||||
|
case CommentsOrder.newestFirst:
|
||||||
|
return kids.sorted((int a, int b) => b.compareTo(a));
|
||||||
|
case CommentsOrder.oldestFirst:
|
||||||
|
return kids.sorted((int a, int b) => a.compareTo(b));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDone() {
|
void _onDone() {
|
||||||
@ -155,20 +341,34 @@ 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(
|
||||||
|
comment.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
final BuildableComment buildableComment =
|
||||||
|
BuildableComment.fromComment(comment, elements: elements);
|
||||||
|
|
||||||
final List<Comment> updatedComments = <Comment>[
|
final List<Comment> updatedComments = <Comment>[
|
||||||
...state.comments,
|
...state.comments,
|
||||||
comment
|
buildableComment
|
||||||
];
|
];
|
||||||
|
|
||||||
emit(state.copyWith(comments: updatedComments));
|
emit(state.copyWith(comments: updatedComments));
|
||||||
|
|
||||||
if (updatedComments.length >= _pageSize + _pageSize * state.currentPage &&
|
if (state.fetchMode == FetchMode.eager) {
|
||||||
|
if (updatedComments.length >=
|
||||||
|
_pageSize + _pageSize * state.currentPage &&
|
||||||
updatedComments.length <=
|
updatedComments.length <=
|
||||||
_pageSize * 2 + _pageSize * state.currentPage) {
|
_pageSize * 2 + _pageSize * state.currentPage) {
|
||||||
|
final bool isHidden = _collapseCache.isHidden(comment.id);
|
||||||
|
|
||||||
|
if (!isHidden) {
|
||||||
_streamSubscription?.pause();
|
_streamSubscription?.pause();
|
||||||
|
}
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -179,10 +379,39 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<LinkifyElement> _linkify(
|
||||||
|
String text, {
|
||||||
|
LinkifyOptions options = const LinkifyOptions(),
|
||||||
|
List<Linkifier> linkifiers = const <Linkifier>[
|
||||||
|
UrlLinkifier(),
|
||||||
|
EmailLinkifier(),
|
||||||
|
],
|
||||||
|
}) {
|
||||||
|
List<LinkifyElement> list = <LinkifyElement>[TextElement(text)];
|
||||||
|
|
||||||
|
if (text.isEmpty) {
|
||||||
|
return <LinkifyElement>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linkifiers.isEmpty) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final Linkifier linkifier in linkifiers) {
|
||||||
|
list = linkifier.parse(list, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
@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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,12 +8,25 @@ enum CommentsStatus {
|
|||||||
failure,
|
failure,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum CommentsOrder {
|
||||||
|
natural,
|
||||||
|
newestFirst,
|
||||||
|
oldestFirst,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FetchMode {
|
||||||
|
lazy,
|
||||||
|
eager,
|
||||||
|
}
|
||||||
|
|
||||||
class CommentsState extends Equatable {
|
class CommentsState extends Equatable {
|
||||||
const CommentsState({
|
const CommentsState({
|
||||||
required this.story,
|
required this.item,
|
||||||
required this.comments,
|
required this.comments,
|
||||||
required this.status,
|
required this.status,
|
||||||
required this.collapsed,
|
required this.fetchParentStatus,
|
||||||
|
required this.order,
|
||||||
|
required this.fetchMode,
|
||||||
required this.onlyShowTargetComment,
|
required this.onlyShowTargetComment,
|
||||||
required this.offlineReading,
|
required this.offlineReading,
|
||||||
required this.currentPage,
|
required this.currentPage,
|
||||||
@ -21,35 +34,43 @@ class CommentsState extends Equatable {
|
|||||||
|
|
||||||
CommentsState.init({
|
CommentsState.init({
|
||||||
required this.offlineReading,
|
required this.offlineReading,
|
||||||
required this.story,
|
required this.item,
|
||||||
|
required this.fetchMode,
|
||||||
|
required this.order,
|
||||||
}) : comments = <Comment>[],
|
}) : comments = <Comment>[],
|
||||||
status = CommentsStatus.init,
|
status = CommentsStatus.init,
|
||||||
collapsed = false,
|
fetchParentStatus = CommentsStatus.init,
|
||||||
onlyShowTargetComment = false,
|
onlyShowTargetComment = false,
|
||||||
currentPage = 0;
|
currentPage = 0;
|
||||||
|
|
||||||
final Story story;
|
final Item item;
|
||||||
final List<Comment> comments;
|
final List<Comment> comments;
|
||||||
final CommentsStatus status;
|
final CommentsStatus status;
|
||||||
final bool collapsed;
|
final CommentsStatus fetchParentStatus;
|
||||||
|
final CommentsOrder order;
|
||||||
|
final FetchMode fetchMode;
|
||||||
final bool onlyShowTargetComment;
|
final bool onlyShowTargetComment;
|
||||||
final bool offlineReading;
|
final bool offlineReading;
|
||||||
final int currentPage;
|
final int currentPage;
|
||||||
|
|
||||||
CommentsState copyWith({
|
CommentsState copyWith({
|
||||||
Story? story,
|
Item? item,
|
||||||
List<Comment>? comments,
|
List<Comment>? comments,
|
||||||
CommentsStatus? status,
|
CommentsStatus? status,
|
||||||
bool? collapsed,
|
CommentsStatus? fetchParentStatus,
|
||||||
|
CommentsOrder? order,
|
||||||
|
FetchMode? fetchMode,
|
||||||
bool? onlyShowTargetComment,
|
bool? onlyShowTargetComment,
|
||||||
bool? offlineReading,
|
bool? offlineReading,
|
||||||
int? currentPage,
|
int? currentPage,
|
||||||
}) {
|
}) {
|
||||||
return CommentsState(
|
return CommentsState(
|
||||||
story: story ?? this.story,
|
item: item ?? this.item,
|
||||||
comments: comments ?? this.comments,
|
comments: comments ?? this.comments,
|
||||||
|
fetchParentStatus: fetchParentStatus ?? this.fetchParentStatus,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
collapsed: collapsed ?? this.collapsed,
|
order: order ?? this.order,
|
||||||
|
fetchMode: fetchMode ?? this.fetchMode,
|
||||||
onlyShowTargetComment:
|
onlyShowTargetComment:
|
||||||
onlyShowTargetComment ?? this.onlyShowTargetComment,
|
onlyShowTargetComment ?? this.onlyShowTargetComment,
|
||||||
offlineReading: offlineReading ?? this.offlineReading,
|
offlineReading: offlineReading ?? this.offlineReading,
|
||||||
@ -57,12 +78,16 @@ 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?>[
|
||||||
story,
|
item,
|
||||||
comments,
|
comments,
|
||||||
status,
|
status,
|
||||||
collapsed,
|
fetchParentStatus,
|
||||||
|
order,
|
||||||
|
fetchMode,
|
||||||
onlyShowTargetComment,
|
onlyShowTargetComment,
|
||||||
offlineReading,
|
offlineReading,
|
||||||
currentPage,
|
currentPage,
|
||||||
|
@ -14,4 +14,5 @@ export 'search/search_cubit.dart';
|
|||||||
export 'split_view/split_view_cubit.dart';
|
export 'split_view/split_view_cubit.dart';
|
||||||
export 'submit/submit_cubit.dart';
|
export 'submit/submit_cubit.dart';
|
||||||
export 'time_machine/time_machine_cubit.dart';
|
export 'time_machine/time_machine_cubit.dart';
|
||||||
|
export 'user/user_cubit.dart';
|
||||||
export 'vote/vote_cubit.dart';
|
export 'vote/vote_cubit.dart';
|
||||||
|
@ -1,26 +1,27 @@
|
|||||||
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/extensions/extensions.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/services/services.dart';
|
import 'package:hacki/services/services.dart';
|
||||||
import 'package:hacki/utils/debouncer.dart';
|
import 'package:hacki/utils/debouncer.dart';
|
||||||
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
|
|
||||||
part 'edit_state.dart';
|
part 'edit_state.dart';
|
||||||
|
|
||||||
class EditCubit extends Cubit<EditState> {
|
class EditCubit extends HydratedCubit<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,9 +45,10 @@ 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());
|
||||||
|
clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onTextChanged(String text) {
|
void onTextChanged(String text) {
|
||||||
@ -54,11 +56,61 @@ 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!,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void deleteDraft() => clear();
|
||||||
|
|
||||||
|
bool called = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
EditState? fromJson(Map<String, dynamic> json) {
|
||||||
|
final String text = json['text'] as String? ?? '';
|
||||||
|
final Map<String, dynamic>? itemJson =
|
||||||
|
json['item'] as Map<String, dynamic>?;
|
||||||
|
final Item? replyingTo = itemJson == null ? null : Item.fromJson(itemJson);
|
||||||
|
|
||||||
|
if (replyingTo != null && text.isNotEmpty) {
|
||||||
|
_draftCache.cacheDraft(text: text, replyingTo: replyingTo.id);
|
||||||
|
|
||||||
|
final EditState state = EditState(
|
||||||
|
text: text,
|
||||||
|
replyingTo: replyingTo,
|
||||||
|
);
|
||||||
|
|
||||||
|
_cachedState = state;
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic>? toJson(EditState state) {
|
||||||
|
EditState selected = state;
|
||||||
|
|
||||||
|
if (state.replyingTo == null ||
|
||||||
|
(state.replyingTo?.id != _cachedState.replyingTo?.id &&
|
||||||
|
state.text.isNullOrEmpty)) {
|
||||||
|
selected = _cachedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected.text.isNullOrEmpty) {
|
||||||
|
clear();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <String, dynamic>{
|
||||||
|
'text': selected.text ?? '',
|
||||||
|
'item': selected.replyingTo?.toJson(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static EditState _cachedState = const EditState.init();
|
||||||
}
|
}
|
||||||
|
@ -39,15 +39,15 @@ class FavCubit extends Cubit<FavState> {
|
|||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
favIds: favIds,
|
favIds: favIds,
|
||||||
favStories: <Story>[],
|
favItems: <Item>[],
|
||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_storiesRepository
|
_storiesRepository
|
||||||
.fetchStoriesStream(
|
.fetchItemsStream(
|
||||||
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
|
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
|
||||||
)
|
)
|
||||||
.listen(_onStoryLoaded)
|
.listen(_onItemLoaded)
|
||||||
.onDone(() {
|
.onDone(() {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -73,13 +73,13 @@ class FavCubit extends Cubit<FavState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final Story? story = await _storiesRepository.fetchStoryBy(id);
|
final Item? item = await _storiesRepository.fetchItemBy(id: id);
|
||||||
|
|
||||||
if (story == null) return;
|
if (item == null) return;
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
favStories: List<Story>.from(state.favStories)..insert(0, story),
|
favItems: List<Item>.from(state.favItems)..insert(0, item),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -96,8 +96,8 @@ class FavCubit extends Cubit<FavState> {
|
|||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
favIds: List<int>.from(state.favIds)..remove(id),
|
favIds: List<int>.from(state.favIds)..remove(id),
|
||||||
favStories: List<Story>.from(state.favStories)
|
favItems: List<Item>.from(state.favItems)
|
||||||
..removeWhere((Story e) => e.id == id),
|
..removeWhere((Item e) => e.id == id),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -120,13 +120,13 @@ class FavCubit extends Cubit<FavState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_storiesRepository
|
_storiesRepository
|
||||||
.fetchStoriesStream(
|
.fetchItemsStream(
|
||||||
ids: state.favIds.sublist(
|
ids: state.favIds.sublist(
|
||||||
lower,
|
lower,
|
||||||
upper,
|
upper,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.listen(_onStoryLoaded)
|
.listen(_onItemLoaded)
|
||||||
.onDone(() {
|
.onDone(() {
|
||||||
emit(state.copyWith(status: FavStatus.loaded));
|
emit(state.copyWith(status: FavStatus.loaded));
|
||||||
});
|
});
|
||||||
@ -142,7 +142,7 @@ class FavCubit extends Cubit<FavState> {
|
|||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: FavStatus.loading,
|
status: FavStatus.loading,
|
||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
favStories: <Story>[],
|
favItems: <Item>[],
|
||||||
favIds: <int>[],
|
favIds: <int>[],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -150,20 +150,20 @@ class FavCubit extends Cubit<FavState> {
|
|||||||
_preferenceRepository.favList(of: username).then((List<int> favIds) {
|
_preferenceRepository.favList(of: username).then((List<int> favIds) {
|
||||||
emit(state.copyWith(favIds: favIds));
|
emit(state.copyWith(favIds: favIds));
|
||||||
_storiesRepository
|
_storiesRepository
|
||||||
.fetchStoriesStream(
|
.fetchItemsStream(
|
||||||
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
|
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
|
||||||
)
|
)
|
||||||
.listen(_onStoryLoaded)
|
.listen(_onItemLoaded)
|
||||||
.onDone(() {
|
.onDone(() {
|
||||||
emit(state.copyWith(status: FavStatus.loaded));
|
emit(state.copyWith(status: FavStatus.loaded));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onStoryLoaded(Story story) {
|
void _onItemLoaded(Item item) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
favStories: List<Story>.from(state.favStories)..add(story),
|
favItems: List<Item>.from(state.favItems)..add(item),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -10,31 +10,31 @@ enum FavStatus {
|
|||||||
class FavState extends Equatable {
|
class FavState extends Equatable {
|
||||||
const FavState({
|
const FavState({
|
||||||
required this.favIds,
|
required this.favIds,
|
||||||
required this.favStories,
|
required this.favItems,
|
||||||
required this.status,
|
required this.status,
|
||||||
required this.currentPage,
|
required this.currentPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
FavState.init()
|
FavState.init()
|
||||||
: favIds = <int>[],
|
: favIds = <int>[],
|
||||||
favStories = <Story>[],
|
favItems = <Item>[],
|
||||||
status = FavStatus.init,
|
status = FavStatus.init,
|
||||||
currentPage = 0;
|
currentPage = 0;
|
||||||
|
|
||||||
final List<int> favIds;
|
final List<int> favIds;
|
||||||
final List<Story> favStories;
|
final List<Item> favItems;
|
||||||
final FavStatus status;
|
final FavStatus status;
|
||||||
final int currentPage;
|
final int currentPage;
|
||||||
|
|
||||||
FavState copyWith({
|
FavState copyWith({
|
||||||
List<int>? favIds,
|
List<int>? favIds,
|
||||||
List<Story>? favStories,
|
List<Item>? favItems,
|
||||||
FavStatus? status,
|
FavStatus? status,
|
||||||
int? currentPage,
|
int? currentPage,
|
||||||
}) {
|
}) {
|
||||||
return FavState(
|
return FavState(
|
||||||
favIds: favIds ?? this.favIds,
|
favIds: favIds ?? this.favIds,
|
||||||
favStories: favStories ?? this.favStories,
|
favItems: favItems ?? this.favItems,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
currentPage: currentPage ?? this.currentPage,
|
currentPage: currentPage ?? this.currentPage,
|
||||||
);
|
);
|
||||||
@ -43,7 +43,7 @@ class FavState extends Equatable {
|
|||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
favIds,
|
favIds,
|
||||||
favStories,
|
favItems,
|
||||||
status,
|
status,
|
||||||
currentPage,
|
currentPage,
|
||||||
];
|
];
|
||||||
|
@ -33,7 +33,8 @@ class NotificationCubit extends Cubit<NotificationState> {
|
|||||||
_preferenceRepository.shouldShowNotification
|
_preferenceRepository.shouldShowNotification
|
||||||
.then((bool showNotification) {
|
.then((bool showNotification) {
|
||||||
if (showNotification) {
|
if (showNotification) {
|
||||||
init();
|
// Delaying the initialization to prevent janks in home screen.
|
||||||
|
Future<void>.delayed(const Duration(seconds: 2), init);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/services.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 +35,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 +80,18 @@ 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;
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
emit(state.copyWith(fetchMode: fetchMode));
|
||||||
|
_preferenceRepository.selectFetchMode(fetchMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void selectCommentsOrder(CommentsOrder? order) {
|
||||||
|
if (order == null || state.order == order) return;
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
emit(state.copyWith(order: order));
|
||||||
|
_preferenceRepository.selectCommentsOrder(order);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,8 @@ class PreferenceState extends Equatable {
|
|||||||
required this.useReader,
|
required this.useReader,
|
||||||
required this.markReadStories,
|
required this.markReadStories,
|
||||||
required this.showMetadata,
|
required this.showMetadata,
|
||||||
|
required this.fetchMode,
|
||||||
|
required this.order,
|
||||||
});
|
});
|
||||||
|
|
||||||
const PreferenceState.init()
|
const PreferenceState.init()
|
||||||
@ -20,7 +22,9 @@ class PreferenceState extends Equatable {
|
|||||||
useTrueDark = false,
|
useTrueDark = false,
|
||||||
useReader = false,
|
useReader = false,
|
||||||
markReadStories = false,
|
markReadStories = false,
|
||||||
showMetadata = false;
|
showMetadata = false,
|
||||||
|
fetchMode = FetchMode.eager,
|
||||||
|
order = CommentsOrder.natural;
|
||||||
|
|
||||||
final bool showNotification;
|
final bool showNotification;
|
||||||
final bool showComplexStoryTile;
|
final bool showComplexStoryTile;
|
||||||
@ -30,6 +34,8 @@ class PreferenceState extends Equatable {
|
|||||||
final bool useReader;
|
final bool useReader;
|
||||||
final bool markReadStories;
|
final bool markReadStories;
|
||||||
final bool showMetadata;
|
final bool showMetadata;
|
||||||
|
final FetchMode fetchMode;
|
||||||
|
final CommentsOrder order;
|
||||||
|
|
||||||
PreferenceState copyWith({
|
PreferenceState copyWith({
|
||||||
bool? showNotification,
|
bool? showNotification,
|
||||||
@ -40,6 +46,8 @@ class PreferenceState extends Equatable {
|
|||||||
bool? useReader,
|
bool? useReader,
|
||||||
bool? markReadStories,
|
bool? markReadStories,
|
||||||
bool? showMetadata,
|
bool? showMetadata,
|
||||||
|
FetchMode? fetchMode,
|
||||||
|
CommentsOrder? order,
|
||||||
}) {
|
}) {
|
||||||
return PreferenceState(
|
return PreferenceState(
|
||||||
showNotification: showNotification ?? this.showNotification,
|
showNotification: showNotification ?? this.showNotification,
|
||||||
@ -50,6 +58,8 @@ class PreferenceState extends Equatable {
|
|||||||
useReader: useReader ?? this.useReader,
|
useReader: useReader ?? this.useReader,
|
||||||
markReadStories: markReadStories ?? this.markReadStories,
|
markReadStories: markReadStories ?? this.markReadStories,
|
||||||
showMetadata: showMetadata ?? this.showMetadata,
|
showMetadata: showMetadata ?? this.showMetadata,
|
||||||
|
fetchMode: fetchMode ?? this.fetchMode,
|
||||||
|
order: order ?? this.order,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,5 +73,7 @@ class PreferenceState extends Equatable {
|
|||||||
useReader,
|
useReader,
|
||||||
markReadStories,
|
markReadStories,
|
||||||
showMetadata,
|
showMetadata,
|
||||||
|
fetchMode,
|
||||||
|
order,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -3,19 +3,25 @@ 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 updateStoryScreenArgs(StoryScreenArgs args) {
|
void updateItemScreenArgs(ItemScreenArgs args) {
|
||||||
_cacheService.resetCollapsedComments();
|
_logger.i('resetting comments in CommentCache');
|
||||||
emit(state.copyWith(storyScreenArgs: args));
|
_commentCache.resetComments();
|
||||||
|
emit(state.copyWith(itemScreenArgs: args));
|
||||||
}
|
}
|
||||||
|
|
||||||
void enableSplitView() => emit(state.copyWith(enabled: true));
|
void enableSplitView() => emit(state.copyWith(enabled: true));
|
||||||
|
@ -2,7 +2,7 @@ part of 'split_view_cubit.dart';
|
|||||||
|
|
||||||
class SplitViewState extends Equatable {
|
class SplitViewState extends Equatable {
|
||||||
const SplitViewState({
|
const SplitViewState({
|
||||||
required this.storyScreenArgs,
|
required this.itemScreenArgs,
|
||||||
required this.expanded,
|
required this.expanded,
|
||||||
required this.enabled,
|
required this.enabled,
|
||||||
});
|
});
|
||||||
@ -10,21 +10,21 @@ class SplitViewState extends Equatable {
|
|||||||
const SplitViewState.init()
|
const SplitViewState.init()
|
||||||
: enabled = false,
|
: enabled = false,
|
||||||
expanded = false,
|
expanded = false,
|
||||||
storyScreenArgs = null;
|
itemScreenArgs = null;
|
||||||
|
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
final bool expanded;
|
final bool expanded;
|
||||||
final StoryScreenArgs? storyScreenArgs;
|
final ItemScreenArgs? itemScreenArgs;
|
||||||
|
|
||||||
SplitViewState copyWith({
|
SplitViewState copyWith({
|
||||||
bool? enabled,
|
bool? enabled,
|
||||||
bool? expanded,
|
bool? expanded,
|
||||||
StoryScreenArgs? storyScreenArgs,
|
ItemScreenArgs? itemScreenArgs,
|
||||||
}) {
|
}) {
|
||||||
return SplitViewState(
|
return SplitViewState(
|
||||||
enabled: enabled ?? this.enabled,
|
enabled: enabled ?? this.enabled,
|
||||||
expanded: expanded ?? this.expanded,
|
expanded: expanded ?? this.expanded,
|
||||||
storyScreenArgs: storyScreenArgs ?? this.storyScreenArgs,
|
itemScreenArgs: itemScreenArgs ?? this.itemScreenArgs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,6 +32,6 @@ class SplitViewState extends Equatable {
|
|||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
enabled,
|
enabled,
|
||||||
expanded,
|
expanded,
|
||||||
storyScreenArgs,
|
itemScreenArgs,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
26
lib/cubits/user/user_cubit.dart
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hacki/config/locator.dart';
|
||||||
|
import 'package:hacki/models/models.dart';
|
||||||
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
|
|
||||||
|
part 'user_state.dart';
|
||||||
|
|
||||||
|
class UserCubit extends Cubit<UserState> {
|
||||||
|
UserCubit({StoriesRepository? storiesRepository})
|
||||||
|
: _storiesRepository =
|
||||||
|
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||||
|
super(UserState.init());
|
||||||
|
|
||||||
|
final StoriesRepository _storiesRepository;
|
||||||
|
|
||||||
|
void init({required String userId}) {
|
||||||
|
emit(state.copyWith(status: UserStatus.loading));
|
||||||
|
_storiesRepository.fetchUserBy(userId: userId).then((User user) {
|
||||||
|
emit(state.copyWith(user: user, status: UserStatus.loaded));
|
||||||
|
}).onError((_, __) {
|
||||||
|
emit(state.copyWith(status: UserStatus.failure));
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
38
lib/cubits/user/user_state.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
part of 'user_cubit.dart';
|
||||||
|
|
||||||
|
enum UserStatus {
|
||||||
|
initial,
|
||||||
|
loading,
|
||||||
|
loaded,
|
||||||
|
failure,
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserState extends Equatable {
|
||||||
|
const UserState({
|
||||||
|
required this.user,
|
||||||
|
required this.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
UserState.init()
|
||||||
|
: user = User.empty(),
|
||||||
|
status = UserStatus.initial;
|
||||||
|
|
||||||
|
final User user;
|
||||||
|
final UserStatus status;
|
||||||
|
|
||||||
|
UserState copyWith({
|
||||||
|
User? user,
|
||||||
|
UserStatus? status,
|
||||||
|
}) {
|
||||||
|
return UserState(
|
||||||
|
user: user ?? this.user,
|
||||||
|
status: status ?? this.status,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[
|
||||||
|
user,
|
||||||
|
status,
|
||||||
|
];
|
||||||
|
}
|
58
lib/extensions/context_extension.dart
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
extension ContextExtension 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 = 430,
|
||||||
|
_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;
|
||||||
|
}
|
||||||
|
}
|
@ -7,10 +7,8 @@ extension DateTimeExtension on DateTime {
|
|||||||
return '$gap year${gap == 1 ? '' : 's'} ago';
|
return '$gap year${gap == 1 ? '' : 's'} ago';
|
||||||
} else if (diff.inDays > 30) {
|
} else if (diff.inDays > 30) {
|
||||||
int gap = now.month - month;
|
int gap = now.month - month;
|
||||||
if (gap == 0) {
|
if (gap <= 0) {
|
||||||
gap = 1;
|
gap = now.month + 12 - month;
|
||||||
} else if (gap < 0) {
|
|
||||||
gap = now.month + (12 - month);
|
|
||||||
}
|
}
|
||||||
return '$gap month${gap == 1 ? '' : 's'} ago';
|
return '$gap month${gap == 1 ? '' : 's'} ago';
|
||||||
} else if (diff.inDays >= 1) {
|
} else if (diff.inDays >= 1) {
|
||||||
|
@ -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';
|
||||||
|
@ -2,7 +2,8 @@ import 'package:flutter/material.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/main.dart';
|
import 'package:hacki/main.dart';
|
||||||
import 'package:hacki/screens/screens.dart' show StoryScreen, StoryScreenArgs;
|
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(
|
||||||
@ -26,14 +27,17 @@ extension StateExtension on State {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void>? goToStoryScreen({required StoryScreenArgs args}) {
|
Future<void>? goToItemScreen({
|
||||||
|
required ItemScreenArgs args,
|
||||||
|
bool forceNewScreen = false,
|
||||||
|
}) {
|
||||||
final bool splitViewEnabled = context.read<SplitViewCubit>().state.enabled;
|
final bool splitViewEnabled = context.read<SplitViewCubit>().state.enabled;
|
||||||
|
|
||||||
if (splitViewEnabled) {
|
if (splitViewEnabled && !forceNewScreen) {
|
||||||
context.read<SplitViewCubit>().updateStoryScreenArgs(args);
|
context.read<SplitViewCubit>().updateItemScreenArgs(args);
|
||||||
} else {
|
} else {
|
||||||
return HackiApp.navigatorKey.currentState?.pushNamed(
|
return HackiApp.navigatorKey.currentState?.pushNamed(
|
||||||
StoryScreen.routeName,
|
ItemScreen.routeName,
|
||||||
arguments: args,
|
arguments: args,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
extension StringExtension on String {
|
extension StringExtension on String {
|
||||||
int? getItemId() {
|
int? getItemId() {
|
||||||
final RegExp regex = RegExp(r'\d+$');
|
final RegExp regex = RegExp(r'\d+$');
|
||||||
final String match = regex.stringMatch(this) ?? '';
|
final RegExp exception = RegExp(r'\)|].*$');
|
||||||
|
final String match = regex.stringMatch(replaceAll(exception, '')) ?? '';
|
||||||
return int.tryParse(match);
|
return int.tryParse(match);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -12,3 +13,12 @@ extension StringExtension on String {
|
|||||||
return replaceAllMapped(regex, (_) => '');
|
return replaceAllMapped(regex, (_) => '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension OptionalStringExtension on String? {
|
||||||
|
bool get isNullOrEmpty {
|
||||||
|
if (this == null) return true;
|
||||||
|
return this!.trim().isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isNotNullOrEmpty => !isNullOrEmpty;
|
||||||
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
extension WidgetModifier on Widget {
|
extension WidgetModifier on Widget {
|
||||||
Widget padding([EdgeInsetsGeometry value = const EdgeInsets.all(12)]) {
|
Widget padded([EdgeInsetsGeometry value = const EdgeInsets.all(12)]) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: value,
|
padding: value,
|
||||||
child: this,
|
child: this,
|
||||||
|
@ -3,18 +3,24 @@ 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/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:flutter_siri_suggestions/flutter_siri_suggestions.dart';
|
||||||
import 'package:hacki/blocs/blocs.dart';
|
import 'package:hacki/blocs/blocs.dart';
|
||||||
import 'package:hacki/config/custom_router.dart';
|
import 'package:hacki/config/custom_router.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
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:hydrated_bloc/hydrated_bloc.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;
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@ -24,9 +30,23 @@ import 'package:workmanager/workmanager.dart';
|
|||||||
final BehaviorSubject<String?> selectNotificationSubject =
|
final BehaviorSubject<String?> selectNotificationSubject =
|
||||||
BehaviorSubject<String?>();
|
BehaviorSubject<String?>();
|
||||||
|
|
||||||
Future<void> main() async {
|
// For receiving payload event from siri suggestions.
|
||||||
|
final BehaviorSubject<String?> siriSuggestionSubject =
|
||||||
|
BehaviorSubject<String?>();
|
||||||
|
|
||||||
|
late final bool isTesting;
|
||||||
|
|
||||||
|
Future<void> main({bool testing = false}) async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
isTesting = testing;
|
||||||
|
|
||||||
|
final HydratedStorage storage = await HydratedStorage.build(
|
||||||
|
storageDirectory: kIsWeb
|
||||||
|
? HydratedStorage.webStorageDirectory
|
||||||
|
: await getTemporaryDirectory(),
|
||||||
|
);
|
||||||
|
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
unawaited(
|
unawaited(
|
||||||
Workmanager().initialize(
|
Workmanager().initialize(
|
||||||
@ -57,6 +77,29 @@ Future<void> main() async {
|
|||||||
badge: true,
|
badge: true,
|
||||||
sound: true,
|
sound: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
FlutterSiriSuggestions.instance.configure(
|
||||||
|
onLaunch: (Map<String, dynamic> message) async {
|
||||||
|
final String? storyId = message['key'] as String?;
|
||||||
|
|
||||||
|
if (storyId == null) return;
|
||||||
|
|
||||||
|
siriSuggestionSubject.add(storyId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (Platform.isAndroid) {
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
|
const SystemUiOverlayStyle(
|
||||||
|
statusBarColor: Colors.transparent,
|
||||||
|
systemNavigationBarColor: Colors.transparent,
|
||||||
|
systemNavigationBarDividerColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await SystemChrome.setEnabledSystemUIMode(
|
||||||
|
SystemUiMode.edgeToEdge,
|
||||||
|
overlays: <SystemUiOverlay>[SystemUiOverlay.top],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Directory tempDir = await getTemporaryDirectory();
|
final Directory tempDir = await getTemporaryDirectory();
|
||||||
@ -70,18 +113,8 @@ 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.
|
Bloc.observer = CustomBlocObserver();
|
||||||
// BlocOverrides.runZoned(
|
HydratedBloc.storage = storage;
|
||||||
// () {
|
|
||||||
// runApp(
|
|
||||||
// HackiApp(
|
|
||||||
// savedThemeMode: savedThemeMode,
|
|
||||||
// trueDarkMode: trueDarkMode,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// blocObserver: CustomBlocObserver(),
|
|
||||||
// );
|
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
HackiApp(
|
HackiApp(
|
||||||
@ -171,19 +204,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(),
|
||||||
|
35
lib/models/buildable_comment.dart
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||||
|
import 'package:hacki/models/comment.dart';
|
||||||
|
import 'package:hacki/models/models.dart';
|
||||||
|
|
||||||
|
class BuildableComment extends Comment {
|
||||||
|
BuildableComment({
|
||||||
|
required super.id,
|
||||||
|
required super.time,
|
||||||
|
required super.parent,
|
||||||
|
required super.score,
|
||||||
|
required super.by,
|
||||||
|
required super.text,
|
||||||
|
required super.kids,
|
||||||
|
required super.dead,
|
||||||
|
required super.deleted,
|
||||||
|
required super.level,
|
||||||
|
required this.elements,
|
||||||
|
});
|
||||||
|
|
||||||
|
BuildableComment.fromComment(Comment comment, {required this.elements})
|
||||||
|
: super(
|
||||||
|
id: comment.id,
|
||||||
|
time: comment.time,
|
||||||
|
parent: comment.parent,
|
||||||
|
score: comment.score,
|
||||||
|
by: comment.by,
|
||||||
|
text: comment.text,
|
||||||
|
kids: comment.kids,
|
||||||
|
dead: comment.dead,
|
||||||
|
deleted: comment.deleted,
|
||||||
|
level: comment.level,
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<LinkifyElement> elements;
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:hacki/extensions/extensions.dart';
|
|
||||||
import 'package:hacki/models/item.dart';
|
import 'package:hacki/models/item.dart';
|
||||||
|
|
||||||
class Comment extends Item {
|
class Comment extends Item {
|
||||||
@ -12,11 +11,11 @@ class Comment extends Item {
|
|||||||
required super.by,
|
required super.by,
|
||||||
required super.text,
|
required super.text,
|
||||||
required super.kids,
|
required super.kids,
|
||||||
|
required super.dead,
|
||||||
required super.deleted,
|
required super.deleted,
|
||||||
required this.level,
|
required this.level,
|
||||||
}) : super(
|
}) : super(
|
||||||
descendants: 0,
|
descendants: 0,
|
||||||
dead: false,
|
|
||||||
parts: <int>[],
|
parts: <int>[],
|
||||||
title: '',
|
title: '',
|
||||||
url: '',
|
url: '',
|
||||||
@ -43,8 +42,7 @@ class Comment extends Item {
|
|||||||
|
|
||||||
final int level;
|
final int level;
|
||||||
|
|
||||||
String get postedDate =>
|
String get metadata => '''by $by $postedDate''';
|
||||||
DateTime.fromMillisecondsSinceEpoch(time * 1000).toReadableString();
|
|
||||||
|
|
||||||
Comment copyWith({int? level}) {
|
Comment copyWith({int? level}) {
|
||||||
return Comment(
|
return Comment(
|
||||||
@ -55,11 +53,13 @@ class Comment extends Item {
|
|||||||
by: by,
|
by: by,
|
||||||
text: text,
|
text: text,
|
||||||
kids: kids,
|
kids: kids,
|
||||||
|
dead: dead,
|
||||||
deleted: deleted,
|
deleted: deleted,
|
||||||
level: level ?? this.level,
|
level: level ?? this.level,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Map<String, dynamic> toJson() => <String, dynamic>{
|
Map<String, dynamic> toJson() => <String, dynamic>{
|
||||||
'id': id,
|
'id': id,
|
||||||
'time': time,
|
'time': time,
|
||||||
|