Compare commits

...

33 Commits

Author SHA1 Message Date
92a743f2f0 update date_display_format.dart (#379) 2024-02-20 22:21:25 -08:00
4148240daf update display_date_format.dart (#378) 2024-02-20 21:50:35 -08:00
90ec117b4b bump version. (#377) 2024-02-18 19:41:38 -08:00
9580e9b3e5 fix date time display. (#376) 2024-02-18 18:51:51 -08:00
0afaa5a0aa add date time display customization. (#375) 2024-02-17 02:04:00 -08:00
fbce1dff73 revert. (#374) 2024-02-13 16:52:14 -08:00
b0d6561486 update publish_ios.yml (#372) 2024-02-13 00:47:16 -08:00
11639118c5 update publish_ios.yml (#371) 2024-02-13 00:21:13 -08:00
e54c893e6c update publish_ios.yml (#370) 2024-02-13 00:07:31 -08:00
8c57e5e323 update publish_ios action. (#369) 2024-02-12 23:41:00 -08:00
b4ec7ec44e update publish_ios action. (#368) 2024-02-12 23:03:35 -08:00
8b65256294 update deploy target. (#367) 2024-02-12 22:30:23 -08:00
58f7bf14d7 bump fastlane version. (#366) 2024-02-12 21:55:58 -08:00
d9aad3d34e bump flutter version. (#365) 2024-02-12 20:54:33 -08:00
ed48d95375 fix comments repo. (#364) 2024-01-03 14:31:03 -08:00
1eaded5694 fix uncaught error. (#359) 2023-12-11 01:01:26 -08:00
70bb78afcb use InterceptorsWrapper for caching. (#358) 2023-12-10 15:29:40 -08:00
df2d2478d5 improve comment fetching. (#357) 2023-12-09 18:20:28 -08:00
d5ae60327d change fetch method based on network condition. (#356) 2023-12-09 10:03:22 -08:00
615a092c1e update fetching strategy. (#355) 2023-12-09 02:05:03 -08:00
5a7699d866 update hacker_news_web_repository.dart (#354) 2023-12-09 00:00:20 -08:00
56a9bab3f2 improve error handling. (#353) 2023-12-08 22:37:57 -08:00
e9bbf46b4f fix comment fetching. (#352) 2023-12-08 21:25:02 -08:00
10f503a6c0 add cache for story metadata. (#350) 2023-12-08 20:09:05 -08:00
582f3156b2 add dev option. (#349) 2023-12-08 17:21:30 -08:00
90fb45146f fix story repository. (#348) 2023-12-08 14:25:32 -08:00
c19c54e762 optimize comment fetching. (#347) 2023-12-08 13:35:52 -08:00
70e5a84b63 improve comment fetching. (#346) 2023-12-08 10:18:03 -08:00
3a51fa83f2 update story tile padding. (#344) 2023-12-08 01:12:49 -08:00
cb90751330 fix flickering image. (#343) 2023-12-07 23:24:43 -08:00
835ed7e841 use different comment fetching strategy. (#342) 2023-12-07 21:46:13 -08:00
125ccd2dd1 use isolate to fetch comments. (#341) 2023-12-05 21:04:40 -08:00
5b991c4287 update theme. (#340) 2023-12-03 17:30:34 -08:00
51 changed files with 1428 additions and 498 deletions

View File

@ -10,7 +10,7 @@ on:
jobs:
build_and_publish:
runs-on: macos-latest
runs-on: macos-13
timeout-minutes: 30
env:
@ -19,6 +19,11 @@ jobs:
BUNDLE_GEMFILE: ${{ github.workspace }}/ios/Gemfile
steps:
- name: Set XCode version
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.0'
- name: Check out from git
uses: actions/checkout@v3
with:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,11 +7,11 @@ PODS:
- Flutter (1.0.0)
- flutter_email_sender (0.0.1):
- Flutter
- flutter_inappwebview (0.0.1):
- flutter_inappwebview_ios (0.0.1):
- Flutter
- flutter_inappwebview/Core (= 0.0.1)
- flutter_inappwebview_ios/Core (= 0.0.1)
- OrderedSet (~> 5.0)
- flutter_inappwebview/Core (0.0.1):
- flutter_inappwebview_ios/Core (0.0.1):
- Flutter
- OrderedSet (~> 5.0)
- flutter_local_notifications (0.0.1):
@ -20,9 +20,6 @@ PODS:
- Flutter
- flutter_siri_suggestions (0.0.1):
- Flutter
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- in_app_review (0.2.0):
- Flutter
- integration_test (0.0.1):
@ -38,7 +35,7 @@ PODS:
- Flutter
- MTBBarcodeScanner
- ReachabilitySwift (5.0.0)
- receive_sharing_intent (0.0.1):
- receive_sharing_intent (1.5.3):
- Flutter
- share_plus (0.0.1):
- Flutter
@ -47,7 +44,7 @@ PODS:
- FlutterMacOS
- sqflite (0.0.3):
- Flutter
- FMDB (>= 2.7.5)
- FlutterMacOS
- synced_shared_preferences (0.0.1):
- Flutter
- url_launcher_ios (0.0.1):
@ -64,7 +61,7 @@ DEPENDENCIES:
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`)
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`)
@ -76,7 +73,7 @@ DEPENDENCIES:
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
- synced_shared_preferences (from `.symlinks/plugins/synced_shared_preferences/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wakelock (from `.symlinks/plugins/wakelock/ios`)
@ -85,7 +82,6 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- FMDB
- MTBBarcodeScanner
- OrderedSet
- ReachabilitySwift
@ -99,8 +95,8 @@ EXTERNAL SOURCES:
:path: Flutter
flutter_email_sender:
:path: ".symlinks/plugins/flutter_email_sender/ios"
flutter_inappwebview:
:path: ".symlinks/plugins/flutter_inappwebview/ios"
flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_secure_storage:
@ -124,7 +120,7 @@ EXTERNAL SOURCES:
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
:path: ".symlinks/plugins/sqflite/darwin"
synced_shared_preferences:
:path: ".symlinks/plugins/synced_shared_preferences/ios"
url_launcher_ios:
@ -139,31 +135,30 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
flutter_inappwebview: 3d32228f1304635e7c028b0d4252937730bbc6cf
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
integration_test: 13825b8a9334a850581300559b8839134b124670
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
receive_sharing_intent: 753f808c6be5550247f6a20f2a14972466a5f33c
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
url_launcher_ios: 68d46cc9766d0c41dbdc884310529557e3cd7a86
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937
PODFILE CHECKSUM: 0957b955069bb512c22bae4cadad9f4c34161dbe
COCOAPODS: 1.13.0

View File

@ -230,13 +230,13 @@
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
E2E6E097A94005D9196D0A71 /* [CP] Check Pods Manifest.lock */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
E51D52B8283B464E00FC8DD8 /* Embed App Extensions */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
E51D52B8283B464E00FC8DD8 /* Embed App Extensions */,
F1959755D5521D58CA193498 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
@ -291,7 +291,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1330;
LastUpgradeCheck = 1430;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@ -548,7 +548,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@ -636,7 +636,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 15;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@ -685,7 +685,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;

View File

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

View File

@ -114,6 +114,6 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
await _authRepository.logout();
await _preferenceRepository.updateUnreadCommentsIds(<int>[]);
await _sembastRepository.deleteAll();
await _sembastRepository.deleteCachedComments();
}
}

View File

@ -35,7 +35,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
super(const StoriesState.init()) {
on<LoadStories>(
onLoadStories,
transformer: sequential(),
transformer: concurrent(),
);
on<StoriesInitialize>(onInitialize);
on<StoriesRefresh>(onRefresh);

View File

@ -83,4 +83,7 @@ abstract class AppDurations {
static const Duration oneSecond = Duration(seconds: 1);
static const Duration twoSeconds = Duration(seconds: 2);
static const Duration tenSeconds = Duration(seconds: 10);
static const Duration sec30 = Duration(seconds: 30);
static const Duration oneMinute = Duration(minutes: 1);
static const Duration twoMinutes = Duration(minutes: 2);
}

View File

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:hacki/config/constants.dart';
@ -25,6 +26,7 @@ part 'comments_state.dart';
class CommentsCubit extends Cubit<CommentsState> {
CommentsCubit({
required FilterCubit filterCubit,
required PreferenceCubit preferenceCubit,
required CollapseCache collapseCache,
required bool isOfflineReading,
required Item item,
@ -34,8 +36,10 @@ class CommentsCubit extends Cubit<CommentsState> {
OfflineRepository? offlineRepository,
SembastRepository? sembastRepository,
HackerNewsRepository? hackerNewsRepository,
HackerNewsWebRepository? hackerNewsWebRepository,
Logger? logger,
}) : _filterCubit = filterCubit,
_preferenceCubit = preferenceCubit,
_collapseCache = collapseCache,
_commentCache = commentCache ?? locator.get<CommentCache>(),
_offlineRepository =
@ -44,6 +48,8 @@ class CommentsCubit extends Cubit<CommentsState> {
sembastRepository ?? locator.get<SembastRepository>(),
_hackerNewsRepository =
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
_hackerNewsWebRepository =
hackerNewsWebRepository ?? locator.get<HackerNewsWebRepository>(),
_logger = logger ?? locator.get<Logger>(),
super(
CommentsState.init(
@ -55,11 +61,13 @@ class CommentsCubit extends Cubit<CommentsState> {
);
final FilterCubit _filterCubit;
final PreferenceCubit _preferenceCubit;
final CollapseCache _collapseCache;
final CommentCache _commentCache;
final OfflineRepository _offlineRepository;
final SembastRepository _sembastRepository;
final HackerNewsRepository _hackerNewsRepository;
final HackerNewsWebRepository _hackerNewsWebRepository;
final Logger _logger;
final ItemScrollController itemScrollController = ItemScrollController();
@ -75,6 +83,30 @@ class CommentsCubit extends Cubit<CommentsState> {
final Map<int, StreamSubscription<Comment>> _streamSubscriptions =
<int, StreamSubscription<Comment>>{};
static const int _webFetchingCmtCountLowerLimit = 50;
Future<bool> get _shouldFetchFromWeb async {
final bool isOnWifi = await _isOnWifi;
if (isOnWifi) {
return switch (state.item) {
Story(descendants: final int descendants)
when descendants > _webFetchingCmtCountLowerLimit =>
true,
Comment(kids: final List<int> kids)
when kids.length > _webFetchingCmtCountLowerLimit =>
true,
_ => false,
};
} else {
return true;
}
}
static Future<bool> get _isOnWifi async {
final ConnectivityResult status = await Connectivity().checkConnectivity();
return status == ConnectivityResult.wifi;
}
@override
void emit(CommentsState state) {
if (!isClosed) {
@ -86,6 +118,8 @@ class CommentsCubit extends Cubit<CommentsState> {
bool onlyShowTargetComment = false,
bool useCommentCache = false,
List<Comment>? targetAncestors,
AppExceptionHandler? onError,
bool fetchFromWeb = true,
}) async {
if (onlyShowTargetComment && (targetAncestors?.isNotEmpty ?? false)) {
emit(
@ -143,11 +177,49 @@ class CommentsCubit extends Cubit<CommentsState> {
getFromCache: useCommentCache ? _commentCache.getComment : null,
);
case FetchMode.eager:
commentStream =
_hackerNewsRepository.fetchAllCommentsRecursivelyStream(
ids: kids,
getFromCache: useCommentCache ? _commentCache.getComment : null,
);
switch (state.order) {
case CommentsOrder.natural:
final bool shouldFetchFromWeb = await _shouldFetchFromWeb;
if (fetchFromWeb && shouldFetchFromWeb) {
_logger.d('fetching from web.');
commentStream = _hackerNewsWebRepository
.fetchCommentsStream(state.item)
.handleError((dynamic e) {
_streamSubscription?.cancel();
_logger.e(e);
switch (e.runtimeType) {
case RateLimitedException:
case RateLimitedWithFallbackException:
case PossibleParsingException:
if (_preferenceCubit.state.devModeEnabled) {
onError?.call(e as AppException);
}
/// If fetching from web failed, fetch using API instead.
refresh(onError: onError, fetchFromWeb: false);
default:
onError?.call(GenericException());
}
});
} else {
_logger.d('fetching from API.');
commentStream =
_hackerNewsRepository.fetchAllCommentsRecursivelyStream(
ids: kids,
getFromCache:
useCommentCache ? _commentCache.getComment : null,
);
}
case CommentsOrder.oldestFirst:
case CommentsOrder.newestFirst:
commentStream =
_hackerNewsRepository.fetchAllCommentsRecursivelyStream(
ids: kids,
getFromCache: useCommentCache ? _commentCache.getComment : null,
);
}
}
}
@ -158,7 +230,10 @@ class CommentsCubit extends Cubit<CommentsState> {
..onDone(_onDone);
}
Future<void> refresh() async {
Future<void> refresh({
required AppExceptionHandler? onError,
bool fetchFromWeb = true,
}) async {
emit(
state.copyWith(
status: CommentsStatus.inProgress,
@ -195,14 +270,47 @@ class CommentsCubit extends Cubit<CommentsState> {
final List<int> kids = _sortKids(updatedItem.kids);
late final Stream<Comment> commentStream;
if (state.fetchMode == FetchMode.lazy) {
commentStream = _hackerNewsRepository.fetchCommentsStream(
ids: kids,
);
} else {
commentStream = _hackerNewsRepository.fetchAllCommentsRecursivelyStream(
ids: kids,
);
switch (state.fetchMode) {
case FetchMode.lazy:
commentStream = _hackerNewsRepository.fetchCommentsStream(ids: kids);
case FetchMode.eager:
switch (state.order) {
case CommentsOrder.natural:
final bool shouldFetchFromWeb = await _shouldFetchFromWeb;
if (fetchFromWeb && shouldFetchFromWeb) {
_logger.d('fetching from web.');
commentStream = _hackerNewsWebRepository
.fetchCommentsStream(state.item)
.handleError((dynamic e) {
_logger.e(e);
switch (e.runtimeType) {
case RateLimitedException:
case RateLimitedWithFallbackException:
case PossibleParsingException:
if (_preferenceCubit.state.devModeEnabled) {
onError?.call(e as AppException);
}
/// If fetching from web failed, fetch using API instead.
refresh(onError: onError, fetchFromWeb: false);
default:
onError?.call(GenericException());
}
});
} else {
_logger.d('fetching from API.');
commentStream = _hackerNewsRepository
.fetchAllCommentsRecursivelyStream(ids: kids);
}
case CommentsOrder.oldestFirst:
case CommentsOrder.newestFirst:
commentStream =
_hackerNewsRepository.fetchAllCommentsRecursivelyStream(
ids: kids,
);
}
}
_streamSubscription = commentStream

View File

@ -2,11 +2,13 @@ import 'dart:async';
import 'dart:collection';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:logger/logger.dart';
part 'fav_state.dart';
@ -17,6 +19,7 @@ class FavCubit extends Cubit<FavState> {
PreferenceRepository? preferenceRepository,
HackerNewsRepository? hackerNewsRepository,
HackerNewsWebRepository? hackerNewsWebRepository,
Logger? logger,
}) : _authBloc = authBloc,
_authRepository = authRepository ?? locator.get<AuthRepository>(),
_preferenceRepository =
@ -25,6 +28,7 @@ class FavCubit extends Cubit<FavState> {
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
_hackerNewsWebRepository =
hackerNewsWebRepository ?? locator.get<HackerNewsWebRepository>(),
_logger = logger ?? locator.get<Logger>(),
super(FavState.init()) {
init();
}
@ -34,6 +38,7 @@ class FavCubit extends Cubit<FavState> {
final PreferenceRepository _preferenceRepository;
final HackerNewsRepository _hackerNewsRepository;
final HackerNewsWebRepository _hackerNewsWebRepository;
final Logger _logger;
late final StreamSubscription<String>? _usernameSubscription;
static const int _pageSize = 20;
@ -93,7 +98,9 @@ class FavCubit extends Cubit<FavState> {
}
void removeFav(int id) {
_preferenceRepository.removeFav(username: username, id: id);
_preferenceRepository
..removeFav(username: username, id: id)
..removeFav(username: '', id: id);
emit(
state.copyWith(
@ -167,20 +174,31 @@ class FavCubit extends Cubit<FavState> {
emit(FavState.init());
}
Future<void> merge() async {
Future<void> merge({
required AppExceptionHandler onError,
required VoidCallback onSuccess,
}) async {
if (_authBloc.state.isLoggedIn) {
emit(state.copyWith(mergeStatus: Status.inProgress));
final Iterable<int> ids = await _hackerNewsWebRepository.fetchFavorites(
of: _authBloc.state.username,
);
final List<int> combinedIds = <int>[...ids, ...state.favIds];
final LinkedHashSet<int> mergedIds = LinkedHashSet<int>.from(combinedIds);
await _preferenceRepository.overwriteFav(
username: username,
ids: mergedIds,
);
emit(state.copyWith(mergeStatus: Status.success));
refresh();
try {
final Iterable<int> ids = await _hackerNewsWebRepository.fetchFavorites(
of: _authBloc.state.username,
);
_logger.d('fetched ${ids.length} favorite items from HN.');
final List<int> combinedIds = <int>[...ids, ...state.favIds];
final LinkedHashSet<int> mergedIds =
LinkedHashSet<int>.from(combinedIds);
await _preferenceRepository.overwriteFav(
username: username,
ids: mergedIds,
);
emit(state.copyWith(mergeStatus: Status.success));
onSuccess();
refresh();
} on RateLimitedException catch (e) {
onError(e);
emit(state.copyWith(mergeStatus: Status.failure));
}
}
}

View File

@ -9,6 +9,7 @@ import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:logger/logger.dart';
part 'notification_state.dart';
@ -19,6 +20,7 @@ class NotificationCubit extends Cubit<NotificationState> {
HackerNewsRepository? hackerNewsRepository,
PreferenceRepository? preferenceRepository,
SembastRepository? sembastRepository,
Logger? logger,
}) : _authBloc = authBloc,
_preferenceCubit = preferenceCubit,
_hackerNewsRepository =
@ -27,6 +29,7 @@ class NotificationCubit extends Cubit<NotificationState> {
preferenceRepository ?? locator.get<PreferenceRepository>(),
_sembastRepository =
sembastRepository ?? locator.get<SembastRepository>(),
_logger = logger ?? locator.get<Logger>(),
super(NotificationState.init()) {
_authBloc.stream
.map((AuthState event) => event.username)
@ -58,6 +61,7 @@ class NotificationCubit extends Cubit<NotificationState> {
final HackerNewsRepository _hackerNewsRepository;
final PreferenceRepository _preferenceRepository;
final SembastRepository _sembastRepository;
final Logger _logger;
Timer? _timer;
static const Duration _refreshInterval = Duration(minutes: 5);
@ -74,6 +78,7 @@ class NotificationCubit extends Cubit<NotificationState> {
});
await _preferenceRepository.unreadCommentsIds.then((List<int> unreadIds) {
_logger.i('NotificationCubit: ${unreadIds.length} unread items.');
emit(state.copyWith(unreadCommentsIds: unreadIds));
});

View File

@ -72,8 +72,12 @@ class PreferenceState extends Equatable {
bool get manualPaginationEnabled => _isOn<ManualPaginationPreference>();
bool get trueDarkModeEnabled => _isOn<TrueDarkModePreference>();
bool get hapticFeedbackEnabled => _isOn<HapticFeedbackPreference>();
bool get devModeEnabled => _isOn<DevMode>();
double get textScaleFactor =>
preferences.singleWhereType<TextScaleFactorPreference>().val;
@ -115,6 +119,9 @@ class PreferenceState extends Equatable {
Font get font =>
Font.values.elementAt(preferences.singleWhereType<FontPreference>().val);
DateDisplayFormat get displayDateFormat => DateDisplayFormat.values
.elementAt(preferences.singleWhereType<DateFormatPreference>().val);
@override
List<Object?> get props => <Object?>[
...preferences.map<dynamic>((Preference<dynamic> e) => e.val),

View File

@ -38,9 +38,19 @@ extension ContextExtension on BuildContext {
);
}
void showErrorSnackBar() => showSnackBar(
content: Constants.errorMessage,
);
void showErrorSnackBar([String? message]) {
ScaffoldMessenger.of(this).showSnackBar(
SnackBar(
backgroundColor: Theme.of(this).colorScheme.errorContainer,
content: Text(
message ?? Constants.errorMessage,
style: TextStyle(
color: Theme.of(this).colorScheme.onErrorContainer,
),
),
),
);
}
Rect? get rect {
final RenderBox? box = findRenderObject() as RenderBox?;

View File

@ -27,7 +27,8 @@ mixin ItemActionMixin<T extends StatefulWidget> on State<T> {
);
}
void showErrorSnackBar() => context.showErrorSnackBar();
void showErrorSnackBar([String? message]) =>
context.showErrorSnackBar(message);
Future<void>? goToItemScreen({
required ItemScreenArgs args,

View File

@ -23,6 +23,7 @@ import 'package:hacki/utils/haptic_feedback_util.dart';
import 'package:hacki/utils/theme_util.dart';
import 'package:hive/hive.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:logger/logger.dart';
import 'package:path_provider/path_provider.dart';
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
@ -45,6 +46,8 @@ void notificationReceiver(NotificationResponse details) =>
Future<void> main({bool testing = false}) async {
WidgetsFlutterBinding.ensureInitialized();
await initializeDateFormatting(Platform.localeName);
isTesting = testing;
final Directory tempDir = await getTemporaryDirectory();
@ -239,11 +242,12 @@ class HackiApp extends StatelessWidget {
buildWhen: (PreferenceState previous, PreferenceState current) =>
previous.appColor != current.appColor ||
previous.font != current.font ||
previous.textScaleFactor != current.textScaleFactor,
previous.textScaleFactor != current.textScaleFactor ||
previous.trueDarkModeEnabled != current.trueDarkModeEnabled,
builder: (BuildContext context, PreferenceState state) {
return AdaptiveTheme(
key: ValueKey<String>(
'''${state.appColor}${state.font}''',
'''${state.appColor}${state.font}${state.trueDarkModeEnabled}''',
),
light: ThemeData(
primaryColor: state.appColor,
@ -286,6 +290,9 @@ class HackiApp extends StatelessWidget {
brightness:
isDarkModeEnabled ? Brightness.dark : Brightness.light,
seedColor: state.appColor,
background: isDarkModeEnabled && state.trueDarkModeEnabled
? Palette.black
: null,
);
return FeatureDiscovery(
child: MediaQuery(
@ -297,16 +304,10 @@ class HackiApp extends StatelessWidget {
),
),
child: MaterialApp.router(
key: Key(state.appColor.hashCode.toString()),
title: 'Hacki',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
brightness: isDarkModeEnabled
? Brightness.dark
: Brightness.light,
seedColor: state.appColor,
),
colorScheme: colorScheme,
fontFamily: state.font.name,
dividerTheme: DividerThemeData(
color: Palette.grey.withOpacity(0.2),

View File

@ -0,0 +1,32 @@
typedef AppExceptionHandler = void Function(AppException);
class AppException implements Exception {
AppException({
required this.message,
this.stackTrace,
});
final String? message;
final StackTrace? stackTrace;
}
class RateLimitedException extends AppException {
RateLimitedException() : super(message: 'Rate limited...');
}
class RateLimitedWithFallbackException extends AppException {
RateLimitedWithFallbackException()
: super(message: 'Rate limited, fetching from API instead...');
}
class PossibleParsingException extends AppException {
PossibleParsingException({
required this.itemId,
}) : super(message: 'Possible parsing failure...');
final int itemId;
}
class GenericException extends AppException {
GenericException() : super(message: 'Something went wrong...');
}

View File

@ -0,0 +1,54 @@
import 'dart:io';
import 'package:hacki/extensions/date_time_extension.dart';
import 'package:intl/intl.dart';
enum DateDisplayFormat {
timeAgo,
yMd,
yMEd,
yMMMd,
yMMMEd;
String get description {
final DateTime exampleDate =
DateTime.now().subtract(const Duration(days: 5));
return switch (this) {
timeAgo => exampleDate.toTimeAgoString(),
yMd || yMEd || yMMMd || yMMMEd => () {
final String defaultLocale = Platform.localeName;
final DateFormat formatter = DateFormat(name, defaultLocale).add_Hm();
return formatter.format(exampleDate);
}(),
};
}
String convertToString(int timestamp) {
final bool isTimeAgo = this == timeAgo;
if (!isTimeAgo && _cache.containsKey(timestamp)) {
return _cache[timestamp] ?? 'This is wrong';
}
int updatedTimeStamp = timestamp;
if (updatedTimeStamp < 9999999999) {
updatedTimeStamp = updatedTimeStamp * 1000;
}
final DateTime date = DateTime.fromMillisecondsSinceEpoch(updatedTimeStamp);
if (isTimeAgo) {
return date.toTimeAgoString();
} else {
final String defaultLocale = Platform.localeName;
final DateFormat formatter = DateFormat(name, defaultLocale).add_Hm();
final String dateString = formatter.format(date);
_cache[timestamp] = dateString;
return dateString;
}
}
static void clearCache() => _cache.clear();
static Map<int, String> _cache = <int, String>{};
}

View File

@ -0,0 +1,19 @@
import 'package:dio/dio.dart';
class CachedResponse<T> extends Response<T> {
CachedResponse({
required super.requestOptions,
super.data,
super.statusCode,
}) : setDateTime = DateTime.now();
factory CachedResponse.fromResponse(Response<T> response) {
return CachedResponse<T>(
requestOptions: response.requestOptions,
data: response.data,
statusCode: response.statusCode,
);
}
final DateTime setDateTime;
}

View File

@ -90,8 +90,13 @@ class Item extends Equatable {
final List<int> kids;
final List<int> parts;
String get timeAgo =>
DateTime.fromMillisecondsSinceEpoch(time * 1000).toTimeAgoString();
String get timeAgo {
int time = this.time;
if (time < 9999999999) {
time = time * 1000;
}
return DateTime.fromMillisecondsSinceEpoch(time).toTimeAgoString();
}
bool get isPoll => type == 'poll';

View File

@ -1,4 +1,6 @@
export 'app_exception.dart';
export 'comments_order.dart';
export 'date_display_format.dart';
export 'discoverable_feature.dart';
export 'export_destination.dart';
export 'fetch_mode.dart';

View File

@ -7,7 +7,7 @@ import 'package:hacki/models/displayable.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/styles/palette.dart';
abstract class Preference<T> extends Equatable with SettingsDisplayable {
abstract final class Preference<T> extends Equatable with SettingsDisplayable {
const Preference({required this.val});
final T val;
@ -19,7 +19,7 @@ abstract 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(),
@ -27,15 +27,16 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
TabOrderPreference(),
StoryMarkingModePreference(),
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 MetadataModePreference(),
const StoryUrlModePreference(),
// Divider.
// Divider.
const MarkReadStoriesModePreference(),
// Divider.
// Divider.
const NotificationModePreference(),
const AutoScrollModePreference(),
const CollapseModePreference(),
@ -45,6 +46,8 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
const SwipeGesturePreference(),
const HapticFeedbackPreference(),
const EyeCandyModePreference(),
const TrueDarkModePreference(),
const DevMode(),
],
);
@ -52,46 +55,47 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
List<Object?> get props => <Object?>[key];
}
abstract class BooleanPreference extends Preference<bool> {
abstract final class BooleanPreference extends Preference<bool> {
const BooleanPreference({required super.val});
}
abstract class IntPreference extends Preference<int> {
abstract final class IntPreference extends Preference<int> {
const IntPreference({required super.val});
}
abstract class DoublePreference extends Preference<double> {
abstract final class DoublePreference extends Preference<double> {
const DoublePreference({required super.val});
}
const bool _notificationModeDefaultValue = true;
const bool _swipeGestureModeDefaultValue = false;
const bool _displayModeDefaultValue = true;
const bool _eyeCandyModeDefaultValue = false;
const bool _hapticFeedbackModeDefaultValue = true;
const bool _readerModeDefaultValue = true;
const bool _markReadStoriesModeDefaultValue = true;
const bool _metadataModeDefaultValue = true;
const bool _storyUrlModeDefaultValue = true;
const bool _collapseModeDefaultValue = true;
const bool _autoScrollModeDefaultValue = false;
const bool _customTabModeDefaultValue = false;
const bool _paginationModeDefaultValue = false;
const double _textScaleFactorDefaultValue = 1;
final int _fetchModeDefaultValue = FetchMode.eager.index;
final int _commentsOrderDefaultValue = CommentsOrder.natural.index;
final int _fontSizeDefaultValue = FontSize.regular.index;
final int _appColorDefaultValue = materialColors.indexOf(Palette.deepOrange);
final int _fontDefaultValue = Font.robotoSlab.index;
final int _tabOrderDefaultValue =
StoryType.convertToSettingsValue(StoryType.values);
final int _markStoriesAsReadWhenPreferenceDefaultValue =
StoryMarkingMode.tap.index;
final class DevMode extends BooleanPreference {
const DevMode({bool? val}) : super(val: val ?? _devModeDefaultValue);
class SwipeGesturePreference extends BooleanPreference {
static const bool _devModeDefaultValue = false;
@override
DevMode copyWith({required bool? val}) {
return DevMode(val: val);
}
@override
String get key => 'devMode';
@override
String get title => 'Dev Mode';
@override
String get subtitle => '';
@override
bool get isDisplayable => false;
}
final class SwipeGesturePreference extends BooleanPreference {
const SwipeGesturePreference({bool? val})
: super(val: val ?? _swipeGestureModeDefaultValue);
static const bool _swipeGestureModeDefaultValue = false;
@override
SwipeGesturePreference copyWith({required bool? val}) {
return SwipeGesturePreference(val: val);
@ -108,10 +112,12 @@ class SwipeGesturePreference extends BooleanPreference {
'''enable swipe gesture for switching between tabs. If enabled, long press on Story tile to trigger the action menu.''';
}
class NotificationModePreference extends BooleanPreference {
final class NotificationModePreference extends BooleanPreference {
const NotificationModePreference({bool? val})
: super(val: val ?? _notificationModeDefaultValue);
static const bool _notificationModeDefaultValue = true;
@override
NotificationModePreference copyWith({required bool? val}) {
return NotificationModePreference(val: val);
@ -128,10 +134,12 @@ class NotificationModePreference extends BooleanPreference {
'''Hacki scans for new replies to your 15 most recent comments or stories every 5 minutes while the app is running in the foreground.''';
}
class CollapseModePreference extends BooleanPreference {
final class CollapseModePreference extends BooleanPreference {
const CollapseModePreference({bool? val})
: super(val: val ?? _collapseModeDefaultValue);
static const bool _collapseModeDefaultValue = true;
@override
CollapseModePreference copyWith({required bool? val}) {
return CollapseModePreference(val: val);
@ -148,10 +156,12 @@ class CollapseModePreference extends BooleanPreference {
'''if disabled, tap on the top of comment tile to collapse.''';
}
class AutoScrollModePreference extends BooleanPreference {
final class AutoScrollModePreference extends BooleanPreference {
const AutoScrollModePreference({bool? val})
: super(val: val ?? _autoScrollModeDefaultValue);
static const bool _autoScrollModeDefaultValue = false;
@override
AutoScrollModePreference copyWith({required bool? val}) {
return AutoScrollModePreference(val: val);
@ -161,7 +171,7 @@ class AutoScrollModePreference extends BooleanPreference {
String get key => 'autoScrollMode';
@override
String get title => 'Auto-scroll on collapsing';
String get title => 'Auto-scroll on Collapsing';
@override
String get subtitle =>
@ -170,10 +180,12 @@ class AutoScrollModePreference extends BooleanPreference {
/// The value deciding whether or not the story
/// tile should display link preview. Defaults to true.
class DisplayModePreference extends BooleanPreference {
final class DisplayModePreference extends BooleanPreference {
const DisplayModePreference({bool? val})
: super(val: val ?? _displayModeDefaultValue);
static const bool _displayModeDefaultValue = true;
@override
DisplayModePreference copyWith({required bool? val}) {
return DisplayModePreference(val: val);
@ -189,10 +201,12 @@ class DisplayModePreference extends BooleanPreference {
String get subtitle => 'show web preview in story tile.';
}
class MetadataModePreference extends BooleanPreference {
final class MetadataModePreference extends BooleanPreference {
const MetadataModePreference({bool? val})
: super(val: val ?? _metadataModeDefaultValue);
static const bool _metadataModeDefaultValue = true;
@override
MetadataModePreference copyWith({required bool? val}) {
return MetadataModePreference(val: val);
@ -209,10 +223,12 @@ class MetadataModePreference extends BooleanPreference {
'''show number of comments and post date in story tile.''';
}
class StoryUrlModePreference extends BooleanPreference {
final class StoryUrlModePreference extends BooleanPreference {
const StoryUrlModePreference({bool? val})
: super(val: val ?? _storyUrlModeDefaultValue);
static const bool _storyUrlModeDefaultValue = true;
@override
StoryUrlModePreference copyWith({required bool? val}) {
return StoryUrlModePreference(val: val);
@ -228,10 +244,12 @@ class StoryUrlModePreference extends BooleanPreference {
String get subtitle => '''show url in story tile.''';
}
class ReaderModePreference extends BooleanPreference {
final class ReaderModePreference extends BooleanPreference {
const ReaderModePreference({bool? val})
: super(val: val ?? _readerModeDefaultValue);
static const bool _readerModeDefaultValue = true;
@override
ReaderModePreference copyWith({required bool? val}) {
return ReaderModePreference(val: val);
@ -251,10 +269,12 @@ class ReaderModePreference extends BooleanPreference {
bool get isDisplayable => Platform.isIOS;
}
class MarkReadStoriesModePreference extends BooleanPreference {
final class MarkReadStoriesModePreference extends BooleanPreference {
const MarkReadStoriesModePreference({bool? val})
: super(val: val ?? _markReadStoriesModeDefaultValue);
static const bool _markReadStoriesModeDefaultValue = true;
@override
MarkReadStoriesModePreference copyWith({required bool? val}) {
return MarkReadStoriesModePreference(val: val);
@ -270,10 +290,12 @@ class MarkReadStoriesModePreference extends BooleanPreference {
String get subtitle => 'grey out stories you have read.';
}
class EyeCandyModePreference extends BooleanPreference {
final class EyeCandyModePreference extends BooleanPreference {
const EyeCandyModePreference({bool? val})
: super(val: val ?? _eyeCandyModeDefaultValue);
static const bool _eyeCandyModeDefaultValue = false;
@override
EyeCandyModePreference copyWith({required bool? val}) {
return EyeCandyModePreference(val: val);
@ -289,10 +311,12 @@ class EyeCandyModePreference extends BooleanPreference {
String get subtitle => 'some sort of magic.';
}
class ManualPaginationPreference extends BooleanPreference {
final class ManualPaginationPreference extends BooleanPreference {
const ManualPaginationPreference({bool? val})
: super(val: val ?? _paginationModeDefaultValue);
static const bool _paginationModeDefaultValue = false;
@override
ManualPaginationPreference copyWith({required bool? val}) {
return ManualPaginationPreference(val: val);
@ -312,10 +336,12 @@ class ManualPaginationPreference extends BooleanPreference {
/// If false, default browser will be used.
///
/// https://developer.chrome.com/docs/android/custom-tabs/
class CustomTabPreference extends BooleanPreference {
final class CustomTabPreference extends BooleanPreference {
const CustomTabPreference({bool? val})
: super(val: val ?? _customTabModeDefaultValue);
static const bool _customTabModeDefaultValue = false;
@override
CustomTabPreference copyWith({required bool? val}) {
return CustomTabPreference(val: val);
@ -335,10 +361,33 @@ class CustomTabPreference extends BooleanPreference {
bool get isDisplayable => Platform.isAndroid;
}
class HapticFeedbackPreference extends BooleanPreference {
final class TrueDarkModePreference extends BooleanPreference {
const TrueDarkModePreference({bool? val})
: super(val: val ?? _trueDarkModeDefaultValue);
static const bool _trueDarkModeDefaultValue = false;
@override
TrueDarkModePreference copyWith({required bool? val}) {
return TrueDarkModePreference(val: val);
}
@override
String get key => 'trueDarkMode';
@override
String get title => 'True Dark Mode';
@override
String get subtitle => 'real dark.';
}
final class HapticFeedbackPreference extends BooleanPreference {
const HapticFeedbackPreference({bool? val})
: super(val: val ?? _hapticFeedbackModeDefaultValue);
static const bool _hapticFeedbackModeDefaultValue = true;
@override
HapticFeedbackPreference copyWith({required bool? val}) {
return HapticFeedbackPreference(val: val);
@ -352,14 +401,13 @@ class HapticFeedbackPreference extends BooleanPreference {
@override
String get subtitle => '';
@override
bool get isDisplayable => Platform.isIOS;
}
class FetchModePreference extends IntPreference {
final class FetchModePreference extends IntPreference {
FetchModePreference({int? val}) : super(val: val ?? _fetchModeDefaultValue);
static final int _fetchModeDefaultValue = FetchMode.eager.index;
@override
FetchModePreference copyWith({required int? val}) {
return FetchModePreference(val: val);
@ -372,10 +420,12 @@ class FetchModePreference extends IntPreference {
String get title => 'Default fetch mode';
}
class CommentsOrderPreference extends IntPreference {
final class CommentsOrderPreference extends IntPreference {
CommentsOrderPreference({int? val})
: super(val: val ?? _commentsOrderDefaultValue);
static final int _commentsOrderDefaultValue = CommentsOrder.natural.index;
@override
CommentsOrderPreference copyWith({required int? val}) {
return CommentsOrderPreference(val: val);
@ -388,9 +438,11 @@ class CommentsOrderPreference extends IntPreference {
String get title => 'Default comments order';
}
class FontPreference extends IntPreference {
final class FontPreference extends IntPreference {
FontPreference({int? val}) : super(val: val ?? _fontDefaultValue);
static final int _fontDefaultValue = Font.robotoSlab.index;
@override
FontPreference copyWith({required int? val}) {
return FontPreference(val: val);
@ -403,9 +455,11 @@ class FontPreference extends IntPreference {
String get title => 'Default font';
}
class FontSizePreference extends IntPreference {
final class FontSizePreference extends IntPreference {
FontSizePreference({int? val}) : super(val: val ?? _fontSizeDefaultValue);
static final int _fontSizeDefaultValue = FontSize.regular.index;
@override
FontSizePreference copyWith({required int? val}) {
return FontSizePreference(val: val);
@ -418,9 +472,12 @@ class FontSizePreference extends IntPreference {
String get title => 'Default font size';
}
class TabOrderPreference extends IntPreference {
final class TabOrderPreference extends IntPreference {
TabOrderPreference({int? val}) : super(val: val ?? _tabOrderDefaultValue);
static final int _tabOrderDefaultValue =
StoryType.convertToSettingsValue(StoryType.values);
@override
TabOrderPreference copyWith({required int? val}) {
return TabOrderPreference(val: val);
@ -433,10 +490,13 @@ class TabOrderPreference extends IntPreference {
String get title => 'Tab order';
}
class StoryMarkingModePreference extends IntPreference {
final class StoryMarkingModePreference extends IntPreference {
StoryMarkingModePreference({int? val})
: super(val: val ?? _markStoriesAsReadWhenPreferenceDefaultValue);
static final int _markStoriesAsReadWhenPreferenceDefaultValue =
StoryMarkingMode.tap.index;
@override
StoryMarkingModePreference copyWith({required int? val}) {
return StoryMarkingModePreference(val: val);
@ -449,9 +509,12 @@ class StoryMarkingModePreference extends IntPreference {
String get title => 'Mark as Read on';
}
class AppColorPreference extends IntPreference {
final class AppColorPreference extends IntPreference {
AppColorPreference({int? val}) : super(val: val ?? _appColorDefaultValue);
static final int _appColorDefaultValue =
materialColors.indexOf(Palette.deepOrange);
@override
AppColorPreference copyWith({required int? val}) {
return AppColorPreference(val: val);
@ -464,10 +527,12 @@ class AppColorPreference extends IntPreference {
String get title => 'Accent Color';
}
class TextScaleFactorPreference extends DoublePreference {
final class TextScaleFactorPreference extends DoublePreference {
const TextScaleFactorPreference({double? val})
: super(val: val ?? _textScaleFactorDefaultValue);
static const double _textScaleFactorDefaultValue = 1;
@override
TextScaleFactorPreference copyWith({required double? val}) {
return TextScaleFactorPreference(val: val);
@ -479,3 +544,20 @@ class TextScaleFactorPreference extends DoublePreference {
@override
String get title => 'Default text scale factor';
}
final class DateFormatPreference extends IntPreference {
DateFormatPreference({int? val}) : super(val: val ?? _dateFormatDefaultValue);
static final int _dateFormatDefaultValue = DateDisplayFormat.timeAgo.index;
@override
DateFormatPreference copyWith({required int? val}) {
return DateFormatPreference(val: val);
}
@override
String get key => 'dateFormat';
@override
String get title => 'Date Format';
}

View File

@ -1,11 +1,39 @@
import 'dart:async';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:html/dom.dart';
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/models/models.dart';
import 'package:hacki/utils/utils.dart';
import 'package:html/dom.dart' hide Comment;
import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:html_unescape/html_unescape.dart';
/// For fetching anything that cannot be fetched through Hacker News API.
class HackerNewsWebRepository {
HackerNewsWebRepository();
HackerNewsWebRepository({
Dio? dioWithCache,
Dio? dio,
}) : _dio = dio ?? Dio(),
_dioWithCache = dioWithCache ?? Dio()
..interceptors.addAll(
<Interceptor>[
if (kDebugMode) LoggerInterceptor(),
CacheInterceptor(),
],
);
final Dio _dioWithCache;
final Dio _dio;
static 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',
};
static const String _favoritesBaseUrl =
'https://news.ycombinator.com/favorites?id=';
@ -13,25 +41,39 @@ class HackerNewsWebRepository {
'#hnmain > tbody > tr:nth-child(3) > td > table > tbody > .athing';
Future<Iterable<int>> fetchFavorites({required String of}) async {
final bool isOnWifi = await _isOnWifi;
final String username = of;
final List<int> allIds = <int>[];
int page = 0;
int page = 1;
const int maxPage = 2;
Future<Iterable<int>> fetchIds(int page) async {
final Uri url = Uri.parse('$_favoritesBaseUrl$username&p=$page');
final Response response = await get(url);
final Document document = parse(response.body);
final List<Element> elements = document.querySelectorAll(_aThingSelector);
final Iterable<int> parsedIds = elements
.map(
(Element e) => int.tryParse(e.id),
)
.whereNotNull();
return parsedIds;
Future<Iterable<int>> fetchIds(int page, {bool isComment = false}) async {
try {
final Uri url = Uri.parse(
'''$_favoritesBaseUrl$username${isComment ? '&comments=t' : ''}&p=$page''',
);
final Response<String> response =
await (isOnWifi ? _dioWithCache : _dio).getUri<String>(url);
/// Due to rate limiting, we have a short break here.
await Future<void>.delayed(AppDurations.twoSeconds);
final Document document = parse(response.data);
final List<Element> elements =
document.querySelectorAll(_aThingSelector);
final Iterable<int> parsedIds =
elements.map((Element e) => int.tryParse(e.id)).whereNotNull();
return parsedIds;
} on DioException catch (e) {
if (e.response?.statusCode == HttpStatus.forbidden) {
throw RateLimitedException();
}
throw GenericException();
}
}
Iterable<int> ids;
while (true) {
while (page <= maxPage) {
ids = await fetchIds(page);
if (ids.isEmpty) {
break;
@ -40,6 +82,206 @@ class HackerNewsWebRepository {
page++;
}
page = 1;
while (page <= maxPage) {
ids = await fetchIds(page, isComment: true);
if (ids.isEmpty) {
break;
}
allIds.addAll(ids);
page++;
}
return allIds;
}
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''';
Stream<Comment> fetchCommentsStream(Item item) async* {
final bool isOnWifi = await _isOnWifi;
final int itemId = item.id;
final int? descendants = item is Story ? item.descendants : null;
int parentTextCount = 0;
Future<Iterable<Element>> fetchElements(int page) async {
try {
final Uri url = Uri.parse('$_itemBaseUrl$itemId&p=$page');
final Options option = Options(
headers: _headers,
persistentConnection: true,
);
/// Be more conservative while user is on wifi.
final Response<String> response =
await (isOnWifi ? _dioWithCache : _dio).getUri<String>(
url,
options: option,
);
final String data = response.data ?? '';
if (page == 1) {
parentTextCount = 'parent'.allMatches(data).length;
}
final Document document = parse(data);
final List<Element> elements =
document.querySelectorAll(_athingComtrSelector);
return elements;
} on DioException catch (e) {
if (e.response?.statusCode == HttpStatus.forbidden) {
throw RateLimitedWithFallbackException();
}
throw GenericException();
}
}
if (descendants == 0 || item.kids.isEmpty) return;
final Set<int> fetchedCommentIds = <int>{};
int page = 1;
Iterable<Element> elements = await fetchElements(page);
final Map<int, int> indentToParentId = <int, int>{};
if (item is Story && item.descendants > 0 && elements.isEmpty) {
throw PossibleParsingException(itemId: itemId);
}
while (elements.isNotEmpty) {
for (final Element element in elements) {
/// Get comment id.
final String cmtIdString = element.attributes['id'] ?? '';
final int? cmtId = int.tryParse(cmtIdString);
/// Get comment text.
final Element? cmtTextElement =
element.querySelector(_commentTextSelector);
final String parsedText = await compute(
_parseCommentTextHtml,
cmtTextElement?.innerHtml ?? '',
);
/// Get comment author.
final Element? cmtHeadElement =
element.querySelector(_commentHeadSelector);
final String? cmtAuthor = cmtHeadElement?.text;
/// Get comment age.
final Element? cmtAgeElement =
element.querySelector(_commentAgeSelector);
final String? ageString = cmtAgeElement?.attributes['title'];
final int? timestamp = ageString == null
? null
: DateTime.parse(ageString)
.copyWith(isUtc: true)
.millisecondsSinceEpoch;
/// Get comment indent.
final Element? cmtIndentElement =
element.querySelector(_commentIndentSelector);
final String? indentString = cmtIndentElement?.attributes['indent'];
final int indent =
indentString == null ? 0 : (int.tryParse(indentString) ?? 0);
indentToParentId[indent] = cmtId ?? 0;
final int parentId = indentToParentId[indent - 1] ?? -1;
final Comment cmt = Comment(
id: cmtId ?? 0,
time: timestamp ?? 0,
parent: parentId,
score: 0,
by: cmtAuthor ?? '',
text: parsedText,
kids: const <int>[],
dead: false,
deleted: false,
hidden: false,
level: indent,
isFromCache: false,
);
/// Skip any comment with no valid id or timestamp.
if (cmt.id == 0 || timestamp == 0) {
continue;
}
/// Duplicate comment means we are done fetching all the comments.
if (fetchedCommentIds.contains(cmt.id)) return;
fetchedCommentIds.add(cmt.id);
yield cmt;
}
/// If we didn't successfully got any comment on first page,
/// and we are sure there are comments there based on the count of
/// 'parent' text, then this might be a parsing error and possibly is
/// caused by HN changing their HTML structure, therefore here we
/// throw an error so that we can fallback to use API instead.
if (page == 1 && parentTextCount > 0 && fetchedCommentIds.isEmpty) {
throw PossibleParsingException(itemId: itemId);
}
if (descendants != null && fetchedCommentIds.length >= descendants) {
return;
}
/// Due to rate limiting, we have a short break here.
await Future<void>.delayed(AppDurations.twoSeconds);
page++;
elements = await fetchElements(page);
}
}
static Future<bool> get _isOnWifi async {
final ConnectivityResult status = await Connectivity().checkConnectivity();
return status == ConnectivityResult.wifi;
}
static 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();
}
}

View File

@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/auth_repository.dart';
import 'package:hacki/repositories/post_repository.dart';
import 'package:hacki/utils/service_exception.dart';
/// [PostableRepository] is solely for hosting functionalities shared between
/// [AuthRepository] and [PostRepository].
@ -40,7 +39,7 @@ class PostableRepository {
}
return true;
} on ServiceException {
} on AppException {
return false;
}
}
@ -65,7 +64,7 @@ class PostableRepository {
),
);
} on DioException catch (e) {
throw ServiceException(e.message);
throw AppException(message: e.message);
}
}

View File

@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:io';
import 'package:hacki/models/models.dart';
import 'package:hacki/services/services.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sembast/sembast.dart';
@ -12,23 +14,34 @@ import 'package:sembast/sembast_io.dart';
/// documents directory assigned by host system which you can retrieve
/// by calling [getApplicationDocumentsDirectory].
class SembastRepository {
SembastRepository({Database? database}) {
SembastRepository({
Database? database,
Database? cache,
}) {
if (database == null) {
initializeDatabase();
} else {
_database = database;
}
if (cache == null) {
initializeCache();
} else {
_cache = cache;
}
}
Database? _database;
Database? _cache;
List<int>? _idsOfCommentsRepliedToMe;
static const String _cachedCommentsKey = 'cachedComments';
static const String _commentsKey = 'comments';
static const String _idsOfCommentsRepliedToMeKey = 'idsOfCommentsRepliedToMe';
static const String _metadataCacheKey = 'metadata';
Future<Database> initializeDatabase() async {
final Directory dir = await getApplicationDocumentsDirectory();
final Directory dir = await getApplicationCacheDirectory();
await dir.create(recursive: true);
final String dbPath = join(dir.path, 'hacki.db');
final DatabaseFactory dbFactory = databaseFactoryIo;
@ -37,6 +50,16 @@ class SembastRepository {
return db;
}
Future<Database> initializeCache() async {
final Directory dir = await getTemporaryDirectory();
await dir.create(recursive: true);
final String dbPath = join(dir.path, 'hacki_cache.db');
final DatabaseFactory dbFactory = databaseFactoryIo;
final Database db = await dbFactory.openDatabase(dbPath);
_cache = db;
return db;
}
//#region Cached comments for time machine feature.
Future<Map<String, Object?>> cacheComment(Comment comment) async {
final Database db = _database ?? await initializeDatabase();
@ -177,10 +200,50 @@ class SembastRepository {
//#endregion
Future<FileSystemEntity> deleteAll() async {
//#region
Future<void> cacheMetadata({
required String key,
required WebInfo info,
}) async {
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());
});
}
Future<WebInfo?> getCachedMetadata({
required String key,
}) async {
final Database db = _cache ?? await initializeCache();
final StoreRef<String, Map<String, Object?>> store =
stringMapStoreFactory.store(_metadataCacheKey);
final RecordSnapshot<String, Map<String, Object?>>? snapshot =
await store.record(key).getSnapshot(db);
if (snapshot != null) {
final WebInfo info = WebInfo.fromJson(snapshot.value);
return info;
} else {
return null;
}
}
//#endregion
Future<FileSystemEntity> deleteCachedComments() async {
final Directory dir = await getApplicationDocumentsDirectory();
await dir.create(recursive: true);
final String dbPath = join(dir.path, 'hacki.db');
return File(dbPath).delete();
}
Future<FileSystemEntity> 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();
}
}

View File

@ -49,7 +49,7 @@ class _HomeScreenState extends State<HomeScreen>
super.didPopNext();
if (context.read<StoriesBloc>().deviceScreenType ==
DeviceScreenType.mobile) {
locator.get<Logger>().i('Resetting comments in CommentCache');
locator.get<Logger>().i('resetting comments in CommentCache');
Future<void>.delayed(
AppDurations.ms500,
locator.get<CommentCache>().resetComments,

View File

@ -69,6 +69,7 @@ class ItemScreen extends StatefulWidget {
BlocProvider<CommentsCubit>(
create: (BuildContext context) => CommentsCubit(
filterCubit: context.read<FilterCubit>(),
preferenceCubit: context.read<PreferenceCubit>(),
isOfflineReading:
context.read<StoriesBloc>().state.isOfflineReading,
item: args.item,
@ -79,6 +80,8 @@ class ItemScreen extends StatefulWidget {
onlyShowTargetComment: args.onlyShowTargetComment,
targetAncestors: args.targetComments,
useCommentCache: args.useCommentCache,
onError: (AppException e) =>
context.showErrorSnackBar(e.message),
),
),
],
@ -110,6 +113,7 @@ class ItemScreen extends StatefulWidget {
BlocProvider<CommentsCubit>(
create: (BuildContext context) => CommentsCubit(
filterCubit: context.read<FilterCubit>(),
preferenceCubit: context.read<PreferenceCubit>(),
isOfflineReading:
context.read<StoriesBloc>().state.isOfflineReading,
item: args.item,
@ -121,6 +125,8 @@ class ItemScreen extends StatefulWidget {
)..init(
onlyShowTargetComment: args.onlyShowTargetComment,
targetAncestors: args.targetComments,
onError: (AppException e) =>
context.showErrorSnackBar(e.message),
),
),
],

View File

@ -2,6 +2,7 @@ import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/blocs/auth/auth_bloc.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/cubits/comments/comments_cubit.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/widgets.dart';
@ -65,9 +66,11 @@ class _InThreadSearchViewState extends State<_InThreadSearchView> {
super.initState();
scrollController.addListener(onScroll);
textEditingController.text = widget.commentsCubit.state.inThreadSearchQuery;
if (textEditingController.text.isEmpty) {
focusNode.requestFocus();
}
Future<void>.delayed(AppDurations.ms300, () {
if (textEditingController.text.isEmpty) {
focusNode.requestFocus();
}
});
}
@override

View File

@ -59,7 +59,12 @@ class MainView extends StatelessWidget {
if (context.read<StoriesBloc>().state.isOfflineReading ==
false &&
state.onlyShowTargetComment == false) {
unawaited(context.read<CommentsCubit>().refresh());
unawaited(
context.read<CommentsCubit>().refresh(
onError: (AppException e) =>
context.showErrorSnackBar(e.message),
),
);
if (state.item.isPoll) {
context.read<PollCubit>().refresh();
@ -145,27 +150,28 @@ class MainView extends StatelessWidget {
},
),
),
Positioned(
height: Dimens.pt4,
bottom: Dimens.zero,
left: Dimens.zero,
right: Dimens.zero,
child: BlocBuilder<CommentsCubit, CommentsState>(
buildWhen: (CommentsState prev, CommentsState current) =>
prev.status != current.status,
builder: (BuildContext context, CommentsState state) {
return AnimatedOpacity(
opacity: state.status == CommentsStatus.inProgress
? NumSwitch.on
: NumSwitch.off,
duration: const Duration(
milliseconds: _loadingIndicatorOpacityAnimationDuration,
),
child: const LinearProgressIndicator(),
);
},
if (context.read<PreferenceCubit>().state.devModeEnabled)
Positioned(
height: Dimens.pt4,
bottom: Dimens.zero,
left: Dimens.zero,
right: Dimens.zero,
child: BlocBuilder<CommentsCubit, CommentsState>(
buildWhen: (CommentsState prev, CommentsState current) =>
prev.status != current.status,
builder: (BuildContext context, CommentsState state) {
return AnimatedOpacity(
opacity: state.status == CommentsStatus.inProgress
? NumSwitch.on
: NumSwitch.off,
duration: const Duration(
milliseconds: _loadingIndicatorOpacityAnimationDuration,
),
child: const LinearProgressIndicator(),
);
},
),
),
),
],
);
}
@ -249,9 +255,13 @@ class _ParentItemSection extends StatelessWidget {
),
const Spacer(),
Text(
item.timeAgo,
style: const TextStyle(
color: Palette.grey,
context
.read<PreferenceCubit>()
.state
.displayDateFormat
.convertToString(item.time),
style: TextStyle(
color: Theme.of(context).metadataColor,
),
textScaler: MediaQuery.of(context).textScaler,
),
@ -505,6 +515,9 @@ class _ParentItemSection extends StatelessWidget {
style: TextStyle(color: Palette.grey),
),
),
const SizedBox(
height: 120,
),
],
],
),

View File

@ -147,22 +147,28 @@ class _ProfileScreenState extends State<ProfileScreen>
builder: (
BuildContext context,
Status status,
) =>
TextButton(
onPressed: () {
context.read<FavCubit>().merge();
},
child: status == Status.inProgress
? const SizedBox(
height: Dimens.pt12,
width: Dimens.pt12,
child:
CustomCircularProgressIndicator(
strokeWidth: Dimens.pt2,
),
)
: const Text('Sync from Hacker News'),
),
) {
return TextButton(
onPressed: () =>
context.read<FavCubit>().merge(
onError: (AppException e) =>
showErrorSnackBar(e.message),
onSuccess: () => showSnackBar(
content: '''Sync completed.''',
),
),
child: status == Status.inProgress
? const SizedBox(
height: Dimens.pt12,
width: Dimens.pt12,
child:
CustomCircularProgressIndicator(
strokeWidth: Dimens.pt2,
),
)
: const Text('Sync from Hacker News'),
);
},
)
: null;

View File

@ -89,8 +89,8 @@ class InboxView extends StatelessWidget {
children: <Widget>[
Text(
'''${e.timeAgo} from ${e.by}:''',
style: const TextStyle(
color: Palette.grey,
style: TextStyle(
color: Theme.of(context).metadataColor,
),
),
const SizedBox(

View File

@ -160,6 +160,47 @@ class _SettingsState extends State<Settings> with ItemActionMixin {
const SizedBox(
height: Dimens.pt12,
),
Row(
children: <Widget>[
const SizedBox(
width: Dimens.pt16,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text(
'Date time display of comments',
),
DropdownMenu<DateDisplayFormat>(
initialSelection: preferenceState.displayDateFormat,
dropdownMenuEntries: DateDisplayFormat.values
.map(
(DateDisplayFormat val) =>
DropdownMenuEntry<DateDisplayFormat>(
value: val,
label: val.description,
),
)
.toList(),
onSelected: (DateDisplayFormat? order) {
if (order != null) {
HapticFeedbackUtil.selection();
context.read<PreferenceCubit>().update(
DateFormatPreference(
val: order.index,
),
);
DateDisplayFormat.clearCache();
}
},
),
],
),
],
),
const SizedBox(
height: Dimens.pt12,
),
const TabBarSettings(),
const TextScaleFactorSettings(),
const Divider(),
@ -301,6 +342,17 @@ class _SettingsState extends State<Settings> with ItemActionMixin {
title: const Text('About'),
subtitle: const Text('nothing interesting here.'),
onTap: showAboutHackiDialog,
onLongPress: () {
final DevMode updatedDevMode =
DevMode(val: !preferenceState.devModeEnabled);
context.read<PreferenceCubit>().update(updatedDevMode);
HapticFeedbackUtil.heavy();
if (updatedDevMode.val) {
showSnackBar(content: 'You are a dev now.');
} else {
showSnackBar(content: 'Dev mode disabled');
}
},
),
const SizedBox(
height: Dimens.pt48,
@ -498,6 +550,12 @@ class _SettingsState extends State<Settings> with ItemActionMixin {
.whenComplete(
DefaultCacheManager().emptyCache,
)
.whenComplete(
locator.get<SembastRepository>().deleteCachedComments,
)
.whenComplete(
locator.get<SembastRepository>().deleteCachedMetadata,
)
.whenComplete(() {
showSnackBar(content: 'Cache cleared!');
});
@ -645,6 +703,9 @@ class _SettingsState extends State<Settings> with ItemActionMixin {
context: context,
builder: (BuildContext context) {
return AlertDialog(
actionsPadding: const EdgeInsets.all(
Dimens.pt16,
),
actions: <Widget>[
ElevatedButton(
onPressed: onSendEmailTapped,

View File

@ -188,20 +188,22 @@ class CommentTile extends StatelessWidget {
color: Palette.grey,
),
),
if (!comment.dead && isNew)
const Padding(
padding: EdgeInsets.only(left: 4),
child: Icon(
Icons.sunny_snowing,
size: 16,
color: Palette.grey,
),
),
// Commented out for now, maybe review later.
// if (!comment.dead && isNew)
// const Padding(
// padding: EdgeInsets.only(left: 4),
// child: Icon(
// Icons.sunny_snowing,
// size: 16,
// color: Palette.grey,
// ),
// ),
const Spacer(),
Text(
comment.timeAgo,
style: const TextStyle(
color: Palette.grey,
prefState.displayDateFormat
.convertToString(comment.time),
style: TextStyle(
color: Theme.of(context).metadataColor,
),
textScaler: MediaQuery.of(context).textScaler,
),

View File

@ -97,8 +97,8 @@ class ItemsListView<T extends Item> extends StatelessWidget {
showAuthor
? '''${e.timeAgo} by ${e.by}'''
: e.timeAgo,
style: const TextStyle(
color: Palette.grey,
style: TextStyle(
color: Theme.of(context).metadataColor,
),
),
const SizedBox(
@ -147,6 +147,11 @@ class ItemsListView<T extends Item> extends StatelessWidget {
if (useSimpleTileForStory || !showWebPreviewOnStoryTile)
const Divider(
height: Dimens.zero,
)
else if (context.read<SplitViewCubit>().state.enabled)
const Divider(
height: Dimens.pt6,
color: Palette.transparent,
),
];
} else if (e is Comment) {
@ -186,8 +191,8 @@ class ItemsListView<T extends Item> extends StatelessWidget {
showAuthor
? '''${e.timeAgo} by ${e.by}'''
: e.timeAgo,
style: const TextStyle(
color: Palette.grey,
style: TextStyle(
color: Theme.of(context).metadataColor,
),
),
const SizedBox(

View File

@ -119,10 +119,15 @@ class LinkView extends StatelessWidget {
: CachedNetworkImage(
imageUrl: imageUri!,
fit: isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
memCacheHeight: layoutHeight.toInt() * 4,
cacheKey: imageUri,
errorWidget: (_, __, ___) =>
const SizedBox.shrink(),
errorWidget: (_, __, ___) => Center(
child: Text(
r'¯\_(ツ)_/¯',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
),
),
),
),
),

View File

@ -17,7 +17,6 @@ class _OnboardingViewState extends State<OnboardingView> {
final Throttle throttle = Throttle(delay: _throttleDelay);
static const Duration _throttleDelay = AppDurations.ms100;
static const double _screenshotHeight = 600;
@override
Widget build(BuildContext context) {
@ -44,7 +43,7 @@ class _OnboardingViewState extends State<OnboardingView> {
left: Dimens.zero,
right: Dimens.zero,
child: SizedBox(
height: _screenshotHeight,
height: MediaQuery.of(context).size.height * 0.8,
child: PageView(
controller: pageController,
scrollDirection: Axis.vertical,
@ -69,7 +68,7 @@ class _OnboardingViewState extends State<OnboardingView> {
),
),
Positioned(
bottom: Dimens.pt40,
bottom: MediaQuery.of(context).viewPadding.bottom,
left: Dimens.zero,
right: Dimens.zero,
child: ElevatedButton(
@ -115,16 +114,14 @@ class _PageViewChild extends StatelessWidget {
final String path;
final String description;
static const double _height = 500;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Material(
elevation: 8,
elevation: Dimens.pt8,
child: SizedBox(
height: _height,
height: MediaQuery.of(context).size.height * 0.5,
child: Image.asset(path),
),
),

View File

@ -14,9 +14,9 @@ import 'package:html/dom.dart' hide Comment, Text;
import 'package:html/parser.dart' as parser;
import 'package:http/http.dart';
import 'package:http/io_client.dart';
import 'package:logger/logger.dart';
abstract class InfoBase {
late DateTime _timeout;
late bool _shouldRetry;
Map<String, dynamic> toJson();
@ -97,13 +97,10 @@ class WebAnalyzer {
/// Get web information
/// return [InfoBase]
static InfoBase? getInfoFromCache(String? cacheKey) {
if (cacheKey == null) return null;
final InfoBase? info = cacheMap[cacheKey];
if (info != null) {
if (!info._timeout.isAfter(DateTime.now())) {
cacheMap.remove(cacheKey);
}
}
return info;
}
@ -118,23 +115,31 @@ class WebAnalyzer {
final String key = getKey(story);
final String url = story.url;
/// [1] Try to fetch from mem cache.
InfoBase? info = getInfoFromCache(key);
if (info != null) return info;
if (info != null) {
locator.get<Logger>().d('''
fetched mem cached metadata using key $key for $story:
${info.toJson()}
''');
return info;
}
/// [2] If story doesn't have a url and text is not empty,
/// just use story title and text.
if (story.url.isEmpty && story.text.isNotEmpty) {
info = WebInfo(
title: story.title,
description: story.text,
)
.._timeout = DateTime.now().add(cache)
.._shouldRetry = false;
).._shouldRetry = false;
cacheMap[key] = info;
return info;
}
/// [3] If in offline mode, use comment text for description.
if (offlineReading) {
int index = 0;
Comment? comment;
@ -149,9 +154,7 @@ class WebAnalyzer {
info = WebInfo(
title: story.title,
description: comment != null ? '${comment.by}: ${comment.text}' : null,
)
.._shouldRetry = false
.._timeout = DateTime.now();
).._shouldRetry = false;
cacheMap[key] = info;
@ -159,15 +162,41 @@ class WebAnalyzer {
}
try {
/// [4] Try to fetch from file cache.
info = await locator.get<SembastRepository>().getCachedMetadata(key: key);
/// [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:
${info.toJson()}
''');
cacheMap[key] = info;
return info;
}
/// [6] Try to analyze the web for metadata.
info = await _getInfoByIsolate(
url: url,
multimedia: multimedia,
story: story,
);
/// [7] If web analyzing was successful, cache it in both mem and file.
if (info != null && !info._shouldRetry) {
info._timeout = DateTime.now().add(cache);
cacheMap[key] = info;
if (info is WebInfo) {
locator
.get<Logger>()
.d('caching metadata using key $key for $story.');
unawaited(
locator.get<SembastRepository>().cacheMetadata(
key: key,
info: info,
),
);
}
}
return info;
@ -175,9 +204,7 @@ class WebAnalyzer {
return WebInfo(
title: story.title,
description: story.text,
)
.._shouldRetry = true
.._timeout = DateTime.now();
).._shouldRetry = true;
}
}
@ -393,10 +420,9 @@ class WebAnalyzer {
try {
html = gbk.decode(response.bodyBytes);
} catch (e) {
// locator.get<Logger>().log(
// Level.error,
// 'Web page resolution failure from:$url Error:$e',
// );
locator
.get<Logger>()
.e('''web page resolution failure from:$url Error:$e''');
}
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
extension ThemeDataExtension on ThemeData {
Color get readGrey => colorScheme.onSurface.withOpacity(0.4);
Color get readGrey => colorScheme.onSurface.withOpacity(0.6);
Color get metadataColor => colorScheme.onSurface.withOpacity(0.6);
Color get metadataColor => colorScheme.onSurface.withOpacity(0.8);
}

View File

@ -0,0 +1,46 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/models/dio/cached_response.dart';
class CacheInterceptor extends InterceptorsWrapper {
CacheInterceptor()
: super(
onResponse: (
Response<dynamic> response,
ResponseInterceptorHandler handler,
) async {
final String key = response.requestOptions.uri.toString();
if (response.statusCode == HttpStatus.ok) {
final CachedResponse<dynamic> cachedResponse =
CachedResponse<dynamic>.fromResponse(response);
_cache[key] = cachedResponse;
}
return handler.next(response);
},
onRequest: (
RequestOptions options,
RequestInterceptorHandler handler,
) async {
final String key = options.uri.toString();
final CachedResponse<dynamic>? cachedResponse = _cache[key];
if (cachedResponse != null &&
DateTime.now()
.difference(cachedResponse.setDateTime)
.inSeconds <
_delay.inSeconds) {
return handler.resolve(cachedResponse);
}
return handler.next(options);
},
);
static const Duration _delay = AppDurations.oneMinute;
static final Map<String, CachedResponse<dynamic>> _cache =
<String, CachedResponse<dynamic>>{};
}

View File

@ -0,0 +1,2 @@
export 'cache_interceptor.dart';
export 'logger_interceptor.dart';

View File

@ -0,0 +1,14 @@
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
class LoggerInterceptor extends PrettyDioLogger {
LoggerInterceptor()
: super(
requestHeader: true,
requestBody: true,
responseBody: false,
responseHeader: true,
error: true,
compact: true,
maxWidth: 90,
);
}

View File

@ -14,4 +14,10 @@ abstract class HapticFeedbackUtil {
HapticFeedback.lightImpact();
}
}
static void heavy() {
if (enabled) {
HapticFeedback.heavyImpact();
}
}
}

View File

@ -79,16 +79,15 @@ abstract class LinkUtil {
final Color primaryColor = Theme.of(context).colorScheme.primary;
_browser
.open(
url: uri,
options: ChromeSafariBrowserClassOptions(
ios: IOSSafariOptions(
entersReaderIfAvailable: useReader,
preferredControlTintColor: primaryColor,
),
android: AndroidChromeCustomTabsOptions(
toolbarBackgroundColor: primaryColor,
),
),
url: WebUri.uri(uri),
settings: Platform.isAndroid
? ChromeSafariBrowserSettings(
toolbarBackgroundColor: primaryColor,
)
: ChromeSafariBrowserSettings(
entersReaderIfAvailable: useReader,
preferredControlTintColor: primaryColor,
),
)
.onError((_, __) => launchUrl(uri));
}

View File

@ -1,14 +0,0 @@
class ServiceException implements Exception {
ServiceException([this.message]);
final String? message;
@override
String toString() {
String result = 'ServiceException';
if (message != null) {
result = '$result: $message';
}
return result;
}
}

View File

@ -1,9 +1,9 @@
export 'debouncer.dart';
export 'dio_interceptors/interceptors.dart';
export 'haptic_feedback_util.dart';
export 'html_util.dart';
export 'link_util.dart';
export 'linkifier_util.dart';
export 'log_util.dart';
export 'service_exception.dart';
export 'theme_util.dart';
export 'throttle.dart';

View File

@ -5,34 +5,34 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
url: "https://pub.dev"
source: hosted
version: "61.0.0"
version: "67.0.0"
adaptive_theme:
dependency: "direct main"
description:
name: adaptive_theme
sha256: "28df95a6b86993b38a51ee97d33a9f1d845fd1c7320c21c5d5e2183b5605e152"
sha256: f4ee609b464e5efc68131d9d15ba9aa1de4e3b5ede64be17781c6e19a52d637d
url: "https://pub.dev"
source: hosted
version: "3.4.0"
version: "3.6.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
url: "https://pub.dev"
source: hosted
version: "5.13.0"
version: "6.4.1"
animations:
dependency: "direct main"
description:
name: animations
sha256: ef57563eed3620bd5d75ad96189846aca1e033c0c45fc9a7d26e80ab02b88a70
sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb
url: "https://pub.dev"
source: hosted
version: "2.0.8"
version: "2.0.11"
args:
dependency: transitive
description:
@ -61,26 +61,26 @@ packages:
dependency: "direct main"
description:
name: bloc
sha256: "3820f15f502372d979121de1f6b97bfcf1630ebff8fe1d52fb2b0bfa49be5b49"
sha256: f53a110e3b48dcd78136c10daa5d51512443cea5e1348c9d80a320095fa2db9e
url: "https://pub.dev"
source: hosted
version: "8.1.2"
version: "8.1.3"
bloc_concurrency:
dependency: "direct main"
description:
name: bloc_concurrency
sha256: "44535c9f429cd7e91d548cf89fde1c23a8b4b3637decdb1865bb583091a00d4e"
sha256: "5857eb6653b4dd5e30e1ffab91037957fd64a9b9c5e53d26714ef25a46c04679"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
version: "0.2.4"
bloc_test:
dependency: "direct dev"
description:
name: bloc_test
sha256: af0de1a1e16a7536e95dcd7491e0a6d6078e11d2d691988e862280b74f5c7968
sha256: "55a48f69e0d480717067c5377c8485a3fcd41f1701a820deef72fa0f4ee7215f"
url: "https://pub.dev"
source: hosted
version: "9.1.4"
version: "9.1.6"
boolean_selector:
dependency: transitive
description:
@ -93,26 +93,26 @@ packages:
dependency: "direct main"
description:
name: cached_network_image
sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f
sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f"
url: "https://pub.dev"
source: hosted
version: "3.3.0"
version: "3.3.1"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613"
sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "4.0.0"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257"
sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.1.1"
characters:
dependency: transitive
description:
@ -149,10 +149,10 @@ packages:
dependency: "direct main"
description:
name: connectivity_plus
sha256: b502a681ba415272ecc41400bd04fe543ed1a62632137dc84d25a91e7746f55f
sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0"
url: "https://pub.dev"
source: hosted
version: "5.0.1"
version: "5.0.2"
connectivity_plus_platform_interface:
dependency: transitive
description:
@ -173,18 +173,18 @@ packages:
dependency: transitive
description:
name: coverage
sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097"
sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76"
url: "https://pub.dev"
source: hosted
version: "1.6.3"
version: "1.7.2"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: fd832b5384d0d6da4f6df60b854d33accaaeb63aa9e10e736a87381f08dee2cb
sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e
url: "https://pub.dev"
source: hosted
version: "0.3.3+5"
version: "0.3.3+8"
crypto:
dependency: transitive
description:
@ -205,18 +205,18 @@ packages:
dependency: transitive
description:
name: dbus
sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263"
sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac"
url: "https://pub.dev"
source: hosted
version: "0.7.8"
version: "0.7.10"
device_info_plus:
dependency: "direct main"
description:
name: device_info_plus
sha256: "7035152271ff67b072a211152846e9f1259cf1be41e34cd3e0b5463d2d6b8419"
sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110"
url: "https://pub.dev"
source: hosted
version: "9.1.0"
version: "9.1.2"
device_info_plus_platform_interface:
dependency: transitive
description:
@ -237,10 +237,10 @@ packages:
dependency: "direct main"
description:
name: dio
sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7"
sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3"
url: "https://pub.dev"
source: hosted
version: "5.3.3"
version: "5.4.0"
equatable:
dependency: "direct main"
description:
@ -278,18 +278,26 @@ packages:
dependency: transitive
description:
name: ffi
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.2"
file:
dependency: transitive
description:
name: file
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev"
source: hosted
version: "6.1.4"
version: "7.0.0"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
flutter:
dependency: "direct main"
description: flutter
@ -299,10 +307,10 @@ packages:
dependency: "direct main"
description:
name: flutter_bloc
sha256: e74efb89ee6945bcbce74a5b3a5a3376b088e5f21f55c263fc38cbdc6237faae
sha256: "87325da1ac757fcc4813e6b34ed5dd61169973871fdf181d6c2109dd6935ece1"
url: "https://pub.dev"
source: hosted
version: "8.1.3"
version: "8.1.4"
flutter_cache_manager:
dependency: "direct main"
description:
@ -344,18 +352,66 @@ packages:
dependency: "direct main"
description:
name: flutter_inappwebview
sha256: d198297060d116b94048301ee6749cd2e7d03c1f2689783f52d210a6b7aba350
sha256: "3e9a443a18ecef966fb930c3a76ca5ab6a7aafc0c7b5e14a4a850cf107b09959"
url: "https://pub.dev"
source: hosted
version: "5.8.0"
version: "6.0.0"
flutter_inappwebview_android:
dependency: transitive
description:
name: flutter_inappwebview_android
sha256: d247f6ed417f1f8c364612fa05a2ecba7f775c8d0c044c1d3b9ee33a6515c421
url: "https://pub.dev"
source: hosted
version: "1.0.13"
flutter_inappwebview_internal_annotations:
dependency: transitive
description:
name: flutter_inappwebview_internal_annotations
sha256: "5f80fd30e208ddded7dbbcd0d569e7995f9f63d45ea3f548d8dd4c0b473fb4c8"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter_inappwebview_ios:
dependency: transitive
description:
name: flutter_inappwebview_ios
sha256: f363577208b97b10b319cd0c428555cd8493e88b468019a8c5635a0e4312bd0f
url: "https://pub.dev"
source: hosted
version: "1.0.13"
flutter_inappwebview_macos:
dependency: transitive
description:
name: flutter_inappwebview_macos
sha256: b55b9e506c549ce88e26580351d2c71d54f4825901666bd6cfa4be9415bb2636
url: "https://pub.dev"
source: hosted
version: "1.0.11"
flutter_inappwebview_platform_interface:
dependency: transitive
description:
name: flutter_inappwebview_platform_interface
sha256: "545fd4c25a07d2775f7d5af05a979b2cac4fbf79393b0a7f5d33ba39ba4f6187"
url: "https://pub.dev"
source: hosted
version: "1.0.10"
flutter_inappwebview_web:
dependency: transitive
description:
name: flutter_inappwebview_web
sha256: d8c680abfb6fec71609a700199635d38a744df0febd5544c5a020bd73de8ee07
url: "https://pub.dev"
source: hosted
version: "1.0.8"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "6d11ea777496061e583623aaf31923f93a9409ef8fcaeeefdd6cd78bf4fe5bb3"
sha256: c18f1de98fe0bb9dd5ba91e1330d4febc8b6a7de6aae3ffe475ef423723e72f3
url: "https://pub.dev"
source: hosted
version: "16.1.0"
version: "16.3.2"
flutter_local_notifications_linux:
dependency: transitive
description:
@ -440,10 +496,10 @@ packages:
dependency: "direct main"
description:
name: flutter_slidable
sha256: cc4231579e3eae41ae166660df717f4bad1359c87f4a4322ad8ba1befeb3d2be
sha256: "19ed4813003a6ff4e9c6bcce37e792a2a358919d7603b2b31ff200229191e44c"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "3.0.1"
flutter_test:
dependency: "direct dev"
description: flutter
@ -458,10 +514,10 @@ packages:
dependency: "direct main"
description:
name: font_awesome_flutter
sha256: "5fb789145cae1f4c3245c58b3f8fb287d055c26323879eab57a7bf0cfd1e45f3"
sha256: "275ff26905134bcb59417cf60ad979136f1f8257f2f449914b2c3e05bbb4cd6f"
url: "https://pub.dev"
source: hosted
version: "10.5.0"
version: "10.7.0"
frontend_server_client:
dependency: transitive
description:
@ -479,10 +535,10 @@ packages:
dependency: "direct main"
description:
name: get_it
sha256: f79870884de16d689cf9a7d15eedf31ed61d750e813c538a6efb92660fea83c3
sha256: e6017ce7fdeaf218dc51a100344d8cb70134b80e28b760f8bb23c242437bafd7
url: "https://pub.dev"
source: hosted
version: "7.6.4"
version: "7.6.7"
glob:
dependency: transitive
description:
@ -495,10 +551,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: c247a4f76071c3b97bb5ae8912968870d5565644801c5e09f3bc961b4d874895
sha256: "170c46e237d6eb0e6e9f0e8b3f56101e14fb64f787016e42edd74c39cf8b176a"
url: "https://pub.dev"
source: hosted
version: "12.1.1"
version: "13.2.0"
hive:
dependency: "direct main"
description:
@ -527,10 +583,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.2.0"
http_multi_server:
dependency: transitive
description:
@ -551,10 +607,10 @@ packages:
dependency: "direct main"
description:
name: hydrated_bloc
sha256: "24994e61f64904d911683cce1a31dc4ef611619da5253f1de2b7b8fc6f79a118"
sha256: "00a2099680162e74b5a836b8a7f446e478520a9cae9f6032e028ad8129f4432d"
url: "https://pub.dev"
source: hosted
version: "9.1.2"
version: "9.1.4"
in_app_review:
dependency: "direct main"
description:
@ -566,10 +622,10 @@ packages:
dependency: transitive
description:
name: in_app_review_platform_interface
sha256: b12ec9aaf6b34d3a72aa95895eb252b381896246bdad4ef378d444affe8410ef
sha256: fed2c755f2125caa9ae10495a3c163aa7fab5af3585a9c62ef4a6920c5b45f10
url: "https://pub.dev"
source: hosted
version: "2.0.4"
version: "2.0.5"
integration_test:
dependency: "direct dev"
description: flutter
@ -579,10 +635,10 @@ packages:
dependency: "direct main"
description:
name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.18.1"
version: "0.19.0"
io:
dependency: transitive
description:
@ -599,6 +655,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.7"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
url: "https://pub.dev"
source: hosted
version: "10.0.0"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
url: "https://pub.dev"
source: hosted
version: "2.0.1"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
url: "https://pub.dev"
source: hosted
version: "2.0.1"
linkify:
dependency: "direct main"
description:
@ -627,18 +707,18 @@ packages:
dependency: transitive
description:
name: matcher
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16"
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
version: "0.8.0"
memoize:
dependency: "direct main"
description:
@ -651,26 +731,26 @@ packages:
dependency: transitive
description:
name: meta
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
url: "https://pub.dev"
source: hosted
version: "1.10.0"
version: "1.11.0"
mime:
dependency: transitive
description:
name: mime
sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
version: "1.0.5"
mocktail:
dependency: "direct dev"
description:
name: mocktail
sha256: "9503969a7c2c78c7292022c70c0289ed6241df7a9ba720010c0b215af29a5a58"
sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.0.3"
nested:
dependency: transitive
description:
@ -715,10 +795,10 @@ packages:
dependency: "direct main"
description:
name: package_info_plus
sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a"
sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
version: "5.0.1"
package_info_plus_platform_interface:
dependency: transitive
description:
@ -731,34 +811,34 @@ packages:
dependency: "direct main"
description:
name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.8.3"
version: "1.9.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
path_provider_android:
dependency: "direct main"
description:
name: path_provider_android
sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1"
sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.2.2"
path_provider_foundation:
dependency: "direct main"
description:
name: path_provider_foundation
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
version: "2.3.2"
path_provider_linux:
dependency: transitive
description:
@ -771,10 +851,10 @@ packages:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
@ -787,26 +867,26 @@ packages:
dependency: transitive
description:
name: petitparser
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "5.4.0"
version: "6.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
version: "3.1.4"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.6"
version: "2.1.8"
pool:
dependency: transitive
description:
@ -815,22 +895,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.1"
pretty_dio_logger:
dependency: "direct main"
description:
name: pretty_dio_logger
sha256: "00b80053063935cf9a6190da344c5373b9d0e92da4c944c878ff2fbef0ef6dc2"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
process:
dependency: transitive
description:
name: process
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
url: "https://pub.dev"
source: hosted
version: "4.2.4"
version: "5.0.2"
provider:
dependency: transitive
description:
name: provider
sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f
sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096"
url: "https://pub.dev"
source: hosted
version: "6.0.5"
version: "6.1.1"
pub_semver:
dependency: transitive
description:
@ -876,10 +964,10 @@ packages:
dependency: "direct main"
description:
name: receive_sharing_intent
sha256: "912bebb551bce75a14098891fd750305b30d53eba0d61cc70cd9973be9866e8d"
sha256: "252e5b5018aebfa93a068bdf08dc58152b3ac5958a22b1027e9ccbbe71912115"
url: "https://pub.dev"
source: hosted
version: "1.4.5"
version: "1.5.4"
responsive_builder:
dependency: "direct main"
description:
@ -908,18 +996,18 @@ packages:
dependency: "direct main"
description:
name: sembast
sha256: "85ff944434f7b566fdc388be4f85b23e954736b7d6e51f965f4f419d966c15b1"
sha256: "9a9f0c7aca07043fef857b8b365f41592e48832b61462292699b57978e241c11"
url: "https://pub.dev"
source: hosted
version: "3.5.0+1"
version: "3.6.0"
share_plus:
dependency: "direct main"
description:
name: share_plus
sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd
sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900"
url: "https://pub.dev"
source: hosted
version: "7.2.1"
version: "7.2.2"
share_plus_platform_interface:
dependency: transitive
description:
@ -948,42 +1036,42 @@ packages:
dependency: "direct main"
description:
name: shared_preferences_foundation
sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7"
sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
url: "https://pub.dev"
source: hosted
version: "2.3.4"
version: "2.3.5"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
version: "2.3.2"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
version: "2.3.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf
sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.2.2"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
version: "2.3.2"
shelf:
dependency: transitive
description:
@ -1053,22 +1141,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.0"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
sqflite:
dependency: transitive
description:
name: sqflite
sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a"
sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "2.3.2"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a"
sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5"
url: "https://pub.dev"
source: hosted
version: "2.5.0"
version: "2.5.3"
stack_trace:
dependency: transitive
description:
@ -1120,10 +1216,10 @@ packages:
dependency: transitive
description:
name: synchronized
sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.1.0+1"
term_glyph:
dependency: transitive
description:
@ -1184,34 +1280,34 @@ packages:
dependency: "direct main"
description:
name: url_launcher
sha256: b1c9e98774adf8820c96fbc7ae3601231d324a7d5ebd8babe27b6dfac91357ba
sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c
url: "https://pub.dev"
source: hosted
version: "6.2.1"
version: "6.2.4"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def"
sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745
url: "https://pub.dev"
source: hosted
version: "6.2.0"
version: "6.3.0"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "4ac97281cf60e2e8c5cc703b2b28528f9b50c8f7cebc71df6bdf0845f647268a"
sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03"
url: "https://pub.dev"
source: hosted
version: "6.2.0"
version: "6.2.4"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd"
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.1.1"
url_launcher_macos:
dependency: transitive
description:
@ -1224,34 +1320,34 @@ packages:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50"
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2"
sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.2.3"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc"
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.1.1"
uuid:
dependency: transitive
description:
name: uuid
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8
url: "https://pub.dev"
source: hosted
version: "3.0.7"
version: "4.3.3"
vector_math:
dependency: transitive
description:
@ -1280,10 +1376,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
url: "https://pub.dev"
source: hosted
version: "11.10.0"
version: "13.0.0"
wakelock:
dependency: "direct main"
description:
@ -1336,26 +1432,26 @@ packages:
dependency: transitive
description:
name: web
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
version: "0.4.2"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.4.3"
webdriver:
dependency: transitive
description:
name: webdriver
sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49"
sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
@ -1368,42 +1464,42 @@ packages:
dependency: "direct main"
description:
name: webview_flutter
sha256: c1ab9b81090705c6069197d9fdc1625e587b52b8d70cdde2339d177ad0dbb98e
sha256: "25e1b6e839e8cbfbd708abc6f85ed09d1727e24e08e08c6b8590d7c65c9a8932"
url: "https://pub.dev"
source: hosted
version: "4.4.1"
version: "4.7.0"
webview_flutter_android:
dependency: transitive
description:
name: webview_flutter_android
sha256: b0cd33dd7d3dd8e5f664e11a19e17ba12c352647269921a3b568406b001f1dff
sha256: "3e5f4e9d818086b0d01a66fb1ff9cc72ab0cc58c71980e3d3661c5685ea0efb0"
url: "https://pub.dev"
source: hosted
version: "3.12.0"
version: "3.15.0"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f"
sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d
url: "https://pub.dev"
source: hosted
version: "2.6.0"
version: "2.10.0"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: "30b9af6bdd457b44c08748b9190d23208b5165357cc2eb57914fee1366c42974"
sha256: "9bf168bccdf179ce90450b5f37e36fe263f591c9338828d6bf09b6f8d0f57f86"
url: "https://pub.dev"
source: hosted
version: "3.9.1"
version: "3.12.0"
win32:
dependency: "direct overridden"
description:
name: win32
sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3"
sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8"
url: "https://pub.dev"
source: hosted
version: "5.0.9"
version: "5.2.0"
win32_registry:
dependency: transitive
description:
@ -1416,26 +1512,26 @@ packages:
dependency: "direct main"
description:
name: workmanager
sha256: e0be7e35d644643f164ee45d2ce14414f0e0fdde19456aa66065f35a0b1d2ea1
sha256: ed13530cccd28c5c9959ad42d657cd0666274ca74c56dea0ca183ddd527d3a00
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "0.5.2"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2"
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
url: "https://pub.dev"
source: hosted
version: "1.0.3"
version: "1.0.4"
xml:
dependency: transitive
description:
name: xml
sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84"
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.5.0"
yaml:
dependency: transitive
description:
@ -1445,5 +1541,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.2.0-194.0.dev <4.0.0"
flutter: ">=3.16.2"
dart: ">=3.3.0-279.1.beta <4.0.0"
flutter: ">=3.19.0"

View File

@ -1,11 +1,11 @@
name: hacki
description: A Hacker News reader.
version: 2.5.0+134
version: 2.7.1+139
publish_to: none
environment:
sdk: ">=3.0.0 <4.0.0"
flutter: "3.16.2"
flutter: "3.19.0"
dependencies:
adaptive_theme: ^3.2.0
@ -32,7 +32,7 @@ dependencies:
flutter_email_sender: ^6.0.1
flutter_fadein: ^2.0.0
flutter_feather_icons: 2.0.0+1
flutter_inappwebview: ^5.8.0
flutter_inappwebview: ^6.0.0
flutter_local_notifications: ^16.1.0
flutter_material_color_picker: ^1.2.0
flutter_secure_storage: ^9.0.0
@ -40,7 +40,7 @@ dependencies:
flutter_slidable: ^3.0.0
font_awesome_flutter: ^10.3.0
get_it: ^7.2.0
go_router: ^12.1.1
go_router: ^13.2.0
hive: ^2.2.3
html: ^0.15.1
html_unescape: ^2.0.0
@ -48,22 +48,23 @@ dependencies:
hydrated_bloc: ^9.1.0
in_app_review:
path: components/in_app_review
intl: ^0.18.0
intl: ^0.19.0
linkify: ^5.0.0
logger: ^2.0.1
memoize: ^3.0.0
package_info_plus: ^4.0.0
package_info_plus: ^5.0.1
path: ^1.8.2
path_provider: ^2.0.12
path_provider_android: ^2.0.22
path_provider_foundation: ^2.1.1
pretty_dio_logger: ^1.3.1
pull_to_refresh:
git:
url: https://github.com/livinglist/flutter_pulltorefresh
ref: master
qr_code_scanner: ^1.0.1
qr_flutter: ^4.1.0
receive_sharing_intent: ^1.4.5
receive_sharing_intent: 1.5.4
responsive_builder: ^0.7.0
rxdart: ^0.27.7
scrollable_positioned_list: ^0.3.5