Compare commits

...

27 Commits

Author SHA1 Message Date
f0d6cac3fd v0.2.24 (#61)
* bumped version.

* improved collapse.

* improved download speed.

* improved err handling.

* improved error handling.

* improved error handling.

* improved logging.

* improved logging.

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

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

* cleaned up code.

* bumped version.

* fixed lint.

* updated android config.

* prevent backup of secure storage.

* small fix.

* cleaned up code.

* cleaned up.

* small fix
2022-06-22 23:44:49 -07:00
502faaf188 corrected spelling. 2022-06-22 10:28:27 -07:00
b952f349fc v0.2.21 (#57)
* replaced StoryScreen with ItemScreen.

* use ItemScreen for share extension.

* fixed getItemId()

* bumped version.

* force new screen on viewing comments in separate thread.

* disable comment thread if comment is deleted or dead.

* navigate to new screen on viewing parent thread.

* bumped version.

* fixed inconsistent fontsize.

* bumped version.
2022-06-21 20:20:09 -07:00
9cefffa518 v0.2.20 (#56)
* bumped version.

* fixed web analyzer.

* improved comments loading mechanism.

* fixed delete all button.

* improved reply box logic.

* improved web analyzer.

* allow users to sort comments.

* fixed styles.

* fixed bugs.

* bumped version.

* fixed comments cubit.

* fixed dead comments.
2022-06-21 02:38:24 -07:00
fe630ea7a9 v0.2.20 (#55)
* bumped version.

* fixed web analyzer.

* improved comments loading mechanism.

* fixed delete all button.

* improved reply box logic.

* improved web analyzer.

* allow users to sort comments.

* fixed styles.

* fixed bugs.

* bumped version.

* fixed comments cubit.
2022-06-21 02:15:42 -07:00
459ab961d1 v0.2.19 hotfix (#52)
* improved offline mode.

* bumped version.

* reset stories count on download event.

* fixed overflow.
2022-06-18 23:40:56 -07:00
362d7005df fixed jank while exiting story screen. 2022-06-18 11:44:34 -07:00
6b7c1d42de fixed offline mode. 2022-06-18 02:38:31 -07:00
2a889bca56 v0.2.19 (#51)
* improved comments loading.

* added web page caching to offline mode.

* improved offline webview.

* fixed web analyzer.

* updated description.
2022-06-18 02:18:03 -07:00
e25026f129 created FUNDING.yml 2022-06-17 21:25:24 -07:00
fefc86275d v0.2.18 (#49)
* allow plaintext http connections (#48)

via https://stackoverflow.com/a/56837613

I was unable to open an article just now, and a fww minutes later unable to open a comment (since the webview is still being forced on my not-yet-updated client, i have to find the artocle to open it.

* bumped version.

* improved link preview.

* added share button.

* added ability to launch third party app for url on Android.

* added support for siri suggestions.

* bumped version.

* added support for app links on Android.

Co-authored-by: Efreak <Efreak@users.noreply.github.com>
2022-06-15 23:56:45 -07:00
1a73a6991e updated screenshots. 2022-06-10 02:28:31 -07:00
36d7f4606e updated screenshots. 2022-06-10 02:27:22 -07:00
9312c56dd0 v0.2.17 (#47)
* improved story screen scrolling.

* bumped version.

* shrink instead of return on tapping back button. #42

* allowed users to view other user's profile. #45

* bumped version.

* added back underline to links.

* fixed overlow of popup menu,
2022-06-10 02:10:23 -07:00
6e71de5913 updated README.md 2022-06-05 22:39:54 -07:00
a9590af3e9 v0.2.16 (#41)
* bumped version.

* add badges for fdroid, GitHub release versions (#40)

* updated README.md

* replaced jobs with best.

* updated README.md

Co-authored-by: Efreak <Efreak@users.noreply.github.com>
2022-06-05 22:34:48 -07:00
18dadaa5ec v0.2.15 (#39)
* Add capitlzation to text fields (#37)

* added ability for split view to be expanded to full screen. (#38)

Co-authored-by: Efreak <Efreak@users.noreply.github.com>
2022-06-05 00:58:49 -07:00
d506297f4c v0.2.14 (#35)
* added custom search feature.

* removed equatable.

* added custom range.

* updated README.md
2022-06-04 02:11:26 -07:00
4e8cf9837f v0.2.13 (#34)
* fixed #32 and #33

* fall back to default browser #31

* fixed story tile.

* fixed version.

* use Listview.builder instead of listview.

* updated link utls.
2022-06-03 00:26:30 -07:00
f73269404a updated README.md 2022-06-01 09:13:35 -07:00
c4c109e4a3 v0.2.12 (#30)
* added metadata to story tile.

* bumped version

* fixed metadata tile

* bumped version.

* fixed pinned stories not being refreshed.
2022-06-01 01:34:21 -07:00
b7720d0584 v0.2.12 (#29)
* added metadata to story tile.

* bumped version

* fixed metadata tile

* bumped version.
2022-06-01 01:13:21 -07:00
7d5788dec3 updated README.md 2022-05-28 00:03:46 -07:00
80a836c6b9 updated README.md 2022-05-28 00:02:03 -07:00
913be9fc5d v0.2.11 (#27)
* fixed pinned story tile.

* bumped version.
2022-05-27 16:18:36 -07:00
156 changed files with 3494 additions and 1778 deletions

13
.github/FUNDING.yml vendored Normal file
View 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']

View File

@ -3,15 +3,14 @@
A simple noiseless [Hacker News](https://news.ycombinator.com/) client made with Flutter that is just enough. A simple noiseless [Hacker News](https://news.ycombinator.com/) client made with Flutter that is just enough.
[![App Store](https://img.shields.io/itunes/v/1602043763?label=App%20Store)](https://apps.apple.com/us/app/hacki/id1602043763) [![App Store](https://img.shields.io/itunes/v/1602043763?label=App%20Store)](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone)
[![Play Store](https://img.shields.io/badge/Play%20Store--yellow)](https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US) [![Fdroid version](https://img.shields.io/f-droid/v/com.jiaqifeng.hacki)](https://f-droid.org/en/packages/com.jiaqifeng.hacki/)
[![GH version](https://img.shields.io/github/release/livinglist/hacki.svg?logo=github)](https://github.com/Livinglist/Hacki/releases/latest)
[![Visits Badge](https://badges.pufler.dev/visits/livinglist/Hacki)](https://badges.pufler.dev) [![Visits Badge](https://badges.pufler.dev/visits/livinglist/Hacki)](https://badges.pufler.dev)
[![GitHub](https://img.shields.io/github/stars/livinglist/Hacki?style=social)](https://img.shields.io/github/stars/livinglist/Hacki?style=social) [![GitHub](https://img.shields.io/github/stars/livinglist/Hacki?style=social)](https://img.shields.io/github/stars/livinglist/Hacki?style=social)
[![style: effective dart](https://img.shields.io/badge/style-effective_dart-40c4ff.svg)](https://pub.dev/packages/effective_dart) [![style: effective dart](https://img.shields.io/badge/style-effective_dart-40c4ff.svg)](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/)
[<img src="assets/images/app_store_badge.png" height="50">](https://apps.apple.com/us/app/hacki/id1602043763) [<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/)
Features: Features:
@ -36,24 +35,22 @@ Features:
<p align="center"> <p align="center">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159799288-6e98352a-fe89-4a2e-8a74-c5782463a1e1.png"> <img width="200" alt="01" src="assets/screenshots/01.png">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159799297-75b52eac-2066-4df9-bdfc-7c82bf7b81c8.png"> <img width="200" alt="02" src="assets/screenshots/02.png">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159799302-860c61b8-abba-486a-9592-bc84a6af3232.png"> <img width="200" alt="03" src="assets/screenshots/03.png">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159799305-308743d3-1c89-45de-9645-3b6ec789c282.png"> <img width="200" alt="04" src="assets/screenshots/04.png">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798176-5212e9bf-296d-4d9b-ab48-19b741684c8a.png"> <img width="200" alt="05" src="assets/screenshots/05.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="06" src="assets/screenshots/06.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="07" src="assets/screenshots/07.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="08" src="assets/screenshots/08.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="09" src="assets/screenshots/09.png">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798185-d7c81348-956e-483c-a1bc-5cd872bdad62.png"> <img width="200" alt="10" src="assets/screenshots/10.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="11" src="assets/screenshots/11.png">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798187-4404adea-b2bc-472e-8568-2379e6db01a4.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="ipad-01" src="assets/screenshots/ipad-01.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="ipad-02" src="assets/screenshots/ipad-02.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="ipad-03" src="assets/screenshots/ipad-03.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="ipad-04" src="assets/screenshots/ipad-04.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">
</p> </p>

View File

@ -5,4 +5,5 @@ linter:
public_member_api_docs: false public_member_api_docs: false
library_private_types_in_public_api: false library_private_types_in_public_api: false
omit_local_variable_types: false omit_local_variable_types: false
one_member_abstracts: false
always_specify_types: true always_specify_types: true

View File

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

View File

@ -9,17 +9,25 @@
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<data android:scheme="https" /> <data android:scheme="https" />
</intent> </intent>
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" />
</intent>
</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
@ -48,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 -->

View File

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

View File

@ -1,12 +1,12 @@
buildscript { buildscript {
ext.kotlin_version = '1.6.10' ext.kotlin_version = '1.7.0'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.1.1' classpath 'com.android.tools.build:gradle:7.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }

BIN
assets/screenshots/01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

BIN
assets/screenshots/02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 KiB

BIN
assets/screenshots/03.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

BIN
assets/screenshots/04.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

BIN
assets/screenshots/05.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 KiB

BIN
assets/screenshots/06.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 KiB

BIN
assets/screenshots/07.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
assets/screenshots/08.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

BIN
assets/screenshots/09.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

BIN
assets/screenshots/10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

BIN
assets/screenshots/11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 KiB

BIN
assets/screenshots/12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
- Offline mode now includes web pages.

View File

@ -0,0 +1 @@
- Offline mode now includes web pages.

View File

@ -0,0 +1 @@
- Offline mode now includes web pages.

View File

@ -0,0 +1 @@
- Offline mode now includes web pages.

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 KiB

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 KiB

After

Width:  |  Height:  |  Size: 835 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 KiB

After

Width:  |  Height:  |  Size: 935 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 931 KiB

After

Width:  |  Height:  |  Size: 868 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 KiB

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 420 KiB

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

View File

@ -14,6 +14,8 @@ 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)
@ -23,6 +25,8 @@ PODS:
- 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 +36,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 +49,14 @@ 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`)
- 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 +78,14 @@ 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"
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 +94,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:
@ -102,16 +107,17 @@ SPEC CHECKSUMS:
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
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

View File

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

View File

@ -39,6 +39,12 @@
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string> <string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
<string>http</string>
<string>mailto</string>
</array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>

View File

@ -7,7 +7,9 @@ import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
import 'package:logger/logger.dart';
import 'package:responsive_builder/responsive_builder.dart'; import 'package:responsive_builder/responsive_builder.dart';
import 'package:rxdart/rxdart.dart';
part 'stories_event.dart'; part 'stories_event.dart';
part 'stories_state.dart'; part 'stories_state.dart';
@ -32,6 +34,7 @@ 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);
@ -48,6 +51,15 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
static const int _tabletSmallPageSize = 15; static const int _tabletSmallPageSize = 15;
static const int _tabletLargePageSize = 25; 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( Future<void> onInitialize(
StoriesInitialize event, StoriesInitialize event,
Emitter<StoriesState> emit, Emitter<StoriesState> emit,
@ -70,11 +82,9 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
currentPageSize: pageSize, currentPageSize: pageSize,
), ),
); );
await loadStories(of: StoryType.top, emit: emit); for (final StoryType type in types) {
await loadStories(of: StoryType.latest, emit: emit); await loadStories(of: type, emit: emit);
await loadStories(of: StoryType.ask, emit: emit); }
await loadStories(of: StoryType.show, emit: emit);
await loadStories(of: StoryType.jobs, emit: emit);
} }
Future<void> loadStories({ Future<void> loadStories({
@ -237,52 +247,42 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
await _cacheRepository.deleteAllStories(); await _cacheRepository.deleteAllStories();
await _cacheRepository.deleteAllComments(); await _cacheRepository.deleteAllComments();
final List<int> topIds = final Set<int> prioritizedIds = <int>{};
await _storiesRepository.fetchStoryIds(of: StoryType.top); final List<StoryType> prioritizedTypes = <StoryType>[...types]
final List<int> newIds = ..remove(StoryType.latest);
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);
await _cacheRepository.cacheStoryIds(of: StoryType.top, ids: topIds); for (final StoryType type in prioritizedTypes) {
await _cacheRepository.cacheStoryIds(of: StoryType.latest, ids: newIds); final List<int> ids = await _storiesRepository.fetchStoryIds(of: type);
await _cacheRepository.cacheStoryIds(of: StoryType.ask, ids: askIds); await _cacheRepository.cacheStoryIds(of: type, ids: ids);
await _cacheRepository.cacheStoryIds(of: StoryType.show, ids: showIds); prioritizedIds.addAll(ids);
await _cacheRepository.cacheStoryIds(of: StoryType.jobs, ids: jobIds);
final List<int> allIds = <int>[
...topIds,
...newIds,
...askIds,
...showIds,
...jobIds
];
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( 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 _cacheRepository.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(
@ -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( Future<void> onPageSizeChanged(
StoriesPageSizeChanged event, StoriesPageSizeChanged event,
Emitter<StoriesState> emit, Emitter<StoriesState> emit,
@ -307,6 +381,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
await _cacheRepository.deleteAllStoryIds(); await _cacheRepository.deleteAllStoryIds();
await _cacheRepository.deleteAllStories(); await _cacheRepository.deleteAllStories();
await _cacheRepository.deleteAllComments(); await _cacheRepository.deleteAllComments();
await _cacheRepository.deleteAllWebPages();
emit(state.copyWith(offlineReading: false)); emit(state.copyWith(offlineReading: false));
add(StoriesInitialize()); add(StoriesInitialize());
} }

View File

@ -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 {

View File

@ -23,11 +23,14 @@ 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({
this.storiesByType = const <StoryType, List<Story>>{ this.storiesByType = const <StoryType, List<Story>>{
StoryType.top: <Story>[], StoryType.top: <Story>[],
StoryType.best: <Story>[],
StoryType.latest: <Story>[], StoryType.latest: <Story>[],
StoryType.ask: <Story>[], StoryType.ask: <Story>[],
StoryType.show: <Story>[], StoryType.show: <Story>[],
@ -35,6 +38,7 @@ class StoriesState extends Equatable {
}, },
this.storyIdsByType = const <StoryType, List<int>>{ this.storyIdsByType = const <StoryType, List<int>>{
StoryType.top: <int>[], StoryType.top: <int>[],
StoryType.best: <int>[],
StoryType.latest: <int>[], StoryType.latest: <int>[],
StoryType.ask: <int>[], StoryType.ask: <int>[],
StoryType.show: <int>[], StoryType.show: <int>[],
@ -42,6 +46,7 @@ class StoriesState extends Equatable {
}, },
this.statusByType = const <StoryType, StoriesStatus>{ this.statusByType = const <StoryType, StoriesStatus>{
StoryType.top: StoriesStatus.initial, StoryType.top: StoriesStatus.initial,
StoryType.best: StoriesStatus.initial,
StoryType.latest: StoriesStatus.initial, StoryType.latest: StoriesStatus.initial,
StoryType.ask: StoriesStatus.initial, StoryType.ask: StoriesStatus.initial,
StoryType.show: StoriesStatus.initial, StoryType.show: StoriesStatus.initial,
@ -49,6 +54,7 @@ class StoriesState extends Equatable {
}, },
this.currentPageByType = const <StoryType, int>{ this.currentPageByType = const <StoryType, int>{
StoryType.top: 0, StoryType.top: 0,
StoryType.best: 0,
StoryType.latest: 0, StoryType.latest: 0,
StoryType.ask: 0, StoryType.ask: 0,
StoryType.show: 0, StoryType.show: 0,
@ -57,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;
@ -67,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,
@ -77,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,
@ -87,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,
); );
} }
@ -98,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,
); );
} }
@ -120,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,
); );
} }
@ -139,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,
); );
} }
@ -158,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,
); );
} }
@ -183,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,
); );
} }
@ -205,5 +189,7 @@ class StoriesState extends Equatable {
offlineReading, offlineReading,
downloadStatus, downloadStatus,
currentPageSize, currentPageSize,
storiesDownloaded,
storiesToBeDownloaded,
]; ];
} }

View File

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

View File

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

View File

@ -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:

View File

@ -1,5 +1,5 @@
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';
@ -19,8 +19,5 @@ Future<void> setUpLocator() async {
..registerSingleton<CacheRepository>(CacheRepository()) ..registerSingleton<CacheRepository>(CacheRepository())
..registerSingleton<CacheService>(CacheService()) ..registerSingleton<CacheService>(CacheService())
..registerSingleton<LocalNotification>(LocalNotification()) ..registerSingleton<LocalNotification>(LocalNotification())
..registerSingleton<Logger>(Logger()) ..registerSingleton<Logger>(Logger(filter: CustomLogFilter()));
..registerSingleton<RouteObserver<ModalRoute<dynamic>>>(
RouteObserver<ModalRoute<dynamic>>(),
);
} }

View File

@ -55,21 +55,21 @@ class CollapseCubit extends Cubit<CollapseState> {
} }
void hiddenCommentsStreamListener(Map<int, Set<int>> event) { void hiddenCommentsStreamListener(Map<int, Set<int>> event) {
int collapsedCount = 0;
for (final int key in event.keys) { for (final int key in event.keys) {
if (key == _commentId && !isClosed) { if (key == _commentId && !isClosed) {
emit( collapsedCount = event[key]?.length ?? 0;
state.copyWith( break;
collapsedCount: event[key]?.length ?? 0,
),
);
return;
} }
} }
for (final Set<int> val in event.values) { for (final Set<int> val in event.values) {
if (val.contains(_commentId) && !isClosed) { if (val.contains(_commentId) && !isClosed) {
emit( emit(
state.copyWith(hidden: true), state.copyWith(
hidden: true,
collapsedCount: collapsedCount,
),
); );
return; return;
} }
@ -77,7 +77,10 @@ class CollapseCubit extends Cubit<CollapseState> {
if (!isClosed) { if (!isClosed) {
emit( emit(
state.copyWith(hidden: false), state.copyWith(
hidden: false,
collapsedCount: collapsedCount,
),
); );
} }
} }

View File

@ -1,10 +1,15 @@
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';
part 'comments_state.dart'; part 'comments_state.dart';
@ -16,14 +21,14 @@ class CommentsCubit extends Cubit<CommentsState> {
StoriesRepository? storiesRepository, StoriesRepository? storiesRepository,
SembastRepository? sembastRepository, SembastRepository? sembastRepository,
required bool offlineReading, required bool offlineReading,
required Story story, required Item item,
}) : _cacheService = cacheService ?? locator.get<CacheService>(), }) : _cacheService = cacheService ?? locator.get<CacheService>(),
_cacheRepository = cacheRepository ?? locator.get<CacheRepository>(), _cacheRepository = cacheRepository ?? locator.get<CacheRepository>(),
_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)); super(CommentsState.init(offlineReading: offlineReading, item: item));
final CacheService _cacheService; final CacheService _cacheService;
final CacheRepository _cacheRepository; final CacheRepository _cacheRepository;
@ -66,32 +71,29 @@ class CommentsCubit extends Cubit<CommentsState> {
emit(state.copyWith(status: CommentsStatus.loading)); emit(state.copyWith(status: CommentsStatus.loading));
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 = _cacheRepository
.getCachedCommentsStream(ids: updatedStory.kids) .getCachedCommentsStream(ids: kids)
.listen(_onCommentFetched) .listen(_onCommentFetched)
..onDone(_onDone); ..onDone(_onDone);
} else { } else {
_streamSubscription = _storiesRepository _streamSubscription = _storiesRepository
.fetchCommentsStream(ids: updatedStory.kids) .fetchCommentsStream(ids: kids)
.listen(_onCommentFetched) .listen(_onCommentFetched)
..onDone(_onDone); ..onDone(_onDone);
} }
} }
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,
@ -100,6 +102,10 @@ class CommentsCubit extends Cubit<CommentsState> {
return; return;
} }
_cacheService
..resetComments()
..resetCollapsedComments();
emit( emit(
state.copyWith( state.copyWith(
status: CommentsStatus.loading, status: CommentsStatus.loading,
@ -109,28 +115,31 @@ class CommentsCubit extends Cubit<CommentsState> {
await _streamSubscription?.cancel(); await _streamSubscription?.cancel();
final Story story = state.story; final Item item = state.item;
final Story updatedStory = final Item updatedItem =
await _storiesRepository.fetchStoryBy(story.id) ?? story; await _storiesRepository.fetchItemBy(id: item.id) ?? item;
final List<int> kids = sortKids(updatedItem.kids);
_streamSubscription = _storiesRepository _streamSubscription = _storiesRepository
.fetchCommentsStream(ids: updatedStory.kids) .fetchCommentsStream(ids: kids)
.listen(_onCommentFetched) .listen(_onCommentFetched)
..onDone(_onDone); ..onDone(_onDone);
emit( emit(
state.copyWith( state.copyWith(
story: updatedStory, 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>[], comments: <Comment>[],
story: story, item: story,
), ),
); );
init(); 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() { void _onDone() {
_streamSubscription?.cancel(); _streamSubscription?.cancel();
_streamSubscription = null; _streamSubscription = null;
@ -159,16 +209,29 @@ class CommentsCubit extends Cubit<CommentsState> {
..addKid(comment.id, to: comment.parent) ..addKid(comment.id, to: comment.parent)
..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 (updatedComments.length >= _pageSize + _pageSize * state.currentPage &&
updatedComments.length <= updatedComments.length <=
_pageSize * 2 + _pageSize * state.currentPage) { _pageSize * 2 + _pageSize * state.currentPage) {
final bool isHidden = _cacheService.isHidden(comment.id);
if (!isHidden) {
_streamSubscription?.pause(); _streamSubscription?.pause();
}
emit( emit(
state.copyWith( 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 @override
Future<void> close() async { Future<void> close() async {
await _streamSubscription?.cancel(); await _streamSubscription?.cancel();

View File

@ -8,12 +8,19 @@ enum CommentsStatus {
failure, failure,
} }
enum CommentsOrder {
natural,
newestFirst,
oldestFirst,
}
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.onlyShowTargetComment, required this.onlyShowTargetComment,
required this.offlineReading, required this.offlineReading,
required this.currentPage, required this.currentPage,
@ -21,35 +28,39 @@ class CommentsState extends Equatable {
CommentsState.init({ CommentsState.init({
required this.offlineReading, required this.offlineReading,
required this.story, required this.item,
}) : comments = <Comment>[], }) : comments = <Comment>[],
status = CommentsStatus.init, status = CommentsStatus.init,
collapsed = false, fetchParentStatus = CommentsStatus.init,
order = CommentsOrder.natural,
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 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,
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,
onlyShowTargetComment: onlyShowTargetComment:
onlyShowTargetComment ?? this.onlyShowTargetComment, onlyShowTargetComment ?? this.onlyShowTargetComment,
offlineReading: offlineReading ?? this.offlineReading, offlineReading: offlineReading ?? this.offlineReading,
@ -59,10 +70,11 @@ class CommentsState extends Equatable {
@override @override
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
story, item,
comments, comments,
status, status,
collapsed, fetchParentStatus,
order,
onlyShowTargetComment, onlyShowTargetComment,
offlineReading, offlineReading,
currentPage, currentPage,

View File

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

View File

@ -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),
), ),
); );
} }

View File

@ -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,
]; ];

View File

@ -52,6 +52,8 @@ class PinCubit extends Cubit<PinState> {
_preferenceRepository.updatePinnedStoriesIds(state.pinnedStoriesIds); _preferenceRepository.updatePinnedStoriesIds(state.pinnedStoriesIds);
} }
void refresh() => init();
void _onStoryFetched(Story story) { void _onStoryFetched(Story story) {
emit(state.copyWith(pinnedStories: <Story>[...state.pinnedStories, story])); emit(state.copyWith(pinnedStories: <Story>[...state.pinnedStories, story]));
} }

View File

@ -31,6 +31,8 @@ class PreferenceCubit extends Cubit<PreferenceState> {
.then((bool value) => emit(state.copyWith(useReader: value))); .then((bool value) => emit(state.copyWith(useReader: value)));
_preferenceRepository.markReadStories _preferenceRepository.markReadStories
.then((bool value) => emit(state.copyWith(markReadStories: value))); .then((bool value) => emit(state.copyWith(markReadStories: value)));
_preferenceRepository.shouldShowMetadata
.then((bool value) => emit(state.copyWith(showMetadata: value)));
} }
void toggleNotificationMode() { void toggleNotificationMode() {
@ -67,4 +69,9 @@ class PreferenceCubit extends Cubit<PreferenceState> {
emit(state.copyWith(markReadStories: !state.markReadStories)); emit(state.copyWith(markReadStories: !state.markReadStories));
_preferenceRepository.toggleMarkReadStoriesMode(); _preferenceRepository.toggleMarkReadStoriesMode();
} }
void toggleMetadataMode() {
emit(state.copyWith(showMetadata: !state.showMetadata));
_preferenceRepository.toggleMetadataMode();
}
} }

View File

@ -9,6 +9,7 @@ class PreferenceState extends Equatable {
required this.useTrueDark, required this.useTrueDark,
required this.useReader, required this.useReader,
required this.markReadStories, required this.markReadStories,
required this.showMetadata,
}); });
const PreferenceState.init() const PreferenceState.init()
@ -18,7 +19,8 @@ class PreferenceState extends Equatable {
showEyeCandy = false, showEyeCandy = false,
useTrueDark = false, useTrueDark = false,
useReader = false, useReader = false,
markReadStories = false; markReadStories = false,
showMetadata = false;
final bool showNotification; final bool showNotification;
final bool showComplexStoryTile; final bool showComplexStoryTile;
@ -27,6 +29,7 @@ class PreferenceState extends Equatable {
final bool useTrueDark; final bool useTrueDark;
final bool useReader; final bool useReader;
final bool markReadStories; final bool markReadStories;
final bool showMetadata;
PreferenceState copyWith({ PreferenceState copyWith({
bool? showNotification, bool? showNotification,
@ -36,6 +39,7 @@ class PreferenceState extends Equatable {
bool? useTrueDark, bool? useTrueDark,
bool? useReader, bool? useReader,
bool? markReadStories, bool? markReadStories,
bool? showMetadata,
}) { }) {
return PreferenceState( return PreferenceState(
showNotification: showNotification ?? this.showNotification, showNotification: showNotification ?? this.showNotification,
@ -45,6 +49,7 @@ class PreferenceState extends Equatable {
useTrueDark: useTrueDark ?? this.useTrueDark, useTrueDark: useTrueDark ?? this.useTrueDark,
useReader: useReader ?? this.useReader, useReader: useReader ?? this.useReader,
markReadStories: markReadStories ?? this.markReadStories, markReadStories: markReadStories ?? this.markReadStories,
showMetadata: showMetadata ?? this.showMetadata,
); );
} }
@ -57,5 +62,6 @@ class PreferenceState extends Equatable {
useTrueDark, useTrueDark,
useReader, useReader,
markReadStories, markReadStories,
showMetadata,
]; ];
} }

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
@ -13,42 +15,94 @@ class SearchCubit extends Cubit<SearchState> {
final SearchRepository _searchRepository; final SearchRepository _searchRepository;
StreamSubscription<Story>? streamSubscription;
void search(String query) { void search(String query) {
streamSubscription?.cancel();
emit( emit(
state.copyWith( state.copyWith(
results: <Story>[], results: <Story>[],
currentPage: 0,
status: SearchStatus.loading, status: SearchStatus.loading,
query: query, searchFilters: state.searchFilters.copyWith(query: query, page: 0),
), ),
); );
_searchRepository.search(query).listen(_onStoryFetched).onDone(() { streamSubscription = _searchRepository
.search(filters: state.searchFilters)
.listen(_onStoryFetched)
..onDone(() {
emit(state.copyWith(status: SearchStatus.loaded)); emit(state.copyWith(status: SearchStatus.loaded));
}); });
} }
void loadMore() { void loadMore() {
final int updatedPage = state.currentPage + 1; if (state.status != SearchStatus.loading) {
final int updatedPage = state.searchFilters.page + 1;
emit( emit(
state.copyWith( state.copyWith(
status: SearchStatus.loadingMore, status: SearchStatus.loadingMore,
currentPage: updatedPage, searchFilters: state.searchFilters.copyWith(page: updatedPage),
), ),
); );
_searchRepository streamSubscription = _searchRepository
.search(state.query, page: updatedPage) .search(filters: state.searchFilters)
.listen(_onStoryFetched) .listen(_onStoryFetched)
.onDone(() { ..onDone(() {
emit(state.copyWith(status: SearchStatus.loaded)); emit(state.copyWith(status: SearchStatus.loaded));
}); });
} }
}
void addFilter<T extends SearchFilter>(T filter) {
if (state.searchFilters.contains<T>()) {
emit(
state.copyWith(
searchFilters: state.searchFilters.copyWithFilterRemoved<T>(),
),
);
}
emit(
state.copyWith(
searchFilters: state.searchFilters.copyWithFilterAdded(filter),
),
);
search(state.searchFilters.query);
}
void removeFilter<T extends SearchFilter>() {
emit(
state.copyWith(
searchFilters: state.searchFilters.copyWithFilterRemoved<T>(),
),
);
search(state.searchFilters.query);
}
void onSortToggled() {
emit(
state.copyWith(
searchFilters: state.searchFilters.copyWith(
sorted: !state.searchFilters.sorted,
),
),
);
search(state.searchFilters.query);
}
void _onStoryFetched(Story story) { void _onStoryFetched(Story story) {
emit( emit(
state.copyWith( state.copyWith(
results: List<Story>.from(state.results)..add(story), results: List<Story>.from(state.results)..add(story),
status: SearchStatus.loaded,
), ),
); );
} }
@override
Future<void> close() async {
await streamSubscription?.cancel();
await super.close();
}
} }

View File

@ -9,42 +9,36 @@ enum SearchStatus {
class SearchState extends Equatable { class SearchState extends Equatable {
const SearchState({ const SearchState({
required this.query,
required this.status, required this.status,
required this.results, required this.results,
required this.currentPage, required this.searchFilters,
}); });
SearchState.init() SearchState.init()
: query = '', : status = SearchStatus.initial,
status = SearchStatus.initial,
results = <Story>[], results = <Story>[],
currentPage = 0; searchFilters = SearchFilters.init();
final String query;
final List<Story> results; final List<Story> results;
final SearchStatus status; final SearchStatus status;
final int currentPage; final SearchFilters searchFilters;
SearchState copyWith({ SearchState copyWith({
String? query,
List<Story>? results, List<Story>? results,
SearchStatus? status, SearchStatus? status,
int? currentPage, SearchFilters? searchFilters,
}) { }) {
return SearchState( return SearchState(
query: query ?? this.query,
results: results ?? this.results, results: results ?? this.results,
status: status ?? this.status, status: status ?? this.status,
currentPage: currentPage ?? this.currentPage, searchFilters: searchFilters ?? this.searchFilters,
); );
} }
@override @override
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
query,
status, status,
results, results,
currentPage, searchFilters,
]; ];
} }

View File

@ -13,12 +13,14 @@ class SplitViewCubit extends Cubit<SplitViewState> {
final CacheService _cacheService; final CacheService _cacheService;
void updateStoryScreenArgs(StoryScreenArgs args) { void updateItemScreenArgs(ItemScreenArgs args) {
_cacheService.resetCollapsedComments(); _cacheService.resetCollapsedComments();
emit(state.copyWith(storyScreenArgs: args)); emit(state.copyWith(itemScreenArgs: args));
} }
void enableSplitView() => emit(state.copyWith(enabled: true)); void enableSplitView() => emit(state.copyWith(enabled: true));
void disableSplitView() => emit(state.copyWith(enabled: false)); void disableSplitView() => emit(state.copyWith(enabled: false));
void zoom() => emit(state.copyWith(expanded: !state.expanded));
} }

View File

@ -2,27 +2,36 @@ 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.enabled, required this.enabled,
}); });
const SplitViewState.init() const SplitViewState.init()
: enabled = false, : enabled = false,
storyScreenArgs = null; expanded = false,
itemScreenArgs = null;
final bool enabled; 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( return SplitViewState(
enabled: enabled ?? this.enabled, enabled: enabled ?? this.enabled,
storyScreenArgs: storyScreenArgs ?? this.storyScreenArgs, expanded: expanded ?? this.expanded,
itemScreenArgs: itemScreenArgs ?? this.itemScreenArgs,
); );
} }
@override @override
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
enabled, enabled,
storyScreenArgs, expanded,
itemScreenArgs,
]; ];
} }

View 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;
});
}
}

View 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,
];
}

View File

@ -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,
); );
} }

View File

@ -1,7 +1,15 @@
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);
} }
String removeAllEmojis() {
final RegExp regex = RegExp(
r'(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])',
);
return replaceAllMapped(regex, (_) => '');
}
} }

View File

@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
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';
@ -14,6 +15,7 @@ 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/fetcher.dart'; import 'package:hacki/services/fetcher.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:rxdart/rxdart.dart' show BehaviorSubject; import 'package:rxdart/rxdart.dart' show BehaviorSubject;
@ -24,6 +26,10 @@ import 'package:workmanager/workmanager.dart';
final BehaviorSubject<String?> selectNotificationSubject = final BehaviorSubject<String?> selectNotificationSubject =
BehaviorSubject<String?>(); BehaviorSubject<String?>();
// For receiving payload event from siri suggestions.
final BehaviorSubject<String?> siriSuggestionSubject =
BehaviorSubject<String?>();
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -57,6 +63,16 @@ 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);
},
);
} }
final Directory tempDir = await getTemporaryDirectory(); final Directory tempDir = await getTemporaryDirectory();
@ -164,26 +180,22 @@ class HackiApp extends StatelessWidget {
lazy: false, lazy: false,
create: (BuildContext context) => PostCubit(), create: (BuildContext context) => PostCubit(),
), ),
BlocProvider<EditCubit>(
lazy: false,
create: (BuildContext context) => EditCubit(),
),
], ],
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(),
@ -209,9 +221,6 @@ class HackiApp extends StatelessWidget {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: useTrueDark ? trueDarkTheme : theme, theme: useTrueDark ? trueDarkTheme : theme,
navigatorKey: navigatorKey, navigatorKey: navigatorKey,
navigatorObservers: <NavigatorObserver>[
locator.get<RouteObserver<ModalRoute<dynamic>>>(),
],
onGenerateRoute: CustomRouter.onGenerateRoute, onGenerateRoute: CustomRouter.onGenerateRoute,
initialRoute: HomeScreen.routeName, initialRoute: HomeScreen.routeName,
), ),

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

View File

@ -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,6 +53,7 @@ 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,
); );

View File

@ -1,4 +1,5 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:hacki/extensions/date_time_extension.dart';
abstract class Item extends Equatable { abstract class Item extends Equatable {
const Item({ const Item({
@ -54,6 +55,9 @@ abstract class Item extends Equatable {
final List<int> kids; final List<int> kids;
final List<int> parts; final List<int> parts;
String get postedDate =>
DateTime.fromMillisecondsSinceEpoch(time * 1000).toReadableString();
bool get isPoll => type == 'poll'; bool get isPoll => type == 'poll';
bool get isStory => type == 'story'; bool get isStory => type == 'story';

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