mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
32 Commits
Author | SHA1 | Date | |
---|---|---|---|
d3ede8546b | |||
53562ad260 | |||
6c8e7a7cb9 | |||
56c0245335 | |||
0cbd38a530 | |||
7c6da2c36a | |||
185140feb4 | |||
03c01a0b78 | |||
f823fdf241 | |||
fe87ddd8ff | |||
613ba12b05 | |||
8d7f66ecbc | |||
461aae253b | |||
a1b491cf0d | |||
edf0c82040 | |||
946a3c5a9a | |||
d8bc60c071 | |||
48477cd5c8 | |||
38df6293fe | |||
a5fe9e45fc | |||
9de5baa77a | |||
2daccd64e8 | |||
d0c68f9419 | |||
5f1dbfc510 | |||
90eee37c17 | |||
5630e61a74 | |||
eaad4b01dd | |||
3ab172f3d3 | |||
5450eba64b | |||
e2d6bb44d0 | |||
ffbd3a2449 | |||
2405a6d30c |
3
.github/workflows/commit_check.yml
vendored
3
.github/workflows/commit_check.yml
vendored
@ -4,11 +4,13 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- "**"
|
||||||
|
- '!master'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
releases:
|
releases:
|
||||||
name: Check commit
|
name: Check commit
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
env:
|
env:
|
||||||
FLUTTER_VERSION: "3.3.10"
|
FLUTTER_VERSION: "3.3.10"
|
||||||
steps:
|
steps:
|
||||||
@ -20,3 +22,4 @@ jobs:
|
|||||||
- run: flutter pub get
|
- run: flutter pub get
|
||||||
- run: flutter format --set-exit-if-changed .
|
- run: flutter format --set-exit-if-changed .
|
||||||
- run: flutter analyze
|
- run: flutter analyze
|
||||||
|
- run: flutter test
|
7
.github/workflows/publish_ios.yml
vendored
7
.github/workflows/publish_ios.yml
vendored
@ -6,13 +6,12 @@ on:
|
|||||||
# Run the workflow whenever a new tag named 'v*' is pushed
|
# Run the workflow whenever a new tag named 'v*' is pushed
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "!*"
|
- master
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_and_publish:
|
build_and_publish:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Point the `ruby/setup-ruby` action at this Gemfile, so it
|
# Point the `ruby/setup-ruby` action at this Gemfile, so it
|
||||||
@ -51,4 +50,4 @@ jobs:
|
|||||||
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
|
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
|
||||||
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
|
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
|
||||||
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
|
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
|
||||||
run: cd ios && bundle exec fastlane beta "build_name:${{ github.ref_name }}"
|
run: cd ios && bundle exec fastlane beta
|
||||||
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[submodule "flutter"]
|
||||||
|
path = submodules/flutter
|
||||||
|
url = https://github.com/flutter/flutter
|
@ -7,3 +7,7 @@ linter:
|
|||||||
omit_local_variable_types: false
|
omit_local_variable_types: false
|
||||||
one_member_abstracts: false
|
one_member_abstracts: false
|
||||||
always_specify_types: true
|
always_specify_types: true
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
- "submodules/**"
|
||||||
|
@ -1,5 +1,82 @@
|
|||||||
import Flutter
|
import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
typealias APNSHandler = ()->Void
|
||||||
|
|
||||||
|
let keyKey = "key"
|
||||||
|
let valKey = "val"
|
||||||
|
|
||||||
|
final class SharedPrefsCore {
|
||||||
|
fileprivate static let shared: SharedPrefsCore = SharedPrefsCore()
|
||||||
|
|
||||||
|
fileprivate func setBool(key: String?, val: Bool?) -> Bool {
|
||||||
|
guard let key = key,
|
||||||
|
let val = val else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let keyStore = NSUbiquitousKeyValueStore()
|
||||||
|
let allVals = keyStore.dictionaryRepresentation;
|
||||||
|
let allKeys = allVals.keys
|
||||||
|
|
||||||
|
// Limit is 1024, reserve rest slots for fav and pins.
|
||||||
|
if allKeys.count >= 1000 {
|
||||||
|
for key in allKeys.filter({ $0.contains("hasRead") }) {
|
||||||
|
keyStore.removeObject(forKey: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keyStore.set(val, forKey: key)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func getBool(key: String?) -> Bool {
|
||||||
|
guard let key = key else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let keyStore = NSUbiquitousKeyValueStore()
|
||||||
|
let val = keyStore.bool(forKey: key)
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func setStringList(key: String?, val: [String]?) -> Bool {
|
||||||
|
guard let key = key,
|
||||||
|
let val = val else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let keyStore = NSUbiquitousKeyValueStore()
|
||||||
|
keyStore.set(val, forKey: key)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func getStringList(key: String?) -> [Any] {
|
||||||
|
guard let key = key else {
|
||||||
|
return [Any]()
|
||||||
|
}
|
||||||
|
|
||||||
|
let keyStore = NSUbiquitousKeyValueStore()
|
||||||
|
let list = keyStore.array(forKey: key) as [Any]? ?? [Any]()
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func clearAll() -> Bool{
|
||||||
|
let keyStore = NSUbiquitousKeyValueStore()
|
||||||
|
let allVals = keyStore.dictionaryRepresentation;
|
||||||
|
let allKeys = allVals.keys
|
||||||
|
|
||||||
|
for key in allKeys.filter({ $0.contains("hasRead") }) {
|
||||||
|
keyStore.removeObject(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
|
public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
|
||||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||||
@ -12,41 +89,44 @@ public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
|
|||||||
switch call.method {
|
switch call.method {
|
||||||
case "setBool":
|
case "setBool":
|
||||||
if let params = call.arguments as? [String: Any] {
|
if let params = call.arguments as? [String: Any] {
|
||||||
let info: [String: Any] = ["result": result,
|
let val = params[valKey] as? Bool
|
||||||
"params": params]
|
let key = params[keyKey] as? String
|
||||||
NotificationCenter.default.post(name: Notification.Name("setBool"), object: nil, userInfo: info)
|
|
||||||
|
let res = SharedPrefsCore.shared.setBool(key: key, val: val)
|
||||||
|
result(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
case "getBool":
|
case "getBool":
|
||||||
if let params = call.arguments as? [String: Any] {
|
if let params = call.arguments as? [String: Any] {
|
||||||
let info: [String: Any] = ["result": result,
|
let key = params[keyKey] as? String
|
||||||
"params": params]
|
let res = SharedPrefsCore.shared.getBool(key: key)
|
||||||
NotificationCenter.default.post(name: Notification.Name("getBool"), object: nil, userInfo: info)
|
result(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
case "setStringList":
|
case "setStringList":
|
||||||
if let params = call.arguments as? [String: Any] {
|
if let params = call.arguments as? [String: Any] {
|
||||||
let info: [String: Any] = ["result": result,
|
let val = params[valKey] as? [String]
|
||||||
"params": params]
|
let key = params[keyKey] as? String
|
||||||
NotificationCenter.default.post(name: Notification.Name("setStringList"), object: nil, userInfo: info)
|
|
||||||
|
let res = SharedPrefsCore.shared.setStringList(key: key, val: val)
|
||||||
|
result(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
case "getStringList":
|
case "getStringList":
|
||||||
if let params = call.arguments as? [String: Any] {
|
if let params = call.arguments as? [String: Any] {
|
||||||
let info: [String: Any] = ["result": result,
|
let key = params[keyKey] as? String
|
||||||
"params": params]
|
let res = SharedPrefsCore.shared.getStringList(key: key)
|
||||||
NotificationCenter.default.post(name: Notification.Name("getStringList"), object: nil, userInfo: info)
|
result(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
case "clearAll":
|
case "clearAll":
|
||||||
if let params = call.arguments as? [String: Any] {
|
if let params = call.arguments as? [String: Any] {
|
||||||
let info: [String: Any] = ["result": result,
|
let res = SharedPrefsCore.shared.clearAll()
|
||||||
"params": params]
|
result(res)
|
||||||
NotificationCenter.default.post(name: Notification.Name("clearAll"), object: nil, userInfo: info)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
2
fastlane/metadata/android/en-US/changelogs/77.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/77.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
- Fixed app icon.
|
||||||
|
- Added font size setting to comments screen.
|
1
fastlane/metadata/android/en-US/changelogs/78.txt
Normal file
1
fastlane/metadata/android/en-US/changelogs/78.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
- Fixed time machine.
|
1
fastlane/metadata/android/en-US/changelogs/79.txt
Normal file
1
fastlane/metadata/android/en-US/changelogs/79.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
- Fixed time machine.
|
@ -17,20 +17,20 @@ GEM
|
|||||||
artifactory (3.0.15)
|
artifactory (3.0.15)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.2.0)
|
||||||
aws-partitions (1.636.0)
|
aws-partitions (1.680.0)
|
||||||
aws-sdk-core (3.154.0)
|
aws-sdk-core (3.168.4)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.525.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.5)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.58.0)
|
aws-sdk-kms (1.61.0)
|
||||||
aws-sdk-core (~> 3, >= 3.127.0)
|
aws-sdk-core (~> 3, >= 3.165.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.114.0)
|
aws-sdk-s3 (1.117.2)
|
||||||
aws-sdk-core (~> 3, >= 3.127.0)
|
aws-sdk-core (~> 3, >= 3.165.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.4)
|
aws-sigv4 (~> 1.4)
|
||||||
aws-sigv4 (1.5.1)
|
aws-sigv4 (1.5.2)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
babosa (1.0.4)
|
babosa (1.0.4)
|
||||||
claide (1.1.0)
|
claide (1.1.0)
|
||||||
@ -86,7 +86,7 @@ GEM
|
|||||||
escape (0.0.4)
|
escape (0.0.4)
|
||||||
ethon (0.15.0)
|
ethon (0.15.0)
|
||||||
ffi (>= 1.15.0)
|
ffi (>= 1.15.0)
|
||||||
excon (0.92.5)
|
excon (0.95.0)
|
||||||
faraday (1.10.2)
|
faraday (1.10.2)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
faraday-em_synchrony (~> 1.0)
|
faraday-em_synchrony (~> 1.0)
|
||||||
@ -116,7 +116,7 @@ GEM
|
|||||||
faraday_middleware (1.2.0)
|
faraday_middleware (1.2.0)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
fastimage (2.2.6)
|
fastimage (2.2.6)
|
||||||
fastlane (2.210.1)
|
fastlane (2.211.0)
|
||||||
CFPropertyList (>= 2.3, < 4.0.0)
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
addressable (>= 2.8, < 3.0.0)
|
addressable (>= 2.8, < 3.0.0)
|
||||||
artifactory (~> 3.0)
|
artifactory (~> 3.0)
|
||||||
@ -159,9 +159,9 @@ GEM
|
|||||||
fourflusher (2.3.1)
|
fourflusher (2.3.1)
|
||||||
fuzzy_match (2.0.4)
|
fuzzy_match (2.0.4)
|
||||||
gh_inspector (1.1.3)
|
gh_inspector (1.1.3)
|
||||||
google-apis-androidpublisher_v3 (0.27.0)
|
google-apis-androidpublisher_v3 (0.32.0)
|
||||||
google-apis-core (>= 0.7.2, < 2.a)
|
google-apis-core (>= 0.9.1, < 2.a)
|
||||||
google-apis-core (0.9.0)
|
google-apis-core (0.9.2)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
httpclient (>= 2.8.1, < 3.a)
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
@ -170,27 +170,27 @@ GEM
|
|||||||
retriable (>= 2.0, < 4.a)
|
retriable (>= 2.0, < 4.a)
|
||||||
rexml
|
rexml
|
||||||
webrick
|
webrick
|
||||||
google-apis-iamcredentials_v1 (0.14.0)
|
google-apis-iamcredentials_v1 (0.16.0)
|
||||||
google-apis-core (>= 0.7.2, < 2.a)
|
google-apis-core (>= 0.9.1, < 2.a)
|
||||||
google-apis-playcustomapp_v1 (0.10.0)
|
google-apis-playcustomapp_v1 (0.12.0)
|
||||||
google-apis-core (>= 0.7, < 2.a)
|
google-apis-core (>= 0.9.1, < 2.a)
|
||||||
google-apis-storage_v1 (0.17.0)
|
google-apis-storage_v1 (0.19.0)
|
||||||
google-apis-core (>= 0.7, < 2.a)
|
google-apis-core (>= 0.9.0, < 2.a)
|
||||||
google-cloud-core (1.6.0)
|
google-cloud-core (1.6.0)
|
||||||
google-cloud-env (~> 1.0)
|
google-cloud-env (~> 1.0)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (1.6.0)
|
google-cloud-env (1.6.0)
|
||||||
faraday (>= 0.17.3, < 3.0)
|
faraday (>= 0.17.3, < 3.0)
|
||||||
google-cloud-errors (1.3.0)
|
google-cloud-errors (1.3.0)
|
||||||
google-cloud-storage (1.42.0)
|
google-cloud-storage (1.44.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-iamcredentials_v1 (~> 0.1)
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
google-apis-storage_v1 (~> 0.17.0)
|
google-apis-storage_v1 (~> 0.19.0)
|
||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
googleauth (1.2.0)
|
googleauth (1.3.0)
|
||||||
faraday (>= 0.17.3, < 3.a)
|
faraday (>= 0.17.3, < 3.a)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
memoist (~> 0.16)
|
memoist (~> 0.16)
|
||||||
@ -203,11 +203,11 @@ GEM
|
|||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
i18n (1.12.0)
|
i18n (1.12.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
jmespath (1.6.1)
|
jmespath (1.6.2)
|
||||||
json (2.6.2)
|
json (2.6.3)
|
||||||
jwt (2.5.0)
|
jwt (2.5.0)
|
||||||
memoist (0.16.2)
|
memoist (0.16.2)
|
||||||
mini_magick (4.11.0)
|
mini_magick (4.12.0)
|
||||||
mini_mime (1.1.2)
|
mini_mime (1.1.2)
|
||||||
minitest (5.16.3)
|
minitest (5.16.3)
|
||||||
molinillo (0.8.0)
|
molinillo (0.8.0)
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
# Uncomment this line to define a global platform for your project
|
|
||||||
# platform :ios, '11.0'
|
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
|
@ -22,6 +22,8 @@ PODS:
|
|||||||
- integration_test (0.0.1):
|
- integration_test (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- OrderedSet (5.0.0)
|
- OrderedSet (5.0.0)
|
||||||
|
- package_info_plus (0.4.5):
|
||||||
|
- Flutter
|
||||||
- path_provider_ios (0.0.1):
|
- path_provider_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- ReachabilitySwift (5.0.0)
|
- ReachabilitySwift (5.0.0)
|
||||||
@ -53,6 +55,7 @@ DEPENDENCIES:
|
|||||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||||
- flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`)
|
- flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`)
|
||||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||||
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
@ -85,6 +88,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_siri_suggestions/ios"
|
:path: ".symlinks/plugins/flutter_siri_suggestions/ios"
|
||||||
integration_test:
|
integration_test:
|
||||||
:path: ".symlinks/plugins/integration_test/ios"
|
:path: ".symlinks/plugins/integration_test/ios"
|
||||||
|
package_info_plus:
|
||||||
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_ios:
|
path_provider_ios:
|
||||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||||
receive_sharing_intent:
|
receive_sharing_intent:
|
||||||
@ -116,6 +121,7 @@ SPEC CHECKSUMS:
|
|||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
|
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
|
||||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
||||||
|
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||||
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
|
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
|
||||||
@ -128,6 +134,6 @@ SPEC CHECKSUMS:
|
|||||||
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
|
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
|
||||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||||
|
|
||||||
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
|
PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937
|
||||||
|
|
||||||
COCOAPODS: 1.11.3
|
COCOAPODS: 1.11.3
|
||||||
|
@ -21,7 +21,6 @@
|
|||||||
E530B1AD283B54DA004E8EB6 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E530B1AC283B54DA004E8EB6 /* ActionViewController.swift */; };
|
E530B1AD283B54DA004E8EB6 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E530B1AC283B54DA004E8EB6 /* ActionViewController.swift */; };
|
||||||
E530B1B0283B54DA004E8EB6 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E530B1AE283B54DA004E8EB6 /* MainInterface.storyboard */; };
|
E530B1B0283B54DA004E8EB6 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E530B1AE283B54DA004E8EB6 /* MainInterface.storyboard */; };
|
||||||
E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E530B1A6283B54DA004E8EB6 /* Action Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E530B1A6283B54DA004E8EB6 /* Action Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
E54B4753282B3B8900579261 /* HackiCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E54B4752282B3B8900579261 /* HackiCore.swift */; };
|
|
||||||
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E575B6F027EBC6DA002B1508 /* CloudKit.framework */; };
|
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E575B6F027EBC6DA002B1508 /* CloudKit.framework */; };
|
||||||
FC507E94AA7767C155787DB3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFB5AA41D6C22D228077D166 /* Pods_Runner.framework */; };
|
FC507E94AA7767C155787DB3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFB5AA41D6C22D228077D166 /* Pods_Runner.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
@ -56,7 +55,7 @@
|
|||||||
};
|
};
|
||||||
E51D52B8283B464E00FC8DD8 /* Embed App Extensions */ = {
|
E51D52B8283B464E00FC8DD8 /* Embed App Extensions */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 8;
|
||||||
dstPath = "";
|
dstPath = "";
|
||||||
dstSubfolderSpec = 13;
|
dstSubfolderSpec = 13;
|
||||||
files = (
|
files = (
|
||||||
@ -64,7 +63,7 @@
|
|||||||
E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */,
|
E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */,
|
||||||
);
|
);
|
||||||
name = "Embed App Extensions";
|
name = "Embed App Extensions";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 1;
|
||||||
};
|
};
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
@ -97,7 +96,6 @@
|
|||||||
E530B1AF283B54DA004E8EB6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
E530B1AF283B54DA004E8EB6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||||
E530B1B1283B54DA004E8EB6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
E530B1B1283B54DA004E8EB6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
E530B1B9283B54E4004E8EB6 /* Action Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Action Extension.entitlements"; sourceTree = "<group>"; };
|
E530B1B9283B54E4004E8EB6 /* Action Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Action Extension.entitlements"; sourceTree = "<group>"; };
|
||||||
E54B4752282B3B8900579261 /* HackiCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HackiCore.swift; sourceTree = "<group>"; };
|
|
||||||
E575B6EF27EBC6C6002B1508 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
E575B6EF27EBC6C6002B1508 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||||
E575B6F027EBC6DA002B1508 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
|
E575B6F027EBC6DA002B1508 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
|
||||||
E59F28EE283B477D00512089 /* Share Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Share Extension.entitlements"; sourceTree = "<group>"; };
|
E59F28EE283B477D00512089 /* Share Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Share Extension.entitlements"; sourceTree = "<group>"; };
|
||||||
@ -177,7 +175,6 @@
|
|||||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
E54B4752282B3B8900579261 /* HackiCore.swift */,
|
|
||||||
);
|
);
|
||||||
path = Runner;
|
path = Runner;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -437,7 +434,6 @@
|
|||||||
files = (
|
files = (
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
E54B4753282B3B8900579261 /* HackiCore.swift in Sources */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -569,17 +565,19 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 4;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.2.33;
|
MARKETING_VERSION = 0.3.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -709,17 +707,19 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 4;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.2.33;
|
MARKETING_VERSION = 0.3.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -743,17 +743,19 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 4;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.2.33;
|
MARKETING_VERSION = 0.3.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -778,7 +780,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
@ -821,7 +823,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
@ -861,7 +863,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
@ -903,7 +905,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
@ -948,7 +950,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
@ -990,7 +992,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
@ -17,8 +17,6 @@ import flutter_local_notifications
|
|||||||
let center = UNUserNotificationCenter.current()
|
let center = UNUserNotificationCenter.current()
|
||||||
center.delegate = self
|
center.delegate = self
|
||||||
|
|
||||||
HackiCore.start()
|
|
||||||
|
|
||||||
WorkmanagerPlugin.register(with: self.registrar(forPlugin: "be.tramckrijte.workmanager.WorkmanagerPlugin")!)
|
WorkmanagerPlugin.register(with: self.registrar(forPlugin: "be.tramckrijte.workmanager.WorkmanagerPlugin")!)
|
||||||
|
|
||||||
if #available(iOS 10.0, *) {
|
if #available(iOS 10.0, *) {
|
||||||
|
@ -1,134 +0,0 @@
|
|||||||
//
|
|
||||||
// HackiCore.swift
|
|
||||||
// Runner
|
|
||||||
//
|
|
||||||
// Created by Jiaqi Feng on 5/10/22.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Flutter
|
|
||||||
|
|
||||||
extension Notification.Name {
|
|
||||||
static let setBool = Notification.Name("setBool")
|
|
||||||
static let getBool = Notification.Name("getBool")
|
|
||||||
static let setStringList = Notification.Name("setStringList")
|
|
||||||
static let getStringList = Notification.Name("getStringList")
|
|
||||||
static let clearAll = Notification.Name("clearAll")
|
|
||||||
}
|
|
||||||
|
|
||||||
typealias APNSHandler = ()->Void
|
|
||||||
|
|
||||||
final class HackiCore: NSObject {
|
|
||||||
private static let keyKey = "key"
|
|
||||||
private static let valKey = "val"
|
|
||||||
|
|
||||||
private static let shared: HackiCore = HackiCore()
|
|
||||||
private let notificationCenter = NotificationCenter.default
|
|
||||||
|
|
||||||
// Called at app launch
|
|
||||||
class func start() {
|
|
||||||
shared.registerNotifications()
|
|
||||||
}
|
|
||||||
|
|
||||||
private class func setupFlutterEvent(channelName: String, handler: NSObjectProtocol & FlutterStreamHandler) {
|
|
||||||
guard let rootVC = UIApplication.shared.delegate?.window.unsafelyUnwrapped?.rootViewController as? FlutterViewController else { return }
|
|
||||||
let eventChannel = FlutterEventChannel(name: channelName, binaryMessenger: rootVC.binaryMessenger)
|
|
||||||
eventChannel.setStreamHandler(handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func registerNotifications() {
|
|
||||||
// SyncedSharedPreferences
|
|
||||||
notificationCenter.addObserver(self, selector: #selector(setBool(_:)), name: .setBool, object: nil)
|
|
||||||
notificationCenter.addObserver(self, selector: #selector(getBool(_:)), name: .getBool, object: nil)
|
|
||||||
notificationCenter.addObserver(self, selector: #selector(setStringList(_:)), name: .setStringList, object: nil)
|
|
||||||
notificationCenter.addObserver(self, selector: #selector(getStringList(_:)), name: .getStringList, object: nil)
|
|
||||||
notificationCenter.addObserver(self, selector: #selector(clearAll(_:)), name: .clearAll, object: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func setBool(_ notification: Notification) {
|
|
||||||
guard let resultCompletionBlock: FlutterResult = notification.userInfo?["result"] as? FlutterResult else { fatalError(" failed to obtain result block") }
|
|
||||||
guard
|
|
||||||
let params = notification.userInfo?["params"] as? [String: Any],
|
|
||||||
let key = params[HackiCore.keyKey] as? String,
|
|
||||||
let val = params[HackiCore.valKey] as? Bool else {
|
|
||||||
resultCompletionBlock(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let keyStore = NSUbiquitousKeyValueStore()
|
|
||||||
let allVals = keyStore.dictionaryRepresentation;
|
|
||||||
let allKeys = allVals.keys
|
|
||||||
|
|
||||||
// Limit is 1024, reserve rest slots for fav and pins.
|
|
||||||
if allKeys.count >= 1000 {
|
|
||||||
for key in allKeys.filter({ $0.contains("hasRead") }) {
|
|
||||||
keyStore.removeObject(forKey: key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keyStore.set(val, forKey: key)
|
|
||||||
|
|
||||||
resultCompletionBlock(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func getBool(_ notification: Notification) {
|
|
||||||
guard let resultCompletionBlock: FlutterResult = notification.userInfo?["result"] as? FlutterResult else { fatalError(" failed to obtain result block") }
|
|
||||||
guard
|
|
||||||
let params = notification.userInfo?["params"] as? [String: Any],
|
|
||||||
let key = params[HackiCore.keyKey] as? String else {
|
|
||||||
resultCompletionBlock(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let keyStore = NSUbiquitousKeyValueStore()
|
|
||||||
let val = keyStore.bool(forKey: key)
|
|
||||||
|
|
||||||
resultCompletionBlock(val)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func setStringList(_ notification: Notification) {
|
|
||||||
guard let resultCompletionBlock: FlutterResult = notification.userInfo?["result"] as? FlutterResult else { fatalError(" failed to obtain result block") }
|
|
||||||
guard
|
|
||||||
let params = notification.userInfo?["params"] as? [String: Any],
|
|
||||||
let key = params[HackiCore.keyKey] as? String,
|
|
||||||
let val = params[HackiCore.valKey] as? [String] else {
|
|
||||||
resultCompletionBlock(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let keyStore = NSUbiquitousKeyValueStore()
|
|
||||||
keyStore.set(val, forKey: key)
|
|
||||||
|
|
||||||
resultCompletionBlock(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func getStringList(_ notification: Notification) {
|
|
||||||
guard let resultCompletionBlock: FlutterResult = notification.userInfo?["result"] as? FlutterResult else { fatalError(" failed to obtain result block") }
|
|
||||||
guard
|
|
||||||
let params = notification.userInfo?["params"] as? [String: Any],
|
|
||||||
let key = params[HackiCore.keyKey] as? String else {
|
|
||||||
resultCompletionBlock(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let keyStore = NSUbiquitousKeyValueStore()
|
|
||||||
let list = keyStore.array(forKey: key) as [Any]? ?? [Any]()
|
|
||||||
|
|
||||||
resultCompletionBlock(list)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func clearAll(_ notification: Notification) {
|
|
||||||
guard let resultCompletionBlock: FlutterResult = notification.userInfo?["result"] as? FlutterResult else { fatalError(" failed to obtain result block") }
|
|
||||||
|
|
||||||
let keyStore = NSUbiquitousKeyValueStore()
|
|
||||||
let allVals = keyStore.dictionaryRepresentation;
|
|
||||||
let allKeys = allVals.keys
|
|
||||||
|
|
||||||
for key in allKeys.filter({ $0.contains("hasRead") }) {
|
|
||||||
keyStore.removeObject(forKey: key)
|
|
||||||
}
|
|
||||||
|
|
||||||
resultCompletionBlock(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
@ -38,7 +38,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>https</string>
|
<string>https</string>
|
||||||
@ -72,5 +72,7 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>applinks:example.com</string>
|
<string>applinks:example.com</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -31,10 +31,6 @@ platform :ios do
|
|||||||
|
|
||||||
is_example_repo = ENV['CI'] && ENV['GITHUB_REPOSITORY'] == 'jorgenpt/flutter_github_example'
|
is_example_repo = ENV['CI'] && ENV['GITHUB_REPOSITORY'] == 'jorgenpt/flutter_github_example'
|
||||||
|
|
||||||
if !is_example_repo && APP_IDENTIFIER == 'no.tjer.HelloWorld' then
|
|
||||||
UI.user_error! "You need to update your Fastfile to use your own `APP_IDENTIFIER`"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Download code signing certificates using `match` (and the `MATCH_PASSWORD` secret)
|
# Download code signing certificates using `match` (and the `MATCH_PASSWORD` secret)
|
||||||
sync_code_signing(
|
sync_code_signing(
|
||||||
type: "appstore",
|
type: "appstore",
|
||||||
@ -42,15 +38,6 @@ platform :ios do
|
|||||||
readonly: true
|
readonly: true
|
||||||
)
|
)
|
||||||
|
|
||||||
if !is_example_repo then
|
|
||||||
if APPSTORECONNECT_ISSUER_ID == '69a6de83-feb7-47e3-e053-5b8c7c11a4d1' then
|
|
||||||
UI.user_error! "You need to update your Fastfile to use your own `APPSTORECONNECT_ISSUER_ID`"
|
|
||||||
end
|
|
||||||
if APPSTORECONNECT_KEY_ID == 'YRQDJRKMR9' then
|
|
||||||
UI.user_error! "You need to update your Fastfile to use your own `APPSTORECONNECT_KEY_ID`"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# We expose the key data using `APP_STORE_CONNECT_API_KEY_KEY` secret on GH
|
# We expose the key data using `APP_STORE_CONNECT_API_KEY_KEY` secret on GH
|
||||||
app_store_connect_api_key(
|
app_store_connect_api_key(
|
||||||
key_id: APPSTORECONNECT_KEY_ID,
|
key_id: APPSTORECONNECT_KEY_ID,
|
||||||
@ -59,21 +46,18 @@ platform :ios do
|
|||||||
latest_testflight_build_number
|
latest_testflight_build_number
|
||||||
# Figure out the build number (and optionally build name)
|
# Figure out the build number (and optionally build name)
|
||||||
new_build_number = ( + 1)
|
new_build_number = ( + 1)
|
||||||
extra_config_args = []
|
|
||||||
if options.key?(:build_name) then
|
|
||||||
extra_config_args = ["--build-name", options[:build_name].delete_prefix('v')]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Prep the xcodeproject from Flutter without building (`--config-only`)
|
# Prep the xcodeproject from Flutter without building (`--config-only`)
|
||||||
sh(
|
sh(
|
||||||
"flutter", "build", "ios", "--config-only",
|
"flutter", "build", "ios", "--config-only",
|
||||||
"--release", "--no-pub", "--no-codesign",
|
"--release", "--no-pub", "--no-codesign",
|
||||||
"--build-number", new_build_number.to_s,
|
"--build-number", new_build_number.to_s
|
||||||
*extra_config_args
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
version = get_version_number(xcodeproj: "Runner.xcodeproj", target: 'Runner')
|
||||||
|
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: options[:build_name].delete_prefix('v').delete_suffix('-rc')
|
version_number: version
|
||||||
)
|
)
|
||||||
|
|
||||||
increment_build_number({
|
increment_build_number({
|
||||||
|
@ -20,7 +20,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||||
_sembastRepository =
|
_sembastRepository =
|
||||||
sembastRepository ?? locator.get<SembastRepository>(),
|
sembastRepository ?? locator.get<SembastRepository>(),
|
||||||
super(AuthState.init()) {
|
super(const AuthState.init()) {
|
||||||
on<AuthInitialize>(onInitialize);
|
on<AuthInitialize>(onInitialize);
|
||||||
on<AuthLogin>(onLogin);
|
on<AuthLogin>(onLogin);
|
||||||
on<AuthLogout>(onLogout);
|
on<AuthLogout>(onLogout);
|
||||||
@ -101,7 +101,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
Future<void> onLogout(AuthLogout event, Emitter<AuthState> emit) async {
|
Future<void> onLogout(AuthLogout event, Emitter<AuthState> emit) async {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
user: User.empty(),
|
user: const User.empty(),
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
agreedToEULA: false,
|
agreedToEULA: false,
|
||||||
),
|
),
|
||||||
|
@ -14,8 +14,8 @@ class AuthState extends Equatable {
|
|||||||
required this.agreedToEULA,
|
required this.agreedToEULA,
|
||||||
});
|
});
|
||||||
|
|
||||||
AuthState.init()
|
const AuthState.init()
|
||||||
: user = User.empty(),
|
: user = const User.empty(),
|
||||||
isLoggedIn = false,
|
isLoggedIn = false,
|
||||||
status = AuthStatus.loaded,
|
status = AuthStatus.loaded,
|
||||||
agreedToEULA = false;
|
agreedToEULA = false;
|
||||||
|
@ -10,6 +10,8 @@ abstract class Constants {
|
|||||||
static const String googlePlayLink =
|
static const String googlePlayLink =
|
||||||
'https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US';
|
'https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US';
|
||||||
static const String sponsorLink = 'https://github.com/sponsors/Livinglist';
|
static const String sponsorLink = 'https://github.com/sponsors/Livinglist';
|
||||||
|
static const String guidelineLink =
|
||||||
|
'https://news.ycombinator.com/newsguidelines.html';
|
||||||
|
|
||||||
static const String _imagePath = 'assets/images';
|
static const String _imagePath = 'assets/images';
|
||||||
static const String hackerNewsLogoPath = '$_imagePath/hacker_news_logo.png';
|
static const String hackerNewsLogoPath = '$_imagePath/hacker_news_logo.png';
|
||||||
@ -48,3 +50,8 @@ abstract class Constants {
|
|||||||
'(ㆆ_ㆆ)',
|
'(ㆆ_ㆆ)',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract class RegExpConstants {
|
||||||
|
static const String linkSuffix = r'(\)|])(.)*$';
|
||||||
|
static const String number = '[0-9]+';
|
||||||
|
}
|
||||||
|
@ -3,6 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/services/services.dart';
|
import 'package:hacki/services/services.dart';
|
||||||
|
|
||||||
part 'collapse_state.dart';
|
part 'collapse_state.dart';
|
||||||
@ -10,13 +11,16 @@ part 'collapse_state.dart';
|
|||||||
class CollapseCubit extends Cubit<CollapseState> {
|
class CollapseCubit extends Cubit<CollapseState> {
|
||||||
CollapseCubit({
|
CollapseCubit({
|
||||||
required int commentId,
|
required int commentId,
|
||||||
|
required CommentsCubit? commentsCubit,
|
||||||
CollapseCache? collapseCache,
|
CollapseCache? collapseCache,
|
||||||
}) : _commentId = commentId,
|
}) : _commentId = commentId,
|
||||||
_collapseCache = collapseCache ?? locator.get<CollapseCache>(),
|
_collapseCache = collapseCache ?? locator.get<CollapseCache>(),
|
||||||
|
_commentsCubit = commentsCubit,
|
||||||
super(const CollapseState.init());
|
super(const CollapseState.init());
|
||||||
|
|
||||||
final int _commentId;
|
final int _commentId;
|
||||||
final CollapseCache _collapseCache;
|
final CollapseCache _collapseCache;
|
||||||
|
final CommentsCubit? _commentsCubit;
|
||||||
late final StreamSubscription<Map<int, Set<int>>> _streamSubscription;
|
late final StreamSubscription<Map<int, Set<int>>> _streamSubscription;
|
||||||
|
|
||||||
void init() {
|
void init() {
|
||||||
@ -43,12 +47,21 @@ class CollapseCubit extends Cubit<CollapseState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final int count = _collapseCache.collapse(_commentId);
|
if (_commentsCubit == null) return;
|
||||||
|
|
||||||
|
final Set<int> collapsedCommentIds = _collapseCache.collapse(_commentId);
|
||||||
|
final int lastCommentId = _commentsCubit!.state.comments.last.id;
|
||||||
|
final bool shouldLoadMore = _commentId == lastCommentId ||
|
||||||
|
collapsedCommentIds.contains(lastCommentId);
|
||||||
|
|
||||||
|
if (shouldLoadMore) {
|
||||||
|
_commentsCubit!.loadMore();
|
||||||
|
}
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
collapsedCount: state.collapsed ? 0 : count,
|
collapsedCount: state.collapsed ? 0 : collapsedCommentIds.length,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
state.copyWith(
|
state.copyWith(
|
||||||
comments: targetParents,
|
comments: targetParents,
|
||||||
onlyShowTargetComment: true,
|
onlyShowTargetComment: true,
|
||||||
status: CommentsStatus.loaded,
|
status: CommentsStatus.allLoaded,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -141,21 +141,21 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refresh() async {
|
Future<void> refresh() async {
|
||||||
if (state.offlineReading) {
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: CommentsStatus.loaded,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: CommentsStatus.loading,
|
status: CommentsStatus.loading,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (state.offlineReading) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: CommentsStatus.allLoaded,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_collapseCache.resetCollapsedComments();
|
_collapseCache.resetCollapsedComments();
|
||||||
|
|
||||||
await _streamSubscription?.cancel();
|
await _streamSubscription?.cancel();
|
||||||
@ -195,7 +195,6 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
item: updatedItem,
|
item: updatedItem,
|
||||||
status: CommentsStatus.loaded,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -213,6 +212,8 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
|
|
||||||
/// [comment] is only used for lazy fetching.
|
/// [comment] is only used for lazy fetching.
|
||||||
void loadMore({Comment? comment}) {
|
void loadMore({Comment? comment}) {
|
||||||
|
if (comment == null && state.status == CommentsStatus.loading) return;
|
||||||
|
|
||||||
switch (state.fetchMode) {
|
switch (state.fetchMode) {
|
||||||
case FetchMode.lazy:
|
case FetchMode.lazy:
|
||||||
if (comment == null) return;
|
if (comment == null) return;
|
||||||
@ -368,12 +369,17 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
|
|
||||||
if (!isHidden) {
|
if (!isHidden) {
|
||||||
_streamSubscription?.pause();
|
_streamSubscription?.pause();
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: CommentsStatus.loaded,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
currentPage: state.currentPage + 1,
|
currentPage: state.currentPage + 1,
|
||||||
status: CommentsStatus.loaded,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,8 @@ class PreferenceState extends Equatable {
|
|||||||
|
|
||||||
bool get showMetadata => _isOn<MetadataModePreference>();
|
bool get showMetadata => _isOn<MetadataModePreference>();
|
||||||
|
|
||||||
|
bool get showUrl => _isOn<StoryUrlModePreference>();
|
||||||
|
|
||||||
bool get tapAnywhereToCollapse => _isOn<CollapseModePreference>();
|
bool get tapAnywhereToCollapse => _isOn<CollapseModePreference>();
|
||||||
|
|
||||||
FetchMode get fetchMode => FetchMode.values
|
FetchMode get fetchMode => FetchMode.values
|
||||||
|
@ -10,7 +10,7 @@ class UserCubit extends Cubit<UserState> {
|
|||||||
UserCubit({StoriesRepository? storiesRepository})
|
UserCubit({StoriesRepository? storiesRepository})
|
||||||
: _storiesRepository =
|
: _storiesRepository =
|
||||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||||
super(UserState.init());
|
super(const UserState.init());
|
||||||
|
|
||||||
final StoriesRepository _storiesRepository;
|
final StoriesRepository _storiesRepository;
|
||||||
|
|
||||||
|
@ -13,8 +13,8 @@ class UserState extends Equatable {
|
|||||||
required this.status,
|
required this.status,
|
||||||
});
|
});
|
||||||
|
|
||||||
UserState.init()
|
const UserState.init()
|
||||||
: user = User.empty(),
|
: user = const User.empty(),
|
||||||
status = UserStatus.initial;
|
status = UserStatus.initial;
|
||||||
|
|
||||||
final User user;
|
final User user;
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
import 'package:hacki/config/constants.dart';
|
||||||
|
|
||||||
extension StringExtension on String {
|
extension StringExtension on String {
|
||||||
int? get itemId {
|
int? get itemId {
|
||||||
final RegExp regex = RegExp(r'\d+$');
|
final RegExp regex = RegExp(RegExpConstants.number);
|
||||||
final RegExp exception = RegExp(r'\)|].*$');
|
final RegExp exception = RegExp(RegExpConstants.linkSuffix);
|
||||||
final String match = regex.stringMatch(replaceAll(exception, '')) ?? '';
|
final String match = regex.stringMatch(replaceAll(exception, '')) ?? '';
|
||||||
return int.tryParse(match);
|
return int.tryParse(match);
|
||||||
}
|
}
|
||||||
|
@ -90,9 +90,9 @@ Future<void> main({bool testing = false}) async {
|
|||||||
} else if (Platform.isAndroid) {
|
} else if (Platform.isAndroid) {
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
const SystemUiOverlayStyle(
|
const SystemUiOverlayStyle(
|
||||||
statusBarColor: Colors.transparent,
|
statusBarColor: Palette.transparent,
|
||||||
systemNavigationBarColor: Colors.transparent,
|
systemNavigationBarColor: Palette.transparent,
|
||||||
systemNavigationBarDividerColor: Colors.transparent,
|
systemNavigationBarDividerColor: Palette.transparent,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
|
|
||||||
enum FontSize {
|
enum FontSize {
|
||||||
regular('Regular', TextDimens.pt15),
|
small('Small', TextDimens.pt15),
|
||||||
large('Large', TextDimens.pt16),
|
regular('Regular', TextDimens.pt16),
|
||||||
|
large('Large', TextDimens.pt17),
|
||||||
xlarge('XLarge', TextDimens.pt18);
|
xlarge('XLarge', TextDimens.pt18);
|
||||||
|
|
||||||
const FontSize(this.description, this.fontSize);
|
const FontSize(this.description, this.fontSize);
|
||||||
|
@ -18,10 +18,11 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
|
|||||||
CommentsOrderPreference(),
|
CommentsOrderPreference(),
|
||||||
FontSizePreference(),
|
FontSizePreference(),
|
||||||
// order here reflects the order on settings screen.
|
// order here reflects the order on settings screen.
|
||||||
const NotificationModePreference(),
|
|
||||||
const CollapseModePreference(),
|
|
||||||
const DisplayModePreference(),
|
const DisplayModePreference(),
|
||||||
const MetadataModePreference(),
|
const MetadataModePreference(),
|
||||||
|
const StoryUrlModePreference(),
|
||||||
|
const NotificationModePreference(),
|
||||||
|
const CollapseModePreference(),
|
||||||
NavigationModePreference(),
|
NavigationModePreference(),
|
||||||
const ReaderModePreference(),
|
const ReaderModePreference(),
|
||||||
const MarkReadStoriesModePreference(),
|
const MarkReadStoriesModePreference(),
|
||||||
@ -50,6 +51,7 @@ const bool _trueDarkModeDefaultValue = false;
|
|||||||
const bool _readerModeDefaultValue = true;
|
const bool _readerModeDefaultValue = true;
|
||||||
const bool _markReadStoriesModeDefaultValue = true;
|
const bool _markReadStoriesModeDefaultValue = true;
|
||||||
const bool _metadataModeDefaultValue = true;
|
const bool _metadataModeDefaultValue = true;
|
||||||
|
const bool _storyUrlModeDefaultValue = true;
|
||||||
const bool _collapseModeDefaultValue = false;
|
const bool _collapseModeDefaultValue = false;
|
||||||
final int _fetchModeDefaultValue = FetchMode.eager.index;
|
final int _fetchModeDefaultValue = FetchMode.eager.index;
|
||||||
final int _commentsOrderDefaultValue = CommentsOrder.natural.index;
|
final int _commentsOrderDefaultValue = CommentsOrder.natural.index;
|
||||||
@ -132,6 +134,25 @@ class MetadataModePreference extends BooleanPreference {
|
|||||||
'''show number of comments and post date in story tile.''';
|
'''show number of comments and post date in story tile.''';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StoryUrlModePreference extends BooleanPreference {
|
||||||
|
const StoryUrlModePreference({bool? val})
|
||||||
|
: super(val: val ?? _storyUrlModeDefaultValue);
|
||||||
|
|
||||||
|
@override
|
||||||
|
StoryUrlModePreference copyWith({required bool? val}) {
|
||||||
|
return StoryUrlModePreference(val: val);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key => 'storyUrlMode';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get title => 'Show Url';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get subtitle => '''show url in story tile.''';
|
||||||
|
}
|
||||||
|
|
||||||
/// The value deciding whether or not user should be
|
/// The value deciding whether or not user should be
|
||||||
/// navigated to web view first. Defaults to false.
|
/// navigated to web view first. Defaults to false.
|
||||||
class NavigationModePreference extends BooleanPreference {
|
class NavigationModePreference extends BooleanPreference {
|
||||||
@ -155,7 +176,7 @@ class NavigationModePreference extends BooleanPreference {
|
|||||||
String get title => 'Show Web Page First';
|
String get title => 'Show Web Page First';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get subtitle => ''''show web page first after tapping on story.''';
|
String get subtitle => '''show web page first after tapping on story.''';
|
||||||
}
|
}
|
||||||
|
|
||||||
class ReaderModePreference extends BooleanPreference {
|
class ReaderModePreference extends BooleanPreference {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:hacki/config/constants.dart';
|
||||||
import 'package:hacki/models/item.dart';
|
import 'package:hacki/models/item.dart';
|
||||||
|
|
||||||
enum StoryType {
|
enum StoryType {
|
||||||
@ -67,6 +68,24 @@ class Story extends Item {
|
|||||||
type: '',
|
type: '',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Story.placeholder()
|
||||||
|
: super(
|
||||||
|
id: 0,
|
||||||
|
score: 0,
|
||||||
|
descendants: 0,
|
||||||
|
time: 1171872000,
|
||||||
|
by: 'Y Combinator',
|
||||||
|
title: 'Hacker News Guidelines',
|
||||||
|
url: Constants.guidelineLink,
|
||||||
|
kids: <int>[],
|
||||||
|
dead: false,
|
||||||
|
parts: <int>[],
|
||||||
|
deleted: false,
|
||||||
|
parent: 0,
|
||||||
|
text: '',
|
||||||
|
type: '',
|
||||||
|
);
|
||||||
|
|
||||||
Story.fromJson(Map<String, dynamic> json)
|
Story.fromJson(Map<String, dynamic> json)
|
||||||
: super(
|
: super(
|
||||||
descendants: json['descendants'] as int? ?? 0,
|
descendants: json['descendants'] as int? ?? 0,
|
||||||
@ -91,6 +110,12 @@ class Story extends Item {
|
|||||||
String get simpleMetadata =>
|
String get simpleMetadata =>
|
||||||
'''$score point${score > 1 ? 's' : ''} $descendants comment${descendants > 1 ? 's' : ''} $postedDate''';
|
'''$score point${score > 1 ? 's' : ''} $descendants comment${descendants > 1 ? 's' : ''} $postedDate''';
|
||||||
|
|
||||||
|
String get readableUrl {
|
||||||
|
final Uri url = Uri.parse(this.url);
|
||||||
|
final String authority = url.authority.replaceFirst('www.', '');
|
||||||
|
return authority;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class User {
|
class User extends Equatable {
|
||||||
User({
|
const User({
|
||||||
required this.about,
|
required this.about,
|
||||||
required this.created,
|
required this.created,
|
||||||
required this.delay,
|
required this.delay,
|
||||||
@ -9,7 +10,7 @@ class User {
|
|||||||
required this.karma,
|
required this.karma,
|
||||||
});
|
});
|
||||||
|
|
||||||
User.empty()
|
const User.empty()
|
||||||
: about = '',
|
: about = '',
|
||||||
created = 0,
|
created = 0,
|
||||||
delay = 0,
|
delay = 0,
|
||||||
@ -39,4 +40,13 @@ class User {
|
|||||||
String toString() {
|
String toString() {
|
||||||
return 'User $about, $created, $delay, $id, $karma';
|
return 'User $about, $created, $delay, $id, $karma';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[
|
||||||
|
about,
|
||||||
|
created,
|
||||||
|
delay,
|
||||||
|
id,
|
||||||
|
karma,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
@ -190,6 +190,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
onTap: () => onStoryTapped(story, isPin: true),
|
onTap: () => onStoryTapped(story, isPin: true),
|
||||||
showWebPreview: preferenceState.showComplexStoryTile,
|
showWebPreview: preferenceState.showComplexStoryTile,
|
||||||
showMetadata: preferenceState.showMetadata,
|
showMetadata: preferenceState.showMetadata,
|
||||||
|
showUrl: preferenceState.showUrl,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -255,33 +255,17 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: BlocConsumer<CommentsCubit, CommentsState>(
|
child: BlocListener<CommentsCubit, CommentsState>(
|
||||||
listenWhen: (CommentsState previous, CommentsState current) =>
|
listenWhen: (CommentsState previous, CommentsState current) =>
|
||||||
previous.status != current.status,
|
previous.status != current.status,
|
||||||
listener: (BuildContext context, CommentsState state) {
|
listener: (BuildContext context, CommentsState state) {
|
||||||
if (state.status == CommentsStatus.loaded) {
|
if (state.status != CommentsStatus.loading) {
|
||||||
refreshController
|
refreshController
|
||||||
..refreshCompleted()
|
..refreshCompleted()
|
||||||
..loadComplete();
|
..loadComplete();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (BuildContext context, CommentsState state) {
|
child: BlocListener<EditCubit, EditState>(
|
||||||
final Widget mainView = MainView(
|
|
||||||
scrollController: scrollController,
|
|
||||||
refreshController: refreshController,
|
|
||||||
commentEditingController: commentEditingController,
|
|
||||||
authState: authState,
|
|
||||||
state: state,
|
|
||||||
focusNode: focusNode,
|
|
||||||
topPadding: topPadding,
|
|
||||||
splitViewEnabled: widget.splitViewEnabled,
|
|
||||||
onMoreTapped: onMoreTapped,
|
|
||||||
onStoryLinkTapped: onStoryLinkTapped,
|
|
||||||
onLoginTapped: onLoginTapped,
|
|
||||||
onRightMoreTapped: onRightMoreTapped,
|
|
||||||
);
|
|
||||||
|
|
||||||
return BlocListener<EditCubit, EditState>(
|
|
||||||
listenWhen: (EditState previous, EditState current) {
|
listenWhen: (EditState previous, EditState current) {
|
||||||
return previous.replyingTo != current.replyingTo ||
|
return previous.replyingTo != current.replyingTo ||
|
||||||
previous.itemBeingEdited != current.itemBeingEdited ||
|
previous.itemBeingEdited != current.itemBeingEdited ||
|
||||||
@ -309,7 +293,20 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: mainView,
|
child: MainView(
|
||||||
|
scrollController: scrollController,
|
||||||
|
refreshController: refreshController,
|
||||||
|
commentEditingController:
|
||||||
|
commentEditingController,
|
||||||
|
authState: authState,
|
||||||
|
focusNode: focusNode,
|
||||||
|
topPadding: topPadding,
|
||||||
|
splitViewEnabled: widget.splitViewEnabled,
|
||||||
|
onMoreTapped: onMoreTapped,
|
||||||
|
onStoryLinkTapped: onStoryLinkTapped,
|
||||||
|
onLoginTapped: onLoginTapped,
|
||||||
|
onRightMoreTapped: onRightMoreTapped,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
BlocBuilder<SplitViewCubit, SplitViewState>(
|
BlocBuilder<SplitViewCubit, SplitViewState>(
|
||||||
buildWhen: (
|
buildWhen: (
|
||||||
@ -331,16 +328,14 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
.withOpacity(0.6),
|
.withOpacity(0.6),
|
||||||
item: widget.item,
|
item: widget.item,
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
onBackgroundTap:
|
onBackgroundTap: onFeatureDiscoveryDismissed,
|
||||||
onFeatureDiscoveryDismissed,
|
|
||||||
onDismiss: onFeatureDiscoveryDismissed,
|
onDismiss: onFeatureDiscoveryDismissed,
|
||||||
splitViewEnabled: state.enabled,
|
splitViewEnabled: state.enabled,
|
||||||
expanded: state.expanded,
|
expanded: state.expanded,
|
||||||
onZoomTap:
|
onZoomTap:
|
||||||
context.read<SplitViewCubit>().zoom,
|
context.read<SplitViewCubit>().zoom,
|
||||||
onFontSizeTap: onFontSizeTapped,
|
onFontSizeTap: onFontSizeTapped,
|
||||||
fontSizeIconButtonKey:
|
fontSizeIconButtonKey: fontSizeIconButtonKey,
|
||||||
fontSizeIconButtonKey,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -379,7 +374,19 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
onFontSizeTap: onFontSizeTapped,
|
onFontSizeTap: onFontSizeTapped,
|
||||||
fontSizeIconButtonKey: fontSizeIconButtonKey,
|
fontSizeIconButtonKey: fontSizeIconButtonKey,
|
||||||
),
|
),
|
||||||
body: mainView,
|
body: MainView(
|
||||||
|
scrollController: scrollController,
|
||||||
|
refreshController: refreshController,
|
||||||
|
commentEditingController: commentEditingController,
|
||||||
|
authState: authState,
|
||||||
|
focusNode: focusNode,
|
||||||
|
topPadding: topPadding,
|
||||||
|
splitViewEnabled: widget.splitViewEnabled,
|
||||||
|
onMoreTapped: onMoreTapped,
|
||||||
|
onStoryLinkTapped: onStoryLinkTapped,
|
||||||
|
onLoginTapped: onLoginTapped,
|
||||||
|
onRightMoreTapped: onRightMoreTapped,
|
||||||
|
),
|
||||||
bottomSheet: ReplyBox(
|
bottomSheet: ReplyBox(
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
textEditingController: commentEditingController,
|
textEditingController: commentEditingController,
|
||||||
@ -392,8 +399,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
onChanged: context.read<EditCubit>().onTextChanged,
|
onChanged: context.read<EditCubit>().onTextChanged,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -442,7 +448,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
fontSize: fontSize.fontSize,
|
fontSize: fontSize.fontSize,
|
||||||
color:
|
color:
|
||||||
context.read<PreferenceCubit>().state.fontSize == fontSize
|
context.read<PreferenceCubit>().state.fontSize == fontSize
|
||||||
? Colors.deepOrange
|
? Palette.deepOrange
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -22,7 +22,6 @@ class MainView extends StatelessWidget {
|
|||||||
required this.refreshController,
|
required this.refreshController,
|
||||||
required this.commentEditingController,
|
required this.commentEditingController,
|
||||||
required this.authState,
|
required this.authState,
|
||||||
required this.state,
|
|
||||||
required this.focusNode,
|
required this.focusNode,
|
||||||
required this.topPadding,
|
required this.topPadding,
|
||||||
required this.splitViewEnabled,
|
required this.splitViewEnabled,
|
||||||
@ -36,7 +35,6 @@ class MainView extends StatelessWidget {
|
|||||||
final RefreshController refreshController;
|
final RefreshController refreshController;
|
||||||
final TextEditingController commentEditingController;
|
final TextEditingController commentEditingController;
|
||||||
final AuthState authState;
|
final AuthState authState;
|
||||||
final CommentsState state;
|
|
||||||
final FocusNode focusNode;
|
final FocusNode focusNode;
|
||||||
final double topPadding;
|
final double topPadding;
|
||||||
final bool splitViewEnabled;
|
final bool splitViewEnabled;
|
||||||
@ -47,6 +45,11 @@ class MainView extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
Positioned.fill(
|
||||||
|
child: BlocBuilder<CommentsCubit, CommentsState>(
|
||||||
|
builder: (BuildContext context, CommentsState state) {
|
||||||
return SmartRefresher(
|
return SmartRefresher(
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
enablePullUp: !state.onlyShowTargetComment,
|
enablePullUp: !state.onlyShowTargetComment,
|
||||||
@ -108,6 +111,133 @@ class MainView extends StatelessWidget {
|
|||||||
itemCount: state.comments.length + 2,
|
itemCount: state.comments.length + 2,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
|
return _ParentItemSection(
|
||||||
|
scrollController: scrollController,
|
||||||
|
refreshController: refreshController,
|
||||||
|
commentEditingController: commentEditingController,
|
||||||
|
state: state,
|
||||||
|
authState: authState,
|
||||||
|
focusNode: focusNode,
|
||||||
|
topPadding: topPadding,
|
||||||
|
splitViewEnabled: splitViewEnabled,
|
||||||
|
onMoreTapped: onMoreTapped,
|
||||||
|
onStoryLinkTapped: onStoryLinkTapped,
|
||||||
|
onLoginTapped: onLoginTapped,
|
||||||
|
onRightMoreTapped: onRightMoreTapped,
|
||||||
|
);
|
||||||
|
} else if (index == state.comments.length + 1) {
|
||||||
|
if ((state.status == CommentsStatus.allLoaded &&
|
||||||
|
state.comments.isNotEmpty) ||
|
||||||
|
state.onlyShowTargetComment) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 240,
|
||||||
|
child: Center(
|
||||||
|
child: Text(Constants.happyFaces.pickRandomly()!),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
index = index - 1;
|
||||||
|
final Comment comment = state.comments.elementAt(index);
|
||||||
|
return FadeIn(
|
||||||
|
key: ValueKey<String>('${comment.id}-FadeIn'),
|
||||||
|
child: CommentTile(
|
||||||
|
comment: comment,
|
||||||
|
level: comment.level,
|
||||||
|
myUsername:
|
||||||
|
authState.isLoggedIn ? authState.username : null,
|
||||||
|
opUsername: state.item.by,
|
||||||
|
fetchMode: state.fetchMode,
|
||||||
|
onReplyTapped: (Comment cmt) {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
if (cmt.deleted || cmt.dead) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmt.id !=
|
||||||
|
context.read<EditCubit>().state.replyingTo?.id) {
|
||||||
|
commentEditingController.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.read<EditCubit>().onReplyTapped(cmt);
|
||||||
|
focusNode.requestFocus();
|
||||||
|
},
|
||||||
|
onEditTapped: (Comment cmt) {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
if (cmt.deleted || cmt.dead) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
commentEditingController.clear();
|
||||||
|
context.read<EditCubit>().onEditTapped(cmt);
|
||||||
|
focusNode.requestFocus();
|
||||||
|
},
|
||||||
|
onMoreTapped: onMoreTapped,
|
||||||
|
onStoryLinkTapped: onStoryLinkTapped,
|
||||||
|
onRightMoreTapped: onRightMoreTapped,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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 Visibility(
|
||||||
|
visible: state.status == CommentsStatus.loading,
|
||||||
|
child: const LinearProgressIndicator(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ParentItemSection extends StatelessWidget {
|
||||||
|
const _ParentItemSection({
|
||||||
|
Key? key,
|
||||||
|
required this.scrollController,
|
||||||
|
required this.refreshController,
|
||||||
|
required this.commentEditingController,
|
||||||
|
required this.state,
|
||||||
|
required this.authState,
|
||||||
|
required this.focusNode,
|
||||||
|
required this.topPadding,
|
||||||
|
required this.splitViewEnabled,
|
||||||
|
required this.onMoreTapped,
|
||||||
|
required this.onStoryLinkTapped,
|
||||||
|
required this.onLoginTapped,
|
||||||
|
required this.onRightMoreTapped,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final ScrollController scrollController;
|
||||||
|
final RefreshController refreshController;
|
||||||
|
final TextEditingController commentEditingController;
|
||||||
|
final CommentsState state;
|
||||||
|
final AuthState authState;
|
||||||
|
final FocusNode focusNode;
|
||||||
|
final double topPadding;
|
||||||
|
final bool splitViewEnabled;
|
||||||
|
final Function(Item item, Rect? rect) onMoreTapped;
|
||||||
|
final ValueChanged<String> onStoryLinkTapped;
|
||||||
|
final VoidCallback onLoginTapped;
|
||||||
|
final ValueChanged<Comment> onRightMoreTapped;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@ -171,6 +301,18 @@ class MainView extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||||
|
buildWhen: (
|
||||||
|
PreferenceState previous,
|
||||||
|
PreferenceState current,
|
||||||
|
) =>
|
||||||
|
previous.fontSize != current.fontSize,
|
||||||
|
builder: (
|
||||||
|
BuildContext context,
|
||||||
|
PreferenceState prefState,
|
||||||
|
) {
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
if (state.item is Story)
|
if (state.item is Story)
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () => LinkUtil.launch(
|
onTap: () => LinkUtil.launch(
|
||||||
@ -189,16 +331,50 @@ class MainView extends StatelessWidget {
|
|||||||
bottom: Dimens.pt12,
|
bottom: Dimens.pt12,
|
||||||
top: Dimens.pt12,
|
top: Dimens.pt12,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: RichText(
|
||||||
state.item.title,
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
text: TextSpan(
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: MediaQuery.of(
|
||||||
|
context,
|
||||||
|
).textScaleFactor *
|
||||||
|
prefState.fontSize.fontSize,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyText1
|
||||||
|
?.color,
|
||||||
|
),
|
||||||
|
children: <TextSpan>[
|
||||||
|
TextSpan(
|
||||||
|
text: state.item.title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: MediaQuery.of(
|
||||||
|
context,
|
||||||
|
).textScaleFactor *
|
||||||
|
prefState.fontSize.fontSize,
|
||||||
color: state.item.url.isNotEmpty
|
color: state.item.url.isNotEmpty
|
||||||
? Palette.orange
|
? Palette.orange
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (state.item.url.isNotEmpty)
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
''' (${(state.item as Story).readableUrl})''',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: MediaQuery.of(
|
||||||
|
context,
|
||||||
|
).textScaleFactor *
|
||||||
|
(prefState.fontSize.fontSize - 4),
|
||||||
|
color: Palette.orange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
@ -206,25 +382,14 @@ class MainView extends StatelessWidget {
|
|||||||
height: Dimens.pt6,
|
height: Dimens.pt6,
|
||||||
),
|
),
|
||||||
if (state.item.text.isNotEmpty)
|
if (state.item.text.isNotEmpty)
|
||||||
BlocBuilder<PreferenceCubit, PreferenceState>(
|
Padding(
|
||||||
buildWhen: (
|
|
||||||
PreferenceState previous,
|
|
||||||
PreferenceState current,
|
|
||||||
) =>
|
|
||||||
previous.fontSize != current.fontSize,
|
|
||||||
builder: (
|
|
||||||
BuildContext context,
|
|
||||||
PreferenceState prefState,
|
|
||||||
) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: Dimens.pt10,
|
horizontal: Dimens.pt10,
|
||||||
),
|
),
|
||||||
child: SelectableLinkify(
|
child: SelectableLinkify(
|
||||||
text: state.item.text,
|
text: state.item.text,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize:
|
fontSize: MediaQuery.of(context).textScaleFactor *
|
||||||
MediaQuery.of(context).textScaleFactor *
|
|
||||||
context
|
context
|
||||||
.read<PreferenceCubit>()
|
.read<PreferenceCubit>()
|
||||||
.state
|
.state
|
||||||
@ -232,8 +397,7 @@ class MainView extends StatelessWidget {
|
|||||||
.fontSize,
|
.fontSize,
|
||||||
),
|
),
|
||||||
linkStyle: TextStyle(
|
linkStyle: TextStyle(
|
||||||
fontSize:
|
fontSize: MediaQuery.of(context).textScaleFactor *
|
||||||
MediaQuery.of(context).textScaleFactor *
|
|
||||||
context
|
context
|
||||||
.read<PreferenceCubit>()
|
.read<PreferenceCubit>()
|
||||||
.state
|
.state
|
||||||
@ -249,6 +413,8 @@ class MainView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -273,9 +439,8 @@ class MainView extends StatelessWidget {
|
|||||||
if (state.onlyShowTargetComment) ...<Widget>[
|
if (state.onlyShowTargetComment) ...<Widget>[
|
||||||
Center(
|
Center(
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () => context
|
onPressed: () =>
|
||||||
.read<CommentsCubit>()
|
context.read<CommentsCubit>().loadAll(state.item as Story),
|
||||||
.loadAll(state.item as Story),
|
|
||||||
child: const Text('View all comments'),
|
child: const Text('View all comments'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -300,10 +465,8 @@ class MainView extends StatelessWidget {
|
|||||||
width: Dimens.pt4,
|
width: Dimens.pt4,
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed:
|
onPressed: context.read<CommentsCubit>().loadParentThread,
|
||||||
context.read<CommentsCubit>().loadParentThread,
|
child: state.fetchParentStatus == CommentsStatus.loading
|
||||||
child:
|
|
||||||
state.fetchParentStatus == CommentsStatus.loading
|
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
height: Dimens.pt12,
|
height: Dimens.pt12,
|
||||||
width: Dimens.pt12,
|
width: Dimens.pt12,
|
||||||
@ -337,8 +500,7 @@ class MainView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
onChanged:
|
onChanged: context.read<CommentsCubit>().onFetchModeChanged,
|
||||||
context.read<CommentsCubit>().onFetchModeChanged,
|
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: Dimens.pt6,
|
width: Dimens.pt6,
|
||||||
@ -348,8 +510,7 @@ class MainView extends StatelessWidget {
|
|||||||
underline: const SizedBox.shrink(),
|
underline: const SizedBox.shrink(),
|
||||||
items: CommentsOrder.values
|
items: CommentsOrder.values
|
||||||
.map(
|
.map(
|
||||||
(CommentsOrder val) =>
|
(CommentsOrder val) => DropdownMenuItem<CommentsOrder>(
|
||||||
DropdownMenuItem<CommentsOrder>(
|
|
||||||
value: val,
|
value: val,
|
||||||
child: Text(
|
child: Text(
|
||||||
val.description,
|
val.description,
|
||||||
@ -385,60 +546,5 @@ class MainView extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else if (index == state.comments.length + 1) {
|
|
||||||
if ((state.status == CommentsStatus.allLoaded &&
|
|
||||||
state.comments.isNotEmpty) ||
|
|
||||||
state.onlyShowTargetComment) {
|
|
||||||
return SizedBox(
|
|
||||||
height: 240,
|
|
||||||
child: Center(
|
|
||||||
child: Text(Constants.happyFaces.pickRandomly()!),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
index = index - 1;
|
|
||||||
final Comment comment = state.comments.elementAt(index);
|
|
||||||
return FadeIn(
|
|
||||||
key: ValueKey<String>('${comment.id}-FadeIn'),
|
|
||||||
child: CommentTile(
|
|
||||||
comment: comment,
|
|
||||||
level: comment.level,
|
|
||||||
myUsername: authState.isLoggedIn ? authState.username : null,
|
|
||||||
opUsername: state.item.by,
|
|
||||||
fetchMode: state.fetchMode,
|
|
||||||
onReplyTapped: (Comment cmt) {
|
|
||||||
HapticFeedback.lightImpact();
|
|
||||||
if (cmt.deleted || cmt.dead) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmt.id != context.read<EditCubit>().state.replyingTo?.id) {
|
|
||||||
commentEditingController.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
context.read<EditCubit>().onReplyTapped(cmt);
|
|
||||||
focusNode.requestFocus();
|
|
||||||
},
|
|
||||||
onEditTapped: (Comment cmt) {
|
|
||||||
HapticFeedback.lightImpact();
|
|
||||||
if (cmt.deleted || cmt.dead) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
commentEditingController.clear();
|
|
||||||
context.read<EditCubit>().onEditTapped(cmt);
|
|
||||||
focusNode.requestFocus();
|
|
||||||
},
|
|
||||||
onMoreTapped: onMoreTapped,
|
|
||||||
onStoryLinkTapped: onStoryLinkTapped,
|
|
||||||
onRightMoreTapped: onRightMoreTapped,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import 'package:hacki/screens/screens.dart';
|
|||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
import 'package:hacki/utils/utils.dart';
|
import 'package:hacki/utils/utils.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
@ -122,6 +123,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
return ItemsListView<Item>(
|
return ItemsListView<Item>(
|
||||||
showWebPreview: false,
|
showWebPreview: false,
|
||||||
showMetadata: false,
|
showMetadata: false,
|
||||||
|
showUrl: false,
|
||||||
useConsistentFontSize: true,
|
useConsistentFontSize: true,
|
||||||
refreshController: refreshControllerHistory,
|
refreshController: refreshControllerHistory,
|
||||||
items: historyState.submittedItems
|
items: historyState.submittedItems
|
||||||
@ -174,6 +176,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
showWebPreview:
|
showWebPreview:
|
||||||
preferenceState.showComplexStoryTile,
|
preferenceState.showComplexStoryTile,
|
||||||
showMetadata: preferenceState.showMetadata,
|
showMetadata: preferenceState.showMetadata,
|
||||||
|
showUrl: preferenceState.showUrl,
|
||||||
useCommentTile: true,
|
useCommentTile: true,
|
||||||
refreshController: refreshControllerFav,
|
refreshController: refreshControllerFav,
|
||||||
items: favState.favItems,
|
items: favState.favItems,
|
||||||
@ -374,6 +377,17 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const Divider(),
|
||||||
|
StoryTile(
|
||||||
|
showWebPreview:
|
||||||
|
preferenceState.showComplexStoryTile,
|
||||||
|
showMetadata: preferenceState.showMetadata,
|
||||||
|
showUrl: preferenceState.showUrl,
|
||||||
|
story: Story.placeholder(),
|
||||||
|
onTap: () =>
|
||||||
|
LinkUtil.launch(Constants.guidelineLink),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
for (final Preference<dynamic> preference
|
for (final Preference<dynamic> preference
|
||||||
in preferenceState.preferences
|
in preferenceState.preferences
|
||||||
.whereType<BooleanPreference>()
|
.whereType<BooleanPreference>()
|
||||||
@ -422,93 +436,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
title: const Text('About'),
|
title: const Text('About'),
|
||||||
subtitle:
|
subtitle:
|
||||||
const Text('nothing interesting here.'),
|
const Text('nothing interesting here.'),
|
||||||
onTap: () {
|
onTap: showAboutHackiDialog,
|
||||||
showAboutDialog(
|
|
||||||
context: context,
|
|
||||||
applicationName: 'Hacki',
|
|
||||||
applicationVersion: 'v0.2.33',
|
|
||||||
applicationIcon: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(
|
|
||||||
Radius.circular(
|
|
||||||
Dimens.pt12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Image.asset(
|
|
||||||
Constants.hackiIconPath,
|
|
||||||
height: Dimens.pt50,
|
|
||||||
width: Dimens.pt50,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
children: <Widget>[
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => LinkUtil.launch(
|
|
||||||
Constants.portfolioLink,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: const <Widget>[
|
|
||||||
Icon(
|
|
||||||
FontAwesomeIcons.addressCard,
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: Dimens.pt12,
|
|
||||||
),
|
|
||||||
Text('Developer'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => LinkUtil.launch(
|
|
||||||
Constants.githubLink,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: const <Widget>[
|
|
||||||
Icon(
|
|
||||||
FontAwesomeIcons.github,
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: Dimens.pt12,
|
|
||||||
),
|
|
||||||
Text('Source code'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => LinkUtil.launch(
|
|
||||||
Platform.isIOS
|
|
||||||
? Constants.appStoreLink
|
|
||||||
: Constants.googlePlayLink,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: const <Widget>[
|
|
||||||
Icon(
|
|
||||||
Icons.thumb_up,
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: Dimens.pt12,
|
|
||||||
),
|
|
||||||
Text('Like the app?'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => LinkUtil.launch(
|
|
||||||
Constants.sponsorLink,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: const <Widget>[
|
|
||||||
Icon(
|
|
||||||
FeatherIcons.coffee,
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: Dimens.pt12,
|
|
||||||
),
|
|
||||||
Text('Buy me a coffee'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: Dimens.pt48,
|
height: Dimens.pt48,
|
||||||
@ -718,6 +646,95 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> showAboutHackiDialog() async {
|
||||||
|
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
|
final String version = packageInfo.version;
|
||||||
|
|
||||||
|
showAboutDialog(
|
||||||
|
context: context,
|
||||||
|
applicationName: 'Hacki',
|
||||||
|
applicationVersion: 'v$version',
|
||||||
|
applicationIcon: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(
|
||||||
|
Dimens.pt12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Image.asset(
|
||||||
|
Constants.hackiIconPath,
|
||||||
|
height: Dimens.pt50,
|
||||||
|
width: Dimens.pt50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => LinkUtil.launch(
|
||||||
|
Constants.portfolioLink,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: const <Widget>[
|
||||||
|
Icon(
|
||||||
|
FontAwesomeIcons.addressCard,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: Dimens.pt12,
|
||||||
|
),
|
||||||
|
Text('Developer'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => LinkUtil.launch(
|
||||||
|
Constants.githubLink,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: const <Widget>[
|
||||||
|
Icon(
|
||||||
|
FontAwesomeIcons.github,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: Dimens.pt12,
|
||||||
|
),
|
||||||
|
Text('Source code'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => LinkUtil.launch(
|
||||||
|
Platform.isIOS ? Constants.appStoreLink : Constants.googlePlayLink,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: const <Widget>[
|
||||||
|
Icon(
|
||||||
|
Icons.thumb_up,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: Dimens.pt12,
|
||||||
|
),
|
||||||
|
Text('Like the app?'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => LinkUtil.launch(
|
||||||
|
Constants.sponsorLink,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: const <Widget>[
|
||||||
|
Icon(
|
||||||
|
FeatherIcons.coffee,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: Dimens.pt12,
|
||||||
|
),
|
||||||
|
Text('Buy me a coffee'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void onCommentTapped(Comment comment, {VoidCallback? then}) {
|
void onCommentTapped(Comment comment, {VoidCallback? then}) {
|
||||||
throttle.run(() {
|
throttle.run(() {
|
||||||
locator
|
locator
|
||||||
|
@ -166,6 +166,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
showWebPreview:
|
showWebPreview:
|
||||||
prefState.showComplexStoryTile,
|
prefState.showComplexStoryTile,
|
||||||
showMetadata: prefState.showMetadata,
|
showMetadata: prefState.showMetadata,
|
||||||
|
showUrl: prefState.showUrl,
|
||||||
story: e,
|
story: e,
|
||||||
onTap: () => goToItemScreen(
|
onTap: () => goToItemScreen(
|
||||||
args: ItemScreenArgs(item: e),
|
args: ItemScreenArgs(item: e),
|
||||||
|
62
lib/screens/widgets/bloc_builder_3.dart
Normal file
62
lib/screens/widgets/bloc_builder_3.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
typedef BlocBuilderCondition<S> = bool Function(S previous, S current);
|
||||||
|
typedef BlocWidgetBuilder3<StateA, StateB, StateC> = Widget Function(
|
||||||
|
BuildContext,
|
||||||
|
StateA,
|
||||||
|
StateB,
|
||||||
|
StateC,
|
||||||
|
);
|
||||||
|
|
||||||
|
class BlocBuilder3<
|
||||||
|
BlocA extends StateStreamable<BlocAState>,
|
||||||
|
BlocAState,
|
||||||
|
BlocB extends StateStreamable<BlocBState>,
|
||||||
|
BlocBState,
|
||||||
|
BlocC extends StateStreamable<BlocCState>,
|
||||||
|
BlocCState> extends StatelessWidget {
|
||||||
|
const BlocBuilder3({
|
||||||
|
Key? key,
|
||||||
|
required this.builder,
|
||||||
|
this.blocA,
|
||||||
|
this.blocB,
|
||||||
|
this.blocC,
|
||||||
|
this.buildWhenA,
|
||||||
|
this.buildWhenB,
|
||||||
|
this.buildWhenC,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final BlocWidgetBuilder3<BlocAState, BlocBState, BlocCState> builder;
|
||||||
|
|
||||||
|
final BlocA? blocA;
|
||||||
|
final BlocB? blocB;
|
||||||
|
final BlocC? blocC;
|
||||||
|
|
||||||
|
final BlocBuilderCondition<BlocAState>? buildWhenA;
|
||||||
|
final BlocBuilderCondition<BlocBState>? buildWhenB;
|
||||||
|
final BlocBuilderCondition<BlocCState>? buildWhenC;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<BlocA, BlocAState>(
|
||||||
|
bloc: blocA,
|
||||||
|
buildWhen: buildWhenA,
|
||||||
|
builder: (BuildContext context, BlocAState blocAState) {
|
||||||
|
return BlocBuilder<BlocB, BlocBState>(
|
||||||
|
bloc: blocB,
|
||||||
|
buildWhen: buildWhenB,
|
||||||
|
builder: (BuildContext context, BlocBState blocBState) {
|
||||||
|
return BlocBuilder<BlocC, BlocCState>(
|
||||||
|
bloc: blocC,
|
||||||
|
buildWhen: buildWhenC,
|
||||||
|
builder: (BuildContext context, BlocCState blocCState) {
|
||||||
|
return builder(context, blocAState, blocBState, blocCState);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ import 'package:hacki/blocs/blocs.dart';
|
|||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/extensions/extensions.dart';
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
|
import 'package:hacki/screens/widgets/bloc_builder_3.dart';
|
||||||
import 'package:hacki/services/services.dart';
|
import 'package:hacki/services/services.dart';
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
import 'package:hacki/utils/utils.dart';
|
import 'package:hacki/utils/utils.dart';
|
||||||
@ -46,16 +47,19 @@ class CommentTile extends StatelessWidget {
|
|||||||
lazy: false,
|
lazy: false,
|
||||||
create: (_) => CollapseCubit(
|
create: (_) => CollapseCubit(
|
||||||
commentId: comment.id,
|
commentId: comment.id,
|
||||||
|
commentsCubit: context.tryRead<CommentsCubit>(),
|
||||||
collapseCache: context.tryRead<CollapseCache>() ?? CollapseCache(),
|
collapseCache: context.tryRead<CollapseCache>() ?? CollapseCache(),
|
||||||
)..init(),
|
)..init(),
|
||||||
child: BlocBuilder<CollapseCubit, CollapseState>(
|
child: BlocBuilder3<CollapseCubit, CollapseState, PreferenceCubit,
|
||||||
builder: (BuildContext context, CollapseState state) {
|
PreferenceState, BlocklistCubit, BlocklistState>(
|
||||||
|
builder: (
|
||||||
|
BuildContext context,
|
||||||
|
CollapseState state,
|
||||||
|
PreferenceState prefState,
|
||||||
|
BlocklistState blocklistState,
|
||||||
|
) {
|
||||||
if (actionable && state.hidden) return const SizedBox.shrink();
|
if (actionable && state.hidden) return const SizedBox.shrink();
|
||||||
|
|
||||||
return BlocBuilder<PreferenceCubit, PreferenceState>(
|
|
||||||
builder: (BuildContext context, PreferenceState prefState) {
|
|
||||||
return BlocBuilder<BlocklistCubit, BlocklistState>(
|
|
||||||
builder: (BuildContext context, BlocklistState blocklistState) {
|
|
||||||
const Color orange = Color.fromRGBO(255, 152, 0, 1);
|
const Color orange = Color.fromRGBO(255, 152, 0, 1);
|
||||||
final Color color = _getColor(level);
|
final Color color = _getColor(level);
|
||||||
|
|
||||||
@ -70,21 +74,15 @@ class CommentTile extends StatelessWidget {
|
|||||||
motion: const StretchMotion(),
|
motion: const StretchMotion(),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
SlidableAction(
|
SlidableAction(
|
||||||
onPressed: (_) =>
|
onPressed: (_) => onReplyTapped?.call(comment),
|
||||||
onReplyTapped?.call(comment),
|
|
||||||
backgroundColor: Palette.orange,
|
backgroundColor: Palette.orange,
|
||||||
foregroundColor: Palette.white,
|
foregroundColor: Palette.white,
|
||||||
icon: Icons.message,
|
icon: Icons.message,
|
||||||
),
|
),
|
||||||
if (context
|
if (context.read<AuthBloc>().state.user.id ==
|
||||||
.read<AuthBloc>()
|
|
||||||
.state
|
|
||||||
.user
|
|
||||||
.id ==
|
|
||||||
comment.by)
|
comment.by)
|
||||||
SlidableAction(
|
SlidableAction(
|
||||||
onPressed: (_) =>
|
onPressed: (_) => onEditTapped?.call(comment),
|
||||||
onEditTapped?.call(comment),
|
|
||||||
backgroundColor: Palette.orange,
|
backgroundColor: Palette.orange,
|
||||||
foregroundColor: Palette.white,
|
foregroundColor: Palette.white,
|
||||||
icon: Icons.edit,
|
icon: Icons.edit,
|
||||||
@ -137,9 +135,8 @@ class CommentTile extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
comment.by,
|
comment.by,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: prefState.showEyeCandy
|
color:
|
||||||
? orange
|
prefState.showEyeCandy ? orange : color,
|
||||||
: color,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (comment.by == opUsername)
|
if (comment.by == opUsername)
|
||||||
@ -202,8 +199,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else if (blocklistState.blocklist
|
else if (blocklistState.blocklist.contains(comment.by))
|
||||||
.contains(comment.by))
|
|
||||||
const Center(
|
const Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
@ -229,8 +225,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
? SelectableText.rich(
|
? SelectableText.rich(
|
||||||
key: ValueKey<int>(comment.id),
|
key: ValueKey<int>(comment.id),
|
||||||
buildTextSpan(
|
buildTextSpan(
|
||||||
(comment as BuildableComment)
|
(comment as BuildableComment).elements,
|
||||||
.elements,
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: MediaQuery.of(
|
fontSize: MediaQuery.of(
|
||||||
context,
|
context,
|
||||||
@ -242,14 +237,12 @@ class CommentTile extends StatelessWidget {
|
|||||||
context,
|
context,
|
||||||
).textScaleFactor *
|
).textScaleFactor *
|
||||||
prefState.fontSize.fontSize,
|
prefState.fontSize.fontSize,
|
||||||
decoration:
|
decoration: TextDecoration.underline,
|
||||||
TextDecoration.underline,
|
|
||||||
color: Palette.orange,
|
color: Palette.orange,
|
||||||
),
|
),
|
||||||
onOpen: (LinkableElement link) {
|
onOpen: (LinkableElement link) {
|
||||||
if (link.url.isStoryLink) {
|
if (link.url.isStoryLink) {
|
||||||
onStoryLinkTapped
|
onStoryLinkTapped.call(link.url);
|
||||||
.call(link.url);
|
|
||||||
} else {
|
} else {
|
||||||
LinkUtil.launch(link.url);
|
LinkUtil.launch(link.url);
|
||||||
}
|
}
|
||||||
@ -273,8 +266,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onOpen: (LinkableElement link) {
|
onOpen: (LinkableElement link) {
|
||||||
if (link.url.isStoryLink) {
|
if (link.url.isStoryLink) {
|
||||||
onStoryLinkTapped
|
onStoryLinkTapped.call(link.url);
|
||||||
.call(link.url);
|
|
||||||
} else {
|
} else {
|
||||||
LinkUtil.launch(link.url);
|
LinkUtil.launch(link.url);
|
||||||
}
|
}
|
||||||
@ -300,16 +292,13 @@ class CommentTile extends StatelessWidget {
|
|||||||
horizontal: Dimens.pt12,
|
horizontal: Dimens.pt12,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
context
|
context.read<CommentsCubit>().loadMore(
|
||||||
.read<CommentsCubit>()
|
|
||||||
.loadMore(
|
|
||||||
comment: comment,
|
comment: comment,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -337,9 +326,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final double commentBackgroundColorOpacity =
|
final double commentBackgroundColorOpacity =
|
||||||
Theme.of(context).brightness == Brightness.dark
|
Theme.of(context).brightness == Brightness.dark ? 0.03 : 0.15;
|
||||||
? 0.03
|
|
||||||
: 0.15;
|
|
||||||
|
|
||||||
final Color commentColor = prefState.showEyeCandy
|
final Color commentColor = prefState.showEyeCandy
|
||||||
? color.withOpacity(commentBackgroundColorOpacity)
|
? color.withOpacity(commentBackgroundColorOpacity)
|
||||||
@ -350,7 +337,10 @@ class CommentTile extends StatelessWidget {
|
|||||||
|
|
||||||
if (isMyComment && level == 0) {
|
if (isMyComment && level == 0) {
|
||||||
return Container(
|
return Container(
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
decoration: BoxDecoration(
|
||||||
color: Palette.orange.withOpacity(0.2),
|
color: Palette.orange.withOpacity(0.2),
|
||||||
|
),
|
||||||
child: wrapper,
|
child: wrapper,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -361,7 +351,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
wrapper = Container(
|
wrapper = Container(
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
margin: const EdgeInsets.only(
|
margin: const EdgeInsets.only(
|
||||||
left: Dimens.pt12,
|
left: Dimens.pt8,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: i != 0
|
border: i != 0
|
||||||
@ -381,10 +371,6 @@ class CommentTile extends StatelessWidget {
|
|||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
},
|
},
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.showWebPreview,
|
required this.showWebPreview,
|
||||||
required this.showMetadata,
|
required this.showMetadata,
|
||||||
|
required this.showUrl,
|
||||||
required this.items,
|
required this.items,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
required this.refreshController,
|
required this.refreshController,
|
||||||
@ -39,6 +40,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
|||||||
final bool showCommentBy;
|
final bool showCommentBy;
|
||||||
final bool showWebPreview;
|
final bool showWebPreview;
|
||||||
final bool showMetadata;
|
final bool showMetadata;
|
||||||
|
final bool showUrl;
|
||||||
final bool enablePullDown;
|
final bool enablePullDown;
|
||||||
final bool markReadStories;
|
final bool markReadStories;
|
||||||
final bool showOfflineBanner;
|
final bool showOfflineBanner;
|
||||||
@ -97,6 +99,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
|||||||
onTap: () => onTap(e),
|
onTap: () => onTap(e),
|
||||||
showWebPreview: showWebPreview,
|
showWebPreview: showWebPreview,
|
||||||
showMetadata: showMetadata,
|
showMetadata: showMetadata,
|
||||||
|
showUrl: showUrl,
|
||||||
hasRead: markReadStories && hasRead,
|
hasRead: markReadStories && hasRead,
|
||||||
simpleTileFontSize: useConsistentFontSize
|
simpleTileFontSize: useConsistentFontSize
|
||||||
? TextDimens.pt14
|
? TextDimens.pt14
|
||||||
|
@ -15,6 +15,7 @@ class LinkPreview extends StatefulWidget {
|
|||||||
required this.link,
|
required this.link,
|
||||||
required this.story,
|
required this.story,
|
||||||
required this.showMetadata,
|
required this.showMetadata,
|
||||||
|
required this.showUrl,
|
||||||
required this.offlineReading,
|
required this.offlineReading,
|
||||||
this.cache = const Duration(days: 30),
|
this.cache = const Duration(days: 30),
|
||||||
this.titleStyle,
|
this.titleStyle,
|
||||||
@ -103,6 +104,7 @@ class LinkPreview extends StatefulWidget {
|
|||||||
final List<BoxShadow>? boxShadow;
|
final List<BoxShadow>? boxShadow;
|
||||||
|
|
||||||
final bool showMetadata;
|
final bool showMetadata;
|
||||||
|
final bool showUrl;
|
||||||
final bool offlineReading;
|
final bool offlineReading;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -180,6 +182,7 @@ class _LinkPreviewState extends State<LinkPreview> {
|
|||||||
key: widget.key ?? Key(widget.link),
|
key: widget.key ?? Key(widget.link),
|
||||||
metadata: widget.story.simpleMetadata,
|
metadata: widget.story.simpleMetadata,
|
||||||
url: widget.link,
|
url: widget.link,
|
||||||
|
readableUrl: widget.story.readableUrl,
|
||||||
title: widget.story.title,
|
title: widget.story.title,
|
||||||
description: desc ?? title ?? 'no comment yet.',
|
description: desc ?? title ?? 'no comment yet.',
|
||||||
imageUri: imageUri,
|
imageUri: imageUri,
|
||||||
@ -194,6 +197,7 @@ class _LinkPreviewState extends State<LinkPreview> {
|
|||||||
bgColor: widget.backgroundColor,
|
bgColor: widget.backgroundColor,
|
||||||
radius: widget.borderRadius ?? 12,
|
radius: widget.borderRadius ?? 12,
|
||||||
showMetadata: widget.showMetadata,
|
showMetadata: widget.showMetadata,
|
||||||
|
showUrl: widget.showUrl,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,12 @@ class LinkView extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.metadata,
|
required this.metadata,
|
||||||
required this.url,
|
required this.url,
|
||||||
|
required this.readableUrl,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
required this.showMetadata,
|
required this.showMetadata,
|
||||||
|
required this.showUrl,
|
||||||
this.imageUri,
|
this.imageUri,
|
||||||
this.imagePath,
|
this.imagePath,
|
||||||
this.titleTextStyle,
|
this.titleTextStyle,
|
||||||
@ -30,6 +32,7 @@ class LinkView extends StatelessWidget {
|
|||||||
|
|
||||||
final String metadata;
|
final String metadata;
|
||||||
final String url;
|
final String url;
|
||||||
|
final String readableUrl;
|
||||||
final String title;
|
final String title;
|
||||||
final String description;
|
final String description;
|
||||||
final String? imageUri;
|
final String? imageUri;
|
||||||
@ -44,6 +47,7 @@ class LinkView extends StatelessWidget {
|
|||||||
final double radius;
|
final double radius;
|
||||||
final Color? bgColor;
|
final Color? bgColor;
|
||||||
final bool showMetadata;
|
final bool showMetadata;
|
||||||
|
final bool showUrl;
|
||||||
|
|
||||||
double computeTitleFontSize(double width) {
|
double computeTitleFontSize(double width) {
|
||||||
double size = width * 0.13;
|
double size = width * 0.13;
|
||||||
@ -146,6 +150,7 @@ class LinkView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTitleContainer(TextStyle _titleTS, int _maxLines) {
|
Widget _buildTitleContainer(TextStyle _titleTS, int _maxLines) {
|
||||||
|
final bool showUrl = this.showUrl && url.isNotEmpty;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(4, 2, 3, 0),
|
padding: const EdgeInsets.fromLTRB(4, 2, 3, 0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -159,6 +164,22 @@ class LinkView extends StatelessWidget {
|
|||||||
maxLines: _maxLines,
|
maxLines: _maxLines,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (showUrl)
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: Text(
|
||||||
|
'($readableUrl)',
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
style: _titleTS.copyWith(
|
||||||
|
color: Palette.grey,
|
||||||
|
fontSize:
|
||||||
|
_titleTS.fontSize == null ? 12 : _titleTS.fontSize! - 4,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
overflow: bodyTextOverflow ?? TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -193,8 +214,9 @@ class LinkView extends StatelessWidget {
|
|||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
style: _bodyTS,
|
style: _bodyTS,
|
||||||
overflow: bodyTextOverflow ?? TextOverflow.ellipsis,
|
overflow: bodyTextOverflow ?? TextOverflow.ellipsis,
|
||||||
maxLines:
|
maxLines: (bodyMaxLines ?? _maxLines) -
|
||||||
(bodyMaxLines ?? _maxLines) - (showMetadata ? 1 : 0),
|
(showMetadata ? 1 : 0) -
|
||||||
|
(showUrl && url.isNotEmpty ? 1 : 0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -68,6 +68,7 @@ class _StoriesListViewState extends State<StoriesListView> {
|
|||||||
context.read<PreferenceCubit>().state.markReadStories,
|
context.read<PreferenceCubit>().state.markReadStories,
|
||||||
showWebPreview: preferenceState.showComplexStoryTile,
|
showWebPreview: preferenceState.showComplexStoryTile,
|
||||||
showMetadata: preferenceState.showMetadata,
|
showMetadata: preferenceState.showMetadata,
|
||||||
|
showUrl: preferenceState.showUrl,
|
||||||
refreshController: refreshController,
|
refreshController: refreshController,
|
||||||
items: state.storiesByType[storyType]!,
|
items: state.storiesByType[storyType]!,
|
||||||
onRefresh: () {
|
onRefresh: () {
|
||||||
|
@ -15,6 +15,7 @@ class StoryTile extends StatelessWidget {
|
|||||||
this.hasRead = false,
|
this.hasRead = false,
|
||||||
required this.showWebPreview,
|
required this.showWebPreview,
|
||||||
required this.showMetadata,
|
required this.showMetadata,
|
||||||
|
required this.showUrl,
|
||||||
required this.story,
|
required this.story,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
this.simpleTileFontSize = 16,
|
this.simpleTileFontSize = 16,
|
||||||
@ -22,6 +23,7 @@ class StoryTile extends StatelessWidget {
|
|||||||
|
|
||||||
final bool showWebPreview;
|
final bool showWebPreview;
|
||||||
final bool showMetadata;
|
final bool showMetadata;
|
||||||
|
final bool showUrl;
|
||||||
final bool hasRead;
|
final bool hasRead;
|
||||||
final Story story;
|
final Story story;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
@ -42,7 +44,108 @@ class StoryTile extends StatelessWidget {
|
|||||||
story: story,
|
story: story,
|
||||||
link: story.url,
|
link: story.url,
|
||||||
offlineReading: context.read<StoriesBloc>().state.offlineReading,
|
offlineReading: context.read<StoriesBloc>().state.offlineReading,
|
||||||
placeholderWidget: FadeIn(
|
placeholderWidget: _LinkPreviewPlaceholder(
|
||||||
|
height: height,
|
||||||
|
),
|
||||||
|
errorImage: Constants.hackerNewsLogoLink,
|
||||||
|
backgroundColor: Palette.transparent,
|
||||||
|
borderRadius: Dimens.zero,
|
||||||
|
removeElevation: true,
|
||||||
|
bodyMaxLines: context.storyTileMaxLines,
|
||||||
|
errorTitle: story.title,
|
||||||
|
titleStyle: TextStyle(
|
||||||
|
color: hasRead
|
||||||
|
? Palette.grey[500]
|
||||||
|
: Theme.of(context).textTheme.subtitle1?.color,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
showMetadata: showMetadata,
|
||||||
|
showUrl: showUrl,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: Dimens.pt12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
const SizedBox(
|
||||||
|
height: Dimens.pt8,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: RichText(
|
||||||
|
textScaleFactor: MediaQuery.of(context).textScaleFactor,
|
||||||
|
text: TextSpan(
|
||||||
|
children: <TextSpan>[
|
||||||
|
TextSpan(
|
||||||
|
text: story.title,
|
||||||
|
style: TextStyle(
|
||||||
|
color: hasRead
|
||||||
|
? Palette.grey[500]
|
||||||
|
: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyText1
|
||||||
|
?.color,
|
||||||
|
fontSize: simpleTileFontSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (showUrl && story.url.isNotEmpty)
|
||||||
|
TextSpan(
|
||||||
|
text: ' (${story.readableUrl})',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Palette.grey[500],
|
||||||
|
fontSize: simpleTileFontSize - 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (showMetadata)
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
story.metadata,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Palette.grey,
|
||||||
|
fontSize: simpleTileFontSize - 2,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: Dimens.pt8,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LinkPreviewPlaceholder extends StatelessWidget {
|
||||||
|
const _LinkPreviewPlaceholder({
|
||||||
|
Key? key,
|
||||||
|
required this.height,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FadeIn(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: height,
|
height: height,
|
||||||
child: Shimmer.fromColors(
|
child: Shimmer.fromColors(
|
||||||
@ -126,70 +229,6 @@ class StoryTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
errorImage: Constants.hackerNewsLogoLink,
|
|
||||||
backgroundColor: Palette.transparent,
|
|
||||||
borderRadius: Dimens.zero,
|
|
||||||
removeElevation: true,
|
|
||||||
bodyMaxLines: context.storyTileMaxLines,
|
|
||||||
errorTitle: story.title,
|
|
||||||
titleStyle: TextStyle(
|
|
||||||
color: hasRead
|
|
||||||
? Palette.grey[500]
|
|
||||||
: Theme.of(context).textTheme.subtitle1!.color,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
showMetadata: showMetadata,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return InkWell(
|
|
||||||
onTap: onTap,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: Dimens.pt12),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
const SizedBox(
|
|
||||||
height: Dimens.pt8,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
story.title,
|
|
||||||
style: TextStyle(
|
|
||||||
color: hasRead ? Palette.grey[500] : null,
|
|
||||||
fontSize: simpleTileFontSize,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (showMetadata)
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
story.metadata,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Palette.grey,
|
|
||||||
fontSize: simpleTileFontSize - 2,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: Dimens.pt8,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
export 'bloc_builder_3.dart';
|
||||||
export 'circle_tab_indicator.dart';
|
export 'circle_tab_indicator.dart';
|
||||||
export 'comment_tile.dart';
|
export 'comment_tile.dart';
|
||||||
export 'countdown_reminder.dart';
|
export 'countdown_reminder.dart';
|
||||||
|
@ -15,7 +15,7 @@ class CollapseCache {
|
|||||||
addIfParentIsHiddenOrCollapsed(commentId, to);
|
addIfParentIsHiddenOrCollapsed(commentId, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
int collapse(int commentId) {
|
Set<int> collapse(int commentId) {
|
||||||
_collapsed.add(commentId);
|
_collapsed.add(commentId);
|
||||||
|
|
||||||
Set<int> findHiddenComments(int commentId) {
|
Set<int> findHiddenComments(int commentId) {
|
||||||
@ -35,7 +35,7 @@ class CollapseCache {
|
|||||||
|
|
||||||
_hiddenCommentsSubject.add(_hidden);
|
_hiddenCommentsSubject.add(_hidden);
|
||||||
|
|
||||||
return hiddenComments.length;
|
return hiddenComments;
|
||||||
}
|
}
|
||||||
|
|
||||||
void uncollapse(int commentId) {
|
void uncollapse(int commentId) {
|
||||||
|
@ -31,6 +31,7 @@ abstract class TextDimens {
|
|||||||
static const double pt14 = 14;
|
static const double pt14 = 14;
|
||||||
static const double pt15 = 15;
|
static const double pt15 = 15;
|
||||||
static const double pt16 = 16;
|
static const double pt16 = 16;
|
||||||
|
static const double pt17 = 17;
|
||||||
static const double pt18 = 18;
|
static const double pt18 = 18;
|
||||||
static const double pt20 = 20;
|
static const double pt20 = 20;
|
||||||
static const double pt24 = 24;
|
static const double pt24 = 24;
|
||||||
|
@ -2,6 +2,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
|
import 'package:hacki/config/constants.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
import 'package:hacki/main.dart';
|
import 'package:hacki/main.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
@ -35,7 +36,7 @@ abstract class LinkUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Uri rinseLink(String link) {
|
Uri rinseLink(String link) {
|
||||||
final RegExp regex = RegExp(r'\)|].*$');
|
final RegExp regex = RegExp(RegExpConstants.linkSuffix);
|
||||||
if (!link.contains('en.wikipedia.org') && link.contains(regex)) {
|
if (!link.contains('en.wikipedia.org') && link.contains(regex)) {
|
||||||
final String match = regex.stringMatch(link) ?? '';
|
final String match = regex.stringMatch(link) ?? '';
|
||||||
return Uri.parse(link.replaceAll(match, ''));
|
return Uri.parse(link.replaceAll(match, ''));
|
||||||
|
14
pubspec.lock
14
pubspec.lock
@ -607,6 +607,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
|
package_info_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: package_info_plus
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
package_info_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_info_plus_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.1"
|
||||||
path:
|
path:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
name: hacki
|
name: hacki
|
||||||
description: A Hacker News reader.
|
description: A Hacker News reader.
|
||||||
version: 0.2.33+76
|
version: 1.0.2+80
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
@ -18,7 +18,6 @@ dependencies:
|
|||||||
dio: ^4.0.4
|
dio: ^4.0.4
|
||||||
equatable: ^2.0.5
|
equatable: ^2.0.5
|
||||||
fast_gbk: ^1.0.0
|
fast_gbk: ^1.0.0
|
||||||
# feature_discovery: ^0.14.0
|
|
||||||
feature_discovery:
|
feature_discovery:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/livinglist/feature_discovery
|
url: https://github.com/livinglist/feature_discovery
|
||||||
@ -45,11 +44,11 @@ dependencies:
|
|||||||
hydrated_bloc: ^9.0.0-dev.3
|
hydrated_bloc: ^9.0.0-dev.3
|
||||||
intl: ^0.17.0
|
intl: ^0.17.0
|
||||||
logger: ^1.1.0
|
logger: ^1.1.0
|
||||||
|
package_info_plus: ^3.0.2
|
||||||
path: ^1.8.0
|
path: ^1.8.0
|
||||||
path_provider: ^2.0.8
|
path_provider: ^2.0.8
|
||||||
path_provider_android: ^2.0.8
|
path_provider_android: ^2.0.8
|
||||||
path_provider_ios: ^2.0.8
|
path_provider_ios: ^2.0.8
|
||||||
# pull_to_refresh: ^2.0.0
|
|
||||||
pull_to_refresh:
|
pull_to_refresh:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/livinglist/flutter_pulltorefresh
|
url: https://github.com/livinglist/flutter_pulltorefresh
|
||||||
|
1
submodules/flutter
Submodule
1
submodules/flutter
Submodule
Submodule submodules/flutter added at 135454af32
158
test/blocs/auth/auth_bloc_test.dart
Normal file
158
test/blocs/auth/auth_bloc_test.dart
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import 'package:bloc_test/bloc_test.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:hacki/blocs/blocs.dart';
|
||||||
|
import 'package:hacki/models/models.dart';
|
||||||
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
class MockAuthRepository extends Mock implements AuthRepository {}
|
||||||
|
|
||||||
|
class MockPreferenceRepository extends Mock implements PreferenceRepository {}
|
||||||
|
|
||||||
|
class MockStoriesRepository extends Mock implements StoriesRepository {}
|
||||||
|
|
||||||
|
class MockSembastRepository extends Mock implements SembastRepository {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final MockAuthRepository mockAuthRepository = MockAuthRepository();
|
||||||
|
final MockPreferenceRepository mockPreferenceRepository =
|
||||||
|
MockPreferenceRepository();
|
||||||
|
final MockStoriesRepository mockStoriesRepository = MockStoriesRepository();
|
||||||
|
final MockSembastRepository mockSembastRepository = MockSembastRepository();
|
||||||
|
|
||||||
|
const int created = 0, delay = 1, karma = 2;
|
||||||
|
const String about = 'about', id = 'id';
|
||||||
|
|
||||||
|
const User tUser = User(
|
||||||
|
about: about,
|
||||||
|
created: created,
|
||||||
|
delay: delay,
|
||||||
|
id: id,
|
||||||
|
karma: karma,
|
||||||
|
);
|
||||||
|
|
||||||
|
group(
|
||||||
|
'AuthBloc',
|
||||||
|
() {
|
||||||
|
setUp(() {
|
||||||
|
when(() => mockAuthRepository.loggedIn)
|
||||||
|
.thenAnswer((_) => Future<bool>.value(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'initial state is AuthState.init',
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
AuthBloc(
|
||||||
|
authRepository: mockAuthRepository,
|
||||||
|
preferenceRepository: mockPreferenceRepository,
|
||||||
|
storiesRepository: mockStoriesRepository,
|
||||||
|
sembastRepository: mockSembastRepository,
|
||||||
|
).state,
|
||||||
|
equals(const AuthState.init()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('AuthAppStarted', () {
|
||||||
|
const String username = 'username', password = 'password';
|
||||||
|
setUp(() {
|
||||||
|
when(() => mockAuthRepository.username)
|
||||||
|
.thenAnswer((_) => Future<String?>.value(username));
|
||||||
|
when(() => mockAuthRepository.password)
|
||||||
|
.thenAnswer((_) => Future<String>.value(password));
|
||||||
|
when(() => mockStoriesRepository.fetchUserBy(userId: username))
|
||||||
|
.thenAnswer((_) => Future<User>.value(tUser));
|
||||||
|
when(() => mockAuthRepository.loggedIn)
|
||||||
|
.thenAnswer((_) => Future<bool>.value(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
blocTest<AuthBloc, AuthState>(
|
||||||
|
'initialize',
|
||||||
|
build: () {
|
||||||
|
return AuthBloc(
|
||||||
|
authRepository: mockAuthRepository,
|
||||||
|
preferenceRepository: mockPreferenceRepository,
|
||||||
|
storiesRepository: mockStoriesRepository,
|
||||||
|
sembastRepository: mockSembastRepository,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
expect: () => <AuthState>[
|
||||||
|
const AuthState.init().copyWith(
|
||||||
|
status: AuthStatus.loaded,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verify: (_) {
|
||||||
|
verify(() => mockAuthRepository.loggedIn).called(2);
|
||||||
|
verifyNever(() => mockAuthRepository.username);
|
||||||
|
verifyNever(() => mockStoriesRepository.fetchUserBy(userId: username));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
blocTest<AuthBloc, AuthState>(
|
||||||
|
'sign in',
|
||||||
|
build: () {
|
||||||
|
when(
|
||||||
|
() => mockAuthRepository.login(
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
),
|
||||||
|
).thenAnswer((_) => Future<bool>.value(true));
|
||||||
|
return AuthBloc(
|
||||||
|
authRepository: mockAuthRepository,
|
||||||
|
preferenceRepository: mockPreferenceRepository,
|
||||||
|
storiesRepository: mockStoriesRepository,
|
||||||
|
sembastRepository: mockSembastRepository,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
act: (AuthBloc bloc) => bloc
|
||||||
|
..add(
|
||||||
|
AuthToggleAgreeToEULA(),
|
||||||
|
)
|
||||||
|
..add(
|
||||||
|
AuthLogin(
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expect: () => <AuthState>[
|
||||||
|
const AuthState(
|
||||||
|
user: User.empty(),
|
||||||
|
isLoggedIn: false,
|
||||||
|
status: AuthStatus.loaded,
|
||||||
|
agreedToEULA: false,
|
||||||
|
),
|
||||||
|
const AuthState(
|
||||||
|
user: User.empty(),
|
||||||
|
isLoggedIn: false,
|
||||||
|
status: AuthStatus.loaded,
|
||||||
|
agreedToEULA: true,
|
||||||
|
),
|
||||||
|
const AuthState(
|
||||||
|
user: User.empty(),
|
||||||
|
isLoggedIn: false,
|
||||||
|
status: AuthStatus.loading,
|
||||||
|
agreedToEULA: true,
|
||||||
|
),
|
||||||
|
const AuthState(
|
||||||
|
user: tUser,
|
||||||
|
isLoggedIn: true,
|
||||||
|
status: AuthStatus.loaded,
|
||||||
|
agreedToEULA: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
verify: (_) {
|
||||||
|
verify(
|
||||||
|
() => mockAuthRepository.login(
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
verify(() => mockStoriesRepository.fetchUserBy(userId: username))
|
||||||
|
.called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
@ -1,28 +0,0 @@
|
|||||||
import 'package:adaptive_theme/adaptive_theme.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:hacki/main.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
|
||||||
// Build our app and trigger a frame.
|
|
||||||
await tester.pumpWidget(
|
|
||||||
const HackiApp(
|
|
||||||
savedThemeMode: AdaptiveThemeMode.light,
|
|
||||||
trueDarkMode: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify that our counter starts at 0.
|
|
||||||
expect(find.text('0'), findsOneWidget);
|
|
||||||
expect(find.text('1'), findsNothing);
|
|
||||||
|
|
||||||
// Tap the '+' icon and trigger a frame.
|
|
||||||
await tester.tap(find.byIcon(Icons.add));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Verify that our counter has incremented.
|
|
||||||
expect(find.text('0'), findsNothing);
|
|
||||||
expect(find.text('1'), findsOneWidget);
|
|
||||||
});
|
|
||||||
}
|
|
Reference in New Issue
Block a user