Compare commits
119 Commits
Author | SHA1 | Date | |
---|---|---|---|
90ec117b4b | |||
9580e9b3e5 | |||
0afaa5a0aa | |||
fbce1dff73 | |||
b0d6561486 | |||
11639118c5 | |||
e54c893e6c | |||
8c57e5e323 | |||
b4ec7ec44e | |||
8b65256294 | |||
58f7bf14d7 | |||
d9aad3d34e | |||
ed48d95375 | |||
1eaded5694 | |||
70bb78afcb | |||
df2d2478d5 | |||
d5ae60327d | |||
615a092c1e | |||
5a7699d866 | |||
56a9bab3f2 | |||
e9bbf46b4f | |||
10f503a6c0 | |||
582f3156b2 | |||
90fb45146f | |||
c19c54e762 | |||
70e5a84b63 | |||
3a51fa83f2 | |||
cb90751330 | |||
835ed7e841 | |||
125ccd2dd1 | |||
5b991c4287 | |||
7dc3618afe | |||
eef4691814 | |||
9f71701845 | |||
d27203b041 | |||
4f280ec4c9 | |||
72cb2737ca | |||
215203bd16 | |||
3e320faece | |||
1049568246 | |||
71aa42118d | |||
4f21d3e6bd | |||
96d0fe9e5e | |||
69eee3e278 | |||
36bcd996c0 | |||
5fc39d8b8b | |||
5dce7787e1 | |||
8888dde792 | |||
6c8fc4cf87 | |||
ae9cc109db | |||
c8976ed17b | |||
ff7e115418 | |||
0310507c96 | |||
58c646e232 | |||
08328e2ca1 | |||
86b7228ffd | |||
e103c88ca6 | |||
94323a04e0 | |||
4776c375a1 | |||
1f4e6cf41c | |||
be6ed35888 | |||
b2ea50cea6 | |||
109b9287cf | |||
939d55ef0d | |||
3ee60e1a44 | |||
6fe567fa02 | |||
bc2d4f32c9 | |||
91290e9743 | |||
934f184b6f | |||
dbd48eae99 | |||
279007191b | |||
b3fdc20fc5 | |||
3fbf5d4eea | |||
332ffbb773 | |||
346a6c709e | |||
d4fe042245 | |||
b82c4a1777 | |||
7e0d1f0f1d | |||
f405a10c2e | |||
edbad79cd3 | |||
c9d8b2950a | |||
f2bc48f980 | |||
d56697c57c | |||
320ec41aae | |||
d85b3535d5 | |||
f8cd1cbba0 | |||
817ec208d6 | |||
554a165789 | |||
0c680370ef | |||
59541d2fcc | |||
32083c3564 | |||
258dbc4b8b | |||
6c8047ebac | |||
00a0135867 | |||
1db7be7a2c | |||
ff400f9c40 | |||
f03b45a98a | |||
cbe5bba986 | |||
268f4054a3 | |||
988c5d9881 | |||
e748e2f818 | |||
1b0a0dbda9 | |||
64d68389ba | |||
381c99b353 | |||
39ee3137f8 | |||
0d76be8634 | |||
9986f72e11 | |||
ef557e7b84 | |||
ec065c0122 | |||
2960c6e59e | |||
92dac6b932 | |||
20365393a3 | |||
8d238744c7 | |||
e33ff417fb | |||
d8922c2641 | |||
c6e0461857 | |||
30ca356dc8 | |||
7d11398e6d | |||
a4f52284ef |
3
.github/workflows/commit_check.yml
vendored
@ -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
|
||||||
|
8
.github/workflows/publish_ios.yml
vendored
@ -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
|
||||||
|
33
README.md
@ -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.
|
||||||
|
|
||||||
[](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone)
|
[](https://apps.apple.com/us/app/hacki/id1602043763?platform=iphone)
|
||||||
[](https://f-droid.org/en/packages/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>
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 940 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 5.4 KiB |
@ -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
|
||||||
}
|
}
|
||||||
|
BIN
assets/fonts/exo_2/Exo2-Bold.ttf
Normal file
BIN
assets/fonts/exo_2/Exo2-Regular.ttf
Normal file
BIN
assets/hacki-github.png
Normal file
After Width: | Height: | Size: 419 KiB |
BIN
assets/hacki-github.xcf
Normal file
BIN
assets/hacki.xcf
Normal file
Before Width: | Height: | Size: 548 KiB After Width: | Height: | Size: 333 KiB |
Before Width: | Height: | Size: 571 KiB After Width: | Height: | Size: 341 KiB |
Before Width: | Height: | Size: 592 KiB After Width: | Height: | Size: 359 KiB |
BIN
assets/screenshots/dark-1.png
Normal file
After Width: | Height: | Size: 1003 KiB |
BIN
assets/screenshots/dark-2.png
Normal file
After Width: | Height: | Size: 912 KiB |
BIN
assets/screenshots/dark-3.png
Normal file
After Width: | Height: | Size: 252 KiB |
BIN
assets/screenshots/dark-4.png
Normal file
After Width: | Height: | Size: 734 KiB |
BIN
assets/screenshots/dark-5.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/screenshots/hacki-1.png
Normal file
After Width: | Height: | Size: 890 KiB |
BIN
assets/screenshots/hacki-2.png
Normal file
After Width: | Height: | Size: 873 KiB |
BIN
assets/screenshots/hacki-3.png
Normal file
After Width: | Height: | Size: 770 KiB |
BIN
assets/screenshots/hacki-4.png
Normal file
After Width: | Height: | Size: 517 KiB |
BIN
assets/screenshots/light-1.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
assets/screenshots/light-2.png
Normal file
After Width: | Height: | Size: 893 KiB |
BIN
assets/screenshots/light-3.png
Normal file
After Width: | Height: | Size: 460 KiB |
BIN
assets/screenshots/light-4.png
Normal file
After Width: | Height: | Size: 712 KiB |
BIN
assets/screenshots/light-5.png
Normal file
After Width: | Height: | Size: 1.0 MiB |
BIN
assets/screenshots/tablet-dark-1.png
Normal file
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/screenshots/tablet-dark-2.png
Normal file
After Width: | Height: | Size: 1.5 MiB |
BIN
assets/screenshots/tablet-light-1.png
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
assets/screenshots/tablet-light-2.png
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/tablet-hacki.xcf
Normal 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(),
|
|
||||||
);
|
|
||||||
}
|
|
@ -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',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
1
fastlane/metadata/android/en-US/changelogs/121.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
- Ability to mark a story as read once scrolling past.
|
2
fastlane/metadata/android/en-US/changelogs/125.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
- Ability to customize text scale factor.
|
||||||
|
- Ability to customize app's accent color.
|
4
fastlane/metadata/android/en-US/changelogs/127.txt
Normal 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.
|
5
fastlane/metadata/android/en-US/changelogs/128.txt
Normal 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.
|
5
fastlane/metadata/android/en-US/changelogs/129.txt
Normal 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.
|
4
fastlane/metadata/android/en-US/changelogs/131.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
- New comment indicator.
|
||||||
|
- Ability to mark stories as read from home page.
|
||||||
|
- Text rendering improvements.
|
||||||
|
- Performance improvements.
|
4
fastlane/metadata/android/en-US/changelogs/132.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
- New comment indicator.
|
||||||
|
- Ability to mark stories as read from home page.
|
||||||
|
- Text rendering improvements.
|
||||||
|
- Performance improvements.
|
4
fastlane/metadata/android/en-US/changelogs/134.txt
Normal 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.
|
3
fastlane/metadata/android/en-US/changelogs/135.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
- Return of true dark mode.
|
||||||
|
- Better comment fetching strategy.
|
||||||
|
- Minor UI fixes.
|
Before Width: | Height: | Size: 522 KiB |
Before Width: | Height: | Size: 835 KiB |
Before Width: | Height: | Size: 282 KiB |
Before Width: | Height: | Size: 298 KiB |
Before Width: | Height: | Size: 820 KiB |
Before Width: | Height: | Size: 868 KiB |
Before Width: | Height: | Size: 121 KiB |
Before Width: | Height: | Size: 375 KiB |
Before Width: | Height: | Size: 282 KiB |
Before Width: | Height: | Size: 414 KiB |
Before Width: | Height: | Size: 530 KiB |
Before Width: | Height: | Size: 406 KiB |
After Width: | Height: | Size: 1003 KiB |
After Width: | Height: | Size: 912 KiB |
After Width: | Height: | Size: 252 KiB |
After Width: | Height: | Size: 734 KiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 1.3 MiB |
After Width: | Height: | Size: 893 KiB |
After Width: | Height: | Size: 460 KiB |
After Width: | Height: | Size: 712 KiB |
After Width: | Height: | Size: 1.0 MiB |
@ -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>
|
||||||
|
111
ios/Gemfile.lock
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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?>[];
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
@ -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,
|
||||||
|
@ -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())
|
||||||
|
@ -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();
|
||||||
|
@ -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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -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)) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|