Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
f0d6cac3fd | |||
a90d52f348 | |||
cff4a3c5c4 | |||
502faaf188 | |||
b952f349fc | |||
9cefffa518 | |||
fe630ea7a9 | |||
459ab961d1 | |||
362d7005df | |||
6b7c1d42de | |||
2a889bca56 | |||
e25026f129 | |||
fefc86275d | |||
1a73a6991e | |||
36d7f4606e | |||
9312c56dd0 | |||
6e71de5913 | |||
a9590af3e9 | |||
18dadaa5ec |
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']
|
39
README.md
@ -4,13 +4,12 @@
|
||||
A simple noiseless [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://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://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)
|
||||
|
||||
<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/)
|
||||
|
||||
|
||||
@ -36,24 +35,22 @@ Features:
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/171450528-02c561ed-0ebb-4c1b-9ee0-a935211db0f2.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/171450536-ea61c176-37d7-4744-8674-4668e0e7e774.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/171450541-a9af3417-526f-4dbd-96d7-781c95c886d3.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/171450543-fb631b02-5f46-4455-b8ff-9ef119a486f1.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/171450548-38e98b02-6201-48c9-9674-87bdfc61f456.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798179-72edbe49-7444-4e54-a07c-fc1244447a74.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798182-28397805-a7cc-4124-b65b-c02c80afbbec.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798183-c2984270-ee99-4419-841e-65e98890464f.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798184-2fce5d97-710e-44a7-b99a-3296ebcf273b.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/171992215-5b43443b-f2b0-4b6e-9a30-a583fda1b189.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798186-1457ae21-f1aa-40a4-9206-0f3a1e24653e.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/171450557-ab038e72-78c4-4daf-9b77-3873be1700db.png">
|
||||
<img width="200" alt="01" src="assets/screenshots/01.png">
|
||||
<img width="200" alt="02" src="assets/screenshots/02.png">
|
||||
<img width="200" alt="03" src="assets/screenshots/03.png">
|
||||
<img width="200" alt="04" src="assets/screenshots/04.png">
|
||||
<img width="200" alt="05" src="assets/screenshots/05.png">
|
||||
<img width="200" alt="06" src="assets/screenshots/06.png">
|
||||
<img width="200" alt="07" src="assets/screenshots/07.png">
|
||||
<img width="200" alt="08" src="assets/screenshots/08.png">
|
||||
<img width="200" alt="09" src="assets/screenshots/09.png">
|
||||
<img width="200" alt="10" src="assets/screenshots/10.png">
|
||||
<img width="200" alt="11" src="assets/screenshots/11.png">
|
||||
<img width="200" alt="12" src="assets/screenshots/12.png">
|
||||
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/160162711-a9146326-9645-4db6-a04e-1f82e6133e40.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/160162726-ef1d3f2a-5179-417c-8a5f-0cddb52249da.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/160162733-906c4afd-39a8-48ae-946a-8019b327eaa0.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/160162735-f2b25119-4702-4308-b2f5-281a2a2c5901.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/160163024-6dcd65b6-bada-4c1c-95af-387fd4f42fb2.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/160163033-7bcf7038-b9aa-4ce4-8b58-64578eae8531.png">
|
||||
<img width="400" alt="ipad-01" src="assets/screenshots/ipad-01.png">
|
||||
<img width="400" alt="ipad-02" src="assets/screenshots/ipad-02.png">
|
||||
<img width="400" alt="ipad-03" src="assets/screenshots/ipad-03.png">
|
||||
<img width="400" alt="ipad-04" src="assets/screenshots/ipad-04.png">
|
||||
</p>
|
||||
|
||||
|
@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
|
||||
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
compileSdkVersion 32
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
@ -49,10 +49,9 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.jiaqifeng.hacki"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 30
|
||||
targetSdkVersion 32
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
@ -78,5 +77,5 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
}
|
||||
|
@ -16,14 +16,18 @@
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:label="hacki"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:label="Hacki"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
@ -52,6 +56,21 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</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>
|
||||
<!-- Don't delete the meta-data below.
|
||||
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 {
|
||||
ext.kotlin_version = '1.6.10'
|
||||
ext.kotlin_version = '1.7.0'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.1.1'
|
||||
classpath 'com.android.tools.build:gradle:7.1.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
BIN
assets/screenshots/01.png
Normal file
After Width: | Height: | Size: 522 KiB |
BIN
assets/screenshots/02.png
Normal file
After Width: | Height: | Size: 835 KiB |
BIN
assets/screenshots/03.png
Normal file
After Width: | Height: | Size: 282 KiB |
BIN
assets/screenshots/04.png
Normal file
After Width: | Height: | Size: 298 KiB |
BIN
assets/screenshots/05.png
Normal file
After Width: | Height: | Size: 935 KiB |
BIN
assets/screenshots/06.png
Normal file
After Width: | Height: | Size: 868 KiB |
BIN
assets/screenshots/07.png
Normal file
After Width: | Height: | Size: 121 KiB |
BIN
assets/screenshots/08.png
Normal file
After Width: | Height: | Size: 375 KiB |
BIN
assets/screenshots/09.png
Normal file
After Width: | Height: | Size: 282 KiB |
BIN
assets/screenshots/10.png
Normal file
After Width: | Height: | Size: 414 KiB |
BIN
assets/screenshots/11.png
Normal file
After Width: | Height: | Size: 530 KiB |
BIN
assets/screenshots/12.png
Normal file
After Width: | Height: | Size: 390 KiB |
BIN
assets/screenshots/ipad-01.png
Normal file
After Width: | Height: | Size: 763 KiB |
BIN
assets/screenshots/ipad-02.png
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/screenshots/ipad-03.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/screenshots/ipad-04.png
Normal file
After Width: | Height: | Size: 1005 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
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.
|
Before Width: | Height: | Size: 422 KiB After Width: | Height: | Size: 522 KiB |
Before Width: | Height: | Size: 712 KiB After Width: | Height: | Size: 835 KiB |
Before Width: | Height: | Size: 252 KiB After Width: | Height: | Size: 282 KiB |
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 298 KiB |
Before Width: | Height: | Size: 672 KiB After Width: | Height: | Size: 935 KiB |
Before Width: | Height: | Size: 931 KiB After Width: | Height: | Size: 868 KiB |
Before Width: | Height: | Size: 298 KiB After Width: | Height: | Size: 121 KiB |
Before Width: | Height: | Size: 247 KiB After Width: | Height: | Size: 375 KiB |
Before Width: | Height: | Size: 496 KiB After Width: | Height: | Size: 282 KiB |
Before Width: | Height: | Size: 617 KiB After Width: | Height: | Size: 414 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/11.png
Normal file
After Width: | Height: | Size: 530 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/12.png
Normal file
After Width: | Height: | Size: 390 KiB |
@ -14,6 +14,8 @@ PODS:
|
||||
- Flutter
|
||||
- flutter_secure_storage (3.3.1):
|
||||
- Flutter
|
||||
- flutter_siri_suggestions (0.0.1):
|
||||
- Flutter
|
||||
- FMDB (2.7.5):
|
||||
- FMDB/standard (= 2.7.5)
|
||||
- FMDB/standard (2.7.5)
|
||||
@ -23,6 +25,8 @@ PODS:
|
||||
- ReachabilitySwift (5.0.0)
|
||||
- receive_sharing_intent (0.0.1):
|
||||
- Flutter
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_ios (0.0.1):
|
||||
- Flutter
|
||||
- sqflite (0.0.2):
|
||||
@ -32,8 +36,6 @@ PODS:
|
||||
- Flutter
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- wakelock (0.0.1):
|
||||
- Flutter
|
||||
- webview_flutter_wkwebview (0.0.1):
|
||||
@ -47,13 +49,14 @@ DEPENDENCIES:
|
||||
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`)
|
||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
- synced_shared_preferences (from `.symlinks/plugins/synced_shared_preferences/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`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
||||
- workmanager (from `.symlinks/plugins/workmanager/ios`)
|
||||
@ -75,10 +78,14 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||
flutter_secure_storage:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
flutter_siri_suggestions:
|
||||
:path: ".symlinks/plugins/flutter_siri_suggestions/ios"
|
||||
path_provider_ios:
|
||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||
receive_sharing_intent:
|
||||
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_ios:
|
||||
:path: ".symlinks/plugins/shared_preferences_ios/ios"
|
||||
sqflite:
|
||||
@ -87,8 +94,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/synced_shared_preferences/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
video_player_avfoundation:
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/ios"
|
||||
wakelock:
|
||||
:path: ".symlinks/plugins/wakelock/ios"
|
||||
webview_flutter_wkwebview:
|
||||
@ -102,16 +107,17 @@ SPEC CHECKSUMS:
|
||||
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
|
||||
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
|
||||
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
|
||||
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
|
||||
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
|
||||
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
|
||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
|
||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||
|
@ -568,7 +568,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -577,7 +577,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.14;
|
||||
MARKETING_VERSION = 0.2.24;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -705,7 +705,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -714,7 +714,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.14;
|
||||
MARKETING_VERSION = 0.2.24;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -736,7 +736,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -745,7 +745,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.14;
|
||||
MARKETING_VERSION = 0.2.24;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -7,7 +7,9 @@ import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
part 'stories_event.dart';
|
||||
part 'stories_state.dart';
|
||||
@ -32,6 +34,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
on<StoryRead>(onStoryRead);
|
||||
on<StoriesLoaded>(onStoriesLoaded);
|
||||
on<StoriesDownload>(onDownload);
|
||||
on<StoryDownloaded>(onStoryDownloaded);
|
||||
on<StoriesExitOffline>(onExitOffline);
|
||||
on<StoriesPageSizeChanged>(onPageSizeChanged);
|
||||
on<ClearAllReadStories>(onClearAllReadStories);
|
||||
@ -48,6 +51,15 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
static const int _tabletSmallPageSize = 15;
|
||||
static const int _tabletLargePageSize = 25;
|
||||
|
||||
/// Types of story to be shown in the tab bar.
|
||||
static const Set<StoryType> types = <StoryType>{
|
||||
StoryType.top,
|
||||
StoryType.best,
|
||||
StoryType.latest,
|
||||
StoryType.ask,
|
||||
StoryType.show,
|
||||
};
|
||||
|
||||
Future<void> onInitialize(
|
||||
StoriesInitialize event,
|
||||
Emitter<StoriesState> emit,
|
||||
@ -70,11 +82,9 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
currentPageSize: pageSize,
|
||||
),
|
||||
);
|
||||
await loadStories(of: StoryType.top, emit: emit);
|
||||
await loadStories(of: StoryType.latest, emit: emit);
|
||||
await loadStories(of: StoryType.ask, emit: emit);
|
||||
await loadStories(of: StoryType.show, emit: emit);
|
||||
await loadStories(of: StoryType.jobs, emit: emit);
|
||||
for (final StoryType type in types) {
|
||||
await loadStories(of: type, emit: emit);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadStories({
|
||||
@ -237,52 +247,42 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
await _cacheRepository.deleteAllStories();
|
||||
await _cacheRepository.deleteAllComments();
|
||||
|
||||
final List<int> topIds =
|
||||
await _storiesRepository.fetchStoryIds(of: StoryType.top);
|
||||
final List<int> newIds =
|
||||
await _storiesRepository.fetchStoryIds(of: StoryType.latest);
|
||||
final List<int> askIds =
|
||||
await _storiesRepository.fetchStoryIds(of: StoryType.ask);
|
||||
final List<int> showIds =
|
||||
await _storiesRepository.fetchStoryIds(of: StoryType.show);
|
||||
final List<int> jobIds =
|
||||
await _storiesRepository.fetchStoryIds(of: StoryType.jobs);
|
||||
final Set<int> prioritizedIds = <int>{};
|
||||
final List<StoryType> prioritizedTypes = <StoryType>[...types]
|
||||
..remove(StoryType.latest);
|
||||
|
||||
await _cacheRepository.cacheStoryIds(of: StoryType.top, ids: topIds);
|
||||
await _cacheRepository.cacheStoryIds(of: StoryType.latest, ids: newIds);
|
||||
await _cacheRepository.cacheStoryIds(of: StoryType.ask, ids: askIds);
|
||||
await _cacheRepository.cacheStoryIds(of: StoryType.show, ids: showIds);
|
||||
await _cacheRepository.cacheStoryIds(of: StoryType.jobs, ids: jobIds);
|
||||
for (final StoryType type in prioritizedTypes) {
|
||||
final List<int> ids = await _storiesRepository.fetchStoryIds(of: type);
|
||||
await _cacheRepository.cacheStoryIds(of: type, ids: ids);
|
||||
prioritizedIds.addAll(ids);
|
||||
}
|
||||
|
||||
final List<int> allIds = <int>[
|
||||
...topIds,
|
||||
...newIds,
|
||||
...askIds,
|
||||
...showIds,
|
||||
...jobIds
|
||||
];
|
||||
emit(
|
||||
state.copyWith(
|
||||
storiesDownloaded: 0,
|
||||
storiesToBeDownloaded: prioritizedIds.length,
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
_storiesRepository
|
||||
.fetchStoriesStream(ids: allIds)
|
||||
.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(
|
||||
state.copyWith(
|
||||
downloadStatus: StoriesDownloadStatus.finished,
|
||||
),
|
||||
);
|
||||
});
|
||||
await fetchAndCacheStories(
|
||||
prioritizedIds,
|
||||
includingWebPage: event.includingWebPage,
|
||||
isPrioritized: true,
|
||||
);
|
||||
|
||||
final Set<int> latestIds = <int>{};
|
||||
final List<int> ids = await _storiesRepository.fetchStoryIds(
|
||||
of: StoryType.latest,
|
||||
);
|
||||
await _cacheRepository.cacheStoryIds(of: StoryType.latest, ids: ids);
|
||||
latestIds.addAll(ids);
|
||||
|
||||
await fetchAndCacheStories(
|
||||
latestIds,
|
||||
includingWebPage: event.includingWebPage,
|
||||
isPrioritized: false,
|
||||
);
|
||||
} catch (_) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -292,6 +292,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 _cacheRepository.cacheStory(story: story);
|
||||
|
||||
if (story.url.isNotEmpty && includingWebPage) {
|
||||
locator.get<Logger>().i('downloading ${story.url}');
|
||||
await _cacheRepository.cacheUrl(url: story.url);
|
||||
}
|
||||
|
||||
_storiesRepository
|
||||
.fetchAllChildrenComments(ids: story.kids)
|
||||
.whereType<Comment>()
|
||||
.listen(
|
||||
(Comment comment) => unawaited(
|
||||
_cacheRepository.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(
|
||||
StoriesPageSizeChanged event,
|
||||
Emitter<StoriesState> emit,
|
||||
@ -307,6 +381,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
await _cacheRepository.deleteAllStoryIds();
|
||||
await _cacheRepository.deleteAllStories();
|
||||
await _cacheRepository.deleteAllComments();
|
||||
await _cacheRepository.deleteAllWebPages();
|
||||
emit(state.copyWith(offlineReading: false));
|
||||
add(StoriesInitialize());
|
||||
}
|
||||
|
@ -38,8 +38,21 @@ class StoriesLoadMore extends StoriesEvent {
|
||||
}
|
||||
|
||||
class StoriesDownload extends StoriesEvent {
|
||||
StoriesDownload({required this.includingWebPage});
|
||||
|
||||
final bool includingWebPage;
|
||||
|
||||
@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 {
|
||||
|
@ -23,11 +23,14 @@ class StoriesState extends Equatable {
|
||||
required this.offlineReading,
|
||||
required this.downloadStatus,
|
||||
required this.currentPageSize,
|
||||
required this.storiesDownloaded,
|
||||
required this.storiesToBeDownloaded,
|
||||
});
|
||||
|
||||
const StoriesState.init({
|
||||
this.storiesByType = const <StoryType, List<Story>>{
|
||||
StoryType.top: <Story>[],
|
||||
StoryType.best: <Story>[],
|
||||
StoryType.latest: <Story>[],
|
||||
StoryType.ask: <Story>[],
|
||||
StoryType.show: <Story>[],
|
||||
@ -35,6 +38,7 @@ class StoriesState extends Equatable {
|
||||
},
|
||||
this.storyIdsByType = const <StoryType, List<int>>{
|
||||
StoryType.top: <int>[],
|
||||
StoryType.best: <int>[],
|
||||
StoryType.latest: <int>[],
|
||||
StoryType.ask: <int>[],
|
||||
StoryType.show: <int>[],
|
||||
@ -42,6 +46,7 @@ class StoriesState extends Equatable {
|
||||
},
|
||||
this.statusByType = const <StoryType, StoriesStatus>{
|
||||
StoryType.top: StoriesStatus.initial,
|
||||
StoryType.best: StoriesStatus.initial,
|
||||
StoryType.latest: StoriesStatus.initial,
|
||||
StoryType.ask: StoriesStatus.initial,
|
||||
StoryType.show: StoriesStatus.initial,
|
||||
@ -49,6 +54,7 @@ class StoriesState extends Equatable {
|
||||
},
|
||||
this.currentPageByType = const <StoryType, int>{
|
||||
StoryType.top: 0,
|
||||
StoryType.best: 0,
|
||||
StoryType.latest: 0,
|
||||
StoryType.ask: 0,
|
||||
StoryType.show: 0,
|
||||
@ -57,7 +63,9 @@ class StoriesState extends Equatable {
|
||||
}) : offlineReading = false,
|
||||
downloadStatus = StoriesDownloadStatus.initial,
|
||||
currentPageSize = 0,
|
||||
readStoriesIds = const <int>{};
|
||||
readStoriesIds = const <int>{},
|
||||
storiesDownloaded = 0,
|
||||
storiesToBeDownloaded = 0;
|
||||
|
||||
final Map<StoryType, List<Story>> storiesByType;
|
||||
final Map<StoryType, List<int>> storyIdsByType;
|
||||
@ -67,6 +75,8 @@ class StoriesState extends Equatable {
|
||||
final StoriesDownloadStatus downloadStatus;
|
||||
final bool offlineReading;
|
||||
final int currentPageSize;
|
||||
final int storiesDownloaded;
|
||||
final int storiesToBeDownloaded;
|
||||
|
||||
StoriesState copyWith({
|
||||
Map<StoryType, List<Story>>? storiesByType,
|
||||
@ -77,6 +87,8 @@ class StoriesState extends Equatable {
|
||||
StoriesDownloadStatus? downloadStatus,
|
||||
bool? offlineReading,
|
||||
int? currentPageSize,
|
||||
int? storiesDownloaded,
|
||||
int? storiesToBeDownloaded,
|
||||
}) {
|
||||
return StoriesState(
|
||||
storiesByType: storiesByType ?? this.storiesByType,
|
||||
@ -87,6 +99,9 @@ class StoriesState extends Equatable {
|
||||
offlineReading: offlineReading ?? this.offlineReading,
|
||||
downloadStatus: downloadStatus ?? this.downloadStatus,
|
||||
currentPageSize: currentPageSize ?? this.currentPageSize,
|
||||
storiesDownloaded: storiesDownloaded ?? this.storiesDownloaded,
|
||||
storiesToBeDownloaded:
|
||||
storiesToBeDownloaded ?? this.storiesToBeDownloaded,
|
||||
);
|
||||
}
|
||||
|
||||
@ -98,18 +113,12 @@ class StoriesState extends Equatable {
|
||||
final Map<StoryType, List<Story>> newMap =
|
||||
Map<StoryType, List<Story>>.from(storiesByType);
|
||||
newMap[of] = List<Story>.from(newMap[of]!)..add(story);
|
||||
return StoriesState(
|
||||
return copyWith(
|
||||
storiesByType: newMap,
|
||||
storyIdsByType: storyIdsByType,
|
||||
statusByType: statusByType,
|
||||
currentPageByType: currentPageByType,
|
||||
readStoriesIds: <int>{
|
||||
...readStoriesIds,
|
||||
if (hasRead) story.id,
|
||||
},
|
||||
offlineReading: offlineReading,
|
||||
downloadStatus: downloadStatus,
|
||||
currentPageSize: currentPageSize,
|
||||
);
|
||||
}
|
||||
|
||||
@ -120,15 +129,8 @@ class StoriesState extends Equatable {
|
||||
final Map<StoryType, List<int>> newMap =
|
||||
Map<StoryType, List<int>>.from(storyIdsByType);
|
||||
newMap[of] = to;
|
||||
return StoriesState(
|
||||
storiesByType: storiesByType,
|
||||
return copyWith(
|
||||
storyIdsByType: newMap,
|
||||
statusByType: statusByType,
|
||||
currentPageByType: currentPageByType,
|
||||
readStoriesIds: readStoriesIds,
|
||||
offlineReading: offlineReading,
|
||||
downloadStatus: downloadStatus,
|
||||
currentPageSize: currentPageSize,
|
||||
);
|
||||
}
|
||||
|
||||
@ -139,15 +141,8 @@ class StoriesState extends Equatable {
|
||||
final Map<StoryType, StoriesStatus> newMap =
|
||||
Map<StoryType, StoriesStatus>.from(statusByType);
|
||||
newMap[of] = to;
|
||||
return StoriesState(
|
||||
storiesByType: storiesByType,
|
||||
storyIdsByType: storyIdsByType,
|
||||
return copyWith(
|
||||
statusByType: newMap,
|
||||
currentPageByType: currentPageByType,
|
||||
readStoriesIds: readStoriesIds,
|
||||
offlineReading: offlineReading,
|
||||
downloadStatus: downloadStatus,
|
||||
currentPageSize: currentPageSize,
|
||||
);
|
||||
}
|
||||
|
||||
@ -158,15 +153,8 @@ class StoriesState extends Equatable {
|
||||
final Map<StoryType, int> newMap =
|
||||
Map<StoryType, int>.from(currentPageByType);
|
||||
newMap[of] = to;
|
||||
return StoriesState(
|
||||
storiesByType: storiesByType,
|
||||
storyIdsByType: storyIdsByType,
|
||||
statusByType: statusByType,
|
||||
return copyWith(
|
||||
currentPageByType: newMap,
|
||||
readStoriesIds: readStoriesIds,
|
||||
offlineReading: offlineReading,
|
||||
downloadStatus: downloadStatus,
|
||||
currentPageSize: currentPageSize,
|
||||
);
|
||||
}
|
||||
|
||||
@ -183,15 +171,11 @@ class StoriesState extends Equatable {
|
||||
final Map<StoryType, int> newCurrentPageMap =
|
||||
Map<StoryType, int>.from(currentPageByType);
|
||||
newCurrentPageMap[of] = 0;
|
||||
return StoriesState(
|
||||
return copyWith(
|
||||
storiesByType: newStoriesMap,
|
||||
storyIdsByType: newStoryIdsMap,
|
||||
statusByType: newStatusMap,
|
||||
currentPageByType: newCurrentPageMap,
|
||||
readStoriesIds: readStoriesIds,
|
||||
offlineReading: offlineReading,
|
||||
downloadStatus: downloadStatus,
|
||||
currentPageSize: currentPageSize,
|
||||
);
|
||||
}
|
||||
|
||||
@ -205,5 +189,7 @@ class StoriesState extends Equatable {
|
||||
offlineReading,
|
||||
downloadStatus,
|
||||
currentPageSize,
|
||||
storiesDownloaded,
|
||||
storiesToBeDownloaded,
|
||||
];
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ abstract class Constants {
|
||||
'https://apps.apple.com/us/app/hacki/id1602043763?action=write-review';
|
||||
static const String googlePlayLink =
|
||||
'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 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) {
|
||||
case HomeScreen.routeName:
|
||||
return HomeScreen.route();
|
||||
case StoryScreen.routeName:
|
||||
return StoryScreen.route(settings.arguments! as StoryScreenArgs);
|
||||
case ItemScreen.routeName:
|
||||
return ItemScreen.route(settings.arguments! as ItemScreenArgs);
|
||||
case SubmitScreen.routeName:
|
||||
return SubmitScreen.route();
|
||||
default:
|
||||
@ -22,8 +22,8 @@ class CustomRouter {
|
||||
/// Nested routing for bottom navigation bar.
|
||||
static Route<dynamic> onGenerateNestedRoute(RouteSettings settings) {
|
||||
switch (settings.name) {
|
||||
case StoryScreen.routeName:
|
||||
return StoryScreen.route(settings.arguments! as StoryScreenArgs);
|
||||
case ItemScreen.routeName:
|
||||
return ItemScreen.route(settings.arguments! as ItemScreenArgs);
|
||||
case SubmitScreen.routeName:
|
||||
return SubmitScreen.route();
|
||||
default:
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:hacki/config/custom_log_filter.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
@ -19,8 +19,5 @@ Future<void> setUpLocator() async {
|
||||
..registerSingleton<CacheRepository>(CacheRepository())
|
||||
..registerSingleton<CacheService>(CacheService())
|
||||
..registerSingleton<LocalNotification>(LocalNotification())
|
||||
..registerSingleton<Logger>(Logger())
|
||||
..registerSingleton<RouteObserver<ModalRoute<dynamic>>>(
|
||||
RouteObserver<ModalRoute<dynamic>>(),
|
||||
);
|
||||
..registerSingleton<Logger>(Logger(filter: CustomLogFilter()));
|
||||
}
|
||||
|
@ -1,10 +1,15 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:collection/collection.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/main.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
|
||||
part 'comments_state.dart';
|
||||
@ -16,14 +21,14 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
StoriesRepository? storiesRepository,
|
||||
SembastRepository? sembastRepository,
|
||||
required bool offlineReading,
|
||||
required Story story,
|
||||
required Item item,
|
||||
}) : _cacheService = cacheService ?? locator.get<CacheService>(),
|
||||
_cacheRepository = cacheRepository ?? locator.get<CacheRepository>(),
|
||||
_storiesRepository =
|
||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||
_sembastRepository =
|
||||
sembastRepository ?? locator.get<SembastRepository>(),
|
||||
super(CommentsState.init(offlineReading: offlineReading, story: story));
|
||||
super(CommentsState.init(offlineReading: offlineReading, item: item));
|
||||
|
||||
final CacheService _cacheService;
|
||||
final CacheRepository _cacheRepository;
|
||||
@ -66,32 +71,29 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
|
||||
emit(state.copyWith(status: CommentsStatus.loading));
|
||||
|
||||
final Story story = state.story;
|
||||
final Story updatedStory = state.offlineReading
|
||||
? story
|
||||
: await _storiesRepository.fetchStoryBy(story.id) ?? story;
|
||||
final Item item = state.item;
|
||||
final Item updatedItem = state.offlineReading
|
||||
? item
|
||||
: 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) {
|
||||
_streamSubscription = _cacheRepository
|
||||
.getCachedCommentsStream(ids: updatedStory.kids)
|
||||
.getCachedCommentsStream(ids: kids)
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
} else {
|
||||
_streamSubscription = _storiesRepository
|
||||
.fetchCommentsStream(ids: updatedStory.kids)
|
||||
.fetchCommentsStream(ids: kids)
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
final bool offlineReading = await _cacheRepository.hasCachedStories;
|
||||
|
||||
_cacheService.resetCollapsedComments();
|
||||
|
||||
if (offlineReading) {
|
||||
if (state.offlineReading) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.loaded,
|
||||
@ -100,6 +102,10 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
return;
|
||||
}
|
||||
|
||||
_cacheService
|
||||
..resetComments()
|
||||
..resetCollapsedComments();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.loading,
|
||||
@ -109,28 +115,31 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
|
||||
await _streamSubscription?.cancel();
|
||||
|
||||
final Story story = state.story;
|
||||
final Story updatedStory =
|
||||
await _storiesRepository.fetchStoryBy(story.id) ?? story;
|
||||
final Item item = state.item;
|
||||
final Item updatedItem =
|
||||
await _storiesRepository.fetchItemBy(id: item.id) ?? item;
|
||||
final List<int> kids = sortKids(updatedItem.kids);
|
||||
|
||||
_streamSubscription = _storiesRepository
|
||||
.fetchCommentsStream(ids: updatedStory.kids)
|
||||
.fetchCommentsStream(ids: kids)
|
||||
.listen(_onCommentFetched)
|
||||
..onDone(_onDone);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
story: updatedStory,
|
||||
item: updatedItem,
|
||||
status: CommentsStatus.loaded,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void loadAll(Story story) {
|
||||
HapticFeedback.lightImpact();
|
||||
emit(
|
||||
state.copyWith(
|
||||
onlyShowTargetComment: false,
|
||||
comments: <Comment>[],
|
||||
story: story,
|
||||
item: story,
|
||||
),
|
||||
);
|
||||
init();
|
||||
@ -143,6 +152,47 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
HapticFeedback.selectionClick();
|
||||
if (order == null) return;
|
||||
_streamSubscription?.cancel();
|
||||
emit(state.copyWith(order: order, comments: <Comment>[]));
|
||||
init();
|
||||
}
|
||||
|
||||
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() {
|
||||
_streamSubscription?.cancel();
|
||||
_streamSubscription = null;
|
||||
@ -159,16 +209,29 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
..addKid(comment.id, to: comment.parent)
|
||||
..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>[
|
||||
...state.comments,
|
||||
comment
|
||||
buildableComment
|
||||
];
|
||||
|
||||
emit(state.copyWith(comments: updatedComments));
|
||||
|
||||
if (updatedComments.length >= _pageSize + _pageSize * state.currentPage &&
|
||||
updatedComments.length <=
|
||||
_pageSize * 2 + _pageSize * state.currentPage) {
|
||||
_streamSubscription?.pause();
|
||||
final bool isHidden = _cacheService.isHidden(comment.id);
|
||||
|
||||
if (!isHidden) {
|
||||
_streamSubscription?.pause();
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -180,6 +243,31 @@ 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
|
||||
Future<void> close() async {
|
||||
await _streamSubscription?.cancel();
|
||||
|
@ -8,12 +8,19 @@ enum CommentsStatus {
|
||||
failure,
|
||||
}
|
||||
|
||||
enum CommentsOrder {
|
||||
natural,
|
||||
newestFirst,
|
||||
oldestFirst,
|
||||
}
|
||||
|
||||
class CommentsState extends Equatable {
|
||||
const CommentsState({
|
||||
required this.story,
|
||||
required this.item,
|
||||
required this.comments,
|
||||
required this.status,
|
||||
required this.collapsed,
|
||||
required this.fetchParentStatus,
|
||||
required this.order,
|
||||
required this.onlyShowTargetComment,
|
||||
required this.offlineReading,
|
||||
required this.currentPage,
|
||||
@ -21,35 +28,39 @@ class CommentsState extends Equatable {
|
||||
|
||||
CommentsState.init({
|
||||
required this.offlineReading,
|
||||
required this.story,
|
||||
required this.item,
|
||||
}) : comments = <Comment>[],
|
||||
status = CommentsStatus.init,
|
||||
collapsed = false,
|
||||
fetchParentStatus = CommentsStatus.init,
|
||||
order = CommentsOrder.natural,
|
||||
onlyShowTargetComment = false,
|
||||
currentPage = 0;
|
||||
|
||||
final Story story;
|
||||
final Item item;
|
||||
final List<Comment> comments;
|
||||
final CommentsStatus status;
|
||||
final bool collapsed;
|
||||
final CommentsStatus fetchParentStatus;
|
||||
final CommentsOrder order;
|
||||
final bool onlyShowTargetComment;
|
||||
final bool offlineReading;
|
||||
final int currentPage;
|
||||
|
||||
CommentsState copyWith({
|
||||
Story? story,
|
||||
Item? item,
|
||||
List<Comment>? comments,
|
||||
CommentsStatus? status,
|
||||
bool? collapsed,
|
||||
CommentsStatus? fetchParentStatus,
|
||||
CommentsOrder? order,
|
||||
bool? onlyShowTargetComment,
|
||||
bool? offlineReading,
|
||||
int? currentPage,
|
||||
}) {
|
||||
return CommentsState(
|
||||
story: story ?? this.story,
|
||||
item: item ?? this.item,
|
||||
comments: comments ?? this.comments,
|
||||
fetchParentStatus: fetchParentStatus ?? this.fetchParentStatus,
|
||||
status: status ?? this.status,
|
||||
collapsed: collapsed ?? this.collapsed,
|
||||
order: order ?? this.order,
|
||||
onlyShowTargetComment:
|
||||
onlyShowTargetComment ?? this.onlyShowTargetComment,
|
||||
offlineReading: offlineReading ?? this.offlineReading,
|
||||
@ -59,10 +70,11 @@ class CommentsState extends Equatable {
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
story,
|
||||
item,
|
||||
comments,
|
||||
status,
|
||||
collapsed,
|
||||
fetchParentStatus,
|
||||
order,
|
||||
onlyShowTargetComment,
|
||||
offlineReading,
|
||||
currentPage,
|
||||
|
@ -14,4 +14,5 @@ export 'search/search_cubit.dart';
|
||||
export 'split_view/split_view_cubit.dart';
|
||||
export 'submit/submit_cubit.dart';
|
||||
export 'time_machine/time_machine_cubit.dart';
|
||||
export 'user/user_cubit.dart';
|
||||
export 'vote/vote_cubit.dart';
|
||||
|
@ -39,15 +39,15 @@ class FavCubit extends Cubit<FavState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
favIds: favIds,
|
||||
favStories: <Story>[],
|
||||
favItems: <Item>[],
|
||||
currentPage: 0,
|
||||
),
|
||||
);
|
||||
_storiesRepository
|
||||
.fetchStoriesStream(
|
||||
.fetchItemsStream(
|
||||
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
|
||||
)
|
||||
.listen(_onStoryLoaded)
|
||||
.listen(_onItemLoaded)
|
||||
.onDone(() {
|
||||
emit(
|
||||
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(
|
||||
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(
|
||||
state.copyWith(
|
||||
favIds: List<int>.from(state.favIds)..remove(id),
|
||||
favStories: List<Story>.from(state.favStories)
|
||||
..removeWhere((Story e) => e.id == id),
|
||||
favItems: List<Item>.from(state.favItems)
|
||||
..removeWhere((Item e) => e.id == id),
|
||||
),
|
||||
);
|
||||
|
||||
@ -120,13 +120,13 @@ class FavCubit extends Cubit<FavState> {
|
||||
}
|
||||
|
||||
_storiesRepository
|
||||
.fetchStoriesStream(
|
||||
.fetchItemsStream(
|
||||
ids: state.favIds.sublist(
|
||||
lower,
|
||||
upper,
|
||||
),
|
||||
)
|
||||
.listen(_onStoryLoaded)
|
||||
.listen(_onItemLoaded)
|
||||
.onDone(() {
|
||||
emit(state.copyWith(status: FavStatus.loaded));
|
||||
});
|
||||
@ -142,7 +142,7 @@ class FavCubit extends Cubit<FavState> {
|
||||
state.copyWith(
|
||||
status: FavStatus.loading,
|
||||
currentPage: 0,
|
||||
favStories: <Story>[],
|
||||
favItems: <Item>[],
|
||||
favIds: <int>[],
|
||||
),
|
||||
);
|
||||
@ -150,20 +150,20 @@ class FavCubit extends Cubit<FavState> {
|
||||
_preferenceRepository.favList(of: username).then((List<int> favIds) {
|
||||
emit(state.copyWith(favIds: favIds));
|
||||
_storiesRepository
|
||||
.fetchStoriesStream(
|
||||
.fetchItemsStream(
|
||||
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
|
||||
)
|
||||
.listen(_onStoryLoaded)
|
||||
.listen(_onItemLoaded)
|
||||
.onDone(() {
|
||||
emit(state.copyWith(status: FavStatus.loaded));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _onStoryLoaded(Story story) {
|
||||
void _onItemLoaded(Item item) {
|
||||
emit(
|
||||
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 {
|
||||
const FavState({
|
||||
required this.favIds,
|
||||
required this.favStories,
|
||||
required this.favItems,
|
||||
required this.status,
|
||||
required this.currentPage,
|
||||
});
|
||||
|
||||
FavState.init()
|
||||
: favIds = <int>[],
|
||||
favStories = <Story>[],
|
||||
favItems = <Item>[],
|
||||
status = FavStatus.init,
|
||||
currentPage = 0;
|
||||
|
||||
final List<int> favIds;
|
||||
final List<Story> favStories;
|
||||
final List<Item> favItems;
|
||||
final FavStatus status;
|
||||
final int currentPage;
|
||||
|
||||
FavState copyWith({
|
||||
List<int>? favIds,
|
||||
List<Story>? favStories,
|
||||
List<Item>? favItems,
|
||||
FavStatus? status,
|
||||
int? currentPage,
|
||||
}) {
|
||||
return FavState(
|
||||
favIds: favIds ?? this.favIds,
|
||||
favStories: favStories ?? this.favStories,
|
||||
favItems: favItems ?? this.favItems,
|
||||
status: status ?? this.status,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
);
|
||||
@ -43,7 +43,7 @@ class FavState extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
favIds,
|
||||
favStories,
|
||||
favItems,
|
||||
status,
|
||||
currentPage,
|
||||
];
|
||||
|
@ -13,12 +13,14 @@ class SplitViewCubit extends Cubit<SplitViewState> {
|
||||
|
||||
final CacheService _cacheService;
|
||||
|
||||
void updateStoryScreenArgs(StoryScreenArgs args) {
|
||||
void updateItemScreenArgs(ItemScreenArgs args) {
|
||||
_cacheService.resetCollapsedComments();
|
||||
emit(state.copyWith(storyScreenArgs: args));
|
||||
emit(state.copyWith(itemScreenArgs: args));
|
||||
}
|
||||
|
||||
void enableSplitView() => emit(state.copyWith(enabled: true));
|
||||
|
||||
void disableSplitView() => emit(state.copyWith(enabled: false));
|
||||
|
||||
void zoom() => emit(state.copyWith(expanded: !state.expanded));
|
||||
}
|
||||
|
@ -2,27 +2,36 @@ part of 'split_view_cubit.dart';
|
||||
|
||||
class SplitViewState extends Equatable {
|
||||
const SplitViewState({
|
||||
required this.storyScreenArgs,
|
||||
required this.itemScreenArgs,
|
||||
required this.expanded,
|
||||
required this.enabled,
|
||||
});
|
||||
|
||||
const SplitViewState.init()
|
||||
: enabled = false,
|
||||
storyScreenArgs = null;
|
||||
expanded = false,
|
||||
itemScreenArgs = null;
|
||||
|
||||
final bool enabled;
|
||||
final StoryScreenArgs? storyScreenArgs;
|
||||
final bool expanded;
|
||||
final ItemScreenArgs? itemScreenArgs;
|
||||
|
||||
SplitViewState copyWith({bool? enabled, StoryScreenArgs? storyScreenArgs}) {
|
||||
SplitViewState copyWith({
|
||||
bool? enabled,
|
||||
bool? expanded,
|
||||
ItemScreenArgs? itemScreenArgs,
|
||||
}) {
|
||||
return SplitViewState(
|
||||
enabled: enabled ?? this.enabled,
|
||||
storyScreenArgs: storyScreenArgs ?? this.storyScreenArgs,
|
||||
expanded: expanded ?? this.expanded,
|
||||
itemScreenArgs: itemScreenArgs ?? this.itemScreenArgs,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
enabled,
|
||||
storyScreenArgs,
|
||||
expanded,
|
||||
itemScreenArgs,
|
||||
];
|
||||
}
|
||||
|
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,
|
||||
];
|
||||
}
|
@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/main.dart';
|
||||
import 'package:hacki/screens/screens.dart' show StoryScreen, StoryScreenArgs;
|
||||
import 'package:hacki/screens/screens.dart' show ItemScreen, ItemScreenArgs;
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
extension StateExtension on State {
|
||||
void showSnackBar({
|
||||
@ -12,7 +13,7 @@ extension StateExtension on State {
|
||||
}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.deepOrange,
|
||||
backgroundColor: Palette.deepOrange,
|
||||
content: Text(content),
|
||||
action: action != null && label != null
|
||||
? SnackBarAction(
|
||||
@ -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;
|
||||
|
||||
if (splitViewEnabled) {
|
||||
context.read<SplitViewCubit>().updateStoryScreenArgs(args);
|
||||
if (splitViewEnabled && !forceNewScreen) {
|
||||
context.read<SplitViewCubit>().updateItemScreenArgs(args);
|
||||
} else {
|
||||
return HackiApp.navigatorKey.currentState?.pushNamed(
|
||||
StoryScreen.routeName,
|
||||
ItemScreen.routeName,
|
||||
arguments: args,
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
extension StringExtension on String {
|
||||
int? getItemId() {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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/config/custom_router.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
@ -14,6 +15,7 @@ import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/repositories/repositories.dart' show PreferenceRepository;
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/services/fetcher.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
|
||||
@ -24,6 +26,10 @@ import 'package:workmanager/workmanager.dart';
|
||||
final BehaviorSubject<String?> selectNotificationSubject =
|
||||
BehaviorSubject<String?>();
|
||||
|
||||
// For receiving payload event from siri suggestions.
|
||||
final BehaviorSubject<String?> siriSuggestionSubject =
|
||||
BehaviorSubject<String?>();
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
@ -57,6 +63,16 @@ Future<void> main() async {
|
||||
badge: 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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final Directory tempDir = await getTemporaryDirectory();
|
||||
@ -164,26 +180,22 @@ class HackiApp extends StatelessWidget {
|
||||
lazy: false,
|
||||
create: (BuildContext context) => PostCubit(),
|
||||
),
|
||||
BlocProvider<EditCubit>(
|
||||
lazy: false,
|
||||
create: (BuildContext context) => EditCubit(),
|
||||
),
|
||||
],
|
||||
child: AdaptiveTheme(
|
||||
light: ThemeData(
|
||||
primarySwatch: Colors.orange,
|
||||
primarySwatch: Palette.orange,
|
||||
),
|
||||
dark: ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
primarySwatch: Colors.orange,
|
||||
canvasColor: trueDarkMode ? Colors.black : null,
|
||||
primarySwatch: Palette.orange,
|
||||
canvasColor: trueDarkMode ? Palette.black : null,
|
||||
),
|
||||
initial: savedThemeMode ?? AdaptiveThemeMode.system,
|
||||
builder: (ThemeData theme, ThemeData darkTheme) {
|
||||
final ThemeData trueDarkTheme = ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
primarySwatch: Colors.orange,
|
||||
canvasColor: Colors.black,
|
||||
primarySwatch: Palette.orange,
|
||||
canvasColor: Palette.black,
|
||||
);
|
||||
return FutureBuilder<AdaptiveThemeMode?>(
|
||||
future: AdaptiveTheme.getThemeMode(),
|
||||
@ -209,9 +221,6 @@ class HackiApp extends StatelessWidget {
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: useTrueDark ? trueDarkTheme : theme,
|
||||
navigatorKey: navigatorKey,
|
||||
navigatorObservers: <NavigatorObserver>[
|
||||
locator.get<RouteObserver<ModalRoute<dynamic>>>(),
|
||||
],
|
||||
onGenerateRoute: CustomRouter.onGenerateRoute,
|
||||
initialRoute: HomeScreen.routeName,
|
||||
),
|
||||
|
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 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/item.dart';
|
||||
|
||||
class Comment extends Item {
|
||||
@ -12,11 +11,11 @@ class Comment extends Item {
|
||||
required super.by,
|
||||
required super.text,
|
||||
required super.kids,
|
||||
required super.dead,
|
||||
required super.deleted,
|
||||
required this.level,
|
||||
}) : super(
|
||||
descendants: 0,
|
||||
dead: false,
|
||||
parts: <int>[],
|
||||
title: '',
|
||||
url: '',
|
||||
@ -43,8 +42,7 @@ class Comment extends Item {
|
||||
|
||||
final int level;
|
||||
|
||||
String get postedDate =>
|
||||
DateTime.fromMillisecondsSinceEpoch(time * 1000).toReadableString();
|
||||
String get metadata => '''by $by $postedDate''';
|
||||
|
||||
Comment copyWith({int? level}) {
|
||||
return Comment(
|
||||
@ -55,6 +53,7 @@ class Comment extends Item {
|
||||
by: by,
|
||||
text: text,
|
||||
kids: kids,
|
||||
dead: dead,
|
||||
deleted: deleted,
|
||||
level: level ?? this.level,
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/extensions/date_time_extension.dart';
|
||||
|
||||
abstract class Item extends Equatable {
|
||||
const Item({
|
||||
@ -54,6 +55,9 @@ abstract class Item extends Equatable {
|
||||
final List<int> kids;
|
||||
final List<int> parts;
|
||||
|
||||
String get postedDate =>
|
||||
DateTime.fromMillisecondsSinceEpoch(time * 1000).toReadableString();
|
||||
|
||||
bool get isPoll => type == 'poll';
|
||||
|
||||
bool get isStory => type == 'story';
|
||||
|
@ -1,3 +1,4 @@
|
||||
export 'buildable_comment.dart';
|
||||
export 'comment.dart';
|
||||
export 'item.dart';
|
||||
export 'poll_option.dart';
|
||||
|
@ -1,6 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/item.dart';
|
||||
|
||||
class PollOption extends Item {
|
||||
@ -63,9 +62,6 @@ class PollOption extends Item {
|
||||
|
||||
final double ratio;
|
||||
|
||||
String get postedDate =>
|
||||
DateTime.fromMillisecondsSinceEpoch(time * 1000).toReadableString();
|
||||
|
||||
PollOption copyWith({double? ratio}) {
|
||||
return PollOption(
|
||||
id: id,
|
||||
|
@ -1,10 +1,10 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/item.dart';
|
||||
|
||||
enum StoryType {
|
||||
top('topstories'),
|
||||
best('beststories'),
|
||||
latest('newstories'),
|
||||
ask('askstories'),
|
||||
show('showstories'),
|
||||
@ -13,6 +13,23 @@ enum StoryType {
|
||||
const StoryType(this.path);
|
||||
|
||||
final String path;
|
||||
|
||||
String get label {
|
||||
switch (this) {
|
||||
case StoryType.top:
|
||||
return 'TOP';
|
||||
case StoryType.best:
|
||||
return 'BEST';
|
||||
case StoryType.latest:
|
||||
return 'NEW';
|
||||
case StoryType.ask:
|
||||
return 'ASK';
|
||||
case StoryType.show:
|
||||
return 'SHOW';
|
||||
case StoryType.jobs:
|
||||
return 'JOBS';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Story extends Item {
|
||||
@ -76,9 +93,6 @@ class Story extends Item {
|
||||
String get simpleMetadata =>
|
||||
'''$score point${score > 1 ? 's' : ''} $descendants comment${descendants > 1 ? 's' : ''} $postedDate''';
|
||||
|
||||
String get postedDate =>
|
||||
DateTime.fromMillisecondsSinceEpoch(time * 1000).toReadableString();
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'descendants': descendants,
|
||||
|