Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
c685f33f99 | |||
518608893d | |||
856efa7c14 | |||
d1957ffb82 | |||
553a37961d | |||
bade5b4356 | |||
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 |
2
.github/workflows/commit_check.yml
vendored
@ -7,7 +7,7 @@ on:
|
||||
- '!master'
|
||||
|
||||
jobs:
|
||||
releases:
|
||||
commit_check:
|
||||
name: Check commit
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 30
|
||||
|
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 }}
|
9
.github/workflows/publish_ios.yml
vendored
@ -1,12 +1,17 @@
|
||||
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:
|
||||
|
@ -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()) {
|
||||
@ -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]
|
||||
|
BIN
android/app/src/main/res/drawable-night-v21/background.png
Normal file
After Width: | Height: | Size: 69 B |
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
</layer-list>
|
BIN
android/app/src/main/res/drawable-night/background.png
Normal file
After Width: | Height: | Size: 69 B |
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
</layer-list>
|
BIN
android/app/src/main/res/drawable-v21/background.png
Normal file
After Width: | Height: | Size: 69 B |
@ -1,12 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
BIN
android/app/src/main/res/drawable/background.png
Normal file
After Width: | Height: | Size: 69 B |
@ -1,12 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
19
android/app/src/main/res/values-night-v31/styles.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
@ -5,6 +5,10 @@
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
19
android/app/src/main/res/values-v31/styles.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
@ -5,6 +5,10 @@
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
@ -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()
|
||||
|
@ -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
7
assets/remote-config-dev.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"
|
||||
}
|
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"
|
||||
}
|
2
fastlane/metadata/android/en-US/changelogs/145.txt
Normal file
@ -0,0 +1,2 @@
|
||||
- Favicon in mini story tile.
|
||||
- UX improvements.
|
@ -1,7 +1,7 @@
|
||||
PODS:
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
@ -16,6 +16,8 @@ PODS:
|
||||
- OrderedSet (~> 5.0)
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- Flutter
|
||||
- flutter_native_splash (0.0.1):
|
||||
- Flutter
|
||||
- flutter_secure_storage (6.0.0):
|
||||
- Flutter
|
||||
- flutter_siri_suggestions (0.0.1):
|
||||
@ -34,7 +36,6 @@ PODS:
|
||||
- qr_code_scanner (0.2.0):
|
||||
- Flutter
|
||||
- MTBBarcodeScanner
|
||||
- ReachabilitySwift (5.0.0)
|
||||
- receive_sharing_intent (1.5.3):
|
||||
- Flutter
|
||||
- share_plus (0.0.1):
|
||||
@ -57,12 +58,13 @@ 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_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`)
|
||||
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||
@ -84,11 +86,10 @@ SPEC REPOS:
|
||||
trunk:
|
||||
- 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:
|
||||
@ -99,6 +100,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
||||
flutter_local_notifications:
|
||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_secure_storage:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
flutter_siri_suggestions:
|
||||
@ -133,28 +136,28 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/workmanager/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
|
||||
flutter_email_sender: 10a22605f92809a11ef52b2f412db806c6082d40
|
||||
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
|
||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
|
||||
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
|
||||
integration_test: 13825b8a9334a850581300559b8839134b124670
|
||||
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
||||
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
receive_sharing_intent: 753f808c6be5550247f6a20f2a14972466a5f33c
|
||||
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
|
||||
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
|
||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
|
||||
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||
webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36
|
||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||
|
22
ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "background.png",
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "darkbackground.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png
vendored
Normal file
After Width: | Height: | Size: 69 B |
BIN
ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png
vendored
Normal file
After Width: | Height: | Size: 69 B |
@ -1,23 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 69 B |
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 69 B |
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 69 B |
@ -16,13 +16,19 @@
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
|
||||
<constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
|
||||
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
|
||||
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
|
||||
<constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="TQA-XW-tRk"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="duK-uY-Gun"/>
|
||||
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
@ -33,5 +39,6 @@
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
<image name="LaunchBackground" width="1" height="1"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
@ -1,84 +1,86 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>workmanager.background.task</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Hacki</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>hacki</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>ShareMedia</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>https</string>
|
||||
<string>http</string>
|
||||
<string>mailto</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:example.com</string>
|
||||
</array>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<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>
|
||||
<dict>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>workmanager.background.task</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Hacki</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>hacki</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>ShareMedia</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>https</string>
|
||||
<string>http</string>
|
||||
<string>mailto</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:example.com</string>
|
||||
</array>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<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/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -4,6 +4,7 @@ import 'dart:math';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:bloc_concurrency/bloc_concurrency.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
@ -13,6 +14,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> {
|
||||
@ -46,11 +48,11 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
);
|
||||
on<StoryRead>(onStoryRead);
|
||||
on<StoryUnread>(onStoryUnread);
|
||||
on<StoriesLoaded>(onStoriesLoaded);
|
||||
on<StoriesDownload>(onDownload);
|
||||
on<StoriesCancelDownload>(onCancelDownload);
|
||||
on<StoryDownloaded>(onStoryDownloaded);
|
||||
on<StoriesExitOffline>(onExitOffline);
|
||||
on<StoriesEnterOfflineMode>(onEnterOfflineMode);
|
||||
on<StoriesExitOfflineMode>(onExitOfflineMode);
|
||||
on<StoriesPageSizeChanged>(onPageSizeChanged);
|
||||
on<ClearAllReadStories>(onClearAllReadStories);
|
||||
}
|
||||
@ -62,40 +64,44 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
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;
|
||||
static const int _tabletLargePageSize = 25;
|
||||
static const String _logPrefix = '[StoriesBloc]';
|
||||
|
||||
Future<void> onInitialize(
|
||||
StoriesInitialize event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
_streamSubscription ??=
|
||||
_preferenceCubit.stream.listen((PreferenceState event) {
|
||||
final bool isComplexTile = event.complexStoryTileEnabled;
|
||||
final int pageSize = getPageSize(isComplexTile: isComplexTile);
|
||||
_preferenceSubscription ??= _preferenceCubit.stream
|
||||
.distinct((PreferenceState previous, PreferenceState next) {
|
||||
return previous.isComplexStoryTileEnabled ==
|
||||
next.isComplexStoryTileEnabled;
|
||||
})
|
||||
.debounceTime(AppDurations.oneSecond)
|
||||
.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;
|
||||
if (pageSize != state.currentPageSize) {
|
||||
add(StoriesPageSizeChanged(pageSize: pageSize));
|
||||
}
|
||||
});
|
||||
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.idle,
|
||||
currentPageSize: pageSize,
|
||||
downloadStatus: state.downloadStatus,
|
||||
storiesDownloaded: state.storiesDownloaded,
|
||||
storiesToBeDownloaded: state.storiesToBeDownloaded,
|
||||
isOfflineReading: state.isOfflineReading,
|
||||
),
|
||||
);
|
||||
for (final StoryType type in StoryType.values) {
|
||||
|
||||
for (final StoryType type in _preferenceCubit.state.tabs) {
|
||||
add(LoadStories(type: type));
|
||||
}
|
||||
}
|
||||
@ -111,31 +117,34 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
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 _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),
|
||||
);
|
||||
await _hackerNewsRepository
|
||||
.fetchStoriesStream(ids: ids.sublist(0, state.currentPageSize))
|
||||
.fetchStoriesStream(
|
||||
ids: ids.sublist(0, state.currentPageSize),
|
||||
sequential: _preferenceCubit.state.isComplexStoryTileEnabled ||
|
||||
_preferenceCubit.state.isFaviconEnabled,
|
||||
)
|
||||
.listen((Story story) {
|
||||
add(StoryLoaded(story: story, type: type));
|
||||
}).asFuture<void>();
|
||||
add(StoriesLoaded(type: type));
|
||||
add(StoryLoadingCompleted(type: type));
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,11 +170,13 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
);
|
||||
} else {
|
||||
emit(state.copyWithRefreshed(type: event.type));
|
||||
add(LoadStories(type: event.type));
|
||||
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,
|
||||
@ -190,39 +201,27 @@ 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 {
|
||||
_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(
|
||||
@ -238,7 +237,20 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
StoryLoaded event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
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(
|
||||
'$_logPrefix story ${story.id} for ${event.type} already exists.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final bool hasRead = await _preferenceRepository.hasRead(story.id);
|
||||
final bool hidden = _filterCubit.state.keywords.any((String keyword) {
|
||||
// Match word only.
|
||||
@ -246,6 +258,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
return regExp.hasMatch(story.title.toLowerCase()) ||
|
||||
regExp.hasMatch(story.text.toLowerCase());
|
||||
});
|
||||
|
||||
emit(
|
||||
state.copyWithStoryAdded(
|
||||
type: event.type,
|
||||
@ -255,12 +268,6 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
);
|
||||
}
|
||||
|
||||
void onStoriesLoaded(StoriesLoaded event, Emitter<StoriesState> emit) {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(type: event.type, to: Status.success),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onDownload(
|
||||
StoriesDownload event,
|
||||
Emitter<StoriesState> emit,
|
||||
@ -274,6 +281,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
await _offlineRepository.deleteAllStoryIds();
|
||||
await _offlineRepository.deleteAllStories();
|
||||
await _offlineRepository.deleteAllComments();
|
||||
await _offlineRepository.deleteAllWebPages();
|
||||
|
||||
final Set<int> prioritizedIds = <int>{};
|
||||
|
||||
@ -344,20 +352,20 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
<StreamSubscription<Comment>>[];
|
||||
for (final int id in ids) {
|
||||
if (state.downloadStatus == StoriesDownloadStatus.canceled) {
|
||||
_logger.d('aborting downloading');
|
||||
_logger.d('$_logPrefix aborting downloading');
|
||||
|
||||
for (final StreamSubscription<Comment> stream in downloadStreams) {
|
||||
await stream.cancel();
|
||||
}
|
||||
|
||||
_logger.d('deleting downloaded contents');
|
||||
_logger.d('$_logPrefix deleting downloaded contents');
|
||||
await _offlineRepository.deleteAllStoryIds();
|
||||
await _offlineRepository.deleteAllStories();
|
||||
await _offlineRepository.deleteAllComments();
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.d('fetching story $id');
|
||||
_logger.d('$_logPrefix fetching story $id');
|
||||
final Story? story = await _hackerNewsRepository.fetchStory(id: id);
|
||||
|
||||
if (story == null) {
|
||||
@ -377,7 +385,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
await _offlineRepository.cacheStory(story: story);
|
||||
|
||||
if (story.url.isNotEmpty && includingWebPage) {
|
||||
_logger.i('downloading ${story.url}');
|
||||
_logger.i('$_logPrefix downloading ${story.url}');
|
||||
await _offlineRepository.cacheUrl(url: story.url);
|
||||
}
|
||||
|
||||
@ -394,19 +402,19 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
.listen(
|
||||
(Comment comment) {
|
||||
if (state.downloadStatus == StoriesDownloadStatus.canceled) {
|
||||
_logger.d('aborting downloading from comments stream');
|
||||
_logger.d('$_logPrefix aborting downloading from comments stream');
|
||||
downloadStream?.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.d('fetched comment ${comment.id}');
|
||||
_logger.d('$_logPrefix fetched comment ${comment.id}');
|
||||
unawaited(
|
||||
_offlineRepository.cacheComment(comment: comment),
|
||||
);
|
||||
},
|
||||
)..onDone(() {
|
||||
_logger.d(
|
||||
'''finished downloading story ${story.id} with ${story.descendants} comments''',
|
||||
'''$_logPrefix finished downloading story ${story.id} with ${story.descendants} comments''',
|
||||
);
|
||||
add(StoryDownloaded(skipped: false));
|
||||
});
|
||||
@ -455,18 +463,22 @@ 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,
|
||||
@ -520,7 +532,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _streamSubscription?.cancel();
|
||||
await _preferenceSubscription?.cancel();
|
||||
await super.close();
|
||||
}
|
||||
}
|
||||
|
@ -6,12 +6,16 @@ abstract class StoriesEvent extends Equatable {
|
||||
}
|
||||
|
||||
class LoadStories extends StoriesEvent {
|
||||
LoadStories({required this.type});
|
||||
LoadStories({required this.type, this.isRefreshing = false});
|
||||
|
||||
final StoryType type;
|
||||
final bool isRefreshing;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[type];
|
||||
List<Object?> get props => <Object?>[
|
||||
type,
|
||||
isRefreshing,
|
||||
];
|
||||
}
|
||||
|
||||
class StoriesInitialize extends StoriesEvent {
|
||||
@ -19,15 +23,6 @@ class StoriesInitialize extends StoriesEvent {
|
||||
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});
|
||||
|
||||
@ -71,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?>[];
|
||||
}
|
||||
@ -95,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});
|
||||
|
||||
|
@ -66,6 +66,12 @@ 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 {
|
||||
|
@ -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';
|
||||
@ -24,6 +25,8 @@ Future<void> setUpLocator() async {
|
||||
),
|
||||
)
|
||||
..registerSingleton<SembastRepository>(SembastRepository())
|
||||
..registerSingleton<RemoteConfigRepository>(RemoteConfigRepository())
|
||||
..registerSingleton<RemoteConfigCubit>(RemoteConfigCubit())
|
||||
..registerSingleton<HackerNewsRepository>(HackerNewsRepository())
|
||||
..registerSingleton<HackerNewsWebRepository>(HackerNewsWebRepository())
|
||||
..registerSingleton<PreferenceRepository>(PreferenceRepository())
|
||||
|
@ -83,7 +83,8 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
final Map<int, StreamSubscription<Comment>> _streamSubscriptions =
|
||||
<int, StreamSubscription<Comment>>{};
|
||||
|
||||
static const int _webFetchingCmtCountLowerLimit = 50;
|
||||
static const int _webFetchingCmtCountLowerLimit = 5;
|
||||
static const String _logPrefix = '[CommentsCubit]';
|
||||
|
||||
Future<bool> get _shouldFetchFromWeb async {
|
||||
final bool isOnWifi = await _isOnWifi;
|
||||
@ -103,8 +104,9 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
}
|
||||
|
||||
static Future<bool> get _isOnWifi async {
|
||||
final ConnectivityResult status = await Connectivity().checkConnectivity();
|
||||
return status == ConnectivityResult.wifi;
|
||||
final List<ConnectivityResult> status =
|
||||
await Connectivity().checkConnectivity();
|
||||
return status.contains(ConnectivityResult.wifi);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -181,19 +183,20 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
case CommentsOrder.natural:
|
||||
final bool shouldFetchFromWeb = await _shouldFetchFromWeb;
|
||||
if (fetchFromWeb && shouldFetchFromWeb) {
|
||||
_logger.d('fetching from web.');
|
||||
_logger
|
||||
.d('$_logPrefix fetching comments of ${item.id} from web.');
|
||||
commentStream = _hackerNewsWebRepository
|
||||
.fetchCommentsStream(state.item)
|
||||
.handleError((dynamic e) {
|
||||
_streamSubscription?.cancel();
|
||||
|
||||
_logger.e(e);
|
||||
_logger.e('$_logPrefix $e');
|
||||
|
||||
switch (e.runtimeType) {
|
||||
case RateLimitedException:
|
||||
case RateLimitedWithFallbackException:
|
||||
case PossibleParsingException:
|
||||
if (_preferenceCubit.state.devModeEnabled) {
|
||||
if (_preferenceCubit.state.isDevModeEnabled) {
|
||||
onError?.call(e as AppException);
|
||||
}
|
||||
|
||||
@ -204,7 +207,8 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_logger.d('fetching from API.');
|
||||
_logger
|
||||
.d('$_logPrefix fetching comments of ${item.id} from API.');
|
||||
commentStream =
|
||||
_hackerNewsRepository.fetchAllCommentsRecursivelyStream(
|
||||
ids: kids,
|
||||
@ -279,17 +283,19 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
case CommentsOrder.natural:
|
||||
final bool shouldFetchFromWeb = await _shouldFetchFromWeb;
|
||||
if (fetchFromWeb && shouldFetchFromWeb) {
|
||||
_logger.d('fetching from web.');
|
||||
_logger.d(
|
||||
'$_logPrefix fetching comments of ${item.id} from web.',
|
||||
);
|
||||
commentStream = _hackerNewsWebRepository
|
||||
.fetchCommentsStream(state.item)
|
||||
.handleError((dynamic e) {
|
||||
_logger.e(e);
|
||||
_logger.e('$_logPrefix $e');
|
||||
|
||||
switch (e.runtimeType) {
|
||||
case RateLimitedException:
|
||||
case RateLimitedWithFallbackException:
|
||||
case PossibleParsingException:
|
||||
if (_preferenceCubit.state.devModeEnabled) {
|
||||
if (_preferenceCubit.state.isDevModeEnabled) {
|
||||
onError?.call(e as AppException);
|
||||
}
|
||||
|
||||
@ -300,7 +306,8 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_logger.d('fetching from API.');
|
||||
_logger
|
||||
.d('$_logPrefix fetching comments of ${item.id} from API.');
|
||||
commentStream = _hackerNewsRepository
|
||||
.fetchAllCommentsRecursivelyStream(ids: kids);
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ export 'poll/poll_cubit.dart';
|
||||
export 'post/post_cubit.dart';
|
||||
export 'preference/preference_cubit.dart';
|
||||
export 'reminder/reminder_cubit.dart';
|
||||
export 'remote_config/remote_config_cubit.dart';
|
||||
export 'search/search_cubit.dart';
|
||||
export 'split_view/split_view_cubit.dart';
|
||||
export 'submit/submit_cubit.dart';
|
||||
|
@ -50,7 +50,7 @@ class FavCubit extends Cubit<FavState> {
|
||||
_preferenceRepository.favList(of: username).then((List<int> favIds) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
favIds: favIds,
|
||||
favIds: LinkedHashSet<int>.from(favIds).toList(),
|
||||
favItems: <Item>[],
|
||||
currentPage: 0,
|
||||
),
|
||||
@ -155,7 +155,7 @@ class FavCubit extends Cubit<FavState> {
|
||||
);
|
||||
|
||||
_preferenceRepository.favList(of: username).then((List<int> favIds) {
|
||||
emit(state.copyWith(favIds: favIds));
|
||||
emit(state.copyWith(favIds: LinkedHashSet<int>.from(favIds).toList()));
|
||||
_hackerNewsRepository
|
||||
.fetchItemsStream(
|
||||
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
|
||||
|
@ -37,16 +37,16 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
.listen((String username) {
|
||||
if (username.isNotEmpty) {
|
||||
// Get the user setting.
|
||||
if (_preferenceCubit.state.notificationEnabled) {
|
||||
if (_preferenceCubit.state.isNotificationEnabled) {
|
||||
Future<void>.delayed(AppDurations.twoSeconds, init);
|
||||
}
|
||||
|
||||
// Listen for setting changes in the future.
|
||||
_preferenceCubit.stream.listen((PreferenceState prefState) {
|
||||
final bool isActive = _timer?.isActive ?? false;
|
||||
if (prefState.notificationEnabled && !isActive) {
|
||||
if (prefState.isNotificationEnabled && !isActive) {
|
||||
init();
|
||||
} else if (!prefState.notificationEnabled) {
|
||||
} else if (!prefState.isNotificationEnabled) {
|
||||
_timer?.cancel();
|
||||
}
|
||||
});
|
||||
@ -67,6 +67,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
static const Duration _refreshInterval = Duration(minutes: 5);
|
||||
static const int _subscriptionUpperLimit = 15;
|
||||
static const int _pageSize = 20;
|
||||
static const String _logPrefix = '[NotificationCubit]';
|
||||
|
||||
Future<void> init() async {
|
||||
emit(NotificationState.init());
|
||||
@ -78,7 +79,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
});
|
||||
|
||||
await _preferenceRepository.unreadCommentsIds.then((List<int> unreadIds) {
|
||||
_logger.i('NotificationCubit: ${unreadIds.length} unread items.');
|
||||
_logger.i('$_logPrefix ${unreadIds.length} unread items.');
|
||||
emit(state.copyWith(unreadCommentsIds: unreadIds));
|
||||
});
|
||||
|
||||
@ -104,36 +105,22 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
}
|
||||
|
||||
void markAsRead(int id) {
|
||||
Future.doWhile(() {
|
||||
if (state.status != Status.inProgress) {
|
||||
if (state.unreadCommentsIds.contains(id)) {
|
||||
final List<int> updatedUnreadIds = <int>[...state.unreadCommentsIds]
|
||||
..remove(id);
|
||||
_preferenceRepository.updateUnreadCommentsIds(updatedUnreadIds);
|
||||
emit(state.copyWith(unreadCommentsIds: updatedUnreadIds));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
if (state.unreadCommentsIds.contains(id)) {
|
||||
final List<int> updatedUnreadIds = <int>[...state.unreadCommentsIds]
|
||||
..remove(id);
|
||||
_preferenceRepository.updateUnreadCommentsIds(updatedUnreadIds);
|
||||
emit(state.copyWith(unreadCommentsIds: updatedUnreadIds));
|
||||
}
|
||||
}
|
||||
|
||||
void markAllAsRead() {
|
||||
Future.doWhile(() {
|
||||
if (state.status != Status.inProgress) {
|
||||
emit(state.copyWith(unreadCommentsIds: <int>[]));
|
||||
_preferenceRepository.updateUnreadCommentsIds(<int>[]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
emit(state.copyWith(unreadCommentsIds: <int>[]));
|
||||
_preferenceRepository.updateUnreadCommentsIds(<int>[]);
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
if (_authBloc.state.isLoggedIn &&
|
||||
_preferenceCubit.state.notificationEnabled) {
|
||||
_preferenceCubit.state.isNotificationEnabled) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: Status.inProgress,
|
||||
@ -248,4 +235,30 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void onCommentTapped(
|
||||
Comment comment, {
|
||||
void Function((Story, List<Comment>)? res)? then,
|
||||
}) {
|
||||
if (state.commentFetchingStatus == Status.inProgress) return;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
commentFetchingStatus: Status.inProgress,
|
||||
tappedCommentId: comment.id,
|
||||
),
|
||||
);
|
||||
|
||||
locator
|
||||
.get<HackerNewsRepository>()
|
||||
.fetchParentStoryWithComments(id: comment.parent)
|
||||
.then(((Story, List<Comment>)? res) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
commentFetchingStatus: Status.success,
|
||||
),
|
||||
);
|
||||
then?.call(res);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ class NotificationState extends Equatable {
|
||||
required this.currentPage,
|
||||
required this.offset,
|
||||
required this.status,
|
||||
required this.commentFetchingStatus,
|
||||
required this.tappedCommentId,
|
||||
});
|
||||
|
||||
NotificationState.init()
|
||||
@ -16,7 +18,9 @@ class NotificationState extends Equatable {
|
||||
allCommentsIds = <int>[],
|
||||
currentPage = 0,
|
||||
offset = 0,
|
||||
status = Status.idle;
|
||||
status = Status.idle,
|
||||
commentFetchingStatus = Status.idle,
|
||||
tappedCommentId = null;
|
||||
|
||||
final List<Comment> comments;
|
||||
final List<int> allCommentsIds;
|
||||
@ -24,6 +28,8 @@ class NotificationState extends Equatable {
|
||||
final int currentPage;
|
||||
final int offset;
|
||||
final Status status;
|
||||
final Status commentFetchingStatus;
|
||||
final int? tappedCommentId;
|
||||
|
||||
NotificationState copyWith({
|
||||
List<Comment>? comments,
|
||||
@ -32,6 +38,8 @@ class NotificationState extends Equatable {
|
||||
int? currentPage,
|
||||
int? offset,
|
||||
Status? status,
|
||||
Status? commentFetchingStatus,
|
||||
int? tappedCommentId,
|
||||
}) {
|
||||
return NotificationState(
|
||||
comments: comments ?? this.comments,
|
||||
@ -40,6 +48,9 @@ class NotificationState extends Equatable {
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
offset: offset ?? this.offset,
|
||||
status: status ?? this.status,
|
||||
commentFetchingStatus:
|
||||
commentFetchingStatus ?? this.commentFetchingStatus,
|
||||
tappedCommentId: tappedCommentId,
|
||||
);
|
||||
}
|
||||
|
||||
@ -51,6 +62,8 @@ class NotificationState extends Equatable {
|
||||
currentPage: currentPage,
|
||||
offset: offset,
|
||||
status: status,
|
||||
commentFetchingStatus: commentFetchingStatus,
|
||||
tappedCommentId: tappedCommentId,
|
||||
);
|
||||
}
|
||||
|
||||
@ -65,6 +78,8 @@ class NotificationState extends Equatable {
|
||||
currentPage: currentPage,
|
||||
offset: offset + 1,
|
||||
status: status,
|
||||
commentFetchingStatus: commentFetchingStatus,
|
||||
tappedCommentId: tappedCommentId,
|
||||
);
|
||||
}
|
||||
|
||||
@ -73,6 +88,7 @@ class NotificationState extends Equatable {
|
||||
currentPage,
|
||||
offset,
|
||||
status,
|
||||
commentFetchingStatus,
|
||||
comments,
|
||||
unreadCommentsIds,
|
||||
allCommentsIds,
|
||||
|
@ -23,6 +23,7 @@ class PreferenceCubit extends Cubit<PreferenceState> {
|
||||
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
final Logger _logger;
|
||||
static const String _logPrefix = '[PreferenceCubit]';
|
||||
|
||||
void init() {
|
||||
for (final BooleanPreference p
|
||||
@ -73,7 +74,7 @@ class PreferenceCubit extends Cubit<PreferenceState> {
|
||||
}
|
||||
|
||||
void update<T>(Preference<T> preference) {
|
||||
_logger.i('updating $preference to ${preference.val}');
|
||||
_logger.i('$_logPrefix updating $preference to ${preference.val}');
|
||||
|
||||
emit(state.copyWithPreference(preference));
|
||||
|
||||
|
@ -48,35 +48,37 @@ class PreferenceState extends Equatable {
|
||||
.val;
|
||||
}
|
||||
|
||||
bool get notificationEnabled => _isOn<NotificationModePreference>();
|
||||
bool get isNotificationEnabled => _isOn<NotificationModePreference>();
|
||||
|
||||
bool get complexStoryTileEnabled => _isOn<DisplayModePreference>();
|
||||
bool get isComplexStoryTileEnabled => _isOn<DisplayModePreference>();
|
||||
|
||||
bool get eyeCandyEnabled => _isOn<EyeCandyModePreference>();
|
||||
bool get isFaviconEnabled => _isOn<FaviconModePreference>();
|
||||
|
||||
bool get readerEnabled => _isOn<ReaderModePreference>();
|
||||
bool get isEyeCandyEnabled => _isOn<EyeCandyModePreference>();
|
||||
|
||||
bool get markReadStoriesEnabled => _isOn<MarkReadStoriesModePreference>();
|
||||
bool get isReaderEnabled => _isOn<ReaderModePreference>();
|
||||
|
||||
bool get metadataEnabled => _isOn<MetadataModePreference>();
|
||||
bool get isMarkReadStoriesEnabled => _isOn<MarkReadStoriesModePreference>();
|
||||
|
||||
bool get urlEnabled => _isOn<StoryUrlModePreference>();
|
||||
bool get isMetadataEnabled => _isOn<MetadataModePreference>();
|
||||
|
||||
bool get tapAnywhereToCollapseEnabled => _isOn<CollapseModePreference>();
|
||||
bool get isUrlEnabled => _isOn<StoryUrlModePreference>();
|
||||
|
||||
bool get swipeGestureEnabled => _isOn<SwipeGesturePreference>();
|
||||
bool get isTapAnywhereToCollapseEnabled => _isOn<CollapseModePreference>();
|
||||
|
||||
bool get autoScrollEnabled => _isOn<AutoScrollModePreference>();
|
||||
bool get isSwipeGestureEnabled => _isOn<SwipeGesturePreference>();
|
||||
|
||||
bool get customTabEnabled => _isOn<CustomTabPreference>();
|
||||
bool get isAutoScrollEnabled => _isOn<AutoScrollModePreference>();
|
||||
|
||||
bool get manualPaginationEnabled => _isOn<ManualPaginationPreference>();
|
||||
bool get isCustomTabEnabled => _isOn<CustomTabPreference>();
|
||||
|
||||
bool get trueDarkModeEnabled => _isOn<TrueDarkModePreference>();
|
||||
bool get isManualPaginationEnabled => _isOn<ManualPaginationPreference>();
|
||||
|
||||
bool get hapticFeedbackEnabled => _isOn<HapticFeedbackPreference>();
|
||||
bool get isTrueDarkModeEnabled => _isOn<TrueDarkModePreference>();
|
||||
|
||||
bool get devModeEnabled => _isOn<DevMode>();
|
||||
bool get isHapticFeedbackEnabled => _isOn<HapticFeedbackPreference>();
|
||||
|
||||
bool get isDevModeEnabled => _isOn<DevMode>();
|
||||
|
||||
double get textScaleFactor =>
|
||||
preferences.singleWhereType<TextScaleFactorPreference>().val;
|
||||
|
@ -9,7 +9,9 @@ class ReminderCubit extends Cubit<ReminderState> {
|
||||
ReminderCubit({PreferenceRepository? preferenceRepository})
|
||||
: _preferenceRepository =
|
||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||
super(const ReminderState.init());
|
||||
super(const ReminderState.init()) {
|
||||
init();
|
||||
}
|
||||
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
|
||||
|
47
lib/cubits/remote_config/remote_config_cubit.dart
Normal file
@ -0,0 +1,47 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/repositories/remote_config_repository.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
part 'remote_config_state.dart';
|
||||
|
||||
class RemoteConfigCubit extends HydratedCubit<RemoteConfigState> {
|
||||
RemoteConfigCubit({
|
||||
RemoteConfigRepository? remoteConfigRepository,
|
||||
Logger? logger,
|
||||
}) : _remoteConfigRepository =
|
||||
remoteConfigRepository ?? locator.get<RemoteConfigRepository>(),
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(RemoteConfigState.init()) {
|
||||
init();
|
||||
}
|
||||
|
||||
final RemoteConfigRepository _remoteConfigRepository;
|
||||
final Logger _logger;
|
||||
static const String _logPrefix = '';
|
||||
|
||||
void init() {
|
||||
_remoteConfigRepository
|
||||
.fetchRemoteConfig()
|
||||
.then((Map<String, dynamic> data) {
|
||||
if (data.isNotEmpty) {
|
||||
_logger.i('$_logPrefix remote config fetched: $data');
|
||||
emit(state.copyWith(data: data));
|
||||
} else {
|
||||
_logger.i('$_logPrefix empty remote config.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
RemoteConfigState? fromJson(Map<String, dynamic> json) {
|
||||
return RemoteConfigState(data: json);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toJson(RemoteConfigState state) {
|
||||
return state.data;
|
||||
}
|
||||
}
|
59
lib/cubits/remote_config/remote_config_state.dart
Normal file
@ -0,0 +1,59 @@
|
||||
part of 'remote_config_cubit.dart';
|
||||
|
||||
final class RemoteConfigState extends Equatable {
|
||||
const RemoteConfigState({
|
||||
required this.data,
|
||||
});
|
||||
|
||||
RemoteConfigState.init() : data = <String, dynamic>{};
|
||||
|
||||
@protected
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
String get athingComtrSelector => getString(
|
||||
key: 'athingComtrSelector',
|
||||
fallback:
|
||||
'''#hnmain > tbody > tr > td > table > tbody > .athing.comtr''',
|
||||
);
|
||||
|
||||
String get commentTextSelector => getString(
|
||||
key: 'commentTextSelector',
|
||||
fallback:
|
||||
'''td > table > tbody > tr > td.default > div.comment > div.commtext''',
|
||||
);
|
||||
|
||||
String get commentHeadSelector => getString(
|
||||
key: 'commentHeadSelector',
|
||||
fallback: '''td > table > tbody > tr > td.default > div > span > a''',
|
||||
);
|
||||
|
||||
String get commentAgeSelector => getString(
|
||||
key: 'commentAgeSelector',
|
||||
fallback:
|
||||
'''td > table > tbody > tr > td.default > div > span > span.age''',
|
||||
);
|
||||
|
||||
String get commentIndentSelector => getString(
|
||||
key: 'commentIndentSelector',
|
||||
fallback: '''td > table > tbody > tr > td.ind''',
|
||||
);
|
||||
|
||||
String getString({required String key, String fallback = ''}) {
|
||||
return data[key] as String? ?? fallback;
|
||||
}
|
||||
|
||||
bool getBool({required String key, bool fallback = false}) {
|
||||
return data[key] as bool? ?? fallback;
|
||||
}
|
||||
|
||||
int getInt({required String key, int fallback = 0}) {
|
||||
return data[key] as int? ?? fallback;
|
||||
}
|
||||
|
||||
RemoteConfigState copyWith({Map<String, dynamic>? data}) {
|
||||
return RemoteConfigState(data: data ?? this.data);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[data];
|
||||
}
|
@ -17,9 +17,10 @@ class SplitViewCubit extends Cubit<SplitViewState> {
|
||||
|
||||
final Logger _logger;
|
||||
final CommentCache _commentCache;
|
||||
static const String _logPrefix = '[SplitViewCubit]';
|
||||
|
||||
void updateItemScreenArgs(ItemScreenArgs args) {
|
||||
_logger.i('resetting comments in CommentCache');
|
||||
_logger.i('$_logPrefix resetting comments in CommentCache');
|
||||
_commentCache.resetComments();
|
||||
emit(state.copyWith(itemScreenArgs: args));
|
||||
}
|
||||
|
@ -25,16 +25,12 @@ class SubmitCubit extends Cubit<SubmitState> {
|
||||
emit(state.copyWith(text: text));
|
||||
}
|
||||
|
||||
void onSubmitTapped() {
|
||||
void submit() {
|
||||
emit(state.copyWith(status: Status.inProgress));
|
||||
|
||||
if (state.title?.isNotEmpty ?? false) {
|
||||
_postRepository
|
||||
.submit(
|
||||
title: state.title!,
|
||||
url: state.url,
|
||||
text: state.text,
|
||||
)
|
||||
.submit(title: state.title!, url: state.url, text: state.text)
|
||||
.then((bool successful) {
|
||||
emit(state.copyWith(status: Status.success));
|
||||
}).onError((Object? error, StackTrace stackTrace) {
|
||||
|
@ -13,21 +13,26 @@ class TabCubit extends Cubit<TabState> {
|
||||
Logger? logger,
|
||||
}) : _preferenceCubit = preferenceCubit,
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(TabState.init());
|
||||
super(TabState.init()) {
|
||||
init();
|
||||
}
|
||||
|
||||
final PreferenceCubit _preferenceCubit;
|
||||
final Logger _logger;
|
||||
static const String _logPrefix = '[TabCubit]';
|
||||
|
||||
void init() {
|
||||
final List<StoryType> tabs = _preferenceCubit.state.tabs;
|
||||
|
||||
_logger.i('updating tabs to $tabs');
|
||||
_logger.i('$_logPrefix updating tabs to $tabs');
|
||||
|
||||
emit(state.copyWith(tabs: tabs));
|
||||
}
|
||||
|
||||
void update(int startIndex, int endIndex) {
|
||||
_logger.d('updating ${state.tabs} by moving $startIndex to $endIndex');
|
||||
_logger.d(
|
||||
'$_logPrefix updating ${state.tabs} by moving $startIndex to $endIndex',
|
||||
);
|
||||
final StoryType tab = state.tabs.elementAt(startIndex);
|
||||
final List<StoryType> updatedTabs = List<StoryType>.from(state.tabs)
|
||||
..insert(endIndex, tab)
|
||||
|
@ -54,6 +54,13 @@ Future<void> main({bool testing = false}) async {
|
||||
final String tempPath = tempDir.path;
|
||||
Hive.init(tempPath);
|
||||
|
||||
final HydratedStorage storage = await HydratedStorage.build(
|
||||
storageDirectory: kIsWeb
|
||||
? HydratedStorage.webStorageDirectory
|
||||
: await getTemporaryDirectory(),
|
||||
);
|
||||
HydratedBloc.storage = storage;
|
||||
|
||||
await setUpLocator();
|
||||
|
||||
EquatableConfig.stringify = true;
|
||||
@ -66,12 +73,6 @@ Future<void> main({bool testing = false}) async {
|
||||
);
|
||||
};
|
||||
|
||||
final HydratedStorage storage = await HydratedStorage.build(
|
||||
storageDirectory: kIsWeb
|
||||
? HydratedStorage.webStorageDirectory
|
||||
: await getTemporaryDirectory(),
|
||||
);
|
||||
|
||||
if (Platform.isIOS) {
|
||||
unawaited(
|
||||
Workmanager().initialize(
|
||||
@ -139,8 +140,6 @@ Future<void> main({bool testing = false}) async {
|
||||
// Uncomment this line to log events from bloc/cubit.
|
||||
// Bloc.observer = CustomBlocObserver();
|
||||
|
||||
HydratedBloc.storage = storage;
|
||||
|
||||
VisibilityDetectorController.instance.updateInterval = AppDurations.ms200;
|
||||
|
||||
runApp(
|
||||
@ -162,6 +161,9 @@ class HackiApp extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: <BlocProvider<dynamic>>[
|
||||
BlocProvider<RemoteConfigCubit>.value(
|
||||
value: locator.get<RemoteConfigCubit>(),
|
||||
),
|
||||
BlocProvider<PreferenceCubit>(
|
||||
lazy: false,
|
||||
create: (BuildContext context) => PreferenceCubit(),
|
||||
@ -217,7 +219,7 @@ class HackiApp extends StatelessWidget {
|
||||
),
|
||||
BlocProvider<ReminderCubit>(
|
||||
lazy: false,
|
||||
create: (BuildContext context) => ReminderCubit()..init(),
|
||||
create: (BuildContext context) => ReminderCubit(),
|
||||
),
|
||||
BlocProvider<PostCubit>(
|
||||
lazy: false,
|
||||
@ -230,24 +232,24 @@ class HackiApp extends StatelessWidget {
|
||||
BlocProvider<TabCubit>(
|
||||
create: (BuildContext context) => TabCubit(
|
||||
preferenceCubit: context.read<PreferenceCubit>(),
|
||||
)..init(),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: BlocConsumer<PreferenceCubit, PreferenceState>(
|
||||
listenWhen: (PreferenceState previous, PreferenceState current) =>
|
||||
previous.hapticFeedbackEnabled != current.hapticFeedbackEnabled,
|
||||
previous.isHapticFeedbackEnabled != current.isHapticFeedbackEnabled,
|
||||
listener: (_, PreferenceState state) {
|
||||
HapticFeedbackUtil.enabled = state.hapticFeedbackEnabled;
|
||||
HapticFeedbackUtil.enabled = state.isHapticFeedbackEnabled;
|
||||
},
|
||||
buildWhen: (PreferenceState previous, PreferenceState current) =>
|
||||
previous.appColor != current.appColor ||
|
||||
previous.font != current.font ||
|
||||
previous.textScaleFactor != current.textScaleFactor ||
|
||||
previous.trueDarkModeEnabled != current.trueDarkModeEnabled,
|
||||
previous.isTrueDarkModeEnabled != current.isTrueDarkModeEnabled,
|
||||
builder: (BuildContext context, PreferenceState state) {
|
||||
return AdaptiveTheme(
|
||||
key: ValueKey<String>(
|
||||
'''${state.appColor}${state.font}${state.trueDarkModeEnabled}''',
|
||||
'''${state.appColor}${state.font}${state.isTrueDarkModeEnabled}''',
|
||||
),
|
||||
light: ThemeData(
|
||||
primaryColor: state.appColor,
|
||||
@ -279,20 +281,25 @@ class HackiApp extends StatelessWidget {
|
||||
.instance.platformDispatcher.platformBrightness,
|
||||
mode,
|
||||
);
|
||||
final bool isDarkModeEnabled =
|
||||
mode == AdaptiveThemeMode.dark ||
|
||||
final bool isDarkModeEnabled = () {
|
||||
if (mode == null) {
|
||||
return View.of(context)
|
||||
.platformDispatcher
|
||||
.platformBrightness ==
|
||||
Brightness.dark;
|
||||
} else {
|
||||
return mode == AdaptiveThemeMode.dark ||
|
||||
(mode == AdaptiveThemeMode.system &&
|
||||
View.of(context)
|
||||
.platformDispatcher
|
||||
.platformBrightness ==
|
||||
Brightness.dark);
|
||||
}
|
||||
}();
|
||||
final ColorScheme colorScheme = ColorScheme.fromSeed(
|
||||
brightness:
|
||||
isDarkModeEnabled ? Brightness.dark : Brightness.light,
|
||||
seedColor: state.appColor,
|
||||
background: isDarkModeEnabled && state.trueDarkModeEnabled
|
||||
? Palette.black
|
||||
: null,
|
||||
);
|
||||
return FeatureDiscovery(
|
||||
child: MediaQuery(
|
||||
@ -309,13 +316,21 @@ class HackiApp extends StatelessWidget {
|
||||
theme: ThemeData(
|
||||
colorScheme: colorScheme,
|
||||
fontFamily: state.font.name,
|
||||
canvasColor:
|
||||
isDarkModeEnabled && state.isTrueDarkModeEnabled
|
||||
? Palette.black
|
||||
: null,
|
||||
scaffoldBackgroundColor:
|
||||
isDarkModeEnabled && state.isTrueDarkModeEnabled
|
||||
? Palette.black
|
||||
: null,
|
||||
dividerTheme: DividerThemeData(
|
||||
color: Palette.grey.withOpacity(0.2),
|
||||
),
|
||||
switchTheme: SwitchThemeData(
|
||||
trackColor: MaterialStateProperty.resolveWith(
|
||||
(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
trackColor: WidgetStateProperty.resolveWith(
|
||||
(Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return colorScheme.primary.withOpacity(0.6);
|
||||
} else {
|
||||
return Palette.grey.withOpacity(0.2);
|
||||
@ -345,7 +360,7 @@ class HackiApp extends StatelessWidget {
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
side: MaterialStateBorderSide.resolveWith(
|
||||
side: WidgetStateBorderSide.resolveWith(
|
||||
(_) => const BorderSide(
|
||||
color: Palette.grey,
|
||||
),
|
||||
|
@ -4,7 +4,8 @@ enum Font {
|
||||
ubuntu('Ubuntu'),
|
||||
ubuntuMono('Ubuntu Mono'),
|
||||
notoSerif('Noto Serif', isSerif: true),
|
||||
exo2('Exo 2');
|
||||
exo2('Exo 2'),
|
||||
atkinsonHyperlegible('AtkinsonHyperlegible');
|
||||
|
||||
const Font(this.uiLabel, {this.isSerif = false});
|
||||
|
||||
|
@ -19,7 +19,7 @@ abstract final class Preference<T> extends Equatable with SettingsDisplayable {
|
||||
static final List<Preference<dynamic>> allPreferences =
|
||||
UnmodifiableListView<Preference<dynamic>>(
|
||||
<Preference<dynamic>>[
|
||||
// Order of these preferences does not matter.
|
||||
/// Order of these preferences does not matter.
|
||||
FetchModePreference(),
|
||||
CommentsOrderPreference(),
|
||||
FontPreference(),
|
||||
@ -29,14 +29,18 @@ abstract final class Preference<T> extends Equatable with SettingsDisplayable {
|
||||
AppColorPreference(),
|
||||
DateFormatPreference(),
|
||||
const TextScaleFactorPreference(),
|
||||
// Order of items below matters and
|
||||
// reflects the order on settings screen.
|
||||
|
||||
/// Order of items below matters and
|
||||
/// reflects the order on settings screen.
|
||||
const DisplayModePreference(),
|
||||
const FaviconModePreference(),
|
||||
const MetadataModePreference(),
|
||||
const StoryUrlModePreference(),
|
||||
// Divider.
|
||||
|
||||
/// Divider.
|
||||
const MarkReadStoriesModePreference(),
|
||||
// Divider.
|
||||
|
||||
/// Divider.
|
||||
const NotificationModePreference(),
|
||||
const AutoScrollModePreference(),
|
||||
const CollapseModePreference(),
|
||||
@ -160,7 +164,7 @@ final class AutoScrollModePreference extends BooleanPreference {
|
||||
const AutoScrollModePreference({bool? val})
|
||||
: super(val: val ?? _autoScrollModeDefaultValue);
|
||||
|
||||
static const bool _autoScrollModeDefaultValue = false;
|
||||
static const bool _autoScrollModeDefaultValue = true;
|
||||
|
||||
@override
|
||||
AutoScrollModePreference copyWith({required bool? val}) {
|
||||
@ -201,6 +205,27 @@ final class DisplayModePreference extends BooleanPreference {
|
||||
String get subtitle => 'show web preview in story tile.';
|
||||
}
|
||||
|
||||
final class FaviconModePreference extends BooleanPreference {
|
||||
const FaviconModePreference({bool? val})
|
||||
: super(val: val ?? _faviconModePreferenceDefaultValue);
|
||||
|
||||
static const bool _faviconModePreferenceDefaultValue = true;
|
||||
|
||||
@override
|
||||
FaviconModePreference copyWith({required bool? val}) {
|
||||
return FaviconModePreference(val: val);
|
||||
}
|
||||
|
||||
@override
|
||||
String get key => 'faviconMode';
|
||||
|
||||
@override
|
||||
String get title => 'Show Favicon';
|
||||
|
||||
@override
|
||||
String get subtitle => 'show favicon in story tile.';
|
||||
}
|
||||
|
||||
final class MetadataModePreference extends BooleanPreference {
|
||||
const MetadataModePreference({bool? val})
|
||||
: super(val: val ?? _metadataModeDefaultValue);
|
||||
|
@ -329,16 +329,32 @@ class HackerNewsRepository {
|
||||
|
||||
/// Fetch a list of [Story] based on ids and return results
|
||||
/// using a stream.
|
||||
Stream<Story> fetchStoriesStream({required List<int> ids}) async* {
|
||||
for (final int id in ids) {
|
||||
final Story? story =
|
||||
await _fetchItemJson(id).then((Map<String, dynamic>? json) async {
|
||||
if (json == null) return null;
|
||||
final Story story = Story.fromJson(json);
|
||||
return story;
|
||||
});
|
||||
Stream<Story> fetchStoriesStream({
|
||||
required List<int> ids,
|
||||
bool sequential = false,
|
||||
}) async* {
|
||||
if (sequential) {
|
||||
for (final int id in ids) {
|
||||
final Story? story =
|
||||
await _fetchItemJson(id).then((Map<String, dynamic>? json) async {
|
||||
if (json == null) return null;
|
||||
final Story story = Story.fromJson(json);
|
||||
return story;
|
||||
});
|
||||
|
||||
if (story != null) {
|
||||
if (story != null) {
|
||||
yield story;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final List<Map<String, dynamic>?> responses = await Future.wait(
|
||||
<Future<Map<String, dynamic>?>>[
|
||||
...ids.map(_fetchItemJson),
|
||||
],
|
||||
);
|
||||
for (final Map<String, dynamic>? json in responses) {
|
||||
if (json == null) continue;
|
||||
final Story story = Story.fromJson(json);
|
||||
yield story;
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:html/dom.dart' hide Comment;
|
||||
@ -15,6 +17,7 @@ import 'package:html_unescape/html_unescape.dart';
|
||||
/// For fetching anything that cannot be fetched through Hacker News API.
|
||||
class HackerNewsWebRepository {
|
||||
HackerNewsWebRepository({
|
||||
RemoteConfigCubit? remoteConfigCubit,
|
||||
Dio? dioWithCache,
|
||||
Dio? dio,
|
||||
}) : _dio = dio ?? Dio(),
|
||||
@ -24,10 +27,13 @@ class HackerNewsWebRepository {
|
||||
if (kDebugMode) LoggerInterceptor(),
|
||||
CacheInterceptor(),
|
||||
],
|
||||
);
|
||||
),
|
||||
_remoteConfigCubit =
|
||||
remoteConfigCubit ?? locator.get<RemoteConfigCubit>();
|
||||
|
||||
final Dio _dioWithCache;
|
||||
final Dio _dio;
|
||||
final RemoteConfigCubit _remoteConfigCubit;
|
||||
|
||||
static const Map<String, String> _headers = <String, String>{
|
||||
'accept': '*/*',
|
||||
@ -96,16 +102,20 @@ class HackerNewsWebRepository {
|
||||
}
|
||||
|
||||
static const String _itemBaseUrl = 'https://news.ycombinator.com/item?id=';
|
||||
static const String _athingComtrSelector =
|
||||
'#hnmain > tbody > tr > td > table > tbody > .athing.comtr';
|
||||
static const String _commentTextSelector =
|
||||
'''td > table > tbody > tr > td.default > div.comment''';
|
||||
static const String _commentHeadSelector =
|
||||
'''td > table > tbody > tr > td.default > div > span > a''';
|
||||
static const String _commentAgeSelector =
|
||||
'''td > table > tbody > tr > td.default > div > span > span.age''';
|
||||
static const String _commentIndentSelector =
|
||||
'''td > table > tbody > tr > td.ind''';
|
||||
|
||||
String get _athingComtrSelector =>
|
||||
_remoteConfigCubit.state.athingComtrSelector;
|
||||
|
||||
String get _commentTextSelector =>
|
||||
_remoteConfigCubit.state.commentTextSelector;
|
||||
|
||||
String get _commentHeadSelector =>
|
||||
_remoteConfigCubit.state.commentHeadSelector;
|
||||
|
||||
String get _commentAgeSelector => _remoteConfigCubit.state.commentAgeSelector;
|
||||
|
||||
String get _commentIndentSelector =>
|
||||
_remoteConfigCubit.state.commentIndentSelector;
|
||||
|
||||
Stream<Comment> fetchCommentsStream(Item item) async* {
|
||||
final bool isOnWifi = await _isOnWifi;
|
||||
@ -246,8 +256,9 @@ class HackerNewsWebRepository {
|
||||
}
|
||||
|
||||
static Future<bool> get _isOnWifi async {
|
||||
final ConnectivityResult status = await Connectivity().checkConnectivity();
|
||||
return status == ConnectivityResult.wifi;
|
||||
final List<ConnectivityResult> status =
|
||||
await Connectivity().checkConnectivity();
|
||||
return status.contains(ConnectivityResult.wifi);
|
||||
}
|
||||
|
||||
static Future<String> _parseCommentTextHtml(String text) async {
|
||||
|
23
lib/repositories/remote_config_repository.dart
Normal file
@ -0,0 +1,23 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class RemoteConfigRepository {
|
||||
RemoteConfigRepository({Dio? dio}) : _dio = dio ?? Dio();
|
||||
|
||||
final Dio _dio;
|
||||
static const String _path =
|
||||
'https://raw.githubusercontent.com/Livinglist/Hacki/master/assets/';
|
||||
|
||||
Future<Map<String, dynamic>> fetchRemoteConfig() async {
|
||||
const String fileName =
|
||||
kReleaseMode ? 'remote-config.json' : 'remote-config-dev.json';
|
||||
final Response<dynamic> response = await _dio.get(
|
||||
'$_path$fileName',
|
||||
);
|
||||
final String data = response.data as String? ?? '';
|
||||
final Map<String, dynamic> json = jsonDecode(data) as Map<String, dynamic>;
|
||||
return json;
|
||||
}
|
||||
}
|
@ -4,5 +4,6 @@ export 'hacker_news_web_repository.dart';
|
||||
export 'offline_repository.dart';
|
||||
export 'post_repository.dart';
|
||||
export 'preference_repository.dart';
|
||||
export 'remote_config_repository.dart';
|
||||
export 'search_repository.dart';
|
||||
export 'sembast_repository.dart';
|
||||
|
@ -1,8 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sembast/sembast.dart';
|
||||
@ -17,7 +19,8 @@ class SembastRepository {
|
||||
SembastRepository({
|
||||
Database? database,
|
||||
Database? cache,
|
||||
}) {
|
||||
Logger? logger,
|
||||
}) : _logger = logger ?? locator.get<Logger>() {
|
||||
if (database == null) {
|
||||
initializeDatabase();
|
||||
} else {
|
||||
@ -31,6 +34,9 @@ class SembastRepository {
|
||||
}
|
||||
}
|
||||
|
||||
final Logger _logger;
|
||||
static const String _logPrefix = '[SembastRepository]';
|
||||
|
||||
Database? _database;
|
||||
Database? _cache;
|
||||
List<int>? _idsOfCommentsRepliedToMe;
|
||||
@ -44,6 +50,9 @@ class SembastRepository {
|
||||
final Directory dir = await getApplicationCacheDirectory();
|
||||
await dir.create(recursive: true);
|
||||
final String dbPath = join(dir.path, 'hacki.db');
|
||||
final File file = File(dbPath);
|
||||
final FileStat stat = file.statSync();
|
||||
_logger.i('$_logPrefix hacki.db file size: ${stat.size / 1000000}MB');
|
||||
final DatabaseFactory dbFactory = databaseFactoryIo;
|
||||
final Database db = await dbFactory.openDatabase(dbPath);
|
||||
_database = db;
|
||||
@ -51,9 +60,12 @@ class SembastRepository {
|
||||
}
|
||||
|
||||
Future<Database> initializeCache() async {
|
||||
final Directory dir = await getTemporaryDirectory();
|
||||
await dir.create(recursive: true);
|
||||
final String dbPath = join(dir.path, 'hacki_cache.db');
|
||||
final Directory tempDir = await getTemporaryDirectory();
|
||||
await tempDir.create(recursive: true);
|
||||
final String dbPath = join(tempDir.path, 'hacki_cache.db');
|
||||
final File file = File(dbPath);
|
||||
final FileStat stat = file.statSync();
|
||||
_logger.i('$_logPrefix hacki_cache.db file size: ${stat.size / 1000000}MB');
|
||||
final DatabaseFactory dbFactory = databaseFactoryIo;
|
||||
final Database db = await dbFactory.openDatabase(dbPath);
|
||||
_cache = db;
|
||||
@ -209,7 +221,6 @@ class SembastRepository {
|
||||
final Database db = _cache ?? await initializeCache();
|
||||
final StoreRef<String, Map<String, Object?>> store =
|
||||
stringMapStoreFactory.store(_metadataCacheKey);
|
||||
|
||||
return db.transaction((Transaction txn) async {
|
||||
await store.record(key).put(txn, info.toJson());
|
||||
});
|
||||
@ -233,17 +244,23 @@ class SembastRepository {
|
||||
|
||||
//#endregion
|
||||
|
||||
Future<FileSystemEntity> deleteCachedComments() async {
|
||||
Future<void> deleteCachedComments() async {
|
||||
final Directory dir = await getApplicationDocumentsDirectory();
|
||||
await dir.create(recursive: true);
|
||||
final String dbPath = join(dir.path, 'hacki.db');
|
||||
return File(dbPath).delete();
|
||||
final File file = File(dbPath);
|
||||
if (file.existsSync()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
Future<FileSystemEntity> deleteCachedMetadata() async {
|
||||
Future<void> deleteCachedMetadata() async {
|
||||
final Directory tempDir = await getTemporaryDirectory();
|
||||
await tempDir.create(recursive: true);
|
||||
final String cachePath = join(tempDir.path, 'hacki_cache.db');
|
||||
return File(cachePath).delete();
|
||||
final File file = File(cachePath);
|
||||
if (file.existsSync()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,13 +43,14 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
late final StreamSubscription<String?> siriSuggestionStreamSubscription;
|
||||
|
||||
static final int tabLength = StoryType.values.length + 1;
|
||||
static const String logPrefix = '[HomeScreen]';
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
super.didPopNext();
|
||||
if (context.read<StoriesBloc>().deviceScreenType ==
|
||||
DeviceScreenType.mobile) {
|
||||
locator.get<Logger>().i('resetting comments in CommentCache');
|
||||
locator.get<Logger>().i('$logPrefix resetting comments in CommentCache');
|
||||
Future<void>.delayed(
|
||||
AppDurations.ms500,
|
||||
locator.get<CommentCache>().resetComments,
|
||||
@ -123,9 +124,10 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
final BlocBuilder<PreferenceCubit, PreferenceState> homeScreen =
|
||||
BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
buildWhen: (PreferenceState previous, PreferenceState current) =>
|
||||
previous.complexStoryTileEnabled != current.complexStoryTileEnabled ||
|
||||
previous.metadataEnabled != current.metadataEnabled ||
|
||||
previous.swipeGestureEnabled != current.swipeGestureEnabled,
|
||||
previous.isComplexStoryTileEnabled !=
|
||||
current.isComplexStoryTileEnabled ||
|
||||
previous.isMetadataEnabled != current.isMetadataEnabled ||
|
||||
previous.isSwipeGestureEnabled != current.isSwipeGestureEnabled,
|
||||
builder: (BuildContext context, PreferenceState preferenceState) {
|
||||
return DefaultTabController(
|
||||
length: tabLength,
|
||||
@ -150,7 +152,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
body: BlocBuilder<TabCubit, TabState>(
|
||||
builder: (BuildContext context, TabState state) {
|
||||
return TabBarView(
|
||||
physics: preferenceState.swipeGestureEnabled
|
||||
physics: preferenceState.isSwipeGestureEnabled
|
||||
? const PageScrollPhysics()
|
||||
: const NeverScrollableScrollPhysics(),
|
||||
controller: tabController,
|
||||
@ -190,12 +192,12 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
|
||||
void onStoryTapped(Story story) {
|
||||
final PreferenceState prefState = context.read<PreferenceCubit>().state;
|
||||
final bool useReader = prefState.readerEnabled;
|
||||
final bool useReader = prefState.isReaderEnabled;
|
||||
final StoryMarkingMode storyMarkingMode = prefState.storyMarkingMode;
|
||||
final bool offlineReading =
|
||||
context.read<StoriesBloc>().state.isOfflineReading;
|
||||
final bool splitViewEnabled = context.read<SplitViewCubit>().state.enabled;
|
||||
final bool markReadStoriesEnabled = prefState.markReadStoriesEnabled;
|
||||
final bool markReadStoriesEnabled = prefState.isMarkReadStoriesEnabled;
|
||||
|
||||
// If a story is a job story and it has a link to the job posting,
|
||||
// it would be better to just navigate to the web page.
|
||||
|
@ -24,6 +24,14 @@ class MobileHomeScreen extends StatelessWidget {
|
||||
bottom: Dimens.pt36,
|
||||
height: Dimens.pt40,
|
||||
child: CountdownReminder(),
|
||||
)
|
||||
else
|
||||
const Positioned(
|
||||
left: Dimens.pt24,
|
||||
right: Dimens.pt24,
|
||||
bottom: Dimens.pt36,
|
||||
height: Dimens.pt40,
|
||||
child: DownloadProgressReminder(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -37,7 +37,7 @@ class PinnedStories extends StatelessWidget {
|
||||
},
|
||||
backgroundColor: Palette.red,
|
||||
foregroundColor: Palette.white,
|
||||
icon: preferenceState.complexStoryTileEnabled
|
||||
icon: preferenceState.isComplexStoryTileEnabled
|
||||
? Icons.close
|
||||
: null,
|
||||
label: 'Unpin',
|
||||
@ -51,9 +51,10 @@ class PinnedStories extends StatelessWidget {
|
||||
key: ValueKey<String>('${story.id}-PinnedStoryTile'),
|
||||
story: story,
|
||||
onTap: () => onStoryTapped(story),
|
||||
showWebPreview: preferenceState.complexStoryTileEnabled,
|
||||
showMetadata: preferenceState.metadataEnabled,
|
||||
showUrl: preferenceState.urlEnabled,
|
||||
showWebPreview: preferenceState.isComplexStoryTileEnabled,
|
||||
showMetadata: preferenceState.isMetadataEnabled,
|
||||
showUrl: preferenceState.isUrlEnabled,
|
||||
showFavicon: preferenceState.isFaviconEnabled,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -41,13 +41,22 @@ class TabletHomeScreen extends StatelessWidget {
|
||||
curve: Curves.elasticOut,
|
||||
child: homeScreen,
|
||||
),
|
||||
Positioned(
|
||||
left: Dimens.pt24,
|
||||
bottom: Dimens.pt36,
|
||||
height: Dimens.pt40,
|
||||
width: homeScreenWidth - Dimens.pt24,
|
||||
child: const CountdownReminder(),
|
||||
),
|
||||
if (!context.read<ReminderCubit>().state.hasShown)
|
||||
Positioned(
|
||||
left: Dimens.pt24,
|
||||
bottom: Dimens.pt36,
|
||||
height: Dimens.pt40,
|
||||
width: homeScreenWidth - Dimens.pt24,
|
||||
child: const CountdownReminder(),
|
||||
)
|
||||
else
|
||||
Positioned(
|
||||
left: Dimens.pt24,
|
||||
bottom: Dimens.pt36,
|
||||
height: Dimens.pt40,
|
||||
width: homeScreenWidth - Dimens.pt24,
|
||||
child: const DownloadProgressReminder(),
|
||||
),
|
||||
AnimatedPositioned(
|
||||
right: Dimens.zero,
|
||||
top: Dimens.zero,
|
||||
|
@ -53,6 +53,7 @@ class _LoginDialogState extends State<LoginDialog> with ItemActionMixin {
|
||||
controller: usernameController,
|
||||
cursorColor: Theme.of(context).colorScheme.primary,
|
||||
autocorrect: false,
|
||||
autofillHints: const <String>[AutofillHints.username],
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Username',
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
@ -75,6 +76,7 @@ class _LoginDialogState extends State<LoginDialog> with ItemActionMixin {
|
||||
cursorColor: Theme.of(context).colorScheme.primary,
|
||||
obscureText: true,
|
||||
autocorrect: false,
|
||||
autofillHints: const <String>[AutofillHints.password],
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Password',
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
@ -182,7 +184,7 @@ class _LoginDialogState extends State<LoginDialog> with ItemActionMixin {
|
||||
}
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
state.agreedToEULA
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Palette.grey,
|
||||
|
@ -150,7 +150,7 @@ class MainView extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (context.read<PreferenceCubit>().state.devModeEnabled)
|
||||
if (context.read<PreferenceCubit>().state.isDevModeEnabled)
|
||||
Positioned(
|
||||
height: Dimens.pt4,
|
||||
bottom: Dimens.zero,
|
||||
@ -289,7 +289,7 @@ class _ParentItemSection extends StatelessWidget {
|
||||
useReader: context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.readerEnabled,
|
||||
.isReaderEnabled,
|
||||
offlineReading: context
|
||||
.read<StoriesBloc>()
|
||||
.state
|
||||
|
@ -5,11 +5,9 @@ import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/screens/profile/models/models.dart';
|
||||
import 'package:hacki/screens/profile/widgets/widgets.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
@ -98,6 +96,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
return ItemsListView<Item>(
|
||||
showWebPreviewOnStoryTile: false,
|
||||
showMetadataOnStoryTile: false,
|
||||
showFavicon: false,
|
||||
showUrl: false,
|
||||
showAuthor: false,
|
||||
useSimpleTileForStory: true,
|
||||
@ -192,21 +191,22 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
PreferenceState previous,
|
||||
PreferenceState current,
|
||||
) =>
|
||||
previous.complexStoryTileEnabled !=
|
||||
current.complexStoryTileEnabled ||
|
||||
previous.metadataEnabled !=
|
||||
current.metadataEnabled ||
|
||||
previous.urlEnabled != current.urlEnabled,
|
||||
previous.isComplexStoryTileEnabled !=
|
||||
current.isComplexStoryTileEnabled ||
|
||||
previous.isMetadataEnabled !=
|
||||
current.isMetadataEnabled ||
|
||||
previous.isUrlEnabled != current.isUrlEnabled,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
PreferenceState prefState,
|
||||
) {
|
||||
return ItemsListView<Item>(
|
||||
showWebPreviewOnStoryTile:
|
||||
prefState.complexStoryTileEnabled,
|
||||
prefState.isComplexStoryTileEnabled,
|
||||
showMetadataOnStoryTile:
|
||||
prefState.metadataEnabled,
|
||||
showUrl: prefState.urlEnabled,
|
||||
prefState.isMetadataEnabled,
|
||||
showFavicon: prefState.isFaviconEnabled,
|
||||
showUrl: prefState.isUrlEnabled,
|
||||
useSimpleTileForStory: true,
|
||||
refreshController: refreshControllerFav,
|
||||
items: favState.favItems,
|
||||
@ -417,27 +417,27 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
|
||||
void onCommentTapped(Comment comment, {VoidCallback? then}) {
|
||||
throttle.run(() {
|
||||
locator
|
||||
.get<HackerNewsRepository>()
|
||||
.fetchParentStoryWithComments(id: comment.parent)
|
||||
.then(((Story, List<Comment>)? res) {
|
||||
if (res != null && mounted) {
|
||||
final Story parent = res.$1;
|
||||
final List<Comment> children = res.$2;
|
||||
goToItemScreen(
|
||||
args: ItemScreenArgs(
|
||||
item: parent,
|
||||
targetComments: children.isEmpty
|
||||
? <Comment>[comment]
|
||||
: <Comment>[
|
||||
...children,
|
||||
comment.copyWith(level: children.length),
|
||||
],
|
||||
onlyShowTargetComment: true,
|
||||
),
|
||||
)?.then((_) => then?.call());
|
||||
}
|
||||
});
|
||||
context.read<NotificationCubit>().onCommentTapped(
|
||||
comment,
|
||||
then: ((Story, List<Comment>)? res) {
|
||||
if (res != null && mounted) {
|
||||
final Story parent = res.$1;
|
||||
final List<Comment> children = res.$2;
|
||||
goToItemScreen(
|
||||
args: ItemScreenArgs(
|
||||
item: parent,
|
||||
targetComments: children.isEmpty
|
||||
? <Comment>[comment]
|
||||
: <Comment>[
|
||||
...children,
|
||||
comment.copyWith(level: children.length),
|
||||
],
|
||||
onlyShowTargetComment: true,
|
||||
),
|
||||
)?.then((_) => then?.call());
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/utils/haptic_feedback_util.dart';
|
||||
|
||||
class EnterOfflineModeListTile extends StatelessWidget {
|
||||
const EnterOfflineModeListTile({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<StoriesBloc, StoriesState>(
|
||||
buildWhen: (StoriesState previous, StoriesState current) =>
|
||||
previous.isOfflineReading != current.isOfflineReading,
|
||||
builder: (BuildContext context, StoriesState state) {
|
||||
return SwitchListTile(
|
||||
value: state.isOfflineReading,
|
||||
title: const Text('Offline Mode'),
|
||||
onChanged: (bool value) {
|
||||
HapticFeedbackUtil.light();
|
||||
context.read<StoriesBloc>().add(
|
||||
value ? StoriesEnterOfflineMode() : StoriesExitOfflineMode(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_fadein/flutter_fadein.dart';
|
||||
import 'package:hacki/cubits/notification/notification_cubit.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
@ -30,7 +32,9 @@ class InboxView extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
if (unreadCommentsIds.isNotEmpty)
|
||||
if (context.read<NotificationCubit>().state.status !=
|
||||
Status.inProgress &&
|
||||
unreadCommentsIds.isNotEmpty)
|
||||
TextButton(
|
||||
onPressed: onMarkAllAsReadTapped,
|
||||
child: const Text('Mark all as read'),
|
||||
@ -69,64 +73,79 @@ class InboxView extends StatelessWidget {
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
...comments.map((Comment e) {
|
||||
final NotificationState state =
|
||||
context.read<NotificationCubit>().state;
|
||||
return <Widget>[
|
||||
FadeIn(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () => onCommentTapped(e),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: Dimens.pt8,
|
||||
horizontal: Dimens.pt6,
|
||||
Stack(
|
||||
children: <Widget>[
|
||||
if (state.commentFetchingStatus == Status.inProgress &&
|
||||
state.tappedCommentId == e.id)
|
||||
Positioned(
|
||||
left: Dimens.zero,
|
||||
right: Dimens.zero,
|
||||
top: Dimens.zero,
|
||||
bottom: Dimens.zero,
|
||||
child: LinearProgressIndicator(
|
||||
color: Theme.of(context)
|
||||
.primaryColor
|
||||
.withOpacity(0.1),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'''${e.timeAgo} from ${e.by}:''',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).metadataColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
],
|
||||
),
|
||||
Linkify(
|
||||
text: e.text,
|
||||
style: TextStyle(
|
||||
color: unreadCommentsIds.contains(e.id)
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
: Theme.of(context).readGrey,
|
||||
fontSize: TextDimens.pt16,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(
|
||||
unreadCommentsIds.contains(e.id)
|
||||
? 1
|
||||
: 0.6,
|
||||
),
|
||||
FadeIn(
|
||||
child: InkWell(
|
||||
onTap: () => onCommentTapped(e),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: Dimens.pt8,
|
||||
horizontal: Dimens.pt12,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'''${e.timeAgo} from ${e.by}:''',
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).metadataColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
],
|
||||
),
|
||||
maxLines: 4,
|
||||
onOpen: (LinkableElement link) =>
|
||||
LinkUtil.launch(link.url, context),
|
||||
),
|
||||
],
|
||||
Linkify(
|
||||
text: e.text,
|
||||
style: TextStyle(
|
||||
color: unreadCommentsIds.contains(e.id)
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
: Theme.of(context).readGrey,
|
||||
fontSize: TextDimens.pt16,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(
|
||||
unreadCommentsIds.contains(e.id)
|
||||
? 1
|
||||
: 0.6,
|
||||
),
|
||||
),
|
||||
maxLines: 4,
|
||||
onOpen: (LinkableElement link) =>
|
||||
LinkUtil.launch(link.url, context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
|
@ -91,8 +91,10 @@ class OfflineListTile extends StatelessWidget {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Connectivity().checkConnectivity().then((ConnectivityResult res) {
|
||||
if (res != ConnectivityResult.none) {
|
||||
Connectivity()
|
||||
.checkConnectivity()
|
||||
.then((List<ConnectivityResult> res) {
|
||||
if (!res.contains(ConnectivityResult.none)) {
|
||||
showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
|
@ -23,6 +23,7 @@ import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/screens/profile/models/page_type.dart';
|
||||
import 'package:hacki/screens/profile/qr_code_scanner_screen.dart';
|
||||
import 'package:hacki/screens/profile/qr_code_view_screen.dart';
|
||||
import 'package:hacki/screens/profile/widgets/enter_offline_mode_list_tile.dart';
|
||||
import 'package:hacki/screens/profile/widgets/offline_list_tile.dart';
|
||||
import 'package:hacki/screens/profile/widgets/tab_bar_settings.dart';
|
||||
import 'package:hacki/screens/profile/widgets/text_scale_factor_settings.dart';
|
||||
@ -78,6 +79,7 @@ class _SettingsState extends State<Settings> with ItemActionMixin {
|
||||
}
|
||||
},
|
||||
),
|
||||
const EnterOfflineModeListTile(),
|
||||
const OfflineListTile(),
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
@ -205,9 +207,10 @@ class _SettingsState extends State<Settings> with ItemActionMixin {
|
||||
const TextScaleFactorSettings(),
|
||||
const Divider(),
|
||||
StoryTile(
|
||||
showWebPreview: preferenceState.complexStoryTileEnabled,
|
||||
showMetadata: preferenceState.metadataEnabled,
|
||||
showUrl: preferenceState.urlEnabled,
|
||||
showWebPreview: preferenceState.isComplexStoryTileEnabled,
|
||||
showMetadata: preferenceState.isMetadataEnabled,
|
||||
showUrl: preferenceState.isUrlEnabled,
|
||||
showFavicon: preferenceState.isFaviconEnabled,
|
||||
story: Story.placeholder(),
|
||||
onTap: () => LinkUtil.launch(
|
||||
Constants.guidelineLink,
|
||||
@ -252,7 +255,7 @@ class _SettingsState extends State<Settings> with ItemActionMixin {
|
||||
horizontal: Dimens.pt16,
|
||||
),
|
||||
child: DropdownMenu<StoryMarkingMode>(
|
||||
enabled: preferenceState.markReadStoriesEnabled,
|
||||
enabled: preferenceState.isMarkReadStoriesEnabled,
|
||||
label: Text(StoryMarkingModePreference().title),
|
||||
initialSelection: preferenceState.storyMarkingMode,
|
||||
onSelected: (StoryMarkingMode? storyMarkingMode) {
|
||||
@ -338,13 +341,20 @@ class _SettingsState extends State<Settings> with ItemActionMixin {
|
||||
),
|
||||
onTap: showClearCacheDialog,
|
||||
),
|
||||
if (preferenceState.isDevModeEnabled)
|
||||
ListTile(
|
||||
title: const Text(
|
||||
'Logs',
|
||||
),
|
||||
onTap: () {},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('About'),
|
||||
subtitle: const Text('nothing interesting here.'),
|
||||
onTap: showAboutHackiDialog,
|
||||
onLongPress: () {
|
||||
final DevMode updatedDevMode =
|
||||
DevMode(val: !preferenceState.devModeEnabled);
|
||||
DevMode(val: !preferenceState.isDevModeEnabled);
|
||||
context.read<PreferenceCubit>().update(updatedDevMode);
|
||||
HapticFeedbackUtil.heavy();
|
||||
if (updatedDevMode.val) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
export 'centered_message_view.dart';
|
||||
export 'enter_offline_mode_list_tile.dart';
|
||||
export 'inbox_view.dart';
|
||||
export 'offline_list_tile.dart';
|
||||
export 'settings.dart';
|
||||
|
@ -46,7 +46,9 @@ class _SearchScreenState extends State<SearchScreen> with ItemActionMixin {
|
||||
void dispose() {
|
||||
refreshController.dispose();
|
||||
scrollController.dispose();
|
||||
focusNode.dispose();
|
||||
focusNode
|
||||
..unfocus()
|
||||
..dispose();
|
||||
textEditingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@ -359,9 +361,11 @@ class _SearchScreenState extends State<SearchScreen> with ItemActionMixin {
|
||||
FadeIn(
|
||||
child: StoryTile(
|
||||
showWebPreview:
|
||||
prefState.complexStoryTileEnabled,
|
||||
showMetadata: prefState.metadataEnabled,
|
||||
showUrl: prefState.urlEnabled,
|
||||
prefState.isComplexStoryTileEnabled,
|
||||
showMetadata:
|
||||
prefState.isMetadataEnabled,
|
||||
showUrl: prefState.isUrlEnabled,
|
||||
showFavicon: prefState.isFaviconEnabled,
|
||||
story: e,
|
||||
onTap: () => goToItemScreen(
|
||||
args: ItemScreenArgs(item: e),
|
||||
@ -383,7 +387,7 @@ class _SearchScreenState extends State<SearchScreen> with ItemActionMixin {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!prefState.complexStoryTileEnabled)
|
||||
if (!prefState.isComplexStoryTileEnabled)
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
|
@ -76,7 +76,7 @@ class PostedByFilterChip extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.pop(null),
|
||||
onPressed: () => context.pop(),
|
||||
child: const Text(
|
||||
'Clear',
|
||||
),
|
||||
@ -87,7 +87,7 @@ class PostedByFilterChip extends StatelessWidget {
|
||||
context.pop(text.isEmpty ? null : text);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
|
@ -56,6 +56,14 @@ class _SubmitScreenState extends State<SubmitScreen> with ItemActionMixin {
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
onPressed: () {
|
||||
// Don't show confirmation dialog if content is empty.
|
||||
if (state.text.isNullOrEmpty &&
|
||||
state.url.isNullOrEmpty &&
|
||||
state.title.isNullOrEmpty) {
|
||||
context.pop();
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
@ -114,7 +122,36 @@ class _SubmitScreenState extends State<SubmitScreen> with ItemActionMixin {
|
||||
Icons.send,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: context.read<SubmitCubit>().onSubmitTapped,
|
||||
onPressed: () {
|
||||
showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Submit?'),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => context.pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.pop(true),
|
||||
child: const Text(
|
||||
'Yes',
|
||||
style: TextStyle(
|
||||
color: Palette.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
).then((bool? value) {
|
||||
if (value ?? false) {
|
||||
context.read<SubmitCubit>().submit();
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
@ -10,6 +11,7 @@ import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
class CommentTile extends StatelessWidget {
|
||||
const CommentTile({
|
||||
@ -308,7 +310,7 @@ class CommentTile extends StatelessWidget {
|
||||
final double commentBackgroundColorOpacity =
|
||||
Theme.of(context).canvasColor != Palette.white ? 0.03 : 0.15;
|
||||
|
||||
final Color commentColor = prefState.eyeCandyEnabled
|
||||
final Color commentColor = prefState.isEyeCandyEnabled
|
||||
? color.withOpacity(commentBackgroundColorOpacity)
|
||||
: Palette.transparent;
|
||||
final bool isMyComment = comment.deleted == false &&
|
||||
@ -404,7 +406,7 @@ class CommentTile extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _onTextTapped(BuildContext context) {
|
||||
if (context.read<PreferenceCubit>().state.tapAnywhereToCollapseEnabled) {
|
||||
if (context.read<PreferenceCubit>().state.isTapAnywhereToCollapseEnabled) {
|
||||
_collapse(context);
|
||||
}
|
||||
}
|
||||
@ -414,21 +416,31 @@ class CommentTile extends StatelessWidget {
|
||||
final CollapseCubit collapseCubit = context.read<CollapseCubit>()
|
||||
..collapse(onStateChanged: HapticFeedbackUtil.selection);
|
||||
if (collapseCubit.state.collapsed &&
|
||||
preferenceCubit.state.autoScrollEnabled) {
|
||||
preferenceCubit.state.isAutoScrollEnabled) {
|
||||
final CommentsCubit commentsCubit = context.read<CommentsCubit>();
|
||||
final List<Comment> comments = commentsCubit.state.comments;
|
||||
final int indexOfNextComment = comments.indexOf(comment) + 1;
|
||||
if (indexOfNextComment < comments.length) {
|
||||
Future<void>.delayed(
|
||||
AppDurations.ms300,
|
||||
() {
|
||||
commentsCubit.itemScrollController.scrollTo(
|
||||
index: indexOfNextComment,
|
||||
alignment: 0.1,
|
||||
duration: AppDurations.ms300,
|
||||
);
|
||||
},
|
||||
);
|
||||
final int indexOfComment = comments.indexOf(comment);
|
||||
if (indexOfComment < comments.length) {
|
||||
final double? leadingEdge =
|
||||
commentsCubit.itemPositionsListener.itemPositions.value
|
||||
.singleWhereOrNull(
|
||||
(ItemPosition e) => e.index - 1 == indexOfComment,
|
||||
)
|
||||
?.itemLeadingEdge;
|
||||
final bool willBeOutsideOfScreen =
|
||||
leadingEdge != null && leadingEdge < 0.1;
|
||||
if (willBeOutsideOfScreen) {
|
||||
Future<void>.delayed(
|
||||
AppDurations.ms200,
|
||||
() {
|
||||
commentsCubit.itemScrollController.scrollTo(
|
||||
index: indexOfComment + 1,
|
||||
alignment: 0.15,
|
||||
duration: AppDurations.ms300,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,13 +18,15 @@ class CustomChip extends StatelessWidget {
|
||||
return FilterChip(
|
||||
shadowColor: Palette.transparent,
|
||||
selectedShadowColor: Palette.transparent,
|
||||
backgroundColor: Palette.transparent,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
side: selected
|
||||
? BorderSide(color: Theme.of(context).colorScheme.primary)
|
||||
: BorderSide(color: Theme.of(context).colorScheme.onSurface),
|
||||
label: Text(label),
|
||||
labelStyle: TextStyle(
|
||||
color: selected ? Theme.of(context).colorScheme.onPrimary : null,
|
||||
color: selected
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
checkmarkColor: selected ? Theme.of(context).colorScheme.onPrimary : null,
|
||||
selected: selected,
|
||||
|
83
lib/screens/widgets/download_progress_reminder.dart
Normal file
@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_fadein/flutter_fadein.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class DownloadProgressReminder extends StatefulWidget {
|
||||
const DownloadProgressReminder({super.key});
|
||||
|
||||
@override
|
||||
State<DownloadProgressReminder> createState() =>
|
||||
_DownloadProgressReminderState();
|
||||
}
|
||||
|
||||
class _DownloadProgressReminderState extends State<DownloadProgressReminder>
|
||||
with SingleTickerProviderStateMixin, ItemActionMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocSelector<StoriesBloc, StoriesState,
|
||||
(int, int, StoriesDownloadStatus)>(
|
||||
selector: (StoriesState state) {
|
||||
return (
|
||||
state.storiesDownloaded,
|
||||
state.storiesToBeDownloaded,
|
||||
state.downloadStatus
|
||||
);
|
||||
},
|
||||
builder: (BuildContext context, (int, int, StoriesDownloadStatus) state) {
|
||||
final int storiesDownloaded = state.$1;
|
||||
final int storiesToBeDownloaded = state.$2;
|
||||
final StoriesDownloadStatus status = state.$3;
|
||||
final double progress = storiesToBeDownloaded == 0
|
||||
? 0
|
||||
: storiesDownloaded / storiesToBeDownloaded;
|
||||
final bool isVisible = status == StoriesDownloadStatus.downloading;
|
||||
return Visibility(
|
||||
visible: isVisible,
|
||||
child: FadeIn(
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(
|
||||
Dimens.pt4,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt12,
|
||||
top: Dimens.pt10,
|
||||
right: Dimens.pt10,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Downloading all stories ($storiesDownloaded/$storiesToBeDownloaded)',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
fontSize: TextDimens.pt12,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
LinearProgressIndicator(
|
||||
value: progress,
|
||||
color:
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
const ItemsListView({
|
||||
required this.showWebPreviewOnStoryTile,
|
||||
required this.showMetadataOnStoryTile,
|
||||
required this.showFavicon,
|
||||
required this.showUrl,
|
||||
required this.items,
|
||||
required this.onTap,
|
||||
@ -40,6 +41,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
final bool useSimpleTileForStory;
|
||||
final bool showWebPreviewOnStoryTile;
|
||||
final bool showMetadataOnStoryTile;
|
||||
final bool showFavicon;
|
||||
final bool showUrl;
|
||||
final bool enablePullDown;
|
||||
final bool markReadStories;
|
||||
@ -74,7 +76,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
if (e is Story) {
|
||||
final bool hasRead = context.read<StoriesBloc>().hasRead(e);
|
||||
final bool swipeGestureEnabled =
|
||||
context.read<PreferenceCubit>().state.swipeGestureEnabled;
|
||||
context.read<PreferenceCubit>().state.isSwipeGestureEnabled;
|
||||
return <Widget>[
|
||||
if (useSimpleTileForStory)
|
||||
FadeIn(
|
||||
@ -140,6 +142,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
showWebPreview: showWebPreviewOnStoryTile,
|
||||
showMetadata: showMetadataOnStoryTile,
|
||||
showUrl: showUrl,
|
||||
showFavicon: showFavicon,
|
||||
hasRead: markReadStories && hasRead,
|
||||
),
|
||||
),
|
||||
|
@ -145,6 +145,7 @@ class _LinkPreviewState extends State<LinkPreview> {
|
||||
String? title = '',
|
||||
String? desc = '',
|
||||
String? imageUri = '',
|
||||
String? iconUri = '',
|
||||
bool isIcon = false,
|
||||
}) {
|
||||
return Container(
|
||||
@ -169,6 +170,7 @@ class _LinkPreviewState extends State<LinkPreview> {
|
||||
title: widget.story.title,
|
||||
description: desc ?? title ?? 'no comment yet.',
|
||||
imageUri: imageUri,
|
||||
iconUri: iconUri,
|
||||
imagePath: Constants.hackerNewsLogoPath,
|
||||
onTap: widget.onTap,
|
||||
hasRead: widget.hasRead,
|
||||
@ -209,6 +211,7 @@ class _LinkPreviewState extends State<LinkPreview> {
|
||||
title: _errorTitle,
|
||||
desc: _errorBody,
|
||||
imageUri: null,
|
||||
iconUri: null,
|
||||
)
|
||||
: _buildLinkContainer(
|
||||
context.storyTileHeight,
|
||||
@ -216,14 +219,8 @@ class _LinkPreviewState extends State<LinkPreview> {
|
||||
desc: WebAnalyzer.isNotEmpty(info!.description)
|
||||
? info.description
|
||||
: _errorBody,
|
||||
imageUri: widget.showMultimedia
|
||||
? (WebAnalyzer.isNotEmpty(info.image)
|
||||
? info.image
|
||||
: WebAnalyzer.isNotEmpty(info.icon)
|
||||
? info.icon
|
||||
: null)
|
||||
: null,
|
||||
isIcon: !WebAnalyzer.isNotEmpty(info.image),
|
||||
imageUri: widget.showMultimedia ? info.image : null,
|
||||
iconUri: widget.showMultimedia ? info.icon : null,
|
||||
);
|
||||
|
||||
return AnimatedCrossFade(
|
||||
|
@ -3,7 +3,9 @@ import 'dart:math';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_fadein/flutter_fadein.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/screens/widgets/tap_down_wrapper.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/link_util.dart';
|
||||
@ -23,6 +25,7 @@ class LinkView extends StatelessWidget {
|
||||
required this.bodyMaxLines,
|
||||
super.key,
|
||||
this.imageUri,
|
||||
this.iconUri,
|
||||
this.imagePath,
|
||||
this.showMultiMedia = true,
|
||||
this.bodyTextOverflow,
|
||||
@ -43,6 +46,7 @@ class LinkView extends StatelessWidget {
|
||||
final String title;
|
||||
final String description;
|
||||
final String? imageUri;
|
||||
final String? iconUri;
|
||||
final String? imagePath;
|
||||
final VoidCallback onTap;
|
||||
final bool showMultiMedia;
|
||||
@ -111,24 +115,37 @@ class LinkView extends StatelessWidget {
|
||||
child: SizedBox(
|
||||
height: layoutHeight,
|
||||
width: layoutHeight,
|
||||
child: (imageUri?.isEmpty ?? true) && imagePath != null
|
||||
? Image.asset(
|
||||
imagePath!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: imageUri!,
|
||||
fit: isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
|
||||
cacheKey: imageUri,
|
||||
errorWidget: (_, __, ___) => Center(
|
||||
child: Text(
|
||||
r'¯\_(ツ)_/¯',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageUri ?? '',
|
||||
fit: isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
|
||||
cacheKey: imageUri,
|
||||
errorWidget: (_, __, ___) {
|
||||
if (url.isEmpty) {
|
||||
return FadeIn(
|
||||
child: Center(
|
||||
child: _HackerNewsImage(
|
||||
height: layoutHeight,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Center(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: Constants.favicon(url),
|
||||
fit: BoxFit.scaleDown,
|
||||
cacheKey: iconUri,
|
||||
errorWidget: (_, __, ___) {
|
||||
return const FadeIn(
|
||||
child: Icon(
|
||||
Icons.public,
|
||||
size: Dimens.pt20,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -156,3 +173,23 @@ class LinkView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HackerNewsImage extends StatelessWidget {
|
||||
const _HackerNewsImage({
|
||||
required this.height,
|
||||
});
|
||||
|
||||
final double height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeIn(
|
||||
child: Image.asset(
|
||||
Constants.hackerNewsLogoPath,
|
||||
height: height,
|
||||
width: height,
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -59,7 +59,9 @@ class OfflineBanner extends StatelessWidget {
|
||||
},
|
||||
).then((bool? value) {
|
||||
if (value ?? false) {
|
||||
context.read<StoriesBloc>().add(StoriesExitOffline());
|
||||
context
|
||||
.read<StoriesBloc>()
|
||||
.add(StoriesExitOfflineMode());
|
||||
context.read<AuthBloc>().add(AuthInitialize());
|
||||
context.read<PinCubit>().init();
|
||||
WebAnalyzer.cacheMap.clear();
|
||||
|
@ -49,9 +49,11 @@ class _StoriesListViewState extends State<StoriesListView>
|
||||
|
||||
return BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
buildWhen: (PreferenceState previous, PreferenceState current) =>
|
||||
previous.complexStoryTileEnabled != current.complexStoryTileEnabled ||
|
||||
previous.metadataEnabled != current.metadataEnabled ||
|
||||
previous.manualPaginationEnabled != current.manualPaginationEnabled,
|
||||
previous.isComplexStoryTileEnabled !=
|
||||
current.isComplexStoryTileEnabled ||
|
||||
previous.isMetadataEnabled != current.isMetadataEnabled ||
|
||||
previous.isManualPaginationEnabled !=
|
||||
current.isManualPaginationEnabled,
|
||||
builder: (BuildContext context, PreferenceState preferenceState) {
|
||||
return BlocConsumer<StoriesBloc, StoriesState>(
|
||||
listenWhen: (StoriesState previous, StoriesState current) =>
|
||||
@ -75,7 +77,7 @@ class _StoriesListViewState extends State<StoriesListView>
|
||||
current.statusByType[widget.storyType]),
|
||||
builder: (BuildContext context, StoriesState state) {
|
||||
bool shouldShowLoadButton() {
|
||||
return preferenceState.manualPaginationEnabled &&
|
||||
return preferenceState.isManualPaginationEnabled &&
|
||||
state.statusByType[widget.storyType] == Status.success &&
|
||||
(state.storiesByType[widget.storyType]?.length ?? 0) <
|
||||
(state.storyIdsByType[widget.storyType]?.length ?? 0);
|
||||
@ -83,11 +85,12 @@ class _StoriesListViewState extends State<StoriesListView>
|
||||
|
||||
return ItemsListView<Story>(
|
||||
showOfflineBanner: true,
|
||||
markReadStories: preferenceState.markReadStoriesEnabled,
|
||||
markReadStories: preferenceState.isMarkReadStoriesEnabled,
|
||||
showWebPreviewOnStoryTile:
|
||||
preferenceState.complexStoryTileEnabled,
|
||||
showMetadataOnStoryTile: preferenceState.metadataEnabled,
|
||||
showUrl: preferenceState.urlEnabled,
|
||||
preferenceState.isComplexStoryTileEnabled,
|
||||
showMetadataOnStoryTile: preferenceState.isMetadataEnabled,
|
||||
showFavicon: preferenceState.isFaviconEnabled,
|
||||
showUrl: preferenceState.isUrlEnabled,
|
||||
refreshController: refreshController,
|
||||
scrollController: scrollController,
|
||||
items: state.storiesByType[storyType]!,
|
||||
@ -99,7 +102,7 @@ class _StoriesListViewState extends State<StoriesListView>
|
||||
context.read<PinCubit>().refresh();
|
||||
},
|
||||
onLoadMore: () {
|
||||
if (preferenceState.manualPaginationEnabled) {
|
||||
if (preferenceState.isManualPaginationEnabled) {
|
||||
refreshController
|
||||
..refreshCompleted(resetFooterState: true)
|
||||
..loadComplete();
|
||||
@ -128,10 +131,10 @@ class _StoriesListViewState extends State<StoriesListView>
|
||||
child: OutlinedButton(
|
||||
onPressed: loadMoreStories,
|
||||
style: ButtonStyle(
|
||||
minimumSize: MaterialStateProperty.all(
|
||||
minimumSize: WidgetStateProperty.all(
|
||||
const Size(double.infinity, Dimens.pt48),
|
||||
),
|
||||
foregroundColor: MaterialStateColor.resolveWith(
|
||||
foregroundColor: WidgetStateColor.resolveWith(
|
||||
(_) => Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
@ -147,7 +150,7 @@ class _StoriesListViewState extends State<StoriesListView>
|
||||
itemBuilder: (Widget child, Story story) {
|
||||
return Slidable(
|
||||
key: ValueKey<Story>(story),
|
||||
enabled: !preferenceState.swipeGestureEnabled,
|
||||
enabled: !preferenceState.isSwipeGestureEnabled,
|
||||
startActionPane: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: <Widget>[
|
||||
@ -159,10 +162,10 @@ class _StoriesListViewState extends State<StoriesListView>
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
icon: preferenceState.complexStoryTileEnabled
|
||||
icon: preferenceState.isComplexStoryTileEnabled
|
||||
? Icons.push_pin_outlined
|
||||
: null,
|
||||
label: preferenceState.complexStoryTileEnabled
|
||||
label: preferenceState.isComplexStoryTileEnabled
|
||||
? null
|
||||
: 'Pin',
|
||||
),
|
||||
@ -171,10 +174,10 @@ class _StoriesListViewState extends State<StoriesListView>
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
icon: preferenceState.complexStoryTileEnabled
|
||||
icon: preferenceState.isComplexStoryTileEnabled
|
||||
? Icons.more_horiz
|
||||
: null,
|
||||
label: preferenceState.complexStoryTileEnabled
|
||||
label: preferenceState.isComplexStoryTileEnabled
|
||||
? null
|
||||
: 'More',
|
||||
),
|
||||
@ -193,9 +196,10 @@ class _StoriesListViewState extends State<StoriesListView>
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) => mark(story),
|
||||
backgroundColor: preferenceState.markReadStoriesEnabled
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Palette.grey,
|
||||
backgroundColor:
|
||||
preferenceState.isMarkReadStoriesEnabled
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Palette.grey,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
icon: state.readStoriesIds.contains(story.id)
|
||||
@ -241,7 +245,7 @@ class _StoriesListViewState extends State<StoriesListView>
|
||||
HapticFeedbackUtil.light();
|
||||
final StoriesBloc storiesBloc = context.read<StoriesBloc>();
|
||||
final bool markReadStoriesEnabled =
|
||||
context.read<PreferenceCubit>().state.markReadStoriesEnabled;
|
||||
context.read<PreferenceCubit>().state.isMarkReadStoriesEnabled;
|
||||
if (markReadStoriesEnabled) {
|
||||
if (storiesBloc.state.readStoriesIds.contains(story.id)) {
|
||||
storiesBloc.add(StoryUnread(story: story));
|
||||
|
@ -1,18 +1,22 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_fadein/flutter_fadein.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
class StoryTile extends StatelessWidget {
|
||||
const StoryTile({
|
||||
required this.showWebPreview,
|
||||
required this.showMetadata,
|
||||
required this.showFavicon,
|
||||
required this.showUrl,
|
||||
required this.story,
|
||||
required this.onTap,
|
||||
@ -23,6 +27,7 @@ class StoryTile extends StatelessWidget {
|
||||
|
||||
final bool showWebPreview;
|
||||
final bool showMetadata;
|
||||
final bool showFavicon;
|
||||
final bool showUrl;
|
||||
final bool hasRead;
|
||||
final Story story;
|
||||
@ -123,71 +128,125 @@ class StoryTile extends StatelessWidget {
|
||||
excludeSemantics: true,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: () {
|
||||
if (story.url.isNotEmpty) {
|
||||
LinkUtil.launch(
|
||||
story.url,
|
||||
context,
|
||||
useReader:
|
||||
context.read<PreferenceCubit>().state.isReaderEnabled,
|
||||
offlineReading:
|
||||
context.read<StoriesBloc>().state.isOfflineReading,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: Dimens.pt12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: story.title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(
|
||||
color: hasRead
|
||||
? Theme.of(context).readGrey
|
||||
: null,
|
||||
fontWeight:
|
||||
hasRead ? null : FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (showUrl && story.url.isNotEmpty)
|
||||
if (showFavicon) ...<Widget>[
|
||||
if (story.url.isNotEmpty)
|
||||
SizedBox(
|
||||
height: Dimens.pt20,
|
||||
width: Dimens.pt24,
|
||||
child: Center(
|
||||
child: CachedNetworkImage(
|
||||
fit: BoxFit.fitHeight,
|
||||
imageUrl: Constants.favicon(story.url),
|
||||
errorWidget: (_, __, ___) {
|
||||
return const Icon(
|
||||
Icons.public,
|
||||
size: Dimens.pt20,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(
|
||||
height: Dimens.pt20,
|
||||
width: Dimens.pt24,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.public,
|
||||
size: Dimens.pt20,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt8,
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
text: ' (${story.readableUrl})',
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: story.title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(
|
||||
color: hasRead
|
||||
? Theme.of(context).readGrey
|
||||
: null,
|
||||
fontWeight:
|
||||
hasRead ? null : FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (showUrl && story.url.isNotEmpty)
|
||||
TextSpan(
|
||||
text: ' (${story.readableUrl})',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(
|
||||
color: hasRead
|
||||
? Theme.of(context).readGrey
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textScaler: MediaQuery.of(context).textScaler,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showMetadata)
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
story.metadata,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(
|
||||
color: hasRead
|
||||
? Theme.of(context).readGrey
|
||||
: null,
|
||||
: Theme.of(context).metadataColor,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textScaler: MediaQuery.of(context).textScaler,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showMetadata)
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
story.metadata,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: hasRead
|
||||
? Theme.of(context).readGrey
|
||||
: Theme.of(context).metadataColor,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
const SizedBox(
|
||||
height: Dimens.pt14,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -10,6 +10,7 @@ export 'custom_dropdown_menu.dart';
|
||||
export 'custom_linkify/custom_linkify.dart';
|
||||
export 'custom_tab_bar.dart';
|
||||
export 'device_gesture_wrapper.dart';
|
||||
export 'download_progress_reminder.dart';
|
||||
export 'item_text.dart';
|
||||
export 'items_list_view.dart';
|
||||
export 'link_preview/link_preview.dart';
|
||||
|
@ -38,15 +38,15 @@ abstract class Fetcher {
|
||||
final Logger logger = Logger();
|
||||
final PreferenceRepository preferenceRepository =
|
||||
PreferenceRepository(logger: logger);
|
||||
|
||||
final AuthRepository authRepository = AuthRepository(
|
||||
preferenceRepository: preferenceRepository,
|
||||
logger: logger,
|
||||
);
|
||||
|
||||
final HackerNewsRepository hackerNewsRepository = HackerNewsRepository();
|
||||
final SembastRepository sembastRepository = SembastRepository();
|
||||
|
||||
final HackerNewsRepository hackerNewsRepository = HackerNewsRepository(
|
||||
sembastRepository: sembastRepository,
|
||||
logger: logger,
|
||||
);
|
||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
|
@ -85,6 +85,7 @@ class WebAnalyzer {
|
||||
RegExp('(title|icon|description|image)', caseSensitive: false);
|
||||
static final RegExp _lineReg = RegExp(r'[\n\r]| |>');
|
||||
static final RegExp _spaceReg = RegExp(r'\s+');
|
||||
static const String _logPrefix = '[WebAnalyzer]';
|
||||
|
||||
static bool isEmpty(String? str) {
|
||||
return !isNotEmpty(str);
|
||||
@ -120,7 +121,7 @@ class WebAnalyzer {
|
||||
|
||||
if (info != null) {
|
||||
locator.get<Logger>().d('''
|
||||
fetched mem cached metadata using key $key for $story:
|
||||
$_logPrefix fetched mem cached metadata using key $key for $story:
|
||||
${info.toJson()}
|
||||
''');
|
||||
return info;
|
||||
@ -168,7 +169,7 @@ ${info.toJson()}
|
||||
/// [5] If there is file cache, move it to mem cache for later retrieval.
|
||||
if (info != null) {
|
||||
locator.get<Logger>().d('''
|
||||
fetched file cached metadata using key $key for $story:
|
||||
$_logPrefix fetched file cached metadata using key $key for $story:
|
||||
${info.toJson()}
|
||||
''');
|
||||
cacheMap[key] = info;
|
||||
@ -189,7 +190,7 @@ ${info.toJson()}
|
||||
if (info is WebInfo) {
|
||||
locator
|
||||
.get<Logger>()
|
||||
.d('caching metadata using key $key for $story.');
|
||||
.d('$_logPrefix caching metadata using key $key for $story.');
|
||||
unawaited(
|
||||
locator.get<SembastRepository>().cacheMetadata(
|
||||
key: key,
|
||||
@ -232,23 +233,20 @@ ${info.toJson()}
|
||||
required Story story,
|
||||
String? url,
|
||||
}) async {
|
||||
List<dynamic>? res;
|
||||
|
||||
if (url != null) {
|
||||
res = await compute(
|
||||
_fetchInfoFromUrl,
|
||||
<dynamic>[url, multimedia],
|
||||
);
|
||||
}
|
||||
if (url == null) return null;
|
||||
final List<dynamic>? res = await compute(
|
||||
_fetchInfoFromUrl,
|
||||
<dynamic>[url, multimedia],
|
||||
);
|
||||
|
||||
late final bool shouldRetry;
|
||||
InfoBase? info;
|
||||
String? fallbackDescription;
|
||||
|
||||
// If description is empty, use one of the comments under the story.
|
||||
if (res == null || isEmpty(res[2] as String?)) {
|
||||
final List<int> ids = <int>[story.id, ...story.kids];
|
||||
final String? commentText = await _fetchInfoFromStory(ids);
|
||||
|
||||
shouldRetry = commentText == null;
|
||||
fallbackDescription = commentText ?? 'no comment yet';
|
||||
} else {
|
||||
@ -279,7 +277,6 @@ ${info.toJson()}
|
||||
description: fallbackDescription,
|
||||
).._shouldRetry = shouldRetry;
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
@ -291,7 +288,6 @@ ${info.toJson()}
|
||||
final bool multimedia = message[1] as bool;
|
||||
|
||||
final InfoBase? info = await _getInfo(url, multimedia);
|
||||
|
||||
if (info is WebInfo) {
|
||||
return <dynamic>[
|
||||
'0',
|
||||
@ -419,14 +415,12 @@ ${info.toJson()}
|
||||
} catch (e) {
|
||||
try {
|
||||
html = gbk.decode(response.bodyBytes);
|
||||
} catch (e) {
|
||||
locator
|
||||
.get<Logger>()
|
||||
.e('''web page resolution failure from:$url Error:$e''');
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (html == null) return null;
|
||||
if (html == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String headHtml = _getHeadHtml(html);
|
||||
final Document document = parser.parse(headHtml);
|
||||
@ -443,7 +437,7 @@ ${info.toJson()}
|
||||
|
||||
final WebInfo info = WebInfo(
|
||||
title: _analyzeTitle(document),
|
||||
icon: _analyzeIcon(document, uri),
|
||||
icon: await _analyzeIcon(document, uri),
|
||||
description: _analyzeDescription(document, html),
|
||||
image: _analyzeImage(document, uri),
|
||||
);
|
||||
@ -530,7 +524,7 @@ ${info.toJson()}
|
||||
return description;
|
||||
}
|
||||
|
||||
static String? _analyzeIcon(Document document, Uri uri) {
|
||||
static Future<String?> _analyzeIcon(Document document, Uri uri) async {
|
||||
final List<Element> meta = document.head!.getElementsByTagName('link');
|
||||
String? icon = '';
|
||||
// get icon first
|
||||
@ -559,7 +553,7 @@ ${info.toJson()}
|
||||
if (metaIcon != null) {
|
||||
icon = metaIcon.attributes['href'];
|
||||
} else {
|
||||
return '${uri.origin}/favicon.ico';
|
||||
return null;
|
||||
}
|
||||
|
||||
return _handleUrl(uri, icon);
|
||||
|
@ -73,7 +73,8 @@ abstract class LinkUtil {
|
||||
if (val) {
|
||||
if (link.contains('http')) {
|
||||
if (Platform.isAndroid &&
|
||||
context.read<PreferenceCubit>().state.customTabEnabled == false) {
|
||||
context.read<PreferenceCubit>().state.isCustomTabEnabled ==
|
||||
false) {
|
||||
launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
final Color primaryColor = Theme.of(context).colorScheme.primary;
|
||||
|
@ -8,10 +8,12 @@ import 'package:path_provider/path_provider.dart';
|
||||
|
||||
abstract class LogUtil {
|
||||
static LogPrinter get logPrinter => kReleaseMode
|
||||
? SimplePrinter(colors: false)
|
||||
: PrettyPrinter(
|
||||
methodCount: 0,
|
||||
? SimplePrinter(
|
||||
colors: false,
|
||||
printTime: true,
|
||||
)
|
||||
: PrettyPrinter(
|
||||
printTime: true,
|
||||
);
|
||||
|
||||
static LogOutput logOutput(File outputFile) => MultiOutput(
|
||||
|
325
pubspec.lock
@ -33,14 +33,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.11"
|
||||
ansicolor:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ansicolor
|
||||
sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.6.1"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
|
||||
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "2.5.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -61,26 +77,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: bloc
|
||||
sha256: f53a110e3b48dcd78136c10daa5d51512443cea5e1348c9d80a320095fa2db9e
|
||||
sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.3"
|
||||
version: "8.1.4"
|
||||
bloc_concurrency:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: bloc_concurrency
|
||||
sha256: "5857eb6653b4dd5e30e1ffab91037957fd64a9b9c5e53d26714ef25a46c04679"
|
||||
sha256: "456b7a3616a7c1ceb975c14441b3f198bf57d81cb95b7c6de5cb0c60201afcd8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.4"
|
||||
version: "0.2.5"
|
||||
bloc_test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: bloc_test
|
||||
sha256: "55a48f69e0d480717067c5377c8485a3fcd41f1701a820deef72fa0f4ee7215f"
|
||||
sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.1.6"
|
||||
version: "9.1.7"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -109,10 +125,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_web
|
||||
sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316"
|
||||
sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.2.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -149,18 +165,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0"
|
||||
sha256: db7a4e143dc72cc3cb2044ef9b052a7ebfe729513e6a82943bc3526f784365b8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
version: "6.0.3"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus_platform_interface
|
||||
sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a
|
||||
sha256: b6a56efe1e6675be240de39107281d4034b64ac23438026355b4234042a35adb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.4"
|
||||
version: "2.0.0"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -173,18 +189,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76"
|
||||
sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.7.2"
|
||||
version: "1.8.0"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e
|
||||
sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3+8"
|
||||
version: "0.3.4+1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -213,10 +229,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110"
|
||||
sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.1.2"
|
||||
version: "10.1.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -237,10 +253,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3"
|
||||
sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.0"
|
||||
version: "5.4.3+1"
|
||||
equatable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -269,8 +285,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: flutter3_compatibility
|
||||
resolved-ref: "896306d04130f870c7ce99ce832fd977283b2392"
|
||||
ref: bcf4ef28542acb0c98ec7dfa9d20d8fa7a0a594b
|
||||
resolved-ref: bcf4ef28542acb0c98ec7dfa9d20d8fa7a0a594b
|
||||
url: "https://github.com/livinglist/feature_discovery"
|
||||
source: git
|
||||
version: "0.14.1"
|
||||
@ -307,18 +323,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_bloc
|
||||
sha256: "87325da1ac757fcc4813e6b34ed5dd61169973871fdf181d6c2109dd6935ece1"
|
||||
sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.4"
|
||||
version: "8.1.5"
|
||||
flutter_cache_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba"
|
||||
sha256: "395d6b7831f21f3b989ebedbb785545932adb9afe2622c1ffacf7f4b53a7e544"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
version: "3.3.2"
|
||||
flutter_driver:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@ -328,10 +344,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_email_sender
|
||||
sha256: "5001e9158f91a8799140fb30a11ad89cd587244f30b4f848d87085985c49b60f"
|
||||
sha256: fb515d4e073d238d0daf1d765e5318487b6396d46b96e0ae9745dbc9a133f97a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.2"
|
||||
version: "6.0.3"
|
||||
flutter_fadein:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -408,10 +424,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: c18f1de98fe0bb9dd5ba91e1330d4febc8b6a7de6aae3ffe475ef423723e72f3
|
||||
sha256: "40e6fbd2da7dcc7ed78432c5cdab1559674b4af035fddbfb2f9a8f9c2112fcef"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "16.3.2"
|
||||
version: "17.1.2"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -424,10 +440,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_platform_interface
|
||||
sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef"
|
||||
sha256: "340abf67df238f7f0ef58f4a26d2a83e1ab74c77ab03cd2b2d5018ac64db30b7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0+1"
|
||||
version: "7.1.0"
|
||||
flutter_material_color_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -436,70 +452,79 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
flutter_native_splash:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_native_splash
|
||||
sha256: aa06fec78de2190f3db4319dd60fdc8d12b2626e93ef9828633928c2dcaea840
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685
|
||||
sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
version: "9.2.2"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_linux
|
||||
sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e"
|
||||
sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
flutter_secure_storage_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_macos
|
||||
sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c
|
||||
sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.1.2"
|
||||
flutter_secure_storage_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_platform_interface
|
||||
sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e"
|
||||
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "1.1.2"
|
||||
flutter_secure_storage_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_web
|
||||
sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20"
|
||||
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.2.1"
|
||||
flutter_secure_storage_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_windows
|
||||
sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108"
|
||||
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.1.2"
|
||||
flutter_siri_suggestions:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_siri_suggestions
|
||||
sha256: db5473d79fb47067fb52fdbfff3399dc4f6bbd162eeb5bde0c6d61fafcbc104b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "."
|
||||
ref: master
|
||||
resolved-ref: "849f17a0b26c1388c45178b554c0690637527849"
|
||||
url: "https://github.com/Livinglist/flutter_siri_suggestions"
|
||||
source: git
|
||||
version: "2.1.0"
|
||||
flutter_slidable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_slidable
|
||||
sha256: "19ed4813003a6ff4e9c6bcce37e792a2a358919d7603b2b31ff200229191e44c"
|
||||
sha256: "673403d2eeef1f9e8483bd6d8d92aae73b1d8bd71f382bc3930f699c731bc27c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.1.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@ -522,10 +547,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
|
||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "4.0.0"
|
||||
fuchsia_remote_debug_protocol:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@ -535,10 +560,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: get_it
|
||||
sha256: e6017ce7fdeaf218dc51a100344d8cb70134b80e28b760f8bb23c242437bafd7
|
||||
sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.6.7"
|
||||
version: "7.7.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -551,10 +576,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: "170c46e237d6eb0e6e9f0e8b3f56101e14fb64f787016e42edd74c39cf8b176a"
|
||||
sha256: abec47eb8c8c36ebf41d0a4c64dbbe7f956e39a012b3aafc530e951bdc12fe3f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.2.0"
|
||||
version: "14.1.4"
|
||||
hive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -583,10 +608,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
|
||||
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -607,10 +632,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hydrated_bloc
|
||||
sha256: "00a2099680162e74b5a836b8a7f446e478520a9cae9f6032e028ad8129f4432d"
|
||||
sha256: af35b357739fe41728df10bec03aad422cdc725a1e702e03af9d2a41ea05160c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.1.4"
|
||||
version: "9.1.5"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
in_app_review:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -659,26 +692,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
|
||||
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.0"
|
||||
version: "10.0.4"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
|
||||
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "3.0.3"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "3.0.1"
|
||||
linkify:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -691,10 +724,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logger
|
||||
sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac"
|
||||
sha256: af05cc8714f356fd1f3888fb6741cbe9fbe25cdb6eedbab80e1a6db21047d4a4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2+1"
|
||||
version: "2.3.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -731,10 +764,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.0"
|
||||
version: "1.12.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -795,18 +828,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79"
|
||||
sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.1"
|
||||
version: "8.0.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6"
|
||||
sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "3.0.0"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -819,26 +852,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b
|
||||
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.3"
|
||||
path_provider_android:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668"
|
||||
sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
version: "2.2.5"
|
||||
path_provider_foundation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
|
||||
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.4.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -915,10 +948,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: provider
|
||||
sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096"
|
||||
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.1"
|
||||
version: "6.1.2"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -996,50 +1029,50 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sembast
|
||||
sha256: "9a9f0c7aca07043fef857b8b365f41592e48832b61462292699b57978e241c11"
|
||||
sha256: dbe19600cff55d43f19405be79138c3fd2c08a87b0b152b18609b9427d113a64
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.6.0"
|
||||
version: "3.7.1"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900"
|
||||
sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.2"
|
||||
version: "9.0.0"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956
|
||||
sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
version: "4.0.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02"
|
||||
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
version: "2.2.3"
|
||||
shared_preferences_android:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
|
||||
sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.2.3"
|
||||
shared_preferences_foundation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
|
||||
sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.5"
|
||||
version: "2.4.0"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1060,10 +1093,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21"
|
||||
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
version: "2.3.0"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1153,18 +1186,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6
|
||||
sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.3.3+1"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5"
|
||||
sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.3"
|
||||
version: "2.5.4"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1232,34 +1265,34 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test
|
||||
sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f
|
||||
sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.24.9"
|
||||
version: "1.25.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.1"
|
||||
version: "0.7.0"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a
|
||||
sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.9"
|
||||
version: "0.6.0"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timezone
|
||||
sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0"
|
||||
sha256: a6ccda4a69a442098b602c44e61a1e2b4bf6f5516e875bbf0f427d5df14745d5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.2"
|
||||
version: "0.9.3"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1268,38 +1301,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
universal_io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: universal_io
|
||||
sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
universal_platform:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: universal_platform
|
||||
sha256: d315be0f6641898b280ffa34e2ddb14f3d12b1a37882557869646e0cc363d0cc
|
||||
sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0+1"
|
||||
version: "1.1.0"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c
|
||||
sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.4"
|
||||
version: "6.2.6"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745
|
||||
sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
version: "6.3.3"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03"
|
||||
sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.4"
|
||||
version: "6.3.0"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1312,10 +1353,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234
|
||||
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.2.0"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1328,10 +1369,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b
|
||||
sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
version: "2.3.1"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1344,10 +1385,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8
|
||||
sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.3"
|
||||
version: "4.4.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1376,10 +1417,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
|
||||
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.0"
|
||||
version: "14.2.1"
|
||||
wakelock:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1432,18 +1473,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05"
|
||||
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
version: "0.5.1"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23"
|
||||
sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
version: "2.4.5"
|
||||
webdriver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1464,18 +1505,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: webview_flutter
|
||||
sha256: "25e1b6e839e8cbfbd708abc6f85ed09d1727e24e08e08c6b8590d7c65c9a8932"
|
||||
sha256: "6869c8786d179f929144b4a1f86e09ac0eddfe475984951ea6c634774c16b522"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.0"
|
||||
version: "4.8.0"
|
||||
webview_flutter_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_android
|
||||
sha256: "3e5f4e9d818086b0d01a66fb1ff9cc72ab0cc58c71980e3d3661c5685ea0efb0"
|
||||
sha256: f42447ca49523f11d8f70abea55ea211b3cafe172dd7a0e7ac007bb35dd356dc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.15.0"
|
||||
version: "3.16.4"
|
||||
webview_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1488,26 +1529,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_wkwebview
|
||||
sha256: "9bf168bccdf179ce90450b5f37e36fe263f591c9338828d6bf09b6f8d0f57f86"
|
||||
sha256: "7affdf9d680c015b11587181171d3cad8093e449db1f7d9f0f08f4f33d24f9a0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.12.0"
|
||||
version: "3.13.1"
|
||||
win32:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: win32
|
||||
sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8"
|
||||
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.0"
|
||||
version: "5.5.1"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32_registry
|
||||
sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a"
|
||||
sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.1.3"
|
||||
workmanager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1541,5 +1582,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.3.0-279.1.beta <4.0.0"
|
||||
flutter: ">=3.19.0"
|
||||
dart: ">=3.4.0 <4.0.0"
|
||||
flutter: ">=3.22.3"
|
||||
|
86
pubspec.yaml
@ -1,62 +1,66 @@
|
||||
name: hacki
|
||||
description: A Hacker News reader.
|
||||
version: 2.7.1+139
|
||||
version: 2.8.2+147
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
flutter: "3.19.0"
|
||||
flutter: "3.22.3"
|
||||
|
||||
dependencies:
|
||||
adaptive_theme: ^3.2.0
|
||||
animations: ^2.0.8
|
||||
badges: ^3.0.2
|
||||
bloc: ^8.1.1
|
||||
bloc_concurrency: ^0.2.2
|
||||
bloc_concurrency: ^0.2.5
|
||||
cached_network_image: ^3.3.0
|
||||
clipboard: ^0.1.3
|
||||
collection: ^1.17.1
|
||||
connectivity_plus: ^5.0.1
|
||||
device_info_plus: ^9.1.0
|
||||
dio: ^5.0.3
|
||||
connectivity_plus: ^6.0.3
|
||||
device_info_plus: ^10.1.0
|
||||
dio: ^5.4.3+1
|
||||
equatable: ^2.0.5
|
||||
fast_gbk: ^1.0.0
|
||||
feature_discovery:
|
||||
git:
|
||||
url: https://github.com/livinglist/feature_discovery
|
||||
ref: flutter3_compatibility
|
||||
ref: bcf4ef28542acb0c98ec7dfa9d20d8fa7a0a594b
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_bloc: ^8.1.2
|
||||
flutter_cache_manager: ^3.3.0
|
||||
flutter_email_sender: ^6.0.1
|
||||
flutter_bloc: ^8.1.5
|
||||
flutter_cache_manager: ^3.3.2
|
||||
flutter_email_sender: ^6.0.3
|
||||
flutter_fadein: ^2.0.0
|
||||
flutter_feather_icons: 2.0.0+1
|
||||
flutter_inappwebview: ^6.0.0
|
||||
flutter_local_notifications: ^16.1.0
|
||||
flutter_local_notifications: ^17.1.2
|
||||
flutter_material_color_picker: ^1.2.0
|
||||
flutter_secure_storage: ^9.0.0
|
||||
flutter_siri_suggestions: ^2.1.0
|
||||
flutter_native_splash: ^2.4.1
|
||||
flutter_secure_storage: ^9.2.2
|
||||
flutter_siri_suggestions:
|
||||
git:
|
||||
url: https://github.com/Livinglist/flutter_siri_suggestions
|
||||
ref: master
|
||||
flutter_slidable: ^3.0.0
|
||||
font_awesome_flutter: ^10.3.0
|
||||
get_it: ^7.2.0
|
||||
go_router: ^13.2.0
|
||||
get_it: ^7.7.0
|
||||
go_router: ^14.1.4
|
||||
hive: ^2.2.3
|
||||
html: ^0.15.1
|
||||
html_unescape: ^2.0.0
|
||||
http: ^1.1.0
|
||||
hydrated_bloc: ^9.1.0
|
||||
hydrated_bloc: ^9.1.5
|
||||
in_app_review:
|
||||
path: components/in_app_review
|
||||
intl: ^0.19.0
|
||||
linkify: ^5.0.0
|
||||
logger: ^2.0.1
|
||||
logger: ^2.3.0
|
||||
memoize: ^3.0.0
|
||||
package_info_plus: ^5.0.1
|
||||
package_info_plus: ^8.0.0
|
||||
path: ^1.8.2
|
||||
path_provider: ^2.0.12
|
||||
path_provider_android: ^2.0.22
|
||||
path_provider_foundation: ^2.1.1
|
||||
path_provider: ^2.1.3
|
||||
path_provider_android: ^2.2.5
|
||||
path_provider_foundation: ^2.4.0
|
||||
pretty_dio_logger: ^1.3.1
|
||||
pull_to_refresh:
|
||||
git:
|
||||
@ -68,19 +72,19 @@ dependencies:
|
||||
responsive_builder: ^0.7.0
|
||||
rxdart: ^0.27.7
|
||||
scrollable_positioned_list: ^0.3.5
|
||||
sembast: ^3.5.0+1
|
||||
share_plus: ^7.2.1
|
||||
shared_preferences: ^2.2.2
|
||||
shared_preferences_android: ^2.2.1
|
||||
shared_preferences_foundation: ^2.3.4
|
||||
sembast: ^3.7.1
|
||||
share_plus: ^9.0.0
|
||||
shared_preferences: ^2.2.3
|
||||
shared_preferences_android: ^2.2.3
|
||||
shared_preferences_foundation: ^2.4.0
|
||||
shimmer: ^3.0.0
|
||||
synced_shared_preferences:
|
||||
path: components/synced_shared_preferences
|
||||
universal_platform: ^1.0.0+1
|
||||
url_launcher: ^6.2.1
|
||||
universal_platform: ^1.1.0
|
||||
url_launcher: ^6.2.6
|
||||
visibility_detector: ^0.4.0+2
|
||||
wakelock: ^0.6.2
|
||||
webview_flutter: ^4.4.1
|
||||
webview_flutter: ^4.8.0
|
||||
workmanager: ^0.5.1
|
||||
|
||||
dependency_overrides:
|
||||
@ -130,5 +134,29 @@ flutter:
|
||||
- asset: assets/fonts/exo_2/Exo2-Regular.ttf
|
||||
- asset: assets/fonts/exo_2/Exo2-Bold.ttf
|
||||
weight: 700
|
||||
- family: AtkinsonHyperlegible
|
||||
fonts:
|
||||
- asset: assets/fonts/atkinson_hyperlegible/AtkinsonHyperlegible-Regular.ttf
|
||||
- asset: assets/fonts/atkinson_hyperlegible/AtkinsonHyperlegible-Bold.ttf
|
||||
weight: 700
|
||||
|
||||
flutter_native_splash:
|
||||
# This package generates native code to customize Flutter's default white native splash screen
|
||||
# with background color and splash image.
|
||||
# Customize the parameters below, and run the following command in the terminal:
|
||||
# dart run flutter_native_splash:create
|
||||
# To restore Flutter's default white splash screen, run the following command in the terminal:
|
||||
# dart run flutter_native_splash:remove
|
||||
|
||||
# IMPORTANT NOTE: These parameter do not affect the configuration of Android 12 and later, which
|
||||
# handle splash screens differently that prior versions of Android. Android 12 and later must be
|
||||
# configured specifically in the android_12 section below.
|
||||
|
||||
# color or background_image is the only required parameter. Use color to set the background
|
||||
# of your splash screen to a solid color. Use background_image to set the background of your
|
||||
# splash screen to a png image. This is useful for gradients. The image will be stretch to the
|
||||
# size of the app. Only one parameter can be used, color and background_image cannot both be set.
|
||||
color: "#ffffff"
|
||||
color_dark: "#000000"
|
||||
|
||||
|
||||
|
10
scripts/analysis_options.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
include: package:very_good_analysis/analysis_options.5.0.0.yaml
|
||||
linter:
|
||||
rules:
|
||||
parameter_assignments: false
|
||||
public_member_api_docs: false
|
||||
library_private_types_in_public_api: false
|
||||
omit_local_variable_types: false
|
||||
one_member_abstracts: false
|
||||
always_specify_types: true
|
||||
avoid_print: false
|
133
scripts/bin/parser_verifier.dart
Normal file
@ -0,0 +1,133 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:html/dom.dart' hide Comment;
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:html_unescape/html_unescape.dart';
|
||||
|
||||
Future<void> main(List<String> arguments) async {
|
||||
/// Get the GitHub token from args for so that we can create issues if
|
||||
/// anything doesn't go as expected.
|
||||
final ArgParser parser = ArgParser()
|
||||
..addFlag('github-token', negatable: false, abbr: 't');
|
||||
final ArgResults argResults = parser.parse(arguments);
|
||||
final String token = argResults.rest.first;
|
||||
|
||||
/// The expected parser result.
|
||||
const String text = '''
|
||||
What does it say about the world we live in where blogs do more basic journalism than CNN? All that one would have had to do is read the report actually provided.
|
||||
|
||||
I don't think I'm being too extreme when I say that, apart from maybe PBS, there is no reputable source of news in America. If you don't believe me, pick a random story, watch it as it gets rewritten a million times through Reuters, then check back on the facts of the story one year later. A news story gets twisted to promote some narrative that will sell papers, and when the facts of the story are finally verified (usually not by the news themselves, but lawyers or courts or whoever), the story is dropped and never reported on again.
|
||||
|
||||
Again, if the only thing a reporter had to do was read the report to find the facts of the case to verify what is and isn't true, what the fuck is even the point of a news agency?''';
|
||||
|
||||
/// Get HTML of the thread.
|
||||
const String itemBaseUrl = 'https://news.ycombinator.com/item?id=';
|
||||
const Map<String, String> headers = <String, String>{
|
||||
'accept': '*/*',
|
||||
'user-agent':
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1',
|
||||
};
|
||||
const int itemId = 11536543;
|
||||
final Dio dio = Dio();
|
||||
final Uri url = Uri.parse('$itemBaseUrl$itemId');
|
||||
final Options option = Options(headers: headers, persistentConnection: true);
|
||||
final Response<String> response =
|
||||
await dio.getUri<String>(url, options: option);
|
||||
|
||||
/// Parse the HTML and select all the comment elements.
|
||||
final String data = response.data ?? '';
|
||||
final Document document = parse(data);
|
||||
const String athingComtrSelector =
|
||||
'#hnmain > tbody > tr > td > table > tbody > .athing.comtr';
|
||||
final List<Element> elements = document.querySelectorAll(athingComtrSelector);
|
||||
|
||||
/// Verify comment text parser using the first comment element.
|
||||
if (elements.isNotEmpty) {
|
||||
final Element e = elements.first;
|
||||
const String commentTextSelector =
|
||||
'''td > table > tbody > tr > td.default > div.comment > div.commtext''';
|
||||
final Element? cmtTextElement = e.querySelector(commentTextSelector);
|
||||
final String parsedText =
|
||||
await parseCommentTextHtml(cmtTextElement?.innerHtml ?? '');
|
||||
|
||||
if (parsedText != text) {
|
||||
final Uri url =
|
||||
Uri.parse('https://api.github.com/repos/livinglist/hacki/issues');
|
||||
const String issueTitle = 'Parser check failed.';
|
||||
|
||||
/// Check if an issue with same title already exists.
|
||||
final Response<String> response = await dio.getUri<String>(url);
|
||||
if (response.data?.contains(issueTitle) ?? false) {
|
||||
print('Issue already exists.');
|
||||
return;
|
||||
} else {
|
||||
print('Diff detected, creating issue...');
|
||||
|
||||
/// Create the issue if one does not exist.
|
||||
final Map<String, String> githubHeaders = <String, String>{
|
||||
'Authorization': 'Bearer $token',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final Map<String, dynamic> githubIssuePayload = <String, dynamic>{
|
||||
'assignees': <String>['livinglist'],
|
||||
'title': issueTitle,
|
||||
'body': '''
|
||||
| Expected | Actual |
|
||||
| ------------- | ------------- |
|
||||
| ${text.replaceAll('\n', '<br>')} | ${parsedText.replaceAll('\n', '<br>')} |''',
|
||||
};
|
||||
await dio.postUri<String>(
|
||||
url,
|
||||
data: githubIssuePayload,
|
||||
options: Options(
|
||||
headers: githubHeaders,
|
||||
),
|
||||
);
|
||||
print('Issue created.');
|
||||
}
|
||||
} else {
|
||||
print('Expected:\n$text\n');
|
||||
print('Actual:\n$parsedText\n');
|
||||
}
|
||||
} else {
|
||||
throw Exception('No comment from Hacker News.');
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> parseCommentTextHtml(String text) async {
|
||||
return HtmlUnescape()
|
||||
.convert(text)
|
||||
.replaceAllMapped(
|
||||
RegExp(
|
||||
r'\<div class="reply"\>(.*?)\<\/div\>',
|
||||
dotAll: true,
|
||||
),
|
||||
(Match match) => '',
|
||||
)
|
||||
.replaceAllMapped(
|
||||
RegExp(
|
||||
r'\<span class="(.*?)"\>(.*?)\<\/span\>',
|
||||
dotAll: true,
|
||||
),
|
||||
(Match match) => '${match[2]}',
|
||||
)
|
||||
.replaceAllMapped(
|
||||
RegExp(
|
||||
r'\<p\>(.*?)\<\/p\>',
|
||||
dotAll: true,
|
||||
),
|
||||
(Match match) => '\n\n${match[1]}',
|
||||
)
|
||||
.replaceAllMapped(
|
||||
RegExp(r'\<a href=\"(.*?)\".*?\>.*?\<\/a\>'),
|
||||
(Match match) => match[1] ?? '',
|
||||
)
|
||||
.replaceAllMapped(
|
||||
RegExp(r'\<i\>(.*?)\<\/i\>'),
|
||||
(Match match) => '*${match[1]}*',
|
||||
)
|
||||
.trim();
|
||||
}
|
125
scripts/pubspec.lock
Normal file
@ -0,0 +1,125 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
args:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: args
|
||||
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.3+1"
|
||||
html:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: html
|
||||
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.4"
|
||||
html_unescape:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: html_unescape
|
||||
sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
very_good_analysis:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: very_good_analysis
|
||||
sha256: "9ae7f3a3bd5764fb021b335ca28a34f040cd0ab6eec00a1b213b445dae58a4b8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
sdks:
|
||||
dart: ">=3.0.0 <4.0.0"
|
13
scripts/pubspec.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
name: parser_verifier
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
args: ^2.5.0
|
||||
dio: ^5.0.3
|
||||
html: ^0.15.1
|
||||
html_unescape: ^2.0.0
|
||||
|
||||
dev_dependencies:
|
||||
very_good_analysis: ^5.0.0
|