Compare commits
144 Commits
Author | SHA1 | Date | |
---|---|---|---|
ab43d1a2c4 | |||
cf5c0b3263 | |||
d7295afa41 | |||
1ecddf9d5b | |||
479903ed77 | |||
1e4c10e819 | |||
473a65427a | |||
ad6ccc9376 | |||
995dfed85d | |||
0e74f88a8d | |||
c2e6d7ea98 | |||
e46432b86c | |||
9763a94e1d | |||
077fcbf9da | |||
9cdb6b7383 | |||
d01524020d | |||
fb2072676e | |||
162c7a2689 | |||
e218527953 | |||
3dddfa66cf | |||
3fd0a9a1ea | |||
7a35fe451d | |||
575ba8c2ef | |||
e82998bb32 | |||
3389e98861 | |||
92a743f2f0 | |||
4148240daf | |||
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 |
5
.github/workflows/commit_check.yml
vendored
@ -7,15 +7,16 @@ on:
|
||||
- '!master'
|
||||
|
||||
jobs:
|
||||
releases:
|
||||
commit_check:
|
||||
name: Check commit
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: checkout all the submodules
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- run: submodules/flutter/bin/flutter doctor
|
||||
- run: submodules/flutter/bin/flutter pub get
|
||||
- run: submodules/flutter/bin/dart format --set-exit-if-changed lib test integration_test
|
||||
|
23
.github/workflows/parser_check.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
name: Parser Check
|
||||
|
||||
on:
|
||||
# Allow manual builds of this workflow.
|
||||
workflow_dispatch: { }
|
||||
# Run this job every hour.
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
|
||||
jobs:
|
||||
parser_check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 0.5
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dart-lang/setup-dart@v1
|
||||
- name: Verify comment text parser
|
||||
working-directory: ./scripts/bin
|
||||
run: |
|
||||
dart pub get
|
||||
dart parser_verifier.dart -t ${{ secrets.GITHUB_TOKEN }}
|
17
.github/workflows/publish_ios.yml
vendored
@ -1,16 +1,21 @@
|
||||
name: Publish (iOS)
|
||||
|
||||
on:
|
||||
# Allow manual builds of this workflow
|
||||
# Allow manual builds of this workflow.
|
||||
workflow_dispatch: {}
|
||||
# Run the workflow whenever a new tag named 'v*' is pushed
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
# Only build when any of these directories has been modified.
|
||||
paths:
|
||||
- lib/**
|
||||
- pubspec.lock
|
||||
- pubspec.yaml
|
||||
- submodules/**
|
||||
|
||||
jobs:
|
||||
build_and_publish:
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-13
|
||||
timeout-minutes: 30
|
||||
|
||||
env:
|
||||
@ -19,10 +24,16 @@ jobs:
|
||||
BUNDLE_GEMFILE: ${{ github.workspace }}/ios/Gemfile
|
||||
|
||||
steps:
|
||||
- name: Set XCode version
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '15.0'
|
||||
|
||||
- name: Check out from git
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- run: submodules/flutter/bin/flutter doctor
|
||||
- run: submodules/flutter/bin/flutter pub get
|
||||
- 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
|
||||
|
||||
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://f-droid.org/en/packages/com.jiaqifeng.hacki/)
|
||||
@ -29,27 +29,26 @@ Features:
|
||||
- Download stories and comments for offline reading.
|
||||
- Pick up where you left off.
|
||||
- Synced favorites and pins across devices. (iOS only)
|
||||
- Export or import your favorites.
|
||||
- Launch from system share sheet.
|
||||
- And more...
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img width="200" alt="01" src="assets/screenshots/01.png">
|
||||
<img width="200" alt="02" src="assets/screenshots/02.png">
|
||||
<img width="200" alt="03" src="assets/screenshots/03.png">
|
||||
<img width="200" alt="04" src="assets/screenshots/04.png">
|
||||
<img width="200" alt="05" src="assets/screenshots/05.png">
|
||||
<img width="200" alt="06" src="assets/screenshots/06.png">
|
||||
<img width="200" alt="07" src="assets/screenshots/07.png">
|
||||
<img width="200" alt="08" src="assets/screenshots/08.png">
|
||||
<img width="200" alt="09" src="assets/screenshots/09.png">
|
||||
<img width="200" alt="10" src="assets/screenshots/10.png">
|
||||
<img width="200" alt="11" src="assets/screenshots/11.png">
|
||||
<img width="200" alt="12" src="assets/screenshots/12.png">
|
||||
<img width="400" alt="01" src="assets/screenshots/light-1.png">
|
||||
<img width="400" alt="06" src="assets/screenshots/dark-1.png">
|
||||
<img width="400" alt="02" src="assets/screenshots/light-2.png">
|
||||
<img width="400" alt="07" src="assets/screenshots/dark-2.png">
|
||||
<img width="400" alt="03" src="assets/screenshots/light-3.png">
|
||||
<img width="400" alt="08" src="assets/screenshots/dark-3.png">
|
||||
<img width="400" alt="04" src="assets/screenshots/light-4.png">
|
||||
<img width="400" alt="09" src="assets/screenshots/dark-4.png">
|
||||
<img width="400" alt="05" src="assets/screenshots/light-5.png">
|
||||
<img width="400" alt="10" src="assets/screenshots/dark-5.png">
|
||||
|
||||
<img width="400" alt="ipad-01" src="assets/screenshots/ipad-01.png">
|
||||
<img width="400" alt="ipad-02" src="assets/screenshots/ipad-02.png">
|
||||
<img width="400" alt="ipad-03" src="assets/screenshots/ipad-03.png">
|
||||
<img width="400" alt="ipad-04" src="assets/screenshots/ipad-04.png">
|
||||
<img width="400" alt="ipad-01" src="assets/screenshots/tablet-light-1.png">
|
||||
<img width="400" alt="ipad-02" src="assets/screenshots/tablet-dark-1.png">
|
||||
<img width="400" alt="ipad-03" src="assets/screenshots/tablet-light-2.png">
|
||||
<img width="400" alt="ipad-04" src="assets/screenshots/tablet-dark-2.png">
|
||||
</p>
|
||||
|
||||
|
@ -11,3 +11,4 @@ linter:
|
||||
analyzer:
|
||||
exclude:
|
||||
- "submodules/**"
|
||||
- "scripts/**"
|
||||
|
@ -1,3 +1,10 @@
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
@ -6,11 +13,6 @@ if (localPropertiesFile.exists()) {
|
||||
}
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
@ -21,10 +23,6 @@ if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
@ -33,7 +31,7 @@ if (keystorePropertiesFile.exists()) {
|
||||
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
compileSdkVersion 34
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
@ -50,8 +48,8 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.jiaqifeng.hacki"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 33
|
||||
minSdkVersion 25
|
||||
targetSdkVersion 34
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
@ -80,7 +78,7 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0"
|
||||
}
|
||||
|
||||
ext.abiCodes = ["x86_64": 1, "armeabi-v7a": 2, "arm64-v8a": 3]
|
||||
|
@ -13,6 +13,9 @@
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.support.customtabs.action.CustomTabsService" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
@ -20,7 +23,8 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:usesCleartextTraffic="true">
|
||||
android:usesCleartextTraffic="true"
|
||||
android:enableOnBackInvokedCallback="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
|
@ -2,4 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_adaptive_back"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_adaptive_fore"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
||||
</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 |
@ -1,16 +1,3 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.0'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.1.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
@ -24,6 +11,6 @@ subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
|
||||
|
@ -1,11 +1,25 @@
|
||||
include ':app'
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}()
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.3.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.0" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
BIN
assets/fonts/atkinson_hyperlegible/AtkinsonHyperlegible-Bold.ttf
Normal file
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 |
7
assets/remote-config.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"athingComtrSelector": "#hnmain > tbody > tr > td > table > tbody > .athing.comtr",
|
||||
"commentTextSelector": "td > table > tbody > tr > td.default > div.comment > div.commtext",
|
||||
"commentHeadSelector": "td > table > tbody > tr > td.default > div > span > a",
|
||||
"commentAgeSelector": "td > table > tbody > tr > td.default > div > span > span.age",
|
||||
"commentIndentSelector": "td > table > tbody > tr > td.ind"
|
||||
}
|
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,121 +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();
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fileprivate func remove(key: String?) -> Bool{
|
||||
if let key = key {
|
||||
let keyStore = NSUbiquitousKeyValueStore()
|
||||
keyStore.removeObject(forKey: key)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
|
||||
@ -87,6 +96,14 @@ public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
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":
|
||||
if let params = call.arguments as? [String: Any] {
|
||||
let val = params[valKey] as? Bool
|
||||
|
@ -15,6 +15,14 @@ class SyncedSharedPreferences {
|
||||
const MethodChannel(channel),
|
||||
);
|
||||
|
||||
Future<bool?> remove({
|
||||
required String key,
|
||||
}) async {
|
||||
return _channel.invokeMethod('remove', <String, dynamic>{
|
||||
'key': key,
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool?> setBool({
|
||||
required String key,
|
||||
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.
|
2
fastlane/metadata/android/en-US/changelogs/145.txt
Normal file
@ -0,0 +1,2 @@
|
||||
- Favicon in mini story tile.
|
||||
- UX improvements.
|
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>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>11.0</string>
|
||||
<string>12.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
111
ios/Gemfile.lock
@ -1,7 +1,7 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.5)
|
||||
CFPropertyList (3.0.6)
|
||||
rexml
|
||||
activesupport (6.1.7)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
@ -9,28 +9,28 @@ GEM
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
zeitwerk (~> 2.3)
|
||||
addressable (2.8.1)
|
||||
addressable (2.8.6)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
algoliasearch (1.27.5)
|
||||
httpclient (~> 2.8, >= 2.8.3)
|
||||
json (>= 1.5.1)
|
||||
artifactory (3.0.15)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.680.0)
|
||||
aws-sdk-core (3.168.4)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.889.0)
|
||||
aws-sdk-core (3.191.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (~> 1.8)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.61.0)
|
||||
aws-sdk-core (~> 3, >= 3.165.0)
|
||||
aws-sdk-kms (1.77.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.117.2)
|
||||
aws-sdk-core (~> 3, >= 3.165.0)
|
||||
aws-sdk-s3 (1.143.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.5.2)
|
||||
aws-sigv4 (~> 1.8)
|
||||
aws-sigv4 (1.8.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
claide (1.1.0)
|
||||
@ -77,7 +77,7 @@ GEM
|
||||
highline (~> 2.0.0)
|
||||
concurrent-ruby (1.1.10)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.4)
|
||||
digest-crc (0.6.5)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
@ -86,8 +86,8 @@ GEM
|
||||
escape (0.0.4)
|
||||
ethon (0.15.0)
|
||||
ffi (>= 1.15.0)
|
||||
excon (0.95.0)
|
||||
faraday (1.10.2)
|
||||
excon (0.109.0)
|
||||
faraday (1.10.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
@ -115,8 +115,8 @@ GEM
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.6)
|
||||
fastlane (2.211.0)
|
||||
fastimage (2.3.0)
|
||||
fastlane (2.219.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@ -135,20 +135,22 @@ GEM
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-env (>= 1.6.0, < 2.0.0)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
http-cookie (~> 1.0.5)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (~> 2.0.0)
|
||||
multipart-post (>= 2.0.0, < 3.0.0)
|
||||
naturally (~> 2.2)
|
||||
optparse (~> 0.1.1)
|
||||
optparse (>= 0.1.1)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.3)
|
||||
simctl (~> 1.6.3)
|
||||
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-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
@ -159,9 +161,9 @@ GEM
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.32.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-core (0.9.2)
|
||||
google-apis-androidpublisher_v3 (0.54.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-core (0.11.3)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
@ -169,31 +171,29 @@ GEM
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.16.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.12.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-storage_v1 (0.19.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-apis-iamcredentials_v1 (0.17.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.13.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.29.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-cloud-core (1.6.1)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.3.0)
|
||||
google-cloud-storage (1.44.0)
|
||||
google-cloud-errors (1.3.1)
|
||||
google-cloud-storage (1.45.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
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)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.3.0)
|
||||
googleauth (1.8.1)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
@ -204,49 +204,48 @@ GEM
|
||||
i18n (1.12.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.6.3)
|
||||
jwt (2.5.0)
|
||||
memoist (0.16.2)
|
||||
json (2.7.1)
|
||||
jwt (2.7.1)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.2)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.16.3)
|
||||
molinillo (0.8.0)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.0.0)
|
||||
multipart-post (2.4.0)
|
||||
nanaimo (0.3.0)
|
||||
nap (1.1.0)
|
||||
naturally (2.2.1)
|
||||
netrc (0.11.0)
|
||||
optparse (0.1.1)
|
||||
optparse (0.4.0)
|
||||
os (1.1.4)
|
||||
plist (3.6.0)
|
||||
plist (3.7.1)
|
||||
public_suffix (4.0.7)
|
||||
rake (13.0.6)
|
||||
rake (13.1.0)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
rexml (3.2.6)
|
||||
rouge (2.0.7)
|
||||
ruby-macho (2.5.1)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
security (0.1.3)
|
||||
signet (0.17.0)
|
||||
signet (0.18.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.8)
|
||||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
trailblazer-option (0.1.2)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.8.1)
|
||||
tty-screen (0.8.2)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
typhoeus (1.4.0)
|
||||
@ -256,11 +255,10 @@ GEM
|
||||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (1.8.0)
|
||||
webrick (1.7.0)
|
||||
unf_ext (0.0.9.1)
|
||||
unicode-display_width (2.5.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.22.0)
|
||||
xcodeproj (1.24.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
@ -275,6 +273,7 @@ GEM
|
||||
|
||||
PLATFORMS
|
||||
universal-darwin-21
|
||||
universal-darwin-23
|
||||
x86_64-darwin-19
|
||||
|
||||
DEPENDENCIES
|
||||
|
@ -34,5 +34,8 @@ end
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = 15.0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,17 +1,17 @@
|
||||
PODS:
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- flutter_email_sender (0.0.1):
|
||||
- Flutter
|
||||
- flutter_inappwebview (0.0.1):
|
||||
- flutter_inappwebview_ios (0.0.1):
|
||||
- Flutter
|
||||
- flutter_inappwebview/Core (= 0.0.1)
|
||||
- flutter_inappwebview_ios/Core (= 0.0.1)
|
||||
- OrderedSet (~> 5.0)
|
||||
- flutter_inappwebview/Core (0.0.1):
|
||||
- flutter_inappwebview_ios/Core (0.0.1):
|
||||
- Flutter
|
||||
- OrderedSet (~> 5.0)
|
||||
- flutter_local_notifications (0.0.1):
|
||||
@ -20,21 +20,21 @@ PODS:
|
||||
- Flutter
|
||||
- flutter_siri_suggestions (0.0.1):
|
||||
- Flutter
|
||||
- FMDB (2.7.5):
|
||||
- FMDB/standard (= 2.7.5)
|
||||
- FMDB/standard (2.7.5)
|
||||
- in_app_review (0.2.0):
|
||||
- Flutter
|
||||
- integration_test (0.0.1):
|
||||
- Flutter
|
||||
- MTBBarcodeScanner (5.0.11)
|
||||
- OrderedSet (5.0.0)
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- ReachabilitySwift (5.0.0)
|
||||
- receive_sharing_intent (0.0.1):
|
||||
- qr_code_scanner (0.2.0):
|
||||
- Flutter
|
||||
- MTBBarcodeScanner
|
||||
- receive_sharing_intent (1.5.3):
|
||||
- Flutter
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
@ -43,7 +43,7 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.3):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
- FlutterMacOS
|
||||
- synced_shared_preferences (0.0.1):
|
||||
- Flutter
|
||||
- url_launcher_ios (0.0.1):
|
||||
@ -56,11 +56,11 @@ PODS:
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- 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_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`)
|
||||
@ -68,10 +68,11 @@ DEPENDENCIES:
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/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`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/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`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- wakelock (from `.symlinks/plugins/wakelock/ios`)
|
||||
@ -80,21 +81,20 @@ DEPENDENCIES:
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- FMDB
|
||||
- MTBBarcodeScanner
|
||||
- OrderedSet
|
||||
- ReachabilitySwift
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
:path: ".symlinks/plugins/connectivity_plus/darwin"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_email_sender:
|
||||
:path: ".symlinks/plugins/flutter_email_sender/ios"
|
||||
flutter_inappwebview:
|
||||
:path: ".symlinks/plugins/flutter_inappwebview/ios"
|
||||
flutter_inappwebview_ios:
|
||||
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
||||
flutter_local_notifications:
|
||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||
flutter_secure_storage:
|
||||
@ -109,6 +109,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
qr_code_scanner:
|
||||
:path: ".symlinks/plugins/qr_code_scanner/ios"
|
||||
receive_sharing_intent:
|
||||
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
||||
share_plus:
|
||||
@ -116,7 +118,7 @@ EXTERNAL SOURCES:
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/ios"
|
||||
:path: ".symlinks/plugins/sqflite/darwin"
|
||||
synced_shared_preferences:
|
||||
:path: ".symlinks/plugins/synced_shared_preferences/ios"
|
||||
url_launcher_ios:
|
||||
@ -129,31 +131,31 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/workmanager/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
||||
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
|
||||
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
|
||||
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
|
||||
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_email_sender: 10a22605f92809a11ef52b2f412db806c6082d40
|
||||
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
|
||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
|
||||
integration_test: 13825b8a9334a850581300559b8839134b124670
|
||||
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
||||
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
|
||||
path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
|
||||
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
|
||||
shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c
|
||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
|
||||
receive_sharing_intent: 753f808c6be5550247f6a20f2a14972466a5f33c
|
||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
|
||||
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
|
||||
webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36
|
||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||
|
||||
PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937
|
||||
PODFILE CHECKSUM: 0957b955069bb512c22bae4cadad9f4c34161dbe
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
COCOAPODS: 1.13.0
|
||||
|
@ -230,13 +230,13 @@
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
E2E6E097A94005D9196D0A71 /* [CP] Check Pods Manifest.lock */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
E51D52B8283B464E00FC8DD8 /* Embed App Extensions */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
E51D52B8283B464E00FC8DD8 /* Embed App Extensions */,
|
||||
F1959755D5521D58CA193498 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
@ -291,7 +291,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1330;
|
||||
LastUpgradeCheck = 1300;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
@ -548,7 +548,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@ -636,7 +636,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@ -685,7 +685,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1300"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -76,5 +76,9 @@
|
||||
<false/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs camera access to scan QR codes</string>
|
||||
<key>io.flutter.embedded_views_preview</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -11,13 +11,13 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
AuthBloc({
|
||||
AuthRepository? authRepository,
|
||||
PreferenceRepository? preferenceRepository,
|
||||
StoriesRepository? storiesRepository,
|
||||
HackerNewsRepository? hackerNewsRepository,
|
||||
SembastRepository? sembastRepository,
|
||||
}) : _authRepository = authRepository ?? locator.get<AuthRepository>(),
|
||||
_preferenceRepository =
|
||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||
_storiesRepository =
|
||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||
_hackerNewsRepository =
|
||||
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
|
||||
_sembastRepository =
|
||||
sembastRepository ?? locator.get<SembastRepository>(),
|
||||
super(const AuthState.init()) {
|
||||
@ -31,7 +31,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
|
||||
final AuthRepository _authRepository;
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
final StoriesRepository _storiesRepository;
|
||||
final HackerNewsRepository _hackerNewsRepository;
|
||||
final SembastRepository _sembastRepository;
|
||||
|
||||
Future<void> onInitialize(
|
||||
@ -41,7 +41,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
await _authRepository.loggedIn.then((bool loggedIn) async {
|
||||
if (loggedIn) {
|
||||
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,
|
||||
/// if user has no public activity (posting a comment or story),
|
||||
@ -52,14 +52,14 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
state.copyWith(
|
||||
isLoggedIn: true,
|
||||
user: user,
|
||||
status: AuthStatus.loaded,
|
||||
status: Status.success,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
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 {
|
||||
emit(state.copyWith(status: AuthStatus.loading));
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
|
||||
final bool successful = await _authRepository.login(
|
||||
username: event.username,
|
||||
@ -89,16 +89,17 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
);
|
||||
|
||||
if (successful) {
|
||||
final User? user = await _storiesRepository.fetchUser(id: event.username);
|
||||
final User? user =
|
||||
await _hackerNewsRepository.fetchUser(id: event.username);
|
||||
emit(
|
||||
state.copyWith(
|
||||
user: user ?? User.emptyWithId(event.username),
|
||||
isLoggedIn: true,
|
||||
status: AuthStatus.loaded,
|
||||
status: Status.success,
|
||||
),
|
||||
);
|
||||
} 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 _preferenceRepository.updateUnreadCommentsIds(<int>[]);
|
||||
await _sembastRepository.deleteAll();
|
||||
await _sembastRepository.deleteCachedComments();
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,5 @@
|
||||
part of 'auth_bloc.dart';
|
||||
|
||||
enum AuthStatus {
|
||||
loading,
|
||||
loaded,
|
||||
failure,
|
||||
}
|
||||
|
||||
class AuthState extends Equatable {
|
||||
const AuthState({
|
||||
required this.user,
|
||||
@ -17,13 +11,13 @@ class AuthState extends Equatable {
|
||||
const AuthState.init()
|
||||
: user = const User.empty(),
|
||||
isLoggedIn = false,
|
||||
status = AuthStatus.loaded,
|
||||
status = Status.success,
|
||||
agreedToEULA = false;
|
||||
|
||||
final User user;
|
||||
final bool isLoggedIn;
|
||||
final bool agreedToEULA;
|
||||
final AuthStatus status;
|
||||
final Status status;
|
||||
|
||||
String get username => user.id;
|
||||
|
||||
@ -31,7 +25,7 @@ class AuthState extends Equatable {
|
||||
User? user,
|
||||
bool? isLoggedIn,
|
||||
bool? agreedToEULA,
|
||||
AuthStatus? status,
|
||||
Status? status,
|
||||
}) {
|
||||
return AuthState(
|
||||
user: user ?? this.user,
|
||||
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:bloc_concurrency/bloc_concurrency.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
@ -12,6 +13,7 @@ import 'package:responsive_builder/responsive_builder.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
part 'stories_event.dart';
|
||||
|
||||
part 'stories_state.dart';
|
||||
|
||||
class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
@ -19,29 +21,37 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
required PreferenceCubit preferenceCubit,
|
||||
required FilterCubit filterCubit,
|
||||
OfflineRepository? offlineRepository,
|
||||
StoriesRepository? storiesRepository,
|
||||
HackerNewsRepository? hackerNewsRepository,
|
||||
PreferenceRepository? preferenceRepository,
|
||||
Logger? logger,
|
||||
}) : _preferenceCubit = preferenceCubit,
|
||||
_filterCubit = filterCubit,
|
||||
_offlineRepository =
|
||||
offlineRepository ?? locator.get<OfflineRepository>(),
|
||||
_storiesRepository =
|
||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||
_hackerNewsRepository =
|
||||
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
|
||||
_preferenceRepository =
|
||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(const StoriesState.init()) {
|
||||
on<LoadStories>(
|
||||
onLoadStories,
|
||||
transformer: sequential(),
|
||||
);
|
||||
on<StoriesInitialize>(onInitialize);
|
||||
on<StoriesRefresh>(onRefresh);
|
||||
on<StoriesLoadMore>(onLoadMore);
|
||||
on<StoryLoaded>(onStoryLoaded);
|
||||
on<StoryLoaded>(
|
||||
onStoryLoaded,
|
||||
transformer: sequential(),
|
||||
);
|
||||
on<StoryRead>(onStoryRead);
|
||||
on<StoriesLoaded>(onStoriesLoaded);
|
||||
on<StoryUnread>(onStoryUnread);
|
||||
on<StoriesDownload>(onDownload);
|
||||
on<StoriesCancelDownload>(onCancelDownload);
|
||||
on<StoryDownloaded>(onStoryDownloaded);
|
||||
on<StoriesExitOffline>(onExitOffline);
|
||||
on<StoriesEnterOfflineMode>(onEnterOfflineMode);
|
||||
on<StoriesExitOfflineMode>(onExitOfflineMode);
|
||||
on<StoriesPageSizeChanged>(onPageSizeChanged);
|
||||
on<ClearAllReadStories>(onClearAllReadStories);
|
||||
}
|
||||
@ -49,11 +59,11 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
final PreferenceCubit _preferenceCubit;
|
||||
final FilterCubit _filterCubit;
|
||||
final OfflineRepository _offlineRepository;
|
||||
final StoriesRepository _storiesRepository;
|
||||
final HackerNewsRepository _hackerNewsRepository;
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
final Logger _logger;
|
||||
DeviceScreenType? deviceScreenType;
|
||||
StreamSubscription<PreferenceState>? _streamSubscription;
|
||||
StreamSubscription<PreferenceState>? _preferenceSubscription;
|
||||
static const int _smallPageSize = 10;
|
||||
static const int _largePageSize = 20;
|
||||
static const int _tabletSmallPageSize = 15;
|
||||
@ -63,69 +73,76 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
StoriesInitialize event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
_streamSubscription ??=
|
||||
_preferenceCubit.stream.listen((PreferenceState event) {
|
||||
final bool isComplexTile = event.complexStoryTileEnabled;
|
||||
_preferenceSubscription ??= _preferenceCubit.stream
|
||||
.distinct((PreferenceState previous, PreferenceState next) {
|
||||
return previous.isComplexStoryTileEnabled ==
|
||||
next.isComplexStoryTileEnabled;
|
||||
})
|
||||
//.debounceTime(AppDurations.twoSeconds)
|
||||
.listen((PreferenceState event) {
|
||||
final bool isComplexTile = event.isComplexStoryTileEnabled;
|
||||
final int pageSize = getPageSize(isComplexTile: isComplexTile);
|
||||
|
||||
if (pageSize != state.currentPageSize) {
|
||||
add(StoriesPageSizeChanged(pageSize: pageSize));
|
||||
}
|
||||
});
|
||||
final bool hasCachedStories = await _offlineRepository.hasCachedStories;
|
||||
final bool isComplexTile = _preferenceCubit.state.complexStoryTileEnabled;
|
||||
final bool isComplexTile = _preferenceCubit.state.isComplexStoryTileEnabled;
|
||||
final int pageSize = getPageSize(isComplexTile: isComplexTile);
|
||||
emit(
|
||||
const StoriesState.init().copyWith(
|
||||
isOfflineReading: hasCachedStories &&
|
||||
// Only go into offline mode in the next session.
|
||||
state.downloadStatus == StoriesDownloadStatus.initial,
|
||||
currentPageSize: pageSize,
|
||||
downloadStatus: state.downloadStatus,
|
||||
storiesDownloaded: state.storiesDownloaded,
|
||||
storiesToBeDownloaded: state.storiesToBeDownloaded,
|
||||
isOfflineReading: state.isOfflineReading,
|
||||
),
|
||||
);
|
||||
for (final StoryType type in StoryType.values) {
|
||||
await loadStories(type: type, emit: emit);
|
||||
|
||||
for (final StoryType type in _preferenceCubit.state.tabs) {
|
||||
add(LoadStories(type: type));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadStories({
|
||||
required StoryType type,
|
||||
required Emitter<StoriesState> emit,
|
||||
}) async {
|
||||
Future<void> onLoadStories(
|
||||
LoadStories event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
final StoryType type = event.type;
|
||||
if (state.isOfflineReading) {
|
||||
final List<int> ids =
|
||||
await _offlineRepository.getCachedStoryIds(type: type);
|
||||
emit(
|
||||
state
|
||||
.copyWithStoryIdsUpdated(type: type, to: ids)
|
||||
.copyWithCurrentPageUpdated(type: type, to: 0),
|
||||
.copyWithCurrentPageUpdated(type: type, to: 0)
|
||||
.copyWithStatusUpdated(type: type, to: Status.inProgress),
|
||||
);
|
||||
_offlineRepository
|
||||
.getCachedStoriesStream(
|
||||
ids: ids.sublist(0, min(ids.length, state.currentPageSize)),
|
||||
)
|
||||
.listen((Story story) {
|
||||
add(StoryLoaded(story: story, type: type));
|
||||
}).onDone(() {
|
||||
add(StoriesLoaded(type: type));
|
||||
});
|
||||
ids: ids.sublist(0, min(ids.length, state.currentPageSize)),
|
||||
)
|
||||
.listen((Story story) => add(StoryLoaded(story: story, type: type)))
|
||||
.onDone(() => add(StoryLoadingCompleted(type: type)));
|
||||
} else {
|
||||
final List<int> ids = await _storiesRepository.fetchStoryIds(type: type);
|
||||
final List<int> ids =
|
||||
await _hackerNewsRepository.fetchStoryIds(type: type);
|
||||
emit(
|
||||
state
|
||||
.copyWithStoryIdsUpdated(type: type, to: ids)
|
||||
.copyWithCurrentPageUpdated(type: type, to: 0),
|
||||
.copyWithCurrentPageUpdated(type: type, to: 0)
|
||||
.copyWithStatusUpdated(type: type, to: Status.inProgress),
|
||||
);
|
||||
_storiesRepository
|
||||
.fetchStoriesStream(ids: ids.sublist(0, state.currentPageSize))
|
||||
await _hackerNewsRepository
|
||||
.fetchStoriesStream(
|
||||
ids: ids.sublist(0, state.currentPageSize),
|
||||
sequential: _preferenceCubit.state.isComplexStoryTileEnabled ||
|
||||
_preferenceCubit.state.isFaviconEnabled,
|
||||
)
|
||||
.listen((Story story) {
|
||||
add(StoryLoaded(story: story, type: type));
|
||||
}).onDone(() {
|
||||
add(StoriesLoaded(type: type));
|
||||
});
|
||||
}).asFuture<void>();
|
||||
add(StoryLoadingCompleted(type: type));
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,10 +150,12 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
StoriesRefresh event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
if (state.statusByType[event.type] == Status.inProgress) return;
|
||||
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
type: event.type,
|
||||
to: StoriesStatus.loading,
|
||||
to: Status.inProgress,
|
||||
),
|
||||
);
|
||||
|
||||
@ -144,20 +163,22 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
type: event.type,
|
||||
to: StoriesStatus.loaded,
|
||||
to: Status.success,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(state.copyWithRefreshed(type: event.type));
|
||||
await loadStories(type: event.type, emit: emit);
|
||||
add(LoadStories(type: event.type, isRefreshing: true));
|
||||
}
|
||||
}
|
||||
|
||||
void onLoadMore(StoriesLoadMore event, Emitter<StoriesState> emit) {
|
||||
if (state.statusByType[event.type] == Status.inProgress) return;
|
||||
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
type: event.type,
|
||||
to: StoriesStatus.loading,
|
||||
to: Status.inProgress,
|
||||
),
|
||||
);
|
||||
|
||||
@ -178,45 +199,33 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
if (state.isOfflineReading) {
|
||||
_offlineRepository
|
||||
.getCachedStoriesStream(
|
||||
ids: state.storyIdsByType[event.type]!.sublist(
|
||||
lower,
|
||||
upper,
|
||||
),
|
||||
)
|
||||
.listen((Story story) {
|
||||
add(
|
||||
StoryLoaded(
|
||||
story: story,
|
||||
type: event.type,
|
||||
),
|
||||
);
|
||||
}).onDone(() {
|
||||
add(StoriesLoaded(type: event.type));
|
||||
});
|
||||
ids: state.storyIdsByType[event.type]!.sublist(
|
||||
lower,
|
||||
upper,
|
||||
),
|
||||
)
|
||||
.listen(
|
||||
(Story story) => add(StoryLoaded(story: story, type: event.type)),
|
||||
)
|
||||
.onDone(() => add(StoryLoadingCompleted(type: event.type)));
|
||||
} else {
|
||||
_storiesRepository
|
||||
_hackerNewsRepository
|
||||
.fetchStoriesStream(
|
||||
ids: state.storyIdsByType[event.type]!.sublist(
|
||||
lower,
|
||||
upper,
|
||||
),
|
||||
)
|
||||
.listen((Story story) {
|
||||
add(
|
||||
StoryLoaded(
|
||||
story: story,
|
||||
type: event.type,
|
||||
),
|
||||
);
|
||||
}).onDone(() {
|
||||
add(StoriesLoaded(type: event.type));
|
||||
});
|
||||
ids: state.storyIdsByType[event.type]!.sublist(
|
||||
lower,
|
||||
upper,
|
||||
),
|
||||
)
|
||||
.listen(
|
||||
(Story story) => add(StoryLoaded(story: story, type: event.type)),
|
||||
)
|
||||
.onDone(() => add(StoryLoadingCompleted(type: event.type)));
|
||||
}
|
||||
} else {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
type: event.type,
|
||||
to: StoriesStatus.loaded,
|
||||
to: Status.success,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -226,27 +235,35 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
StoryLoaded event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
final bool hasRead = await _preferenceRepository.hasRead(event.story.id);
|
||||
final bool hidden = _filterCubit.state.keywords.any(
|
||||
(String keyword) =>
|
||||
event.story.title.toLowerCase().contains(keyword) ||
|
||||
event.story.text.toLowerCase().contains(keyword),
|
||||
);
|
||||
if (event is StoryLoadingCompleted) {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(type: event.type, to: Status.success),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final Story story = event.story;
|
||||
if (state.storiesByType[event.type]?.contains(story) ?? false) {
|
||||
_logger.d('story already exists.');
|
||||
return;
|
||||
}
|
||||
final bool hasRead = await _preferenceRepository.hasRead(story.id);
|
||||
final bool hidden = _filterCubit.state.keywords.any((String keyword) {
|
||||
// Match word only.
|
||||
final RegExp regExp = RegExp('\\b($keyword)\\b');
|
||||
return regExp.hasMatch(story.title.toLowerCase()) ||
|
||||
regExp.hasMatch(story.text.toLowerCase());
|
||||
});
|
||||
|
||||
emit(
|
||||
state.copyWithStoryAdded(
|
||||
type: event.type,
|
||||
story: event.story.copyWith(hidden: hidden),
|
||||
story: story.copyWith(hidden: hidden),
|
||||
hasRead: hasRead,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void onStoriesLoaded(StoriesLoaded event, Emitter<StoriesState> emit) {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(type: event.type, to: StoriesStatus.loaded),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onDownload(
|
||||
StoriesDownload event,
|
||||
Emitter<StoriesState> emit,
|
||||
@ -260,6 +277,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
await _offlineRepository.deleteAllStoryIds();
|
||||
await _offlineRepository.deleteAllStories();
|
||||
await _offlineRepository.deleteAllComments();
|
||||
await _offlineRepository.deleteAllWebPages();
|
||||
|
||||
final Set<int> prioritizedIds = <int>{};
|
||||
|
||||
@ -269,7 +287,8 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
..remove(StoryType.latest);
|
||||
|
||||
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);
|
||||
prioritizedIds.addAll(ids);
|
||||
}
|
||||
@ -289,7 +308,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
);
|
||||
|
||||
final Set<int> latestIds = <int>{};
|
||||
final List<int> ids = await _storiesRepository.fetchStoryIds(
|
||||
final List<int> ids = await _hackerNewsRepository.fetchStoryIds(
|
||||
type: StoryType.latest,
|
||||
);
|
||||
await _offlineRepository.cacheStoryIds(type: StoryType.latest, ids: ids);
|
||||
@ -343,7 +362,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
}
|
||||
|
||||
_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 (isPrioritized) {
|
||||
@ -373,7 +392,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
/// In other words, we are prioritizing the story itself instead of
|
||||
/// the comments in the story.
|
||||
late final StreamSubscription<Comment>? downloadStream;
|
||||
downloadStream = _storiesRepository
|
||||
downloadStream = _hackerNewsRepository
|
||||
.fetchAllChildrenComments(ids: story.kids)
|
||||
.whereType<Comment>()
|
||||
.listen(
|
||||
@ -440,23 +459,27 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
add(StoriesInitialize());
|
||||
}
|
||||
|
||||
Future<void> onExitOffline(
|
||||
StoriesExitOffline event,
|
||||
Future<void> onExitOfflineMode(
|
||||
StoriesExitOfflineMode event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
await _offlineRepository.deleteAllStoryIds();
|
||||
await _offlineRepository.deleteAllStories();
|
||||
await _offlineRepository.deleteAllComments();
|
||||
await _offlineRepository.deleteAllWebPages();
|
||||
emit(state.copyWith(isOfflineReading: false));
|
||||
add(StoriesInitialize());
|
||||
}
|
||||
|
||||
Future<void> onEnterOfflineMode(
|
||||
StoriesEnterOfflineMode event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(isOfflineReading: true));
|
||||
add(StoriesInitialize());
|
||||
}
|
||||
|
||||
Future<void> onStoryRead(
|
||||
StoryRead event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
unawaited(_preferenceRepository.updateHasRead(event.story.id));
|
||||
unawaited(_preferenceRepository.addHasRead(event.story.id));
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -465,6 +488,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(
|
||||
ClearAllReadStories event,
|
||||
Emitter<StoriesState> emit,
|
||||
@ -492,7 +528,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _streamSubscription?.cancel();
|
||||
await _preferenceSubscription?.cancel();
|
||||
await super.close();
|
||||
}
|
||||
}
|
||||
|
@ -5,20 +5,24 @@ abstract class StoriesEvent extends Equatable {
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
class LoadStories extends StoriesEvent {
|
||||
LoadStories({required this.type, this.isRefreshing = false});
|
||||
|
||||
final StoryType type;
|
||||
final bool isRefreshing;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
type,
|
||||
isRefreshing,
|
||||
];
|
||||
}
|
||||
|
||||
class StoriesInitialize extends StoriesEvent {
|
||||
@override
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
class StoriesLoaded extends StoriesEvent {
|
||||
StoriesLoaded({required this.type});
|
||||
|
||||
final StoryType type;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[type];
|
||||
}
|
||||
|
||||
class StoriesRefresh extends StoriesEvent {
|
||||
StoriesRefresh({required this.type});
|
||||
|
||||
@ -62,7 +66,12 @@ class StoryDownloaded extends StoriesEvent {
|
||||
List<Object?> get props => <Object?>[skipped];
|
||||
}
|
||||
|
||||
class StoriesExitOffline extends StoriesEvent {
|
||||
class StoriesExitOfflineMode extends StoriesEvent {
|
||||
@override
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
class StoriesEnterOfflineMode extends StoriesEvent {
|
||||
@override
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
@ -86,6 +95,10 @@ class StoryLoaded extends StoriesEvent {
|
||||
List<Object?> get props => <Object?>[story, type];
|
||||
}
|
||||
|
||||
class StoryLoadingCompleted extends StoryLoaded {
|
||||
StoryLoadingCompleted({required super.type}) : super(story: Story.empty());
|
||||
}
|
||||
|
||||
class StoryRead extends StoriesEvent {
|
||||
StoryRead({required this.story});
|
||||
|
||||
@ -95,6 +108,15 @@ class StoryRead extends StoriesEvent {
|
||||
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 {
|
||||
@override
|
||||
List<Object?> get props => <Object?>[];
|
||||
|
@ -1,13 +1,7 @@
|
||||
part of 'stories_bloc.dart';
|
||||
|
||||
enum StoriesStatus {
|
||||
initial,
|
||||
loading,
|
||||
loaded,
|
||||
}
|
||||
|
||||
enum StoriesDownloadStatus {
|
||||
initial,
|
||||
idle,
|
||||
downloading,
|
||||
finished,
|
||||
failure,
|
||||
@ -43,12 +37,12 @@ class StoriesState extends Equatable {
|
||||
StoryType.ask: <int>[],
|
||||
StoryType.show: <int>[],
|
||||
},
|
||||
this.statusByType = const <StoryType, StoriesStatus>{
|
||||
StoryType.top: StoriesStatus.initial,
|
||||
StoryType.best: StoriesStatus.initial,
|
||||
StoryType.latest: StoriesStatus.initial,
|
||||
StoryType.ask: StoriesStatus.initial,
|
||||
StoryType.show: StoriesStatus.initial,
|
||||
this.statusByType = const <StoryType, Status>{
|
||||
StoryType.top: Status.idle,
|
||||
StoryType.best: Status.idle,
|
||||
StoryType.latest: Status.idle,
|
||||
StoryType.ask: Status.idle,
|
||||
StoryType.show: Status.idle,
|
||||
},
|
||||
this.currentPageByType = const <StoryType, int>{
|
||||
StoryType.top: 0,
|
||||
@ -58,7 +52,7 @@ class StoriesState extends Equatable {
|
||||
StoryType.show: 0,
|
||||
},
|
||||
}) : isOfflineReading = false,
|
||||
downloadStatus = StoriesDownloadStatus.initial,
|
||||
downloadStatus = StoriesDownloadStatus.idle,
|
||||
currentPageSize = 0,
|
||||
readStoriesIds = const <int>{},
|
||||
storiesDownloaded = 0,
|
||||
@ -66,7 +60,7 @@ class StoriesState extends Equatable {
|
||||
|
||||
final Map<StoryType, List<Story>> storiesByType;
|
||||
final Map<StoryType, List<int>> storyIdsByType;
|
||||
final Map<StoryType, StoriesStatus> statusByType;
|
||||
final Map<StoryType, Status> statusByType;
|
||||
final Map<StoryType, int> currentPageByType;
|
||||
final Set<int> readStoriesIds;
|
||||
final StoriesDownloadStatus downloadStatus;
|
||||
@ -78,7 +72,7 @@ class StoriesState extends Equatable {
|
||||
StoriesState copyWith({
|
||||
Map<StoryType, List<Story>>? storiesByType,
|
||||
Map<StoryType, List<int>>? storyIdsByType,
|
||||
Map<StoryType, StoriesStatus>? statusByType,
|
||||
Map<StoryType, Status>? statusByType,
|
||||
Map<StoryType, int>? currentPageByType,
|
||||
Set<int>? readStoriesIds,
|
||||
StoriesDownloadStatus? downloadStatus,
|
||||
@ -133,10 +127,10 @@ class StoriesState extends Equatable {
|
||||
|
||||
StoriesState copyWithStatusUpdated({
|
||||
required StoryType type,
|
||||
required StoriesStatus to,
|
||||
required Status to,
|
||||
}) {
|
||||
final Map<StoryType, StoriesStatus> newMap =
|
||||
Map<StoryType, StoriesStatus>.from(statusByType);
|
||||
final Map<StoryType, Status> newMap =
|
||||
Map<StoryType, Status>.from(statusByType);
|
||||
newMap[type] = to;
|
||||
return copyWith(
|
||||
statusByType: newMap,
|
||||
@ -162,9 +156,9 @@ class StoriesState extends Equatable {
|
||||
final Map<StoryType, List<int>> newStoryIdsMap =
|
||||
Map<StoryType, List<int>>.from(storyIdsByType);
|
||||
newStoryIdsMap[type] = <int>[];
|
||||
final Map<StoryType, StoriesStatus> newStatusMap =
|
||||
Map<StoryType, StoriesStatus>.from(statusByType);
|
||||
newStatusMap[type] = StoriesStatus.loading;
|
||||
final Map<StoryType, Status> newStatusMap =
|
||||
Map<StoryType, Status>.from(statusByType);
|
||||
newStatusMap[type] = Status.inProgress;
|
||||
final Map<StoryType, int> newCurrentPageMap =
|
||||
Map<StoryType, int>.from(currentPageByType);
|
||||
newCurrentPageMap[type] = 0;
|
||||
|
@ -20,6 +20,8 @@ abstract class Constants {
|
||||
'$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 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 _imagePath = 'assets/images';
|
||||
@ -34,14 +36,6 @@ abstract class Constants {
|
||||
static const String logFilename = '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>[
|
||||
'(๑•̀ㅂ•́)و✧',
|
||||
'( ͡• ͜ʖ ͡•)',
|
||||
@ -72,9 +66,30 @@ abstract class Constants {
|
||||
static final String errorMessage = 'Something went wrong...$sadFace';
|
||||
static final String loginErrorMessage =
|
||||
'''Failed to log in $sadFace, this could happen if your account requires a CAPTCHA, please try logging in inside a browser to see if this is the case, if so, you may try logging in here again later after CAPTCHA is no longer needed.''';
|
||||
|
||||
static String favicon(String url, {int size = 32}) {
|
||||
final Uri uri = Uri.parse(url);
|
||||
final String host = uri.host;
|
||||
return 'https://www.google.com/s2/favicons?domain=$host&sz=$size';
|
||||
}
|
||||
}
|
||||
|
||||
abstract class RegExpConstants {
|
||||
static const String linkSuffix = r'(\)|]|,|\*)(.)*$';
|
||||
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 {
|
||||
@override
|
||||
Level? get level => Level.verbose;
|
||||
Level? get level => Level.trace;
|
||||
|
||||
/// The minimal level allowed in production.
|
||||
static const Level _minimalLevel = Level.info;
|
||||
|
@ -1,49 +1,76 @@
|
||||
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';
|
||||
|
||||
/// Custom router.
|
||||
///
|
||||
/// Handle named routing.
|
||||
class CustomRouter {
|
||||
/// Top level routing.
|
||||
static Route<dynamic> onGenerateRoute(RouteSettings settings) {
|
||||
switch (settings.name) {
|
||||
case HomeScreen.routeName:
|
||||
return HomeScreen.route();
|
||||
case ItemScreen.routeName:
|
||||
return ItemScreen.route(settings.arguments! as ItemScreenArgs);
|
||||
case SubmitScreen.routeName:
|
||||
return SubmitScreen.route();
|
||||
default:
|
||||
return _errorRoute();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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),
|
||||
final GoRouter router = GoRouter(
|
||||
observers: <NavigatorObserver>[
|
||||
locator.get<RouteObserver<ModalRoute<dynamic>>>(),
|
||||
],
|
||||
initialLocation: HomeScreen.routeName,
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: HomeScreen.routeName,
|
||||
builder: (_, __) => const HomeScreen(),
|
||||
routes: <RouteBase>[
|
||||
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: '/${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;
|
||||
|
||||
@override
|
||||
void init() {
|
||||
Future<void> init() async {
|
||||
_sink = file.openWrite(
|
||||
mode: overrideExisting ? FileMode.writeOnly : FileMode.writeOnlyAppend,
|
||||
encoding: encoding,
|
||||
|
@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:hacki/config/custom_log_filter.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
@ -23,12 +24,15 @@ Future<void> setUpLocator() async {
|
||||
output: LogUtil.logOutput(logOutputFile),
|
||||
),
|
||||
)
|
||||
..registerSingleton<StoriesRepository>(StoriesRepository())
|
||||
..registerSingleton<SembastRepository>(SembastRepository())
|
||||
..registerSingleton<RemoteConfigRepository>(RemoteConfigRepository())
|
||||
..registerSingleton<RemoteConfigCubit>(RemoteConfigCubit())
|
||||
..registerSingleton<HackerNewsRepository>(HackerNewsRepository())
|
||||
..registerSingleton<HackerNewsWebRepository>(HackerNewsWebRepository())
|
||||
..registerSingleton<PreferenceRepository>(PreferenceRepository())
|
||||
..registerSingleton<SearchRepository>(SearchRepository())
|
||||
..registerSingleton<AuthRepository>(AuthRepository())
|
||||
..registerSingleton<PostRepository>(PostRepository())
|
||||
..registerSingleton<SembastRepository>(SembastRepository())
|
||||
..registerSingleton<OfflineRepository>(OfflineRepository())
|
||||
..registerSingleton<DraftCache>(DraftCache())
|
||||
..registerSingleton<CommentCache>(CommentCache())
|
||||
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
|
||||
@ -28,11 +29,12 @@ class CollapseCubit extends Cubit<CollapseState> {
|
||||
collapsedCount: _collapseCache.totalHidden(_commentId),
|
||||
collapsed: _collapseCache.isCollapsed(_commentId),
|
||||
hidden: _collapseCache.isHidden(_commentId),
|
||||
locked: _collapseCache.lockedId == _commentId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void collapse() {
|
||||
void collapse({required VoidCallback onStateChanged}) {
|
||||
if (state.collapsed) {
|
||||
_collapseCache.uncollapse(_commentId);
|
||||
|
||||
@ -42,7 +44,14 @@ class CollapseCubit extends Cubit<CollapseState> {
|
||||
collapsedCount: 0,
|
||||
),
|
||||
);
|
||||
|
||||
onStateChanged();
|
||||
} else {
|
||||
if (state.locked) {
|
||||
emit(state.copyWith(locked: false));
|
||||
return;
|
||||
}
|
||||
|
||||
final Set<int> collapsedCommentIds = _collapseCache.collapse(_commentId);
|
||||
|
||||
emit(
|
||||
@ -51,6 +60,8 @@ class CollapseCubit extends Cubit<CollapseState> {
|
||||
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
|
||||
Future<void> close() async {
|
||||
await _streamSubscription.cancel();
|
||||
|