Compare commits

...

119 Commits

Author SHA1 Message Date
90ec117b4b bump version. (#377) 2024-02-18 19:41:38 -08:00
9580e9b3e5 fix date time display. (#376) 2024-02-18 18:51:51 -08:00
0afaa5a0aa add date time display customization. (#375) 2024-02-17 02:04:00 -08:00
fbce1dff73 revert. (#374) 2024-02-13 16:52:14 -08:00
b0d6561486 update publish_ios.yml (#372) 2024-02-13 00:47:16 -08:00
11639118c5 update publish_ios.yml (#371) 2024-02-13 00:21:13 -08:00
e54c893e6c update publish_ios.yml (#370) 2024-02-13 00:07:31 -08:00
8c57e5e323 update publish_ios action. (#369) 2024-02-12 23:41:00 -08:00
b4ec7ec44e update publish_ios action. (#368) 2024-02-12 23:03:35 -08:00
8b65256294 update deploy target. (#367) 2024-02-12 22:30:23 -08:00
58f7bf14d7 bump fastlane version. (#366) 2024-02-12 21:55:58 -08:00
d9aad3d34e bump flutter version. (#365) 2024-02-12 20:54:33 -08:00
ed48d95375 fix comments repo. (#364) 2024-01-03 14:31:03 -08:00
1eaded5694 fix uncaught error. (#359) 2023-12-11 01:01:26 -08:00
70bb78afcb use InterceptorsWrapper for caching. (#358) 2023-12-10 15:29:40 -08:00
df2d2478d5 improve comment fetching. (#357) 2023-12-09 18:20:28 -08:00
d5ae60327d change fetch method based on network condition. (#356) 2023-12-09 10:03:22 -08:00
615a092c1e update fetching strategy. (#355) 2023-12-09 02:05:03 -08:00
5a7699d866 update hacker_news_web_repository.dart (#354) 2023-12-09 00:00:20 -08:00
56a9bab3f2 improve error handling. (#353) 2023-12-08 22:37:57 -08:00
e9bbf46b4f fix comment fetching. (#352) 2023-12-08 21:25:02 -08:00
10f503a6c0 add cache for story metadata. (#350) 2023-12-08 20:09:05 -08:00
582f3156b2 add dev option. (#349) 2023-12-08 17:21:30 -08:00
90fb45146f fix story repository. (#348) 2023-12-08 14:25:32 -08:00
c19c54e762 optimize comment fetching. (#347) 2023-12-08 13:35:52 -08:00
70e5a84b63 improve comment fetching. (#346) 2023-12-08 10:18:03 -08:00
3a51fa83f2 update story tile padding. (#344) 2023-12-08 01:12:49 -08:00
cb90751330 fix flickering image. (#343) 2023-12-07 23:24:43 -08:00
835ed7e841 use different comment fetching strategy. (#342) 2023-12-07 21:46:13 -08:00
125ccd2dd1 use isolate to fetch comments. (#341) 2023-12-05 21:04:40 -08:00
5b991c4287 update theme. (#340) 2023-12-03 17:30:34 -08:00
7dc3618afe update color. (#339) 2023-12-02 23:31:45 -08:00
eef4691814 update Info.plist (#338) 2023-12-02 20:58:39 -08:00
9f71701845 update story tile. (#336) 2023-12-02 04:46:06 -08:00
d27203b041 update Info.plist (#335) 2023-12-02 04:21:58 -08:00
4f280ec4c9 add ability to sync favorites from Hacker News. (#334) 2023-12-01 21:53:48 -08:00
72cb2737ca fix story tile. (#333) 2023-12-01 12:09:14 -08:00
215203bd16 remove error placeholder. (#332) 2023-12-01 11:27:16 -08:00
3e320faece update story title. (#331) 2023-12-01 09:56:19 -08:00
1049568246 bump Flutter version to 3.16.2 (#330) 2023-12-01 01:11:30 -08:00
71aa42118d fix web analyzer (#327) 2023-11-26 09:43:23 +09:00
4f21d3e6bd update pubspec.yaml (#325) 2023-11-15 10:50:00 -08:00
96d0fe9e5e fix new comment indicator. (#324) 2023-11-15 01:15:10 -08:00
69eee3e278 fix url rendering. (#323) 2023-11-14 23:52:05 -08:00
36bcd996c0 bump Flutter version to 3.13.9 (#322) 2023-11-14 23:22:09 -08:00
5fc39d8b8b fix code block formatting. (#321) 2023-11-14 20:25:42 -08:00
5dce7787e1 improve text rendering performance. (#320) 2023-11-14 17:14:06 -08:00
8888dde792 allow marking stories as read from homepage. (#319) 2023-11-14 14:35:27 -08:00
6c8fc4cf87 fix response indicator when lazy fetching is enabled. (#317) 2023-11-13 21:10:47 -08:00
ae9cc109db revert "improve caching strategy. (#312)" (#316) 2023-11-13 19:42:20 -08:00
c8976ed17b improve caching strategy. (#312) 2023-11-11 00:31:09 -08:00
ff7e115418 fix manual pagination button. (#310) 2023-11-06 22:46:44 -08:00
0310507c96 revert html util change. (#309) 2023-11-06 19:40:53 -08:00
58c646e232 update html_util.dart (#308) 2023-11-06 17:10:10 -08:00
08328e2ca1 update url_linkifier.dart (#307) 2023-11-06 14:19:25 -08:00
86b7228ffd improve response indicator. (#306) 2023-11-06 12:45:46 -08:00
e103c88ca6 fix favorites export. (#305) 2023-11-05 22:47:45 -08:00
94323a04e0 fix response indicator. (#304) 2023-11-05 21:22:02 -08:00
4776c375a1 UX improvements on HN and in-thread search. (#303) 2023-11-05 19:48:01 -08:00
1f4e6cf41c fix pagination button. (#298) 2023-11-02 21:50:09 -07:00
be6ed35888 update version. (#297) 2023-11-02 21:09:55 -07:00
b2ea50cea6 add pagination. (#296) 2023-11-02 20:22:51 -07:00
109b9287cf fix offline webview. (#295) 2023-11-02 17:17:46 -07:00
939d55ef0d fix in-thread search. (#294) 2023-11-02 14:51:46 -07:00
3ee60e1a44 improve in-thread search UX. (#293) 2023-11-02 14:34:24 -07:00
6fe567fa02 update design of about dialog. (#292) 2023-11-02 13:42:33 -07:00
bc2d4f32c9 show index on comment tile. (#291) 2023-11-02 13:11:10 -07:00
91290e9743 update README.md (#290) 2023-11-02 12:28:09 -07:00
934f184b6f fix material 3 colors. (#289) 2023-11-02 12:04:43 -07:00
dbd48eae99 fix reply box. (#288) 2023-11-01 23:00:00 -07:00
279007191b update feature description. (#287) 2023-11-01 22:17:57 -07:00
b3fdc20fc5 add ability to use material 3. (#286) 2023-11-01 19:48:09 -07:00
3fbf5d4eea improve shortcut button. (#284) 2023-10-22 20:34:09 -07:00
332ffbb773 bump version. (#282) 2023-10-22 00:14:12 -07:00
346a6c709e fix inconsistent font size. (#281) 2023-10-21 23:50:06 -07:00
d4fe042245 fix border color of comment tile. (#280) 2023-10-21 21:25:29 -07:00
b82c4a1777 update changelogs. (#279) 2023-10-21 20:45:24 -07:00
7e0d1f0f1d add ability to use custom tabs. (#278) 2023-10-21 20:25:45 -07:00
f405a10c2e fix color of quote element. (#277) 2023-10-21 19:43:06 -07:00
edbad79cd3 add ability to customize text scale factor and improve keyword filter. (#276) 2023-10-21 18:50:51 -07:00
c9d8b2950a add ability to change app's primary color. (#275) 2023-10-21 01:02:44 -07:00
f2bc48f980 update project.pbxproj file. (#271) 2023-09-29 19:30:56 -07:00
d56697c57c add ability to render code block inside comment text. (#266) 2023-09-29 18:50:40 -07:00
320ec41aae update url linkifier. (#270) 2023-09-29 16:21:41 -07:00
d85b3535d5 update url linkifier. 2023-09-29 16:15:06 -07:00
f8cd1cbba0 update url_linkifier.dart (#269) 2023-09-29 14:56:11 -07:00
817ec208d6 fix url parser. (#268) 2023-09-29 12:34:20 -07:00
554a165789 fix selectable text. (#267) 2023-09-28 23:46:24 -07:00
0c680370ef add ability to long press on story title to copy link. (#265) 2023-09-28 14:10:08 -07:00
59541d2fcc update Fastfile (#264) 2023-09-28 01:48:20 -07:00
32083c3564 update fastlane. (#263) 2023-09-28 00:05:16 -07:00
258dbc4b8b fix url parsing. (#262) 2023-09-27 23:17:31 -07:00
6c8047ebac feature discovery cleanup. (#259) 2023-09-19 00:16:27 -07:00
00a0135867 fix draft saving. (#258) 2023-09-18 22:49:11 -07:00
1db7be7a2c fix draft saving. (#257) 2023-09-18 22:16:47 -07:00
ff400f9c40 fix reply view. (#256) 2023-09-18 20:31:44 -07:00
f03b45a98a update pubspec.lock (#255) 2023-09-17 17:59:29 -07:00
cbe5bba986 bump flutter version. (#254) 2023-09-17 17:38:44 -07:00
268f4054a3 improve story marking. (#253) 2023-09-11 20:42:33 -07:00
988c5d9881 add haptic feedback. (#252) 2023-09-11 18:08:21 -07:00
e748e2f818 allow swipe gesture in fav screen. (#251) 2023-09-11 17:01:42 -07:00
1b0a0dbda9 add changelog. (#250) 2023-09-11 15:22:32 -07:00
64d68389ba migrate from Navigator to GoRouter (#249) 2023-09-10 22:26:46 -07:00
381c99b353 fix crashing. (#248) 2023-09-08 09:07:48 -07:00
39ee3137f8 fix reply box in full screen. (#247) 2023-09-05 15:17:23 -07:00
0d76be8634 bump flutter version. (#243) 2023-08-22 06:54:59 -07:00
9986f72e11 improve shortcut buttons. (#242) 2023-07-19 21:09:24 -07:00
ef557e7b84 fix text scaling and url parsing. (#237) 2023-07-10 10:18:12 -07:00
ec065c0122 fix QR code view. (#231) 2023-06-22 19:00:19 -07:00
2960c6e59e add ability to import favorites using QR code. (#230) 2023-06-22 18:14:09 -07:00
92dac6b932 update device_gesture_wrapper.dart (#227) 2023-06-06 18:42:29 -07:00
20365393a3 fix capitalization. (#226) 2023-06-05 21:46:01 -07:00
8d238744c7 add option to disable auto-scroll. (#225) 2023-06-05 18:23:29 -07:00
e33ff417fb update project.pbxproj (#224) 2023-06-04 21:31:14 -07:00
d8922c2641 prevent over scrolling after collapsing a comment. (#223) 2023-06-04 19:16:01 -07:00
c6e0461857 improve date time range picker in search screen and add monochrome icons. (#219) 2023-05-26 21:33:43 -08:00
30ca356dc8 add date filter shortcuts. (#218) 2023-05-26 13:44:50 -08:00
7d11398e6d fix comment tile. (#215) 2023-05-19 12:37:21 -07:00
a4f52284ef bump flutter version. (#214) 2023-05-18 17:00:47 -07:00
237 changed files with 6668 additions and 3646 deletions

View File

@ -9,13 +9,14 @@ on:
jobs: jobs:
releases: releases:
name: Check commit name: Check commit
runs-on: ubuntu-latest runs-on: macos-latest
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- name: checkout all the submodules - name: checkout all the submodules
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
submodules: recursive submodules: recursive
fetch-depth: 0
- run: submodules/flutter/bin/flutter doctor - run: submodules/flutter/bin/flutter doctor
- run: submodules/flutter/bin/flutter pub get - run: submodules/flutter/bin/flutter pub get
- run: submodules/flutter/bin/dart format --set-exit-if-changed lib test integration_test - run: submodules/flutter/bin/dart format --set-exit-if-changed lib test integration_test

View File

@ -10,7 +10,7 @@ on:
jobs: jobs:
build_and_publish: build_and_publish:
runs-on: macos-latest runs-on: macos-13
timeout-minutes: 30 timeout-minutes: 30
env: env:
@ -19,10 +19,16 @@ jobs:
BUNDLE_GEMFILE: ${{ github.workspace }}/ios/Gemfile BUNDLE_GEMFILE: ${{ github.workspace }}/ios/Gemfile
steps: steps:
- name: Set XCode version
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.0'
- name: Check out from git - name: Check out from git
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
submodules: recursive submodules: recursive
fetch-depth: 0
- run: submodules/flutter/bin/flutter doctor - run: submodules/flutter/bin/flutter doctor
- run: submodules/flutter/bin/flutter pub get - run: submodules/flutter/bin/flutter pub get
- run: submodules/flutter/bin/dart format --set-exit-if-changed lib test integration_test - run: submodules/flutter/bin/dart format --set-exit-if-changed lib test integration_test

View File

@ -1,7 +1,7 @@
# <img width="64" src="https://user-images.githubusercontent.com/7277662/167775086-0b234f28-dee4-44f6-aae4-14a28ed4bbb6.png"> Hacki for Hacker News # <img width="64" src="https://user-images.githubusercontent.com/7277662/167775086-0b234f28-dee4-44f6-aae4-14a28ed4bbb6.png"> Hacki for Hacker News
A [Hacker News](https://news.ycombinator.com/) client made with Flutter that is just enough. A [Hacker News](https://news.ycombinator.com/) client built with Flutter.
[![App Store](https://img.shields.io/itunes/v/1602043763?label=App%20Store)](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone) [![App Store](https://img.shields.io/itunes/v/1602043763?label=App%20Store)](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone)
[![Fdroid version](https://img.shields.io/f-droid/v/com.jiaqifeng.hacki)](https://f-droid.org/en/packages/com.jiaqifeng.hacki/) [![Fdroid version](https://img.shields.io/f-droid/v/com.jiaqifeng.hacki)](https://f-droid.org/en/packages/com.jiaqifeng.hacki/)
@ -29,27 +29,26 @@ Features:
- Download stories and comments for offline reading. - Download stories and comments for offline reading.
- Pick up where you left off. - Pick up where you left off.
- Synced favorites and pins across devices. (iOS only) - Synced favorites and pins across devices. (iOS only)
- Export or import your favorites.
- Launch from system share sheet. - Launch from system share sheet.
- And more... - And more...
<p align="center"> <p align="center">
<img width="200" alt="01" src="assets/screenshots/01.png"> <img width="400" alt="01" src="assets/screenshots/light-1.png">
<img width="200" alt="02" src="assets/screenshots/02.png"> <img width="400" alt="06" src="assets/screenshots/dark-1.png">
<img width="200" alt="03" src="assets/screenshots/03.png"> <img width="400" alt="02" src="assets/screenshots/light-2.png">
<img width="200" alt="04" src="assets/screenshots/04.png"> <img width="400" alt="07" src="assets/screenshots/dark-2.png">
<img width="200" alt="05" src="assets/screenshots/05.png"> <img width="400" alt="03" src="assets/screenshots/light-3.png">
<img width="200" alt="06" src="assets/screenshots/06.png"> <img width="400" alt="08" src="assets/screenshots/dark-3.png">
<img width="200" alt="07" src="assets/screenshots/07.png"> <img width="400" alt="04" src="assets/screenshots/light-4.png">
<img width="200" alt="08" src="assets/screenshots/08.png"> <img width="400" alt="09" src="assets/screenshots/dark-4.png">
<img width="200" alt="09" src="assets/screenshots/09.png"> <img width="400" alt="05" src="assets/screenshots/light-5.png">
<img width="200" alt="10" src="assets/screenshots/10.png"> <img width="400" alt="10" src="assets/screenshots/dark-5.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="ipad-01" src="assets/screenshots/ipad-01.png"> <img width="400" alt="ipad-01" src="assets/screenshots/tablet-light-1.png">
<img width="400" alt="ipad-02" src="assets/screenshots/ipad-02.png"> <img width="400" alt="ipad-02" src="assets/screenshots/tablet-dark-1.png">
<img width="400" alt="ipad-03" src="assets/screenshots/ipad-03.png"> <img width="400" alt="ipad-03" src="assets/screenshots/tablet-light-2.png">
<img width="400" alt="ipad-04" src="assets/screenshots/ipad-04.png"> <img width="400" alt="ipad-04" src="assets/screenshots/tablet-dark-2.png">
</p> </p>

View File

@ -1,4 +1,4 @@
include: package:very_good_analysis/analysis_options.3.1.0.yaml include: package:very_good_analysis/analysis_options.5.0.0.yaml
linter: linter:
rules: rules:
parameter_assignments: false parameter_assignments: false

View File

@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
android { android {
compileSdkVersion 33 compileSdkVersion 34
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -50,8 +50,8 @@ android {
defaultConfig { defaultConfig {
applicationId "com.jiaqifeng.hacki" applicationId "com.jiaqifeng.hacki"
minSdkVersion 26 minSdkVersion 25
targetSdkVersion 33 targetSdkVersion 34
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }

View File

@ -13,6 +13,9 @@
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" /> <data android:mimeType="*/*" />
</intent> </intent>
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries> </queries>
<application <application
@ -20,7 +23,8 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:allowBackup="true" android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true"
android:enableOnBackInvokedCallback="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:launchMode="singleTop" android:launchMode="singleTop"

View File

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_adaptive_back"/> <background android:drawable="@mipmap/ic_launcher_adaptive_back"/>
<foreground android:drawable="@mipmap/ic_launcher_adaptive_fore"/> <foreground android:drawable="@mipmap/ic_launcher_adaptive_fore"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon> </adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -24,6 +24,6 @@ subprojects {
project.evaluationDependsOn(':app') project.evaluationDependsOn(':app')
} }
task clean(type: Delete) { tasks.register("clean", Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }

Binary file not shown.

Binary file not shown.

BIN
assets/hacki-github.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 KiB

BIN
assets/hacki-github.xcf Normal file

Binary file not shown.

BIN
assets/hacki.xcf Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 548 KiB

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 571 KiB

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 592 KiB

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
assets/tablet-hacki.xcf Normal file

Binary file not shown.

View File

@ -1,108 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_review/in_app_review.dart';
import 'package:in_app_review_platform_interface/in_app_review_platform_interface.dart';
import 'package:mockito/mockito.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final inAppReview = InAppReview.instance;
late MockInAppReviewPlatform platform;
setUp(() {
platform = MockInAppReviewPlatform();
InAppReviewPlatform.instance = platform;
});
tearDown(() {
verifyNoMoreInteractions(platform);
});
group('isAvailable', () {
test(
'should call InAppReviewPlatform.isAvailable()',
() async {
// ARRANGE
when(platform.isAvailable()).thenAnswer((_) async => true);
// ACT
final result = await inAppReview.isAvailable();
// ASSERT
verify(platform.isAvailable());
expect(result, isTrue);
},
);
});
group('requestReview', () {
test(
'should call InAppReviewPlatform.requestReview()',
() async {
// ARRANGE
when(platform.requestReview()).thenAnswer((_) async {});
// ACT
await inAppReview.requestReview();
// ASSERT
verify(platform.requestReview());
},
);
});
group('openStoreListing', () {
test(
'should call InAppReviewPlatform.openStoreListing()',
() async {
// ARRANGE
const appStoreId = 'app_store_id';
const microsoftStoreId = 'microsoft_store_id';
when(platform.openStoreListing(
appStoreId: appStoreId,
microsoftStoreId: microsoftStoreId,
)).thenAnswer((_) async {});
// ACT
await inAppReview.openStoreListing(
appStoreId: appStoreId,
microsoftStoreId: microsoftStoreId,
);
// ASSERT
verify(platform.openStoreListing(
appStoreId: appStoreId,
microsoftStoreId: microsoftStoreId,
));
},
);
});
}
class MockInAppReviewPlatform extends Mock
with MockPlatformInterfaceMixin
implements InAppReviewPlatform {
@override
Future<bool> isAvailable() => super.noSuchMethod(
Invocation.method(#isAvailable, null),
returnValue: Future.value(true),
);
@override
Future<void> requestReview() => super.noSuchMethod(
Invocation.method(#requestReview, null),
returnValue: Future<void>.value(),
);
@override
Future<void> openStoreListing({
String? appStoreId,
String? microsoftStoreId,
}) =>
super.noSuchMethod(
Invocation.method(
#openStoreListing,
null,
{#appStoreId: appStoreId, #microsoftStoreId: microsoftStoreId},
),
returnValue: Future<void>.value(),
);
}

View File

@ -1,136 +0,0 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_review_platform_interface/method_channel_in_app_review.dart';
import 'package:platform/platform.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late MethodChannelInAppReview methodChannelInAppReview;
late List<MethodCall> log = <MethodCall>[];
const MethodChannel channel = MethodChannel('dev.britannio.in_app_review');
setUp(() {
methodChannelInAppReview = MethodChannelInAppReview();
methodChannelInAppReview.channel = channel;
log = <MethodCall>[];
});
tearDown(() {
log.clear();
});
channel.setMockMethodCallHandler((MethodCall call) async {
log.add(call);
switch (call.method) {
case 'isAvailable':
return true;
case 'requestReview':
case 'openStoreListing':
return null;
default:
assert(false);
return null;
}
});
group('isAvailable', () {
test(
'should invoke the isAvailable method channel',
() async {
// ACT
final bool result = await methodChannelInAppReview.isAvailable();
// ASSERT
expect(log, <Matcher>[isMethodCall('isAvailable', arguments: null)]);
expect(result, isTrue);
},
);
});
group('requestReview', () {
test(
'should invoke the requestReview method channel',
() async {
// ACT
await methodChannelInAppReview.requestReview();
// ASSERT
expect(log, <Matcher>[isMethodCall('requestReview', arguments: null)]);
},
);
});
group('openStoreListing', () {
test(
'should invoke the openStoreListing method channel on Android',
() async {
// ARRANGE
methodChannelInAppReview.platform =
FakePlatform(operatingSystem: 'android');
// ACT
await methodChannelInAppReview.openStoreListing();
// ASSERT
expect(
log,
<Matcher>[isMethodCall('openStoreListing', arguments: null)],
);
},
);
test(
'should invoke the openStoreListing method channel on iOS',
() async {
// ARRANGE
methodChannelInAppReview.platform =
FakePlatform(operatingSystem: 'ios');
final String appStoreId = "store_id";
// ACT
await methodChannelInAppReview.openStoreListing(appStoreId: appStoreId);
// ASSERT
expect(log,
<Matcher>[isMethodCall('openStoreListing', arguments: appStoreId)]);
},
);
test(
'should invoke the openStoreListing method channel on MacOS',
() async {
// ARRANGE
methodChannelInAppReview.platform =
FakePlatform(operatingSystem: 'macos');
final String appStoreId = "store_id";
// ACT
await methodChannelInAppReview.openStoreListing(appStoreId: appStoreId);
// ASSERT
expect(log,
<Matcher>[isMethodCall('openStoreListing', arguments: appStoreId)]);
},
);
test(
'should invoke the openStoreListing method channel on Windows',
() async {
// ARRANGE
methodChannelInAppReview.platform =
FakePlatform(operatingSystem: 'windows');
final String microsoftStoreId = 'store_id';
// ACT
await methodChannelInAppReview.openStoreListing(
microsoftStoreId: microsoftStoreId,
);
// ASSERT
expect(log, <Matcher>[
isMethodCall('openStoreListing', arguments: microsoftStoreId)
]);
},
skip:
'The windows uwp implementation still uses the url_launcher package',
);
});
}

View File

@ -76,6 +76,15 @@ final class SharedPrefsCore {
return true return true
} }
fileprivate func remove(key: String?) -> Bool{
if let key = key {
let keyStore = NSUbiquitousKeyValueStore()
keyStore.removeObject(forKey: key)
}
return true
}
} }
public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin { public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
@ -87,6 +96,14 @@ public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method { switch call.method {
case "remove":
if let params = call.arguments as? [String: Any] {
let key = params[keyKey] as? String
let res = SharedPrefsCore.shared.remove(key: key)
result(res)
}
case "setBool": case "setBool":
if let params = call.arguments as? [String: Any] { if let params = call.arguments as? [String: Any] {
let val = params[valKey] as? Bool let val = params[valKey] as? Bool

View File

@ -15,6 +15,14 @@ class SyncedSharedPreferences {
const MethodChannel(channel), const MethodChannel(channel),
); );
Future<bool?> remove({
required String key,
}) async {
return _channel.invokeMethod('remove', <String, dynamic>{
'key': key,
});
}
Future<bool?> setBool({ Future<bool?> setBool({
required String key, required String key,
required bool val, required bool val,

View File

@ -0,0 +1 @@
- Ability to mark a story as read once scrolling past.

View File

@ -0,0 +1,2 @@
- Ability to customize text scale factor.
- Ability to customize app's accent color.

View File

@ -0,0 +1,4 @@
- Ability to use Material 3.
- Ability to search in thread.
- Ability to customize text scale factor.
- Ability to customize app's accent color.

View File

@ -0,0 +1,5 @@
- Ability to use pagination on home screen.
- Ability to use Material 3 (experimental).
- Ability to search in thread.
- Ability to customize text scale factor.
- Ability to customize app's accent color.

View File

@ -0,0 +1,5 @@
- Ability to use manual pagination on home screen.
- Ability to use Material 3 (experimental).
- Ability to search in thread.
- Ability to customize text scale factor.
- Ability to customize app's accent color.

View File

@ -0,0 +1,4 @@
- New comment indicator.
- Ability to mark stories as read from home page.
- Text rendering improvements.
- Performance improvements.

View File

@ -0,0 +1,4 @@
- New comment indicator.
- Ability to mark stories as read from home page.
- Text rendering improvements.
- Performance improvements.

View File

@ -0,0 +1,4 @@
- RobotoSlab as default font.
- Material 3 design.
- Ability to sync favorites from your Hacker News account.
- Support for predictive back gesture.

View File

@ -0,0 +1,3 @@
- Return of true dark mode.
- Better comment fetching strategy.
- Minor UI fixes.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 835 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 820 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -21,6 +21,6 @@
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key> <key>MinimumOSVersion</key>
<string>11.0</string> <string>12.0</string>
</dict> </dict>
</plist> </plist>

View File

@ -1,7 +1,7 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
CFPropertyList (3.0.5) CFPropertyList (3.0.6)
rexml rexml
activesupport (6.1.7) activesupport (6.1.7)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
@ -9,28 +9,28 @@ GEM
minitest (>= 5.1) minitest (>= 5.1)
tzinfo (~> 2.0) tzinfo (~> 2.0)
zeitwerk (~> 2.3) zeitwerk (~> 2.3)
addressable (2.8.1) addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
algoliasearch (1.27.5) algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3) httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1) json (>= 1.5.1)
artifactory (3.0.15) artifactory (3.0.15)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.2.0) aws-eventstream (1.3.0)
aws-partitions (1.680.0) aws-partitions (1.889.0)
aws-sdk-core (3.168.4) aws-sdk-core (3.191.1)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.61.0) aws-sdk-kms (1.77.0)
aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.117.2) aws-sdk-s3 (1.143.0)
aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4) aws-sigv4 (~> 1.8)
aws-sigv4 (1.5.2) aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4) babosa (1.0.4)
claide (1.1.0) claide (1.1.0)
@ -77,7 +77,7 @@ GEM
highline (~> 2.0.0) highline (~> 2.0.0)
concurrent-ruby (1.1.10) concurrent-ruby (1.1.10)
declarative (0.0.20) declarative (0.0.20)
digest-crc (0.6.4) digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701) domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
@ -86,8 +86,8 @@ GEM
escape (0.0.4) escape (0.0.4)
ethon (0.15.0) ethon (0.15.0)
ffi (>= 1.15.0) ffi (>= 1.15.0)
excon (0.95.0) excon (0.109.0)
faraday (1.10.2) faraday (1.10.3)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0) faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1) faraday-excon (~> 1.1)
@ -115,8 +115,8 @@ GEM
faraday-retry (1.0.3) faraday-retry (1.0.3)
faraday_middleware (1.2.0) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.2.6) fastimage (2.3.0)
fastlane (2.211.0) fastlane (2.219.0)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0) addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0) artifactory (~> 3.0)
@ -135,20 +135,22 @@ GEM
gh_inspector (>= 1.1.2, < 2.0.0) gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3) google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1) google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31) google-cloud-storage (~> 1.31)
highline (~> 2.0) highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0) json (< 3.0.0)
jwt (>= 2.1.0, < 3) jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0) mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (~> 2.0.0) multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2) naturally (~> 2.2)
optparse (~> 0.1.1) optparse (>= 0.1.1)
plist (>= 3.1.0, < 4.0.0) plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0) rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3) security (= 0.1.3)
simctl (~> 1.6.3) simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0) terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0) terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0) tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0) word_wrap (~> 1.0.0)
@ -159,9 +161,9 @@ GEM
fourflusher (2.3.1) fourflusher (2.3.1)
fuzzy_match (2.0.4) fuzzy_match (2.0.4)
gh_inspector (1.1.3) gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.32.0) google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.9.1, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.9.2) google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a) httpclient (>= 2.8.1, < 3.a)
@ -169,31 +171,29 @@ GEM
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.a)
rexml rexml
webrick google-apis-iamcredentials_v1 (0.17.0)
google-apis-iamcredentials_v1 (0.16.0) google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (>= 0.9.1, < 2.a) google-apis-playcustomapp_v1 (0.13.0)
google-apis-playcustomapp_v1 (0.12.0) google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (>= 0.9.1, < 2.a) google-apis-storage_v1 (0.29.0)
google-apis-storage_v1 (0.19.0) google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (>= 0.9.0, < 2.a) google-cloud-core (1.6.1)
google-cloud-core (1.6.0) google-cloud-env (>= 1.0, < 3.a)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0) google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0) faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.0) google-cloud-errors (1.3.1)
google-cloud-storage (1.44.0) google-cloud-storage (1.45.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1) google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.19.0) google-apis-storage_v1 (~> 0.29.0)
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (1.3.0) googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a) faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11) multi_json (~> 1.11)
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
@ -204,49 +204,48 @@ GEM
i18n (1.12.0) i18n (1.12.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
jmespath (1.6.2) jmespath (1.6.2)
json (2.6.3) json (2.7.1)
jwt (2.5.0) jwt (2.7.1)
memoist (0.16.2)
mini_magick (4.12.0) mini_magick (4.12.0)
mini_mime (1.1.2) mini_mime (1.1.5)
minitest (5.16.3) minitest (5.16.3)
molinillo (0.8.0) molinillo (0.8.0)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.0.0) multipart-post (2.4.0)
nanaimo (0.3.0) nanaimo (0.3.0)
nap (1.1.0) nap (1.1.0)
naturally (2.2.1) naturally (2.2.1)
netrc (0.11.0) netrc (0.11.0)
optparse (0.1.1) optparse (0.4.0)
os (1.1.4) os (1.1.4)
plist (3.6.0) plist (3.7.1)
public_suffix (4.0.7) public_suffix (4.0.7)
rake (13.0.6) rake (13.1.0)
representable (3.2.0) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
retriable (3.1.2) retriable (3.1.2)
rexml (3.2.5) rexml (3.2.6)
rouge (2.0.7) rouge (2.0.7)
ruby-macho (2.5.1) ruby-macho (2.5.1)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.3.2) rubyzip (2.3.2)
security (0.1.3) security (0.1.3)
signet (0.17.0) signet (0.18.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a) faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
multi_json (~> 1.10) multi_json (~> 1.10)
simctl (1.6.8) simctl (1.6.10)
CFPropertyList CFPropertyList
naturally naturally
terminal-notifier (2.0.0) terminal-notifier (2.0.0)
terminal-table (1.8.0) terminal-table (3.0.2)
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
tty-cursor (0.7.1) tty-cursor (0.7.1)
tty-screen (0.8.1) tty-screen (0.8.2)
tty-spinner (0.9.3) tty-spinner (0.9.3)
tty-cursor (~> 0.7) tty-cursor (~> 0.7)
typhoeus (1.4.0) typhoeus (1.4.0)
@ -256,11 +255,10 @@ GEM
uber (0.1.0) uber (0.1.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.8.2) unf_ext (0.0.9.1)
unicode-display_width (1.8.0) unicode-display_width (2.5.0)
webrick (1.7.0)
word_wrap (1.0.0) word_wrap (1.0.0)
xcodeproj (1.22.0) xcodeproj (1.24.0)
CFPropertyList (>= 2.3.3, < 4.0) CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3) atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0) claide (>= 1.0.2, < 2.0)
@ -275,6 +273,7 @@ GEM
PLATFORMS PLATFORMS
universal-darwin-21 universal-darwin-21
universal-darwin-23
x86_64-darwin-19 x86_64-darwin-19
DEPENDENCIES DEPENDENCIES

View File

@ -34,5 +34,8 @@ end
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target) flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = 15.0
end
end end
end end

View File

@ -7,11 +7,11 @@ PODS:
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_email_sender (0.0.1): - flutter_email_sender (0.0.1):
- Flutter - Flutter
- flutter_inappwebview (0.0.1): - flutter_inappwebview_ios (0.0.1):
- Flutter - Flutter
- flutter_inappwebview/Core (= 0.0.1) - flutter_inappwebview_ios/Core (= 0.0.1)
- OrderedSet (~> 5.0) - OrderedSet (~> 5.0)
- flutter_inappwebview/Core (0.0.1): - flutter_inappwebview_ios/Core (0.0.1):
- Flutter - Flutter
- OrderedSet (~> 5.0) - OrderedSet (~> 5.0)
- flutter_local_notifications (0.0.1): - flutter_local_notifications (0.0.1):
@ -20,30 +20,31 @@ PODS:
- Flutter - Flutter
- flutter_siri_suggestions (0.0.1): - flutter_siri_suggestions (0.0.1):
- Flutter - Flutter
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- in_app_review (0.2.0): - in_app_review (0.2.0):
- Flutter - Flutter
- integration_test (0.0.1): - integration_test (0.0.1):
- Flutter - Flutter
- MTBBarcodeScanner (5.0.11)
- OrderedSet (5.0.0) - OrderedSet (5.0.0)
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- qr_code_scanner (0.2.0):
- Flutter
- MTBBarcodeScanner
- ReachabilitySwift (5.0.0) - ReachabilitySwift (5.0.0)
- receive_sharing_intent (0.0.1): - receive_sharing_intent (1.5.3):
- Flutter - Flutter
- share_plus (0.0.1): - share_plus (0.0.1):
- Flutter - Flutter
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqflite (0.0.2): - sqflite (0.0.3):
- Flutter - Flutter
- FMDB (>= 2.7.5) - FlutterMacOS
- synced_shared_preferences (0.0.1): - synced_shared_preferences (0.0.1):
- Flutter - Flutter
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
@ -60,18 +61,19 @@ DEPENDENCIES:
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`) - flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`)
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/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`) - flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`)
- in_app_review (from `.symlinks/plugins/in_app_review/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/ios`) - sqflite (from `.symlinks/plugins/sqflite/darwin`)
- 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`)
- wakelock (from `.symlinks/plugins/wakelock/ios`) - wakelock (from `.symlinks/plugins/wakelock/ios`)
@ -80,7 +82,7 @@ DEPENDENCIES:
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- FMDB - MTBBarcodeScanner
- OrderedSet - OrderedSet
- ReachabilitySwift - ReachabilitySwift
@ -93,8 +95,8 @@ EXTERNAL SOURCES:
:path: Flutter :path: Flutter
flutter_email_sender: flutter_email_sender:
:path: ".symlinks/plugins/flutter_email_sender/ios" :path: ".symlinks/plugins/flutter_email_sender/ios"
flutter_inappwebview: flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview/ios" :path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_local_notifications: flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios" :path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_secure_storage: flutter_secure_storage:
@ -108,15 +110,17 @@ EXTERNAL SOURCES:
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation: path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/ios" :path: ".symlinks/plugins/path_provider_foundation/darwin"
qr_code_scanner:
:path: ".symlinks/plugins/qr_code_scanner/ios"
receive_sharing_intent: receive_sharing_intent:
:path: ".symlinks/plugins/receive_sharing_intent/ios" :path: ".symlinks/plugins/receive_sharing_intent/ios"
share_plus: share_plus:
:path: ".symlinks/plugins/share_plus/ios" :path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/ios" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite: sqflite:
:path: ".symlinks/plugins/sqflite/ios" :path: ".symlinks/plugins/sqflite/darwin"
synced_shared_preferences: synced_shared_preferences:
:path: ".symlinks/plugins/synced_shared_preferences/ios" :path: ".symlinks/plugins/synced_shared_preferences/ios"
url_launcher_ios: url_launcher_ios:
@ -129,31 +133,32 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/workmanager/ios" :path: ".symlinks/plugins/workmanager/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 integration_test: 13825b8a9334a850581300559b8839134b124670
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852 path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1 receive_sharing_intent: 753f808c6be5550247f6a20f2a14972466a5f33c
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7 synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
url_launcher_ios: fb12c43172927bb5cf75aeebd073f883801f1993 url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937 PODFILE CHECKSUM: 0957b955069bb512c22bae4cadad9f4c34161dbe
COCOAPODS: 1.11.3 COCOAPODS: 1.13.0

View File

@ -10,6 +10,7 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7A6CD5D595D5F4E8710804C0 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8BF0A917F40A838BF30D8F4C /* Pods_Runner.framework */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@ -22,7 +23,6 @@
E530B1B0283B54DA004E8EB6 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E530B1AE283B54DA004E8EB6 /* MainInterface.storyboard */; }; E530B1B0283B54DA004E8EB6 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E530B1AE283B54DA004E8EB6 /* MainInterface.storyboard */; };
E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E530B1A6283B54DA004E8EB6 /* Action Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E530B1A6283B54DA004E8EB6 /* Action Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E575B6F027EBC6DA002B1508 /* CloudKit.framework */; }; E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E575B6F027EBC6DA002B1508 /* CloudKit.framework */; };
FC507E94AA7767C155787DB3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFB5AA41D6C22D228077D166 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -68,14 +68,14 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
027B292CC58CF92F11FC0A69 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; 0E63A5CE3FDBCCD054072136 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
4449F5D4D39C23F292D07005 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8BF0A917F40A838BF30D8F4C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -83,8 +83,8 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
BFB5AA41D6C22D228077D166 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B9EC882BDD04A309C317E416 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
DF5D5FFF325B7D5DFEE88A3F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; D73EA9FA5E6F35364DCA0CD1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
E51D52AD283B464E00FC8DD8 /* Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; E51D52AD283B464E00FC8DD8 /* Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
E51D52AF283B464E00FC8DD8 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; }; E51D52AF283B464E00FC8DD8 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
E51D52B2283B464E00FC8DD8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; }; E51D52B2283B464E00FC8DD8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
@ -107,7 +107,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */, E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */,
FC507E94AA7767C155787DB3 /* Pods_Runner.framework in Frameworks */, 7A6CD5D595D5F4E8710804C0 /* Pods_Runner.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -183,8 +183,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E575B6F027EBC6DA002B1508 /* CloudKit.framework */, E575B6F027EBC6DA002B1508 /* CloudKit.framework */,
BFB5AA41D6C22D228077D166 /* Pods_Runner.framework */,
E530B1A7283B54DA004E8EB6 /* UniformTypeIdentifiers.framework */, E530B1A7283B54DA004E8EB6 /* UniformTypeIdentifiers.framework */,
8BF0A917F40A838BF30D8F4C /* Pods_Runner.framework */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@ -192,9 +192,9 @@
D79CD63C88FF49EF451AFDDF /* Pods */ = { D79CD63C88FF49EF451AFDDF /* Pods */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
DF5D5FFF325B7D5DFEE88A3F /* Pods-Runner.debug.xcconfig */, 0E63A5CE3FDBCCD054072136 /* Pods-Runner.debug.xcconfig */,
4449F5D4D39C23F292D07005 /* Pods-Runner.release.xcconfig */, D73EA9FA5E6F35364DCA0CD1 /* Pods-Runner.release.xcconfig */,
027B292CC58CF92F11FC0A69 /* Pods-Runner.profile.xcconfig */, B9EC882BDD04A309C317E416 /* Pods-Runner.profile.xcconfig */,
); );
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
@ -229,15 +229,15 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
41DC8215F9CFD708C36ECBA8 /* [CP] Check Pods Manifest.lock */, E2E6E097A94005D9196D0A71 /* [CP] Check Pods Manifest.lock */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
E51D52B8283B464E00FC8DD8 /* Embed App Extensions */,
9740EEB61CF901F6004384FC /* Run Script */, 9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */, 97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */, 97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */, 97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
7714A105B2069B720D0DF18E /* [CP] Embed Pods Frameworks */, F1959755D5521D58CA193498 /* [CP] Embed Pods Frameworks */,
E51D52B8283B464E00FC8DD8 /* Embed App Extensions */,
); );
buildRules = ( buildRules = (
); );
@ -291,7 +291,7 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastSwiftUpdateCheck = 1330; LastSwiftUpdateCheck = 1330;
LastUpgradeCheck = 1300; LastUpgradeCheck = 1510;
ORGANIZATIONNAME = ""; ORGANIZATIONNAME = "";
TargetAttributes = { TargetAttributes = {
97C146ED1CF9000F007C117D = { 97C146ED1CF9000F007C117D = {
@ -365,6 +365,7 @@
files = ( files = (
); );
inputPaths = ( inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
); );
name = "Thin Binary"; name = "Thin Binary";
outputPaths = ( outputPaths = (
@ -373,7 +374,22 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
}; };
41DC8215F9CFD708C36ECBA8 /* [CP] Check Pods Manifest.lock */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
E2E6E097A94005D9196D0A71 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@ -395,7 +411,7 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
7714A105B2069B720D0DF18E /* [CP] Embed Pods Frameworks */ = { F1959755D5521D58CA193498 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@ -412,21 +428,6 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@ -547,7 +548,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 15;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@ -565,11 +566,9 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic;
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = QMWX3X2NF7;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Hacki; INFOPLIST_KEY_CFBundleDisplayName = Hacki;
@ -583,7 +582,6 @@
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 = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.jiaqi.hacki";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
@ -638,7 +636,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 15;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -687,7 +685,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 15;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@ -707,11 +705,9 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic;
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = QMWX3X2NF7;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Hacki; INFOPLIST_KEY_CFBundleDisplayName = Hacki;
@ -725,7 +721,6 @@
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 = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.jiaqi.hacki";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -780,11 +775,9 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic;
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = QMWX3X2NF7;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Share Extension/Info.plist"; INFOPLIST_FILE = "Share Extension/Info.plist";
@ -802,7 +795,6 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension"; PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki share extension profile";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@ -863,11 +855,9 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic;
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = QMWX3X2NF7;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Share Extension/Info.plist"; INFOPLIST_FILE = "Share Extension/Info.plist";
@ -884,7 +874,6 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension"; PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Share-Extension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki share extension profile";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -905,11 +894,9 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements"; CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic;
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = QMWX3X2NF7;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Action Extension/Info.plist"; INFOPLIST_FILE = "Action Extension/Info.plist";
@ -927,7 +914,6 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension"; PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki action extension profile";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@ -992,11 +978,9 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements"; CODE_SIGN_ENTITLEMENTS = "Action Extension/Action Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic;
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = QMWX3X2NF7;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Action Extension/Info.plist"; INFOPLIST_FILE = "Action Extension/Info.plist";
@ -1013,7 +997,6 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension"; PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Action-Extension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "hacki action extension profile";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1300" LastUpgradeVersion = "1510"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -76,5 +76,9 @@
<false/> <false/>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to scan QR codes</string>
<key>io.flutter.embedded_views_preview</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@ -11,13 +11,13 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc({ AuthBloc({
AuthRepository? authRepository, AuthRepository? authRepository,
PreferenceRepository? preferenceRepository, PreferenceRepository? preferenceRepository,
StoriesRepository? storiesRepository, HackerNewsRepository? hackerNewsRepository,
SembastRepository? sembastRepository, SembastRepository? sembastRepository,
}) : _authRepository = authRepository ?? locator.get<AuthRepository>(), }) : _authRepository = authRepository ?? locator.get<AuthRepository>(),
_preferenceRepository = _preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>(), preferenceRepository ?? locator.get<PreferenceRepository>(),
_storiesRepository = _hackerNewsRepository =
storiesRepository ?? locator.get<StoriesRepository>(), hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
_sembastRepository = _sembastRepository =
sembastRepository ?? locator.get<SembastRepository>(), sembastRepository ?? locator.get<SembastRepository>(),
super(const AuthState.init()) { super(const AuthState.init()) {
@ -31,7 +31,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthRepository _authRepository; final AuthRepository _authRepository;
final PreferenceRepository _preferenceRepository; final PreferenceRepository _preferenceRepository;
final StoriesRepository _storiesRepository; final HackerNewsRepository _hackerNewsRepository;
final SembastRepository _sembastRepository; final SembastRepository _sembastRepository;
Future<void> onInitialize( Future<void> onInitialize(
@ -41,7 +41,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
await _authRepository.loggedIn.then((bool loggedIn) async { await _authRepository.loggedIn.then((bool loggedIn) async {
if (loggedIn) { if (loggedIn) {
final String? username = await _authRepository.username; final String? username = await _authRepository.username;
User? user = await _storiesRepository.fetchUser(id: username!); User? user = await _hackerNewsRepository.fetchUser(id: username!);
/// According to Hacker News' API documentation, /// According to Hacker News' API documentation,
/// if user has no public activity (posting a comment or story), /// if user has no public activity (posting a comment or story),
@ -52,14 +52,14 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
state.copyWith( state.copyWith(
isLoggedIn: true, isLoggedIn: true,
user: user, user: user,
status: AuthStatus.loaded, status: Status.success,
), ),
); );
} else { } else {
emit( emit(
state.copyWith( state.copyWith(
isLoggedIn: false, isLoggedIn: false,
status: AuthStatus.loaded, status: Status.success,
), ),
); );
} }
@ -81,7 +81,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
} }
Future<void> onLogin(AuthLogin event, Emitter<AuthState> emit) async { Future<void> onLogin(AuthLogin event, Emitter<AuthState> emit) async {
emit(state.copyWith(status: AuthStatus.loading)); emit(state.copyWith(status: Status.inProgress));
final bool successful = await _authRepository.login( final bool successful = await _authRepository.login(
username: event.username, username: event.username,
@ -89,16 +89,17 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
); );
if (successful) { if (successful) {
final User? user = await _storiesRepository.fetchUser(id: event.username); final User? user =
await _hackerNewsRepository.fetchUser(id: event.username);
emit( emit(
state.copyWith( state.copyWith(
user: user ?? User.emptyWithId(event.username), user: user ?? User.emptyWithId(event.username),
isLoggedIn: true, isLoggedIn: true,
status: AuthStatus.loaded, status: Status.success,
), ),
); );
} else { } else {
emit(state.copyWith(status: AuthStatus.failure)); emit(state.copyWith(status: Status.failure));
} }
} }
@ -113,6 +114,6 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
await _authRepository.logout(); await _authRepository.logout();
await _preferenceRepository.updateUnreadCommentsIds(<int>[]); await _preferenceRepository.updateUnreadCommentsIds(<int>[]);
await _sembastRepository.deleteAll(); await _sembastRepository.deleteCachedComments();
} }
} }

View File

@ -1,11 +1,5 @@
part of 'auth_bloc.dart'; part of 'auth_bloc.dart';
enum AuthStatus {
loading,
loaded,
failure,
}
class AuthState extends Equatable { class AuthState extends Equatable {
const AuthState({ const AuthState({
required this.user, required this.user,
@ -17,13 +11,13 @@ class AuthState extends Equatable {
const AuthState.init() const AuthState.init()
: user = const User.empty(), : user = const User.empty(),
isLoggedIn = false, isLoggedIn = false,
status = AuthStatus.loaded, status = Status.success,
agreedToEULA = false; agreedToEULA = false;
final User user; final User user;
final bool isLoggedIn; final bool isLoggedIn;
final bool agreedToEULA; final bool agreedToEULA;
final AuthStatus status; final Status status;
String get username => user.id; String get username => user.id;
@ -31,7 +25,7 @@ class AuthState extends Equatable {
User? user, User? user,
bool? isLoggedIn, bool? isLoggedIn,
bool? agreedToEULA, bool? agreedToEULA,
AuthStatus? status, Status? status,
}) { }) {
return AuthState( return AuthState(
user: user ?? this.user, user: user ?? this.user,

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
@ -19,24 +20,32 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
required PreferenceCubit preferenceCubit, required PreferenceCubit preferenceCubit,
required FilterCubit filterCubit, required FilterCubit filterCubit,
OfflineRepository? offlineRepository, OfflineRepository? offlineRepository,
StoriesRepository? storiesRepository, HackerNewsRepository? hackerNewsRepository,
PreferenceRepository? preferenceRepository, PreferenceRepository? preferenceRepository,
Logger? logger, Logger? logger,
}) : _preferenceCubit = preferenceCubit, }) : _preferenceCubit = preferenceCubit,
_filterCubit = filterCubit, _filterCubit = filterCubit,
_offlineRepository = _offlineRepository =
offlineRepository ?? locator.get<OfflineRepository>(), offlineRepository ?? locator.get<OfflineRepository>(),
_storiesRepository = _hackerNewsRepository =
storiesRepository ?? locator.get<StoriesRepository>(), hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
_preferenceRepository = _preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>(), preferenceRepository ?? locator.get<PreferenceRepository>(),
_logger = logger ?? locator.get<Logger>(), _logger = logger ?? locator.get<Logger>(),
super(const StoriesState.init()) { super(const StoriesState.init()) {
on<LoadStories>(
onLoadStories,
transformer: concurrent(),
);
on<StoriesInitialize>(onInitialize); on<StoriesInitialize>(onInitialize);
on<StoriesRefresh>(onRefresh); on<StoriesRefresh>(onRefresh);
on<StoriesLoadMore>(onLoadMore); on<StoriesLoadMore>(onLoadMore);
on<StoryLoaded>(onStoryLoaded); on<StoryLoaded>(
onStoryLoaded,
transformer: sequential(),
);
on<StoryRead>(onStoryRead); on<StoryRead>(onStoryRead);
on<StoryUnread>(onStoryUnread);
on<StoriesLoaded>(onStoriesLoaded); on<StoriesLoaded>(onStoriesLoaded);
on<StoriesDownload>(onDownload); on<StoriesDownload>(onDownload);
on<StoriesCancelDownload>(onCancelDownload); on<StoriesCancelDownload>(onCancelDownload);
@ -49,7 +58,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
final PreferenceCubit _preferenceCubit; final PreferenceCubit _preferenceCubit;
final FilterCubit _filterCubit; final FilterCubit _filterCubit;
final OfflineRepository _offlineRepository; final OfflineRepository _offlineRepository;
final StoriesRepository _storiesRepository; final HackerNewsRepository _hackerNewsRepository;
final PreferenceRepository _preferenceRepository; final PreferenceRepository _preferenceRepository;
final Logger _logger; final Logger _logger;
DeviceScreenType? deviceScreenType; DeviceScreenType? deviceScreenType;
@ -79,7 +88,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
const StoriesState.init().copyWith( const StoriesState.init().copyWith(
isOfflineReading: hasCachedStories && isOfflineReading: hasCachedStories &&
// Only go into offline mode in the next session. // Only go into offline mode in the next session.
state.downloadStatus == StoriesDownloadStatus.initial, state.downloadStatus == StoriesDownloadStatus.idle,
currentPageSize: pageSize, currentPageSize: pageSize,
downloadStatus: state.downloadStatus, downloadStatus: state.downloadStatus,
storiesDownloaded: state.storiesDownloaded, storiesDownloaded: state.storiesDownloaded,
@ -87,14 +96,15 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
), ),
); );
for (final StoryType type in StoryType.values) { for (final StoryType type in StoryType.values) {
await loadStories(type: type, emit: emit); add(LoadStories(type: type));
} }
} }
Future<void> loadStories({ Future<void> onLoadStories(
required StoryType type, LoadStories event,
required Emitter<StoriesState> emit, Emitter<StoriesState> emit,
}) async { ) async {
final StoryType type = event.type;
if (state.isOfflineReading) { if (state.isOfflineReading) {
final List<int> ids = final List<int> ids =
await _offlineRepository.getCachedStoryIds(type: type); await _offlineRepository.getCachedStoryIds(type: type);
@ -113,19 +123,19 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
add(StoriesLoaded(type: type)); add(StoriesLoaded(type: type));
}); });
} else { } else {
final List<int> ids = await _storiesRepository.fetchStoryIds(type: type); final List<int> ids =
await _hackerNewsRepository.fetchStoryIds(type: type);
emit( emit(
state state
.copyWithStoryIdsUpdated(type: type, to: ids) .copyWithStoryIdsUpdated(type: type, to: ids)
.copyWithCurrentPageUpdated(type: type, to: 0), .copyWithCurrentPageUpdated(type: type, to: 0),
); );
_storiesRepository await _hackerNewsRepository
.fetchStoriesStream(ids: ids.sublist(0, state.currentPageSize)) .fetchStoriesStream(ids: ids.sublist(0, state.currentPageSize))
.listen((Story story) { .listen((Story story) {
add(StoryLoaded(story: story, type: type)); add(StoryLoaded(story: story, type: type));
}).onDone(() { }).asFuture<void>();
add(StoriesLoaded(type: type)); add(StoriesLoaded(type: type));
});
} }
} }
@ -133,10 +143,12 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
StoriesRefresh event, StoriesRefresh event,
Emitter<StoriesState> emit, Emitter<StoriesState> emit,
) async { ) async {
if (state.statusByType[event.type] == Status.inProgress) return;
emit( emit(
state.copyWithStatusUpdated( state.copyWithStatusUpdated(
type: event.type, type: event.type,
to: StoriesStatus.loading, to: Status.inProgress,
), ),
); );
@ -144,12 +156,12 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
emit( emit(
state.copyWithStatusUpdated( state.copyWithStatusUpdated(
type: event.type, type: event.type,
to: StoriesStatus.loaded, to: Status.success,
), ),
); );
} else { } else {
emit(state.copyWithRefreshed(type: event.type)); emit(state.copyWithRefreshed(type: event.type));
await loadStories(type: event.type, emit: emit); add(LoadStories(type: event.type));
} }
} }
@ -157,7 +169,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
emit( emit(
state.copyWithStatusUpdated( state.copyWithStatusUpdated(
type: event.type, type: event.type,
to: StoriesStatus.loading, to: Status.inProgress,
), ),
); );
@ -194,7 +206,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
add(StoriesLoaded(type: event.type)); add(StoriesLoaded(type: event.type));
}); });
} else { } else {
_storiesRepository _hackerNewsRepository
.fetchStoriesStream( .fetchStoriesStream(
ids: state.storyIdsByType[event.type]!.sublist( ids: state.storyIdsByType[event.type]!.sublist(
lower, lower,
@ -216,7 +228,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
emit( emit(
state.copyWithStatusUpdated( state.copyWithStatusUpdated(
type: event.type, type: event.type,
to: StoriesStatus.loaded, to: Status.success,
), ),
); );
} }
@ -226,16 +238,18 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
StoryLoaded event, StoryLoaded event,
Emitter<StoriesState> emit, Emitter<StoriesState> emit,
) async { ) async {
final bool hasRead = await _preferenceRepository.hasRead(event.story.id); final Story story = event.story;
final bool hidden = _filterCubit.state.keywords.any( final bool hasRead = await _preferenceRepository.hasRead(story.id);
(String keyword) => final bool hidden = _filterCubit.state.keywords.any((String keyword) {
event.story.title.toLowerCase().contains(keyword) || // Match word only.
event.story.text.toLowerCase().contains(keyword), final RegExp regExp = RegExp('\\b($keyword)\\b');
); return regExp.hasMatch(story.title.toLowerCase()) ||
regExp.hasMatch(story.text.toLowerCase());
});
emit( emit(
state.copyWithStoryAdded( state.copyWithStoryAdded(
type: event.type, type: event.type,
story: event.story.copyWith(hidden: hidden), story: story.copyWith(hidden: hidden),
hasRead: hasRead, hasRead: hasRead,
), ),
); );
@ -243,7 +257,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
void onStoriesLoaded(StoriesLoaded event, Emitter<StoriesState> emit) { void onStoriesLoaded(StoriesLoaded event, Emitter<StoriesState> emit) {
emit( emit(
state.copyWithStatusUpdated(type: event.type, to: StoriesStatus.loaded), state.copyWithStatusUpdated(type: event.type, to: Status.success),
); );
} }
@ -269,7 +283,8 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
..remove(StoryType.latest); ..remove(StoryType.latest);
for (final StoryType type in prioritizedTypes) { for (final StoryType type in prioritizedTypes) {
final List<int> ids = await _storiesRepository.fetchStoryIds(type: type); final List<int> ids =
await _hackerNewsRepository.fetchStoryIds(type: type);
await _offlineRepository.cacheStoryIds(type: type, ids: ids); await _offlineRepository.cacheStoryIds(type: type, ids: ids);
prioritizedIds.addAll(ids); prioritizedIds.addAll(ids);
} }
@ -289,7 +304,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
); );
final Set<int> latestIds = <int>{}; final Set<int> latestIds = <int>{};
final List<int> ids = await _storiesRepository.fetchStoryIds( final List<int> ids = await _hackerNewsRepository.fetchStoryIds(
type: StoryType.latest, type: StoryType.latest,
); );
await _offlineRepository.cacheStoryIds(type: StoryType.latest, ids: ids); await _offlineRepository.cacheStoryIds(type: StoryType.latest, ids: ids);
@ -343,7 +358,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
} }
_logger.d('fetching story $id'); _logger.d('fetching story $id');
final Story? story = await _storiesRepository.fetchStory(id: id); final Story? story = await _hackerNewsRepository.fetchStory(id: id);
if (story == null) { if (story == null) {
if (isPrioritized) { if (isPrioritized) {
@ -373,7 +388,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
/// In other words, we are prioritizing the story itself instead of /// In other words, we are prioritizing the story itself instead of
/// the comments in the story. /// the comments in the story.
late final StreamSubscription<Comment>? downloadStream; late final StreamSubscription<Comment>? downloadStream;
downloadStream = _storiesRepository downloadStream = _hackerNewsRepository
.fetchAllChildrenComments(ids: story.kids) .fetchAllChildrenComments(ids: story.kids)
.whereType<Comment>() .whereType<Comment>()
.listen( .listen(
@ -456,7 +471,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
StoryRead event, StoryRead event,
Emitter<StoriesState> emit, Emitter<StoriesState> emit,
) async { ) async {
unawaited(_preferenceRepository.updateHasRead(event.story.id)); unawaited(_preferenceRepository.addHasRead(event.story.id));
emit( emit(
state.copyWith( state.copyWith(
@ -465,6 +480,19 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
); );
} }
Future<void> onStoryUnread(
StoryUnread event,
Emitter<StoriesState> emit,
) async {
unawaited(_preferenceRepository.removeHasRead(event.story.id));
emit(
state.copyWith(
readStoriesIds: <int>{...state.readStoriesIds}..remove(event.story.id),
),
);
}
Future<void> onClearAllReadStories( Future<void> onClearAllReadStories(
ClearAllReadStories event, ClearAllReadStories event,
Emitter<StoriesState> emit, Emitter<StoriesState> emit,

View File

@ -5,6 +5,15 @@ abstract class StoriesEvent extends Equatable {
List<Object?> get props => <Object?>[]; List<Object?> get props => <Object?>[];
} }
class LoadStories extends StoriesEvent {
LoadStories({required this.type});
final StoryType type;
@override
List<Object?> get props => <Object?>[type];
}
class StoriesInitialize extends StoriesEvent { class StoriesInitialize extends StoriesEvent {
@override @override
List<Object?> get props => <Object?>[]; List<Object?> get props => <Object?>[];
@ -95,6 +104,15 @@ class StoryRead extends StoriesEvent {
List<Object?> get props => <Object?>[story]; List<Object?> get props => <Object?>[story];
} }
class StoryUnread extends StoriesEvent {
StoryUnread({required this.story});
final Story story;
@override
List<Object?> get props => <Object?>[story];
}
class ClearAllReadStories extends StoriesEvent { class ClearAllReadStories extends StoriesEvent {
@override @override
List<Object?> get props => <Object?>[]; List<Object?> get props => <Object?>[];

View File

@ -1,13 +1,7 @@
part of 'stories_bloc.dart'; part of 'stories_bloc.dart';
enum StoriesStatus {
initial,
loading,
loaded,
}
enum StoriesDownloadStatus { enum StoriesDownloadStatus {
initial, idle,
downloading, downloading,
finished, finished,
failure, failure,
@ -43,12 +37,12 @@ class StoriesState extends Equatable {
StoryType.ask: <int>[], StoryType.ask: <int>[],
StoryType.show: <int>[], StoryType.show: <int>[],
}, },
this.statusByType = const <StoryType, StoriesStatus>{ this.statusByType = const <StoryType, Status>{
StoryType.top: StoriesStatus.initial, StoryType.top: Status.idle,
StoryType.best: StoriesStatus.initial, StoryType.best: Status.idle,
StoryType.latest: StoriesStatus.initial, StoryType.latest: Status.idle,
StoryType.ask: StoriesStatus.initial, StoryType.ask: Status.idle,
StoryType.show: StoriesStatus.initial, StoryType.show: Status.idle,
}, },
this.currentPageByType = const <StoryType, int>{ this.currentPageByType = const <StoryType, int>{
StoryType.top: 0, StoryType.top: 0,
@ -58,7 +52,7 @@ class StoriesState extends Equatable {
StoryType.show: 0, StoryType.show: 0,
}, },
}) : isOfflineReading = false, }) : isOfflineReading = false,
downloadStatus = StoriesDownloadStatus.initial, downloadStatus = StoriesDownloadStatus.idle,
currentPageSize = 0, currentPageSize = 0,
readStoriesIds = const <int>{}, readStoriesIds = const <int>{},
storiesDownloaded = 0, storiesDownloaded = 0,
@ -66,7 +60,7 @@ class StoriesState extends Equatable {
final Map<StoryType, List<Story>> storiesByType; final Map<StoryType, List<Story>> storiesByType;
final Map<StoryType, List<int>> storyIdsByType; final Map<StoryType, List<int>> storyIdsByType;
final Map<StoryType, StoriesStatus> statusByType; final Map<StoryType, Status> statusByType;
final Map<StoryType, int> currentPageByType; final Map<StoryType, int> currentPageByType;
final Set<int> readStoriesIds; final Set<int> readStoriesIds;
final StoriesDownloadStatus downloadStatus; final StoriesDownloadStatus downloadStatus;
@ -78,7 +72,7 @@ class StoriesState extends Equatable {
StoriesState copyWith({ StoriesState copyWith({
Map<StoryType, List<Story>>? storiesByType, Map<StoryType, List<Story>>? storiesByType,
Map<StoryType, List<int>>? storyIdsByType, Map<StoryType, List<int>>? storyIdsByType,
Map<StoryType, StoriesStatus>? statusByType, Map<StoryType, Status>? statusByType,
Map<StoryType, int>? currentPageByType, Map<StoryType, int>? currentPageByType,
Set<int>? readStoriesIds, Set<int>? readStoriesIds,
StoriesDownloadStatus? downloadStatus, StoriesDownloadStatus? downloadStatus,
@ -133,10 +127,10 @@ class StoriesState extends Equatable {
StoriesState copyWithStatusUpdated({ StoriesState copyWithStatusUpdated({
required StoryType type, required StoryType type,
required StoriesStatus to, required Status to,
}) { }) {
final Map<StoryType, StoriesStatus> newMap = final Map<StoryType, Status> newMap =
Map<StoryType, StoriesStatus>.from(statusByType); Map<StoryType, Status>.from(statusByType);
newMap[type] = to; newMap[type] = to;
return copyWith( return copyWith(
statusByType: newMap, statusByType: newMap,
@ -162,9 +156,9 @@ class StoriesState extends Equatable {
final Map<StoryType, List<int>> newStoryIdsMap = final Map<StoryType, List<int>> newStoryIdsMap =
Map<StoryType, List<int>>.from(storyIdsByType); Map<StoryType, List<int>>.from(storyIdsByType);
newStoryIdsMap[type] = <int>[]; newStoryIdsMap[type] = <int>[];
final Map<StoryType, StoriesStatus> newStatusMap = final Map<StoryType, Status> newStatusMap =
Map<StoryType, StoriesStatus>.from(statusByType); Map<StoryType, Status>.from(statusByType);
newStatusMap[type] = StoriesStatus.loading; newStatusMap[type] = Status.inProgress;
final Map<StoryType, int> newCurrentPageMap = final Map<StoryType, int> newCurrentPageMap =
Map<StoryType, int>.from(currentPageByType); Map<StoryType, int>.from(currentPageByType);
newCurrentPageMap[type] = 0; newCurrentPageMap[type] = 0;

View File

@ -20,6 +20,8 @@ abstract class Constants {
'$githubLink/issues/new?title=Found+a+bug+in+Hacki&body=Please+describe+the+problem.'; '$githubLink/issues/new?title=Found+a+bug+in+Hacki&body=Please+describe+the+problem.';
static const String wikipediaLink = 'https://en.wikipedia.org/wiki/'; static const String wikipediaLink = 'https://en.wikipedia.org/wiki/';
static const String wiktionaryLink = 'https://en.wiktionary.org/wiki/'; static const String wiktionaryLink = 'https://en.wiktionary.org/wiki/';
static const String hackerNewsItemLinkPrefix =
'https://news.ycombinator.com/item?id=';
static const String supportEmail = 'georgefung98@gmail.com'; static const String supportEmail = 'georgefung98@gmail.com';
static const String _imagePath = 'assets/images'; static const String _imagePath = 'assets/images';
@ -34,14 +36,6 @@ abstract class Constants {
static const String logFilename = 'hacki_log.txt'; static const String logFilename = 'hacki_log.txt';
static const String previousLogFileName = 'old_hacki_log.txt'; static const String previousLogFileName = 'old_hacki_log.txt';
/// Feature ids for feature discovery.
static const String featureAddStoryToFavList = 'add_story_to_fav_list';
static const String featureOpenStoryInWebView = 'open_story_in_web_view';
static const String featureLogIn = 'log_in';
static const String featurePinToTop = 'pin_to_top';
static const String featureJumpUpButton = 'jump_up_button';
static const String featureJumpDownButton = 'jump_down_button';
static final String happyFace = <String>[ static final String happyFace = <String>[
'(๑•̀ㅂ•́)و✧', '(๑•̀ㅂ•́)و✧',
'( ͡• ͜ʖ ͡•)', '( ͡• ͜ʖ ͡•)',
@ -78,3 +72,18 @@ abstract class RegExpConstants {
static const String linkSuffix = r'(\)|]|,|\*)(.)*$'; static const String linkSuffix = r'(\)|]|,|\*)(.)*$';
static const String number = '[0-9]+'; static const String number = '[0-9]+';
} }
abstract class AppDurations {
static const Duration ms100 = Duration(milliseconds: 100);
static const Duration ms200 = Duration(milliseconds: 200);
static const Duration ms300 = Duration(milliseconds: 300);
static const Duration ms400 = Duration(milliseconds: 400);
static const Duration ms500 = Duration(milliseconds: 500);
static const Duration ms600 = Duration(milliseconds: 600);
static const Duration oneSecond = Duration(seconds: 1);
static const Duration twoSeconds = Duration(seconds: 2);
static const Duration tenSeconds = Duration(seconds: 10);
static const Duration sec30 = Duration(seconds: 30);
static const Duration oneMinute = Duration(minutes: 1);
static const Duration twoMinutes = Duration(minutes: 2);
}

View File

@ -2,7 +2,7 @@ import 'package:logger/logger.dart';
class CustomLogFilter extends LogFilter { class CustomLogFilter extends LogFilter {
@override @override
Level? get level => Level.verbose; Level? get level => Level.trace;
/// The minimal level allowed in production. /// The minimal level allowed in production.
static const Level _minimalLevel = Level.info; static const Level _minimalLevel = Level.info;

View File

@ -1,49 +1,76 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hacki/config/constants.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/screens/screens.dart'; import 'package:hacki/screens/screens.dart';
/// Custom router. final GoRouter router = GoRouter(
/// observers: <NavigatorObserver>[
/// Handle named routing. locator.get<RouteObserver<ModalRoute<dynamic>>>(),
class CustomRouter { ],
/// Top level routing. initialLocation: HomeScreen.routeName,
static Route<dynamic> onGenerateRoute(RouteSettings settings) { routes: <RouteBase>[
switch (settings.name) { GoRoute(
case HomeScreen.routeName: path: HomeScreen.routeName,
return HomeScreen.route(); builder: (_, __) => const HomeScreen(),
case ItemScreen.routeName: routes: <RouteBase>[
return ItemScreen.route(settings.arguments! as ItemScreenArgs); GoRoute(
case SubmitScreen.routeName: path: ItemScreen.routeName,
return SubmitScreen.route(); builder: (_, GoRouterState state) {
default: final ItemScreenArgs? args = state.extra as ItemScreenArgs?;
return _errorRoute(); if (args == null) {
} throw GoError("args can't be null");
} }
return ItemScreen.phone(args);
/// Nested routing for bottom navigation bar. },
static Route<dynamic> onGenerateNestedRoute(RouteSettings settings) {
switch (settings.name) {
case ItemScreen.routeName:
return ItemScreen.route(settings.arguments! as ItemScreenArgs);
case SubmitScreen.routeName:
return SubmitScreen.route();
default:
return _errorRoute();
}
}
/// Error route.
static Route<dynamic> _errorRoute() {
return MaterialPageRoute<dynamic>(
settings: const RouteSettings(name: '/error'),
builder: (_) => Scaffold(
appBar: AppBar(
title: const Text('Error'),
),
body: Center(
child: Text(Constants.errorMessage),
), ),
],
),
GoRoute(
path: '/${ItemScreen.routeName}',
builder: (_, GoRouterState state) {
final ItemScreenArgs? args = state.extra as ItemScreenArgs?;
if (args == null) {
throw GoError("args can't be null");
}
return ItemScreen.phone(args);
},
),
GoRoute(
path: '/${SubmitScreen.routeName}',
builder: (_, __) => BlocProvider<SubmitCubit>(
create: (_) => SubmitCubit(),
child: const SubmitScreen(),
), ),
); ),
} GoRoute(
} path: '/${QrCodeScannerScreen.routeName}',
builder: (_, __) => const QrCodeScannerScreen(),
),
GoRoute(
path: '/${QrCodeViewScreen.routeName}',
builder: (_, GoRouterState state) {
final String? data = state.extra as String?;
if (data == null) {
throw GoError("data can't be null");
}
return QrCodeViewScreen(
data: data,
);
},
),
GoRoute(
path: '/${WebViewScreen.routeName}',
builder: (_, GoRouterState state) {
final String? link = state.extra as String?;
if (link == null) {
throw GoError("link can't be null");
}
return WebViewScreen(
url: link,
);
},
),
],
);

View File

@ -20,7 +20,7 @@ class CustomFileOutput extends LogOutput {
IOSink? _sink; IOSink? _sink;
@override @override
void init() { Future<void> init() async {
_sink = file.openWrite( _sink = file.openWrite(
mode: overrideExisting ? FileMode.writeOnly : FileMode.writeOnlyAppend, mode: overrideExisting ? FileMode.writeOnly : FileMode.writeOnlyAppend,
encoding: encoding, encoding: encoding,

View File

@ -23,12 +23,13 @@ Future<void> setUpLocator() async {
output: LogUtil.logOutput(logOutputFile), output: LogUtil.logOutput(logOutputFile),
), ),
) )
..registerSingleton<StoriesRepository>(StoriesRepository()) ..registerSingleton<SembastRepository>(SembastRepository())
..registerSingleton<HackerNewsRepository>(HackerNewsRepository())
..registerSingleton<HackerNewsWebRepository>(HackerNewsWebRepository())
..registerSingleton<PreferenceRepository>(PreferenceRepository()) ..registerSingleton<PreferenceRepository>(PreferenceRepository())
..registerSingleton<SearchRepository>(SearchRepository()) ..registerSingleton<SearchRepository>(SearchRepository())
..registerSingleton<AuthRepository>(AuthRepository()) ..registerSingleton<AuthRepository>(AuthRepository())
..registerSingleton<PostRepository>(PostRepository()) ..registerSingleton<PostRepository>(PostRepository())
..registerSingleton<SembastRepository>(SembastRepository())
..registerSingleton<OfflineRepository>(OfflineRepository()) ..registerSingleton<OfflineRepository>(OfflineRepository())
..registerSingleton<DraftCache>(DraftCache()) ..registerSingleton<DraftCache>(DraftCache())
..registerSingleton<CommentCache>(CommentCache()) ..registerSingleton<CommentCache>(CommentCache())

View File

@ -2,6 +2,7 @@ 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:flutter/foundation.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
@ -28,11 +29,12 @@ class CollapseCubit extends Cubit<CollapseState> {
collapsedCount: _collapseCache.totalHidden(_commentId), collapsedCount: _collapseCache.totalHidden(_commentId),
collapsed: _collapseCache.isCollapsed(_commentId), collapsed: _collapseCache.isCollapsed(_commentId),
hidden: _collapseCache.isHidden(_commentId), hidden: _collapseCache.isHidden(_commentId),
locked: _collapseCache.lockedId == _commentId,
), ),
); );
} }
void collapse() { void collapse({required VoidCallback onStateChanged}) {
if (state.collapsed) { if (state.collapsed) {
_collapseCache.uncollapse(_commentId); _collapseCache.uncollapse(_commentId);
@ -42,7 +44,14 @@ class CollapseCubit extends Cubit<CollapseState> {
collapsedCount: 0, collapsedCount: 0,
), ),
); );
onStateChanged();
} else { } else {
if (state.locked) {
emit(state.copyWith(locked: false));
return;
}
final Set<int> collapsedCommentIds = _collapseCache.collapse(_commentId); final Set<int> collapsedCommentIds = _collapseCache.collapse(_commentId);
emit( emit(
@ -51,6 +60,8 @@ class CollapseCubit extends Cubit<CollapseState> {
collapsedCount: state.collapsed ? 0 : collapsedCommentIds.length, collapsedCount: state.collapsed ? 0 : collapsedCommentIds.length,
), ),
); );
onStateChanged();
} }
} }
@ -85,6 +96,13 @@ class CollapseCubit extends Cubit<CollapseState> {
} }
} }
/// Prevent the item to be able to collapse, used when the comment
/// text is selected.
void lock() {
_collapseCache.lockedId = _commentId;
emit(state.copyWith(locked: true));
}
@override @override
Future<void> close() async { Future<void> close() async {
await _streamSubscription.cancel(); await _streamSubscription.cancel();

View File

@ -4,26 +4,39 @@ class CollapseState extends Equatable {
const CollapseState({ const CollapseState({
required this.collapsed, required this.collapsed,
required this.hidden, required this.hidden,
required this.locked,
required this.collapsedCount, required this.collapsedCount,
}); });
const CollapseState.init() const CollapseState.init()
: collapsed = false, : collapsed = false,
hidden = false, hidden = false,
locked = false,
collapsedCount = 0; collapsedCount = 0;
final bool collapsed; final bool collapsed;
/// The value determining whether or not the comment should show up in the
/// screen, this is true when the comment's parent is collapsed.
final bool hidden; final bool hidden;
/// The value determining whether or not the comment is collapsable.
/// If [locked] is true then the comment is not collapsable and vice versa.
final bool locked;
/// The number of children under this collapsed comment.
final int collapsedCount; final int collapsedCount;
CollapseState copyWith({ CollapseState copyWith({
bool? collapsed, bool? collapsed,
bool? hidden, bool? hidden,
bool? locked,
int? collapsedCount, int? collapsedCount,
}) { }) {
return CollapseState( return CollapseState(
collapsed: collapsed ?? this.collapsed, collapsed: collapsed ?? this.collapsed,
hidden: hidden ?? this.hidden, hidden: hidden ?? this.hidden,
locked: locked ?? this.locked,
collapsedCount: collapsedCount ?? this.collapsedCount, collapsedCount: collapsedCount ?? this.collapsedCount,
); );
} }
@ -32,6 +45,7 @@ class CollapseState extends Equatable {
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
collapsed, collapsed,
hidden, hidden,
locked,
collapsedCount, collapsedCount,
]; ];
} }

View File

@ -3,11 +3,14 @@ import 'dart:math';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/custom_router.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/main.dart'; import 'package:hacki/extensions/extensions.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/screens/screens.dart';
@ -23,25 +26,30 @@ part 'comments_state.dart';
class CommentsCubit extends Cubit<CommentsState> { class CommentsCubit extends Cubit<CommentsState> {
CommentsCubit({ CommentsCubit({
required FilterCubit filterCubit, required FilterCubit filterCubit,
required PreferenceCubit preferenceCubit,
required CollapseCache collapseCache, required CollapseCache collapseCache,
CommentCache? commentCache,
OfflineRepository? offlineRepository,
StoriesRepository? storiesRepository,
SembastRepository? sembastRepository,
Logger? logger,
required bool isOfflineReading, required bool isOfflineReading,
required Item item, required Item item,
required FetchMode defaultFetchMode, required FetchMode defaultFetchMode,
required CommentsOrder defaultCommentsOrder, required CommentsOrder defaultCommentsOrder,
CommentCache? commentCache,
OfflineRepository? offlineRepository,
SembastRepository? sembastRepository,
HackerNewsRepository? hackerNewsRepository,
HackerNewsWebRepository? hackerNewsWebRepository,
Logger? logger,
}) : _filterCubit = filterCubit, }) : _filterCubit = filterCubit,
_preferenceCubit = preferenceCubit,
_collapseCache = collapseCache, _collapseCache = collapseCache,
_commentCache = commentCache ?? locator.get<CommentCache>(), _commentCache = commentCache ?? locator.get<CommentCache>(),
_offlineRepository = _offlineRepository =
offlineRepository ?? locator.get<OfflineRepository>(), offlineRepository ?? locator.get<OfflineRepository>(),
_storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(),
_sembastRepository = _sembastRepository =
sembastRepository ?? locator.get<SembastRepository>(), sembastRepository ?? locator.get<SembastRepository>(),
_hackerNewsRepository =
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
_hackerNewsWebRepository =
hackerNewsWebRepository ?? locator.get<HackerNewsWebRepository>(),
_logger = logger ?? locator.get<Logger>(), _logger = logger ?? locator.get<Logger>(),
super( super(
CommentsState.init( CommentsState.init(
@ -53,13 +61,19 @@ class CommentsCubit extends Cubit<CommentsState> {
); );
final FilterCubit _filterCubit; final FilterCubit _filterCubit;
final PreferenceCubit _preferenceCubit;
final CollapseCache _collapseCache; final CollapseCache _collapseCache;
final CommentCache _commentCache; final CommentCache _commentCache;
final OfflineRepository _offlineRepository; final OfflineRepository _offlineRepository;
final StoriesRepository _storiesRepository;
final SembastRepository _sembastRepository; final SembastRepository _sembastRepository;
final HackerNewsRepository _hackerNewsRepository;
final HackerNewsWebRepository _hackerNewsWebRepository;
final Logger _logger; final Logger _logger;
final ItemScrollController itemScrollController = ItemScrollController();
final ItemPositionsListener itemPositionsListener =
ItemPositionsListener.create();
/// The [StreamSubscription] for stream (both lazy or eager) /// The [StreamSubscription] for stream (both lazy or eager)
/// fetching comments posted directly to the story. /// fetching comments posted directly to the story.
StreamSubscription<Comment>? _streamSubscription; StreamSubscription<Comment>? _streamSubscription;
@ -69,6 +83,30 @@ class CommentsCubit extends Cubit<CommentsState> {
final Map<int, StreamSubscription<Comment>> _streamSubscriptions = final Map<int, StreamSubscription<Comment>> _streamSubscriptions =
<int, StreamSubscription<Comment>>{}; <int, StreamSubscription<Comment>>{};
static const int _webFetchingCmtCountLowerLimit = 50;
Future<bool> get _shouldFetchFromWeb async {
final bool isOnWifi = await _isOnWifi;
if (isOnWifi) {
return switch (state.item) {
Story(descendants: final int descendants)
when descendants > _webFetchingCmtCountLowerLimit =>
true,
Comment(kids: final List<int> kids)
when kids.length > _webFetchingCmtCountLowerLimit =>
true,
_ => false,
};
} else {
return true;
}
}
static Future<bool> get _isOnWifi async {
final ConnectivityResult status = await Connectivity().checkConnectivity();
return status == ConnectivityResult.wifi;
}
@override @override
void emit(CommentsState state) { void emit(CommentsState state) {
if (!isClosed) { if (!isClosed) {
@ -80,6 +118,8 @@ class CommentsCubit extends Cubit<CommentsState> {
bool onlyShowTargetComment = false, bool onlyShowTargetComment = false,
bool useCommentCache = false, bool useCommentCache = false,
List<Comment>? targetAncestors, List<Comment>? targetAncestors,
AppExceptionHandler? onError,
bool fetchFromWeb = true,
}) async { }) async {
if (onlyShowTargetComment && (targetAncestors?.isNotEmpty ?? false)) { if (onlyShowTargetComment && (targetAncestors?.isNotEmpty ?? false)) {
emit( emit(
@ -90,7 +130,7 @@ class CommentsCubit extends Cubit<CommentsState> {
), ),
); );
_streamSubscription = _storiesRepository _streamSubscription = _hackerNewsRepository
.fetchAllCommentsRecursivelyStream( .fetchAllCommentsRecursivelyStream(
ids: targetAncestors!.last.kids, ids: targetAncestors!.last.kids,
level: targetAncestors.last.level + 1, level: targetAncestors.last.level + 1,
@ -105,8 +145,10 @@ class CommentsCubit extends Cubit<CommentsState> {
emit( emit(
state.copyWith( state.copyWith(
status: CommentsStatus.loading, status: CommentsStatus.inProgress,
comments: <Comment>[], comments: <Comment>[],
matchedComments: <int>[],
inThreadSearchQuery: '',
currentPage: 0, currentPage: 0,
), ),
); );
@ -114,7 +156,10 @@ class CommentsCubit extends Cubit<CommentsState> {
final Item item = state.item; final Item item = state.item;
final Item updatedItem = state.isOfflineReading final Item updatedItem = state.isOfflineReading
? item ? item
: await _storiesRepository.fetchItem(id: item.id).then(_toBuildable) ?? : await _hackerNewsRepository
.fetchItem(id: item.id)
.then(_toBuildable)
.onError((_, __) => item) ??
item; item;
final List<int> kids = _sortKids(updatedItem.kids); final List<int> kids = _sortKids(updatedItem.kids);
@ -127,17 +172,54 @@ class CommentsCubit extends Cubit<CommentsState> {
} else { } else {
switch (state.fetchMode) { switch (state.fetchMode) {
case FetchMode.lazy: case FetchMode.lazy:
commentStream = _storiesRepository.fetchCommentsStream( commentStream = _hackerNewsRepository.fetchCommentsStream(
ids: kids, ids: kids,
getFromCache: useCommentCache ? _commentCache.getComment : null, getFromCache: useCommentCache ? _commentCache.getComment : null,
); );
break;
case FetchMode.eager: case FetchMode.eager:
commentStream = _storiesRepository.fetchAllCommentsRecursivelyStream( switch (state.order) {
ids: kids, case CommentsOrder.natural:
getFromCache: useCommentCache ? _commentCache.getComment : null, final bool shouldFetchFromWeb = await _shouldFetchFromWeb;
); if (fetchFromWeb && shouldFetchFromWeb) {
break; _logger.d('fetching from web.');
commentStream = _hackerNewsWebRepository
.fetchCommentsStream(state.item)
.handleError((dynamic e) {
_streamSubscription?.cancel();
_logger.e(e);
switch (e.runtimeType) {
case RateLimitedException:
case RateLimitedWithFallbackException:
case PossibleParsingException:
if (_preferenceCubit.state.devModeEnabled) {
onError?.call(e as AppException);
}
/// If fetching from web failed, fetch using API instead.
refresh(onError: onError, fetchFromWeb: false);
default:
onError?.call(GenericException());
}
});
} else {
_logger.d('fetching from API.');
commentStream =
_hackerNewsRepository.fetchAllCommentsRecursivelyStream(
ids: kids,
getFromCache:
useCommentCache ? _commentCache.getComment : null,
);
}
case CommentsOrder.oldestFirst:
case CommentsOrder.newestFirst:
commentStream =
_hackerNewsRepository.fetchAllCommentsRecursivelyStream(
ids: kids,
getFromCache: useCommentCache ? _commentCache.getComment : null,
);
}
} }
} }
@ -148,10 +230,13 @@ class CommentsCubit extends Cubit<CommentsState> {
..onDone(_onDone); ..onDone(_onDone);
} }
Future<void> refresh() async { Future<void> refresh({
required AppExceptionHandler? onError,
bool fetchFromWeb = true,
}) async {
emit( emit(
state.copyWith( state.copyWith(
status: CommentsStatus.loading, status: CommentsStatus.inProgress,
), ),
); );
@ -181,18 +266,51 @@ class CommentsCubit extends Cubit<CommentsState> {
final Item item = state.item; final Item item = state.item;
final Item updatedItem = final Item updatedItem =
await _storiesRepository.fetchItem(id: item.id) ?? item; await _hackerNewsRepository.fetchItem(id: item.id) ?? item;
final List<int> kids = _sortKids(updatedItem.kids); final List<int> kids = _sortKids(updatedItem.kids);
late final Stream<Comment> commentStream; late final Stream<Comment> commentStream;
if (state.fetchMode == FetchMode.lazy) {
commentStream = _storiesRepository.fetchCommentsStream( switch (state.fetchMode) {
ids: kids, case FetchMode.lazy:
); commentStream = _hackerNewsRepository.fetchCommentsStream(ids: kids);
} else { case FetchMode.eager:
commentStream = _storiesRepository.fetchAllCommentsRecursivelyStream( switch (state.order) {
ids: kids, case CommentsOrder.natural:
); final bool shouldFetchFromWeb = await _shouldFetchFromWeb;
if (fetchFromWeb && shouldFetchFromWeb) {
_logger.d('fetching from web.');
commentStream = _hackerNewsWebRepository
.fetchCommentsStream(state.item)
.handleError((dynamic e) {
_logger.e(e);
switch (e.runtimeType) {
case RateLimitedException:
case RateLimitedWithFallbackException:
case PossibleParsingException:
if (_preferenceCubit.state.devModeEnabled) {
onError?.call(e as AppException);
}
/// If fetching from web failed, fetch using API instead.
refresh(onError: onError, fetchFromWeb: false);
default:
onError?.call(GenericException());
}
});
} else {
_logger.d('fetching from API.');
commentStream = _hackerNewsRepository
.fetchAllCommentsRecursivelyStream(ids: kids);
}
case CommentsOrder.oldestFirst:
case CommentsOrder.newestFirst:
commentStream =
_hackerNewsRepository.fetchAllCommentsRecursivelyStream(
ids: kids,
);
}
} }
_streamSubscription = commentStream _streamSubscription = commentStream
@ -214,6 +332,7 @@ class CommentsCubit extends Cubit<CommentsState> {
state.copyWith( state.copyWith(
onlyShowTargetComment: false, onlyShowTargetComment: false,
item: story, item: story,
matchedComments: <int>[],
), ),
); );
init(); init();
@ -225,7 +344,7 @@ class CommentsCubit extends Cubit<CommentsState> {
void Function(Comment)? onCommentFetched, void Function(Comment)? onCommentFetched,
VoidCallback? onDone, VoidCallback? onDone,
}) { }) {
if (comment == null && state.status == CommentsStatus.loading) return; if (comment == null && state.status == CommentsStatus.inProgress) return;
switch (state.fetchMode) { switch (state.fetchMode) {
case FetchMode.lazy: case FetchMode.lazy:
@ -238,14 +357,17 @@ class CommentsCubit extends Cubit<CommentsState> {
/// Ignoring because the subscription will be cancelled in close() /// Ignoring because the subscription will be cancelled in close()
// ignore: cancel_subscriptions // ignore: cancel_subscriptions
final StreamSubscription<Comment> streamSubscription = final StreamSubscription<Comment> streamSubscription =
_storiesRepository _hackerNewsRepository
.fetchCommentsStream(ids: comment.kids) .fetchCommentsStream(ids: comment.kids)
.asyncMap(_toBuildableComment) .asyncMap(_toBuildableComment)
.whereNotNull() .whereNotNull()
.listen((Comment cmt) { .listen((Comment cmt) {
_collapseCache.addKid(cmt.id, to: cmt.parent); _collapseCache.addKid(cmt.id, to: cmt.parent);
_commentCache.cacheComment(cmt); _commentCache.cacheComment(cmt);
_sembastRepository.cacheComment(cmt);
final Map<int, Comment> updatedIdToCommentMap =
Map<int, Comment>.from(state.idToCommentMap);
updatedIdToCommentMap[comment.id] = comment;
emit( emit(
state.copyWith( state.copyWith(
@ -253,6 +375,7 @@ class CommentsCubit extends Cubit<CommentsState> {
state.comments.indexOf(comment) + offset + 1, state.comments.indexOf(comment) + offset + 1,
cmt.copyWith(level: level), cmt.copyWith(level: level),
), ),
idToCommentMap: updatedIdToCommentMap,
), ),
); );
offset++; offset++;
@ -268,30 +391,28 @@ class CommentsCubit extends Cubit<CommentsState> {
}); });
_streamSubscriptions[comment.id] = streamSubscription; _streamSubscriptions[comment.id] = streamSubscription;
break;
case FetchMode.eager: case FetchMode.eager:
if (_streamSubscription != null) { if (_streamSubscription != null) {
emit(state.copyWith(status: CommentsStatus.loading)); emit(state.copyWith(status: CommentsStatus.inProgress));
_streamSubscription _streamSubscription
?..resume() ?..resume()
..onData(onCommentFetched); ..onData(onCommentFetched);
} }
break;
} }
} }
Future<void> loadParentThread() async { Future<void> loadParentThread() async {
HapticFeedbackUtil.light(); HapticFeedbackUtil.light();
emit(state.copyWith(fetchParentStatus: CommentsStatus.loading)); emit(state.copyWith(fetchParentStatus: CommentsStatus.inProgress));
final Item? parent = final Item? parent =
await _storiesRepository.fetchItem(id: state.item.parent); await _hackerNewsRepository.fetchItem(id: state.item.parent);
if (parent == null) { if (parent == null) {
return; return;
} else { } else {
await HackiApp.navigatorKey.currentState?.pushNamed( await router.push(
ItemScreen.routeName, '/${ItemScreen.routeName}',
arguments: ItemScreenArgs(item: parent), extra: ItemScreenArgs(item: parent),
); );
emit( emit(
@ -304,17 +425,17 @@ class CommentsCubit extends Cubit<CommentsState> {
Future<void> loadRootThread() async { Future<void> loadRootThread() async {
HapticFeedbackUtil.light(); HapticFeedbackUtil.light();
emit(state.copyWith(fetchRootStatus: CommentsStatus.loading)); emit(state.copyWith(fetchRootStatus: CommentsStatus.inProgress));
final Story? parent = await _storiesRepository final Story? parent = await _hackerNewsRepository
.fetchParentStory(id: state.item.id) .fetchParentStory(id: state.item.id)
.then(_toBuildableStory); .then(_toBuildableStory);
if (parent == null) { if (parent == null) {
return; return;
} else { } else {
await HackiApp.navigatorKey.currentState?.pushNamed( await router.push(
ItemScreen.routeName, '/${ItemScreen.routeName}',
arguments: ItemScreenArgs(item: parent), extra: ItemScreenArgs(item: parent),
); );
emit( emit(
@ -325,7 +446,7 @@ class CommentsCubit extends Cubit<CommentsState> {
} }
} }
void onOrderChanged(CommentsOrder? order) { void updateOrder(CommentsOrder? order) {
if (order == null) return; if (order == null) return;
if (state.order == order) return; if (state.order == order) return;
HapticFeedbackUtil.selection(); HapticFeedbackUtil.selection();
@ -338,7 +459,7 @@ class CommentsCubit extends Cubit<CommentsState> {
init(useCommentCache: true); init(useCommentCache: true);
} }
void onFetchModeChanged(FetchMode? fetchMode) { void updateFetchMode(FetchMode? fetchMode) {
if (fetchMode == null) return; if (fetchMode == null) return;
if (state.fetchMode == fetchMode) return; if (state.fetchMode == fetchMode) return;
_collapseCache.resetCollapsedComments(); _collapseCache.resetCollapsedComments();
@ -352,16 +473,26 @@ class CommentsCubit extends Cubit<CommentsState> {
init(useCommentCache: true); init(useCommentCache: true);
} }
void jump( void scrollTo({
ItemScrollController itemScrollController, required int index,
ItemPositionsListener itemPositionsListener, double alignment = 0.0,
) { }) {
debugPrint('Scrolling to: $index, alignment: $alignment');
itemScrollController.scrollTo(
index: index,
alignment: alignment,
duration: AppDurations.ms400,
);
}
/// Scroll to next root level comment.
void scrollToNextRoot({VoidCallback? onError}) {
final int totalComments = state.comments.length; final int totalComments = state.comments.length;
final List<Comment> onScreenComments = itemPositionsListener final List<Comment> onScreenComments = itemPositionsListener
.itemPositions.value .itemPositions.value
// The header is also a part of the list view, // The header is also a part of the list view,
// thus ignoring it here. // thus ignoring it here.
.where((ItemPosition e) => e.index >= 1 && e.itemLeadingEdge < 0.7) .where((ItemPosition e) => e.index >= 1 && e.itemLeadingEdge > 0.1)
.sorted((ItemPosition a, ItemPosition b) => a.index.compareTo(b.index)) .sorted((ItemPosition a, ItemPosition b) => a.index.compareTo(b.index))
.map( .map(
(ItemPosition e) => e.index <= state.comments.length (ItemPosition e) => e.index <= state.comments.length
@ -371,9 +502,29 @@ class CommentsCubit extends Cubit<CommentsState> {
.whereNotNull() .whereNotNull()
.toList(); .toList();
/// The index of last comment visible on screen. if (onScreenComments.isEmpty && state.comments.isNotEmpty) {
final int lastVisibleIndex = state.comments.indexOf(onScreenComments.last); itemScrollController.scrollTo(
final int startIndex = min(lastVisibleIndex + 1, totalComments); index: 1,
alignment: 0.15,
duration: AppDurations.ms400,
);
return;
}
final Comment? firstVisibleRootComment =
onScreenComments.firstWhereOrNull((Comment e) => e.isRoot);
late int startIndex;
if (firstVisibleRootComment != null) {
/// The index of first root level comment visible on screen.
final int firstVisibleRootCommentIndex =
state.comments.indexOf(firstVisibleRootComment);
startIndex = min(firstVisibleRootCommentIndex + 1, totalComments);
} else {
final int lastVisibleCommentIndex =
state.comments.indexOf(onScreenComments.last);
startIndex = min(lastVisibleCommentIndex + 1, totalComments);
}
for (int i = startIndex; i < totalComments; i++) { for (int i = startIndex; i < totalComments; i++) {
final Comment cmt = state.comments.elementAt(i); final Comment cmt = state.comments.elementAt(i);
@ -382,17 +533,19 @@ class CommentsCubit extends Cubit<CommentsState> {
itemScrollController.scrollTo( itemScrollController.scrollTo(
index: i + 1, index: i + 1,
alignment: 0.15, alignment: 0.15,
duration: const Duration(milliseconds: 400), duration: AppDurations.ms400,
); );
return; return;
} }
} }
if (state.status == CommentsStatus.allLoaded) {
onError?.call();
}
} }
void jumpUp( /// Scroll to previous root level comment.
ItemScrollController itemScrollController, void scrollToPreviousRoot() {
ItemPositionsListener itemPositionsListener,
) {
final List<Comment> onScreenComments = itemPositionsListener final List<Comment> onScreenComments = itemPositionsListener
.itemPositions.value .itemPositions.value
// The header is also a part of the list view, // The header is also a part of the list view,
@ -420,13 +573,57 @@ class CommentsCubit extends Cubit<CommentsState> {
itemScrollController.scrollTo( itemScrollController.scrollTo(
index: i + 1, index: i + 1,
alignment: 0.15, alignment: 0.15,
duration: const Duration(milliseconds: 400), duration: AppDurations.ms400,
); );
return; return;
} }
} }
} }
void search(String query, {String author = ''}) {
resetSearch();
late final bool Function(Comment cmt) conditionSatisfied;
final String lowercaseQuery = query.toLowerCase();
if (query.isEmpty && author.isEmpty) {
return;
} else if (author.isEmpty) {
conditionSatisfied =
(Comment cmt) => cmt.text.toLowerCase().contains(lowercaseQuery);
} else if (query.isEmpty) {
conditionSatisfied = (Comment cmt) => cmt.by == author;
} else {
conditionSatisfied = (Comment cmt) =>
cmt.text.toLowerCase().contains(lowercaseQuery) && cmt.by == author;
}
emit(
state.copyWith(
inThreadSearchQuery: query,
inThreadSearchAuthor: author,
),
);
for (final int i in 0.to(state.comments.length, inclusive: false)) {
final Comment cmt = state.comments.elementAt(i);
if (conditionSatisfied(cmt)) {
emit(
state.copyWith(
matchedComments: <int>[...state.matchedComments, i],
),
);
}
}
}
void resetSearch() => emit(
state.copyWith(
matchedComments: <int>[],
inThreadSearchQuery: '',
inThreadSearchAuthor: '',
),
);
List<int> _sortKids(List<int> kids) { List<int> _sortKids(List<int> kids) {
switch (state.order) { switch (state.order) {
case CommentsOrder.natural: case CommentsOrder.natural:
@ -452,8 +649,12 @@ class CommentsCubit extends Cubit<CommentsState> {
if (comment != null) { if (comment != null) {
_collapseCache.addKid(comment.id, to: comment.parent); _collapseCache.addKid(comment.id, to: comment.parent);
_commentCache.cacheComment(comment); _commentCache.cacheComment(comment);
_sembastRepository.cacheComment(comment);
if (state.isOfflineReading) {
_sembastRepository.cacheComment(comment);
}
// Hide comment that matches any of the filter keywords.
final bool hidden = _filterCubit.state.keywords.any( final bool hidden = _filterCubit.state.keywords.any(
(String keyword) => comment.text.toLowerCase().contains(keyword), (String keyword) => comment.text.toLowerCase().contains(keyword),
); );
@ -462,7 +663,16 @@ class CommentsCubit extends Cubit<CommentsState> {
comment.copyWith(hidden: hidden), comment.copyWith(hidden: hidden),
]; ];
emit(state.copyWith(comments: updatedComments)); final Map<int, Comment> updatedIdToCommentMap =
Map<int, Comment>.from(state.idToCommentMap);
updatedIdToCommentMap[comment.id] = comment;
emit(
state.copyWith(
comments: updatedComments,
idToCommentMap: updatedIdToCommentMap,
),
);
} }
} }

View File

@ -1,17 +1,19 @@
part of 'comments_cubit.dart'; part of 'comments_cubit.dart';
enum CommentsStatus { enum CommentsStatus {
init, idle,
loading, inProgress,
loaded, loaded,
allLoaded, allLoaded,
failure, error,
} }
class CommentsState extends Equatable { class CommentsState extends Equatable {
const CommentsState({ const CommentsState({
required this.item, required this.item,
required this.comments, required this.comments,
required this.matchedComments,
required this.idToCommentMap,
required this.status, required this.status,
required this.fetchParentStatus, required this.fetchParentStatus,
required this.fetchRootStatus, required this.fetchRootStatus,
@ -20,6 +22,8 @@ class CommentsState extends Equatable {
required this.onlyShowTargetComment, required this.onlyShowTargetComment,
required this.isOfflineReading, required this.isOfflineReading,
required this.currentPage, required this.currentPage,
required this.inThreadSearchQuery,
required this.inThreadSearchAuthor,
}); });
CommentsState.init({ CommentsState.init({
@ -28,14 +32,19 @@ class CommentsState extends Equatable {
required this.fetchMode, required this.fetchMode,
required this.order, required this.order,
}) : comments = <Comment>[], }) : comments = <Comment>[],
status = CommentsStatus.init, matchedComments = <int>[],
fetchParentStatus = CommentsStatus.init, idToCommentMap = <int, Comment>{},
fetchRootStatus = CommentsStatus.init, status = CommentsStatus.idle,
fetchParentStatus = CommentsStatus.idle,
fetchRootStatus = CommentsStatus.idle,
onlyShowTargetComment = false, onlyShowTargetComment = false,
currentPage = 0; currentPage = 0,
inThreadSearchQuery = '',
inThreadSearchAuthor = '';
final Item item; final Item item;
final List<Comment> comments; final List<Comment> comments;
final Map<int, Comment> idToCommentMap;
final CommentsStatus status; final CommentsStatus status;
final CommentsStatus fetchParentStatus; final CommentsStatus fetchParentStatus;
final CommentsStatus fetchRootStatus; final CommentsStatus fetchRootStatus;
@ -44,10 +53,17 @@ class CommentsState extends Equatable {
final bool onlyShowTargetComment; final bool onlyShowTargetComment;
final bool isOfflineReading; final bool isOfflineReading;
final int currentPage; final int currentPage;
final String inThreadSearchQuery;
final String inThreadSearchAuthor;
/// Indexes of comments that matches the query for in-thread search.
final List<int> matchedComments;
CommentsState copyWith({ CommentsState copyWith({
Item? item, Item? item,
List<Comment>? comments, List<Comment>? comments,
List<int>? matchedComments,
Map<int, Comment>? idToCommentMap,
CommentsStatus? status, CommentsStatus? status,
CommentsStatus? fetchParentStatus, CommentsStatus? fetchParentStatus,
CommentsStatus? fetchRootStatus, CommentsStatus? fetchRootStatus,
@ -56,10 +72,13 @@ class CommentsState extends Equatable {
bool? onlyShowTargetComment, bool? onlyShowTargetComment,
bool? isOfflineReading, bool? isOfflineReading,
int? currentPage, int? currentPage,
String? inThreadSearchQuery,
String? inThreadSearchAuthor,
}) { }) {
return CommentsState( return CommentsState(
item: item ?? this.item, item: item ?? this.item,
comments: comments ?? this.comments, comments: comments ?? this.comments,
matchedComments: matchedComments ?? this.matchedComments,
fetchParentStatus: fetchParentStatus ?? this.fetchParentStatus, fetchParentStatus: fetchParentStatus ?? this.fetchParentStatus,
fetchRootStatus: fetchRootStatus ?? this.fetchRootStatus, fetchRootStatus: fetchRootStatus ?? this.fetchRootStatus,
status: status ?? this.status, status: status ?? this.status,
@ -69,11 +88,41 @@ class CommentsState extends Equatable {
onlyShowTargetComment ?? this.onlyShowTargetComment, onlyShowTargetComment ?? this.onlyShowTargetComment,
isOfflineReading: isOfflineReading ?? this.isOfflineReading, isOfflineReading: isOfflineReading ?? this.isOfflineReading,
currentPage: currentPage ?? this.currentPage, currentPage: currentPage ?? this.currentPage,
inThreadSearchQuery: inThreadSearchQuery ?? this.inThreadSearchQuery,
inThreadSearchAuthor: inThreadSearchAuthor ?? this.inThreadSearchAuthor,
idToCommentMap: idToCommentMap ?? this.idToCommentMap,
); );
} }
Set<int> get commentIds => comments.map((Comment e) => e.id).toSet(); Set<int> get commentIds => comments.map((Comment e) => e.id).toSet();
static final Map<int, bool> _isResponseCache = <int, bool>{};
bool isResponse(Comment comment) {
if (_isResponseCache.containsKey(comment.id)) {
return _isResponseCache[comment.id]!;
}
if (comment.isRoot) {
_isResponseCache[comment.id] = false;
return false;
}
final Comment? precedingComment = idToCommentMap[comment.parent];
if (precedingComment == null) {
_isResponseCache[comment.id] = false;
return false;
} else if (item.id == precedingComment.parent && item.by == comment.by) {
_isResponseCache[comment.id] = true;
return true;
} else if (idToCommentMap[precedingComment.parent]?.by == comment.by) {
_isResponseCache[comment.id] = true;
return true;
} else {
_isResponseCache[comment.id] = false;
return false;
}
}
@override @override
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
item, item,
@ -86,5 +135,9 @@ class CommentsState extends Equatable {
isOfflineReading, isOfflineReading,
currentPage, currentPage,
comments, comments,
matchedComments,
inThreadSearchQuery,
inThreadSearchAuthor,
idToCommentMap,
]; ];
} }

View File

@ -1,4 +1,5 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
@ -11,12 +12,14 @@ part 'edit_state.dart';
class EditCubit extends HydratedCubit<EditState> { class EditCubit extends HydratedCubit<EditState> {
EditCubit({DraftCache? draftCache}) EditCubit({DraftCache? draftCache})
: _draftCache = draftCache ?? locator.get<DraftCache>(), : _draftCache = draftCache ?? locator.get<DraftCache>(),
_debouncer = Debouncer(delay: const Duration(seconds: 1)), _debouncer = Debouncer(delay: AppDurations.oneSecond),
super(const EditState.init()); super(const EditState.init());
final DraftCache _draftCache; final DraftCache _draftCache;
final Debouncer _debouncer; final Debouncer _debouncer;
void reset() => emit(const EditState.init());
void onReplyTapped(Item item) { void onReplyTapped(Item item) {
emit( emit(
EditState( EditState(
@ -35,14 +38,6 @@ class EditCubit extends HydratedCubit<EditState> {
); );
} }
void onReplyBoxClosed() {
emit(const EditState.init());
}
void onScrolled() {
emit(const EditState.init());
}
void onReplySubmittedSuccessfully() { void onReplySubmittedSuccessfully() {
if (state.replyingTo != null) { if (state.replyingTo != null) {
_draftCache.removeDraft(replyingTo: state.replyingTo!.id); _draftCache.removeDraft(replyingTo: state.replyingTo!.id);
@ -64,9 +59,14 @@ class EditCubit extends HydratedCubit<EditState> {
} }
} }
void deleteDraft() => clear(); void deleteDraft() {
// Remove draft in storage.
bool called = false; clear();
// Reset cached state.
_cachedState = const EditState.init();
// Reset to init state;
reset();
}
@override @override
EditState? fromJson(Map<String, dynamic> json) { EditState? fromJson(Map<String, dynamic> json) {
@ -95,6 +95,7 @@ class EditCubit extends HydratedCubit<EditState> {
Map<String, dynamic>? toJson(EditState state) { Map<String, dynamic>? toJson(EditState state) {
EditState selected = state; EditState selected = state;
// Override previous draft only when current draft is not empty.
if (state.replyingTo == null || if (state.replyingTo == null ||
(state.replyingTo?.id != _cachedState.replyingTo?.id && (state.replyingTo?.id != _cachedState.replyingTo?.id &&
state.text.isNullOrEmpty)) { state.text.isNullOrEmpty)) {

View File

@ -1,9 +1,14 @@
import 'dart:async';
import 'dart:collection';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/blocs/blocs.dart'; import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.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';
part 'fav_state.dart'; part 'fav_state.dart';
@ -12,13 +17,18 @@ class FavCubit extends Cubit<FavState> {
required AuthBloc authBloc, required AuthBloc authBloc,
AuthRepository? authRepository, AuthRepository? authRepository,
PreferenceRepository? preferenceRepository, PreferenceRepository? preferenceRepository,
StoriesRepository? storiesRepository, HackerNewsRepository? hackerNewsRepository,
HackerNewsWebRepository? hackerNewsWebRepository,
Logger? logger,
}) : _authBloc = authBloc, }) : _authBloc = authBloc,
_authRepository = authRepository ?? locator.get<AuthRepository>(), _authRepository = authRepository ?? locator.get<AuthRepository>(),
_preferenceRepository = _preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>(), preferenceRepository ?? locator.get<PreferenceRepository>(),
_storiesRepository = _hackerNewsRepository =
storiesRepository ?? locator.get<StoriesRepository>(), hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
_hackerNewsWebRepository =
hackerNewsWebRepository ?? locator.get<HackerNewsWebRepository>(),
_logger = logger ?? locator.get<Logger>(),
super(FavState.init()) { super(FavState.init()) {
init(); init();
} }
@ -26,44 +36,43 @@ class FavCubit extends Cubit<FavState> {
final AuthBloc _authBloc; final AuthBloc _authBloc;
final AuthRepository _authRepository; final AuthRepository _authRepository;
final PreferenceRepository _preferenceRepository; final PreferenceRepository _preferenceRepository;
final StoriesRepository _storiesRepository; final HackerNewsRepository _hackerNewsRepository;
final HackerNewsWebRepository _hackerNewsWebRepository;
final Logger _logger;
late final StreamSubscription<String>? _usernameSubscription;
static const int _pageSize = 20; static const int _pageSize = 20;
String? _username;
Future<void> init() async { Future<void> init() async {
_authBloc.stream.listen((AuthState authState) { _usernameSubscription = _authBloc.stream
if (authState.username != _username) { .map((AuthState event) => event.username)
_preferenceRepository .distinct()
.favList(of: authState.username) .listen((String username) {
.then((List<int> favIds) { _preferenceRepository.favList(of: username).then((List<int> favIds) {
emit(
state.copyWith(
favIds: favIds,
favItems: <Item>[],
currentPage: 0,
),
);
_hackerNewsRepository
.fetchItemsStream(
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
)
.listen(_onItemLoaded)
.onDone(() {
emit( emit(
state.copyWith( state.copyWith(
favIds: favIds, status: Status.success,
favItems: <Item>[],
currentPage: 0,
), ),
); );
_storiesRepository
.fetchItemsStream(
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
)
.listen(_onItemLoaded)
.onDone(() {
emit(
state.copyWith(
status: FavStatus.loaded,
),
);
});
}); });
});
_username = authState.username;
}
}); });
} }
Future<void> addFav(int id) async { Future<void> addFav(int id) async {
final String username = _authBloc.state.username; if (state.favIds.contains(id)) return;
await _preferenceRepository.addFav(username: username, id: id); await _preferenceRepository.addFav(username: username, id: id);
@ -73,7 +82,7 @@ class FavCubit extends Cubit<FavState> {
), ),
); );
final Item? item = await _storiesRepository.fetchItem(id: id); final Item? item = await _hackerNewsRepository.fetchItem(id: id);
if (item == null) return; if (item == null) return;
@ -89,9 +98,9 @@ class FavCubit extends Cubit<FavState> {
} }
void removeFav(int id) { void removeFav(int id) {
final String username = _authBloc.state.username; _preferenceRepository
..removeFav(username: username, id: id)
_preferenceRepository.removeFav(username: username, id: id); ..removeFav(username: '', id: id);
emit( emit(
state.copyWith( state.copyWith(
@ -107,7 +116,7 @@ class FavCubit extends Cubit<FavState> {
} }
void loadMore() { void loadMore() {
emit(state.copyWith(status: FavStatus.loading)); emit(state.copyWith(status: Status.inProgress));
final int currentPage = state.currentPage; final int currentPage = state.currentPage;
final int len = state.favIds.length; final int len = state.favIds.length;
emit(state.copyWith(currentPage: currentPage + 1)); emit(state.copyWith(currentPage: currentPage + 1));
@ -119,7 +128,7 @@ class FavCubit extends Cubit<FavState> {
upper = len; upper = len;
} }
_storiesRepository _hackerNewsRepository
.fetchItemsStream( .fetchItemsStream(
ids: state.favIds.sublist( ids: state.favIds.sublist(
lower, lower,
@ -128,19 +137,17 @@ class FavCubit extends Cubit<FavState> {
) )
.listen(_onItemLoaded) .listen(_onItemLoaded)
.onDone(() { .onDone(() {
emit(state.copyWith(status: FavStatus.loaded)); emit(state.copyWith(status: Status.success));
}); });
} else { } else {
emit(state.copyWith(status: FavStatus.loaded)); emit(state.copyWith(status: Status.success));
} }
} }
void refresh() { void refresh() {
final String username = _authBloc.state.username;
emit( emit(
state.copyWith( state.copyWith(
status: FavStatus.loading, status: Status.inProgress,
currentPage: 0, currentPage: 0,
favItems: <Item>[], favItems: <Item>[],
favIds: <int>[], favIds: <int>[],
@ -149,13 +156,13 @@ 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 _hackerNewsRepository
.fetchItemsStream( .fetchItemsStream(
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)), ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
) )
.listen(_onItemLoaded) .listen(_onItemLoaded)
.onDone(() { .onDone(() {
emit(state.copyWith(status: FavStatus.loaded)); emit(state.copyWith(status: Status.success));
}); });
}); });
} }
@ -167,6 +174,34 @@ class FavCubit extends Cubit<FavState> {
emit(FavState.init()); emit(FavState.init());
} }
Future<void> merge({
required AppExceptionHandler onError,
required VoidCallback onSuccess,
}) async {
if (_authBloc.state.isLoggedIn) {
emit(state.copyWith(mergeStatus: Status.inProgress));
try {
final Iterable<int> ids = await _hackerNewsWebRepository.fetchFavorites(
of: _authBloc.state.username,
);
_logger.d('fetched ${ids.length} favorite items from HN.');
final List<int> combinedIds = <int>[...ids, ...state.favIds];
final LinkedHashSet<int> mergedIds =
LinkedHashSet<int>.from(combinedIds);
await _preferenceRepository.overwriteFav(
username: username,
ids: mergedIds,
);
emit(state.copyWith(mergeStatus: Status.success));
onSuccess();
refresh();
} on RateLimitedException catch (e) {
onError(e);
emit(state.copyWith(mergeStatus: Status.failure));
}
}
}
void _onItemLoaded(Item item) { void _onItemLoaded(Item item) {
emit( emit(
state.copyWith( state.copyWith(
@ -174,4 +209,14 @@ class FavCubit extends Cubit<FavState> {
), ),
); );
} }
@override
Future<void> close() {
_usernameSubscription?.cancel();
return super.close();
}
}
extension on FavCubit {
String get username => _authBloc.state.username;
} }

View File

@ -1,41 +1,39 @@
part of 'fav_cubit.dart'; part of 'fav_cubit.dart';
enum FavStatus {
init,
loading,
loaded,
failure,
}
class FavState extends Equatable { class FavState extends Equatable {
const FavState({ const FavState({
required this.favIds, required this.favIds,
required this.favItems, required this.favItems,
required this.status, required this.status,
required this.mergeStatus,
required this.currentPage, required this.currentPage,
}); });
FavState.init() FavState.init()
: favIds = <int>[], : favIds = <int>[],
favItems = <Item>[], favItems = <Item>[],
status = FavStatus.init, status = Status.idle,
mergeStatus = Status.idle,
currentPage = 0; currentPage = 0;
final List<int> favIds; final List<int> favIds;
final List<Item> favItems; final List<Item> favItems;
final FavStatus status; final Status status;
final Status mergeStatus;
final int currentPage; final int currentPage;
FavState copyWith({ FavState copyWith({
List<int>? favIds, List<int>? favIds,
List<Item>? favItems, List<Item>? favItems,
FavStatus? status, Status? status,
Status? mergeStatus,
int? currentPage, int? currentPage,
}) { }) {
return FavState( return FavState(
favIds: favIds ?? this.favIds, favIds: favIds ?? this.favIds,
favItems: favItems ?? this.favItems, favItems: favItems ?? this.favItems,
status: status ?? this.status, status: status ?? this.status,
mergeStatus: mergeStatus ?? this.mergeStatus,
currentPage: currentPage ?? this.currentPage, currentPage: currentPage ?? this.currentPage,
); );
} }
@ -43,6 +41,7 @@ class FavState extends Equatable {
@override @override
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
status, status,
mergeStatus,
currentPage, currentPage,
favIds, favIds,
favItems, favItems,

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