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 |
5
.github/workflows/commit_check.yml
vendored
5
.github/workflows/commit_check.yml
vendored
@ -4,11 +4,13 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
- '!master'
|
||||
|
||||
jobs:
|
||||
releases:
|
||||
name: Check commit
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
FLUTTER_VERSION: "3.3.10"
|
||||
steps:
|
||||
@ -19,4 +21,5 @@ jobs:
|
||||
channel: 'stable'
|
||||
- run: flutter pub get
|
||||
- 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
|
||||
push:
|
||||
branches:
|
||||
- "!*"
|
||||
tags:
|
||||
- "v*"
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build_and_publish:
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
env:
|
||||
# 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 }}
|
||||
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
|
||||
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
|
@ -6,4 +6,8 @@ linter:
|
||||
library_private_types_in_public_api: false
|
||||
omit_local_variable_types: false
|
||||
one_member_abstracts: false
|
||||
always_specify_types: true
|
||||
always_specify_types: true
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- "submodules/**"
|
||||
|
@ -1,5 +1,82 @@
|
||||
import Flutter
|
||||
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 static func register(with registrar: FlutterPluginRegistrar) {
|
||||
@ -7,46 +84,49 @@ public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
|
||||
let instance = SwiftSyncedSharedPreferencesPlugin()
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
}
|
||||
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
case "setBool":
|
||||
if let params = call.arguments as? [String: Any] {
|
||||
let info: [String: Any] = ["result": result,
|
||||
"params": params]
|
||||
NotificationCenter.default.post(name: Notification.Name("setBool"), object: nil, userInfo: info)
|
||||
let val = params[valKey] as? Bool
|
||||
let key = params[keyKey] as? String
|
||||
|
||||
let res = SharedPrefsCore.shared.setBool(key: key, val: val)
|
||||
result(res)
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
case "getBool":
|
||||
if let params = call.arguments as? [String: Any] {
|
||||
let info: [String: Any] = ["result": result,
|
||||
"params": params]
|
||||
NotificationCenter.default.post(name: Notification.Name("getBool"), object: nil, userInfo: info)
|
||||
let key = params[keyKey] as? String
|
||||
let res = SharedPrefsCore.shared.getBool(key: key)
|
||||
result(res)
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
case "setStringList":
|
||||
if let params = call.arguments as? [String: Any] {
|
||||
let info: [String: Any] = ["result": result,
|
||||
"params": params]
|
||||
NotificationCenter.default.post(name: Notification.Name("setStringList"), object: nil, userInfo: info)
|
||||
let val = params[valKey] as? [String]
|
||||
let key = params[keyKey] as? String
|
||||
|
||||
let res = SharedPrefsCore.shared.setStringList(key: key, val: val)
|
||||
result(res)
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
case "getStringList":
|
||||
if let params = call.arguments as? [String: Any] {
|
||||
let info: [String: Any] = ["result": result,
|
||||
"params": params]
|
||||
NotificationCenter.default.post(name: Notification.Name("getStringList"), object: nil, userInfo: info)
|
||||
let key = params[keyKey] as? String
|
||||
let res = SharedPrefsCore.shared.getStringList(key: key)
|
||||
result(res)
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
case "clearAll":
|
||||
if let params = call.arguments as? [String: Any] {
|
||||
let info: [String: Any] = ["result": result,
|
||||
"params": params]
|
||||
NotificationCenter.default.post(name: Notification.Name("clearAll"), object: nil, userInfo: info)
|
||||
let res = SharedPrefsCore.shared.clearAll()
|
||||
result(res)
|
||||
}
|
||||
|
||||
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)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.636.0)
|
||||
aws-sdk-core (3.154.0)
|
||||
aws-partitions (1.680.0)
|
||||
aws-sdk-core (3.168.4)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.58.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-kms (1.61.0)
|
||||
aws-sdk-core (~> 3, >= 3.165.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.114.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-s3 (1.117.2)
|
||||
aws-sdk-core (~> 3, >= 3.165.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.5.1)
|
||||
aws-sigv4 (1.5.2)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
claide (1.1.0)
|
||||
@ -86,7 +86,7 @@ GEM
|
||||
escape (0.0.4)
|
||||
ethon (0.15.0)
|
||||
ffi (>= 1.15.0)
|
||||
excon (0.92.5)
|
||||
excon (0.95.0)
|
||||
faraday (1.10.2)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
@ -116,7 +116,7 @@ GEM
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.6)
|
||||
fastlane (2.210.1)
|
||||
fastlane (2.211.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@ -159,9 +159,9 @@ GEM
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.27.0)
|
||||
google-apis-core (>= 0.7.2, < 2.a)
|
||||
google-apis-core (0.9.0)
|
||||
google-apis-androidpublisher_v3 (0.32.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-core (0.9.2)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
@ -170,27 +170,27 @@ GEM
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.14.0)
|
||||
google-apis-core (>= 0.7.2, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.10.0)
|
||||
google-apis-core (>= 0.7, < 2.a)
|
||||
google-apis-storage_v1 (0.17.0)
|
||||
google-apis-core (>= 0.7, < 2.a)
|
||||
google-apis-iamcredentials_v1 (0.16.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.12.0)
|
||||
google-apis-core (>= 0.9.1, < 2.a)
|
||||
google-apis-storage_v1 (0.19.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.3.0)
|
||||
google-cloud-storage (1.42.0)
|
||||
google-cloud-storage (1.44.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
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)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.2.0)
|
||||
googleauth (1.3.0)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
@ -203,11 +203,11 @@ GEM
|
||||
httpclient (2.8.3)
|
||||
i18n (1.12.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jmespath (1.6.1)
|
||||
json (2.6.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.6.3)
|
||||
jwt (2.5.0)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.11.0)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.2)
|
||||
minitest (5.16.3)
|
||||
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.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
|
@ -22,6 +22,8 @@ PODS:
|
||||
- integration_test (0.0.1):
|
||||
- Flutter
|
||||
- OrderedSet (5.0.0)
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_ios (0.0.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift (5.0.0)
|
||||
@ -53,6 +55,7 @@ DEPENDENCIES:
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/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`)
|
||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
@ -85,6 +88,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_siri_suggestions/ios"
|
||||
integration_test:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_ios:
|
||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||
receive_sharing_intent:
|
||||
@ -116,6 +121,7 @@ SPEC CHECKSUMS:
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
|
||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
|
||||
@ -128,6 +134,6 @@ SPEC CHECKSUMS:
|
||||
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
|
||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||
|
||||
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
|
||||
PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
|
@ -21,7 +21,6 @@
|
||||
E530B1AD283B54DA004E8EB6 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E530B1AC283B54DA004E8EB6 /* ActionViewController.swift */; };
|
||||
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, ); }; };
|
||||
E54B4753282B3B8900579261 /* HackiCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E54B4752282B3B8900579261 /* HackiCore.swift */; };
|
||||
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E575B6F027EBC6DA002B1508 /* CloudKit.framework */; };
|
||||
FC507E94AA7767C155787DB3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFB5AA41D6C22D228077D166 /* Pods_Runner.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
@ -56,7 +55,7 @@
|
||||
};
|
||||
E51D52B8283B464E00FC8DD8 /* Embed App Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
buildActionMask = 8;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
@ -64,7 +63,7 @@
|
||||
E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */,
|
||||
);
|
||||
name = "Embed App Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
runOnlyForDeploymentPostprocessing = 1;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
@ -97,7 +96,6 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -177,7 +175,6 @@
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
E54B4752282B3B8900579261 /* HackiCore.swift */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
@ -437,7 +434,6 @@
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
E54B4753282B3B8900579261 /* HackiCore.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -569,17 +565,19 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.33;
|
||||
MARKETING_VERSION = 0.3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -709,17 +707,19 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.33;
|
||||
MARKETING_VERSION = 0.3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -743,17 +743,19 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.33;
|
||||
MARKETING_VERSION = 0.3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -778,7 +780,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@ -821,7 +823,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@ -861,7 +863,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@ -903,7 +905,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@ -948,7 +950,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@ -990,7 +992,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
|
@ -16,8 +16,6 @@ import flutter_local_notifications
|
||||
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.delegate = self
|
||||
|
||||
HackiCore.start()
|
||||
|
||||
WorkmanagerPlugin.register(with: self.registrar(forPlugin: "be.tramckrijte.workmanager.WorkmanagerPlugin")!)
|
||||
|
||||
|
@ -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>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@ -38,7 +38,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>https</string>
|
||||
@ -72,5 +72,7 @@
|
||||
<array>
|
||||
<string>applinks:example.com</string>
|
||||
</array>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -31,10 +31,6 @@ platform :ios do
|
||||
|
||||
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)
|
||||
sync_code_signing(
|
||||
type: "appstore",
|
||||
@ -42,15 +38,6 @@ platform :ios do
|
||||
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
|
||||
app_store_connect_api_key(
|
||||
key_id: APPSTORECONNECT_KEY_ID,
|
||||
@ -59,21 +46,18 @@ platform :ios do
|
||||
latest_testflight_build_number
|
||||
# Figure out the build number (and optionally build name)
|
||||
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`)
|
||||
sh(
|
||||
"flutter", "build", "ios", "--config-only",
|
||||
"--release", "--no-pub", "--no-codesign",
|
||||
"--build-number", new_build_number.to_s,
|
||||
*extra_config_args
|
||||
"--build-number", new_build_number.to_s
|
||||
)
|
||||
|
||||
version = get_version_number(xcodeproj: "Runner.xcodeproj", target: 'Runner')
|
||||
|
||||
increment_version_number(
|
||||
version_number: options[:build_name].delete_prefix('v').delete_suffix('-rc')
|
||||
version_number: version
|
||||
)
|
||||
|
||||
increment_build_number({
|
||||
@ -93,4 +77,4 @@ latest_testflight_build_number
|
||||
skip_waiting_for_build_processing: true,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -20,7 +20,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||
_sembastRepository =
|
||||
sembastRepository ?? locator.get<SembastRepository>(),
|
||||
super(AuthState.init()) {
|
||||
super(const AuthState.init()) {
|
||||
on<AuthInitialize>(onInitialize);
|
||||
on<AuthLogin>(onLogin);
|
||||
on<AuthLogout>(onLogout);
|
||||
@ -101,7 +101,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
Future<void> onLogout(AuthLogout event, Emitter<AuthState> emit) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
user: User.empty(),
|
||||
user: const User.empty(),
|
||||
isLoggedIn: false,
|
||||
agreedToEULA: false,
|
||||
),
|
||||
|
@ -14,8 +14,8 @@ class AuthState extends Equatable {
|
||||
required this.agreedToEULA,
|
||||
});
|
||||
|
||||
AuthState.init()
|
||||
: user = User.empty(),
|
||||
const AuthState.init()
|
||||
: user = const User.empty(),
|
||||
isLoggedIn = false,
|
||||
status = AuthStatus.loaded,
|
||||
agreedToEULA = false;
|
||||
|
@ -10,6 +10,8 @@ abstract class Constants {
|
||||
static const String googlePlayLink =
|
||||
'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 guidelineLink =
|
||||
'https://news.ycombinator.com/newsguidelines.html';
|
||||
|
||||
static const String _imagePath = 'assets/images';
|
||||
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:equatable/equatable.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
|
||||
part 'collapse_state.dart';
|
||||
@ -10,13 +11,16 @@ part 'collapse_state.dart';
|
||||
class CollapseCubit extends Cubit<CollapseState> {
|
||||
CollapseCubit({
|
||||
required int commentId,
|
||||
required CommentsCubit? commentsCubit,
|
||||
CollapseCache? collapseCache,
|
||||
}) : _commentId = commentId,
|
||||
_collapseCache = collapseCache ?? locator.get<CollapseCache>(),
|
||||
_commentsCubit = commentsCubit,
|
||||
super(const CollapseState.init());
|
||||
|
||||
final int _commentId;
|
||||
final CollapseCache _collapseCache;
|
||||
final CommentsCubit? _commentsCubit;
|
||||
late final StreamSubscription<Map<int, Set<int>>> _streamSubscription;
|
||||
|
||||
void init() {
|
||||
@ -43,12 +47,21 @@ class CollapseCubit extends Cubit<CollapseState> {
|
||||
),
|
||||
);
|
||||
} 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(
|
||||
state.copyWith(
|
||||
collapsed: true,
|
||||
collapsedCount: state.collapsed ? 0 : count,
|
||||
collapsedCount: state.collapsed ? 0 : collapsedCommentIds.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
state.copyWith(
|
||||
comments: targetParents,
|
||||
onlyShowTargetComment: true,
|
||||
status: CommentsStatus.loaded,
|
||||
status: CommentsStatus.allLoaded,
|
||||
),
|
||||
);
|
||||
|
||||
@ -141,21 +141,21 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
if (state.offlineReading) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.loaded,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.loading,
|
||||
),
|
||||
);
|
||||
|
||||
if (state.offlineReading) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.allLoaded,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_collapseCache.resetCollapsedComments();
|
||||
|
||||
await _streamSubscription?.cancel();
|
||||
@ -195,7 +195,6 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
item: updatedItem,
|
||||
status: CommentsStatus.loaded,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -213,6 +212,8 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
|
||||
/// [comment] is only used for lazy fetching.
|
||||
void loadMore({Comment? comment}) {
|
||||
if (comment == null && state.status == CommentsStatus.loading) return;
|
||||
|
||||
switch (state.fetchMode) {
|
||||
case FetchMode.lazy:
|
||||
if (comment == null) return;
|
||||
@ -368,12 +369,17 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
|
||||
if (!isHidden) {
|
||||
_streamSubscription?.pause();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.loaded,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentPage: state.currentPage + 1,
|
||||
status: CommentsStatus.loaded,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -64,6 +64,8 @@ class PreferenceState extends Equatable {
|
||||
|
||||
bool get showMetadata => _isOn<MetadataModePreference>();
|
||||
|
||||
bool get showUrl => _isOn<StoryUrlModePreference>();
|
||||
|
||||
bool get tapAnywhereToCollapse => _isOn<CollapseModePreference>();
|
||||
|
||||
FetchMode get fetchMode => FetchMode.values
|
||||
|
@ -10,7 +10,7 @@ class UserCubit extends Cubit<UserState> {
|
||||
UserCubit({StoriesRepository? storiesRepository})
|
||||
: _storiesRepository =
|
||||
storiesRepository ?? locator.get<StoriesRepository>(),
|
||||
super(UserState.init());
|
||||
super(const UserState.init());
|
||||
|
||||
final StoriesRepository _storiesRepository;
|
||||
|
||||
|
@ -13,8 +13,8 @@ class UserState extends Equatable {
|
||||
required this.status,
|
||||
});
|
||||
|
||||
UserState.init()
|
||||
: user = User.empty(),
|
||||
const UserState.init()
|
||||
: user = const User.empty(),
|
||||
status = UserStatus.initial;
|
||||
|
||||
final User user;
|
||||
|
@ -1,7 +1,9 @@
|
||||
import 'package:hacki/config/constants.dart';
|
||||
|
||||
extension StringExtension on String {
|
||||
int? get itemId {
|
||||
final RegExp regex = RegExp(r'\d+$');
|
||||
final RegExp exception = RegExp(r'\)|].*$');
|
||||
final RegExp regex = RegExp(RegExpConstants.number);
|
||||
final RegExp exception = RegExp(RegExpConstants.linkSuffix);
|
||||
final String match = regex.stringMatch(replaceAll(exception, '')) ?? '';
|
||||
return int.tryParse(match);
|
||||
}
|
||||
|
@ -90,9 +90,9 @@ Future<void> main({bool testing = false}) async {
|
||||
} else if (Platform.isAndroid) {
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
systemNavigationBarColor: Colors.transparent,
|
||||
systemNavigationBarDividerColor: Colors.transparent,
|
||||
statusBarColor: Palette.transparent,
|
||||
systemNavigationBarColor: Palette.transparent,
|
||||
systemNavigationBarDividerColor: Palette.transparent,
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
enum FontSize {
|
||||
regular('Regular', TextDimens.pt15),
|
||||
large('Large', TextDimens.pt16),
|
||||
small('Small', TextDimens.pt15),
|
||||
regular('Regular', TextDimens.pt16),
|
||||
large('Large', TextDimens.pt17),
|
||||
xlarge('XLarge', TextDimens.pt18);
|
||||
|
||||
const FontSize(this.description, this.fontSize);
|
||||
|
@ -18,10 +18,11 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
|
||||
CommentsOrderPreference(),
|
||||
FontSizePreference(),
|
||||
// order here reflects the order on settings screen.
|
||||
const NotificationModePreference(),
|
||||
const CollapseModePreference(),
|
||||
const DisplayModePreference(),
|
||||
const MetadataModePreference(),
|
||||
const StoryUrlModePreference(),
|
||||
const NotificationModePreference(),
|
||||
const CollapseModePreference(),
|
||||
NavigationModePreference(),
|
||||
const ReaderModePreference(),
|
||||
const MarkReadStoriesModePreference(),
|
||||
@ -50,6 +51,7 @@ const bool _trueDarkModeDefaultValue = false;
|
||||
const bool _readerModeDefaultValue = true;
|
||||
const bool _markReadStoriesModeDefaultValue = true;
|
||||
const bool _metadataModeDefaultValue = true;
|
||||
const bool _storyUrlModeDefaultValue = true;
|
||||
const bool _collapseModeDefaultValue = false;
|
||||
final int _fetchModeDefaultValue = FetchMode.eager.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.''';
|
||||
}
|
||||
|
||||
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
|
||||
/// navigated to web view first. Defaults to false.
|
||||
class NavigationModePreference extends BooleanPreference {
|
||||
@ -155,7 +176,7 @@ class NavigationModePreference extends BooleanPreference {
|
||||
String get title => 'Show Web Page First';
|
||||
|
||||
@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 {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/models/item.dart';
|
||||
|
||||
enum StoryType {
|
||||
@ -67,6 +68,24 @@ class Story extends Item {
|
||||
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)
|
||||
: super(
|
||||
descendants: json['descendants'] as int? ?? 0,
|
||||
@ -91,6 +110,12 @@ class Story extends Item {
|
||||
String get simpleMetadata =>
|
||||
'''$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
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
|
@ -1,7 +1,8 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class User {
|
||||
User({
|
||||
class User extends Equatable {
|
||||
const User({
|
||||
required this.about,
|
||||
required this.created,
|
||||
required this.delay,
|
||||
@ -9,7 +10,7 @@ class User {
|
||||
required this.karma,
|
||||
});
|
||||
|
||||
User.empty()
|
||||
const User.empty()
|
||||
: about = '',
|
||||
created = 0,
|
||||
delay = 0,
|
||||
@ -39,4 +40,13 @@ class User {
|
||||
String toString() {
|
||||
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),
|
||||
showWebPreview: preferenceState.showComplexStoryTile,
|
||||
showMetadata: preferenceState.showMetadata,
|
||||
showUrl: preferenceState.showUrl,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -255,145 +255,151 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
},
|
||||
),
|
||||
],
|
||||
child: BlocConsumer<CommentsCubit, CommentsState>(
|
||||
child: BlocListener<CommentsCubit, CommentsState>(
|
||||
listenWhen: (CommentsState previous, CommentsState current) =>
|
||||
previous.status != current.status,
|
||||
listener: (BuildContext context, CommentsState state) {
|
||||
if (state.status == CommentsStatus.loaded) {
|
||||
if (state.status != CommentsStatus.loading) {
|
||||
refreshController
|
||||
..refreshCompleted()
|
||||
..loadComplete();
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, CommentsState state) {
|
||||
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) {
|
||||
return previous.replyingTo != current.replyingTo ||
|
||||
previous.itemBeingEdited != current.itemBeingEdited ||
|
||||
commentEditingController.text != current.text;
|
||||
},
|
||||
listener: (BuildContext context, EditState editState) {
|
||||
if (editState.replyingTo != null ||
|
||||
editState.itemBeingEdited != null) {
|
||||
if (editState.text == null) {
|
||||
commentEditingController.clear();
|
||||
} else {
|
||||
final String text = editState.text!;
|
||||
commentEditingController
|
||||
..text = text
|
||||
..selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: text.length),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
child: BlocListener<EditCubit, EditState>(
|
||||
listenWhen: (EditState previous, EditState current) {
|
||||
return previous.replyingTo != current.replyingTo ||
|
||||
previous.itemBeingEdited != current.itemBeingEdited ||
|
||||
commentEditingController.text != current.text;
|
||||
},
|
||||
listener: (BuildContext context, EditState editState) {
|
||||
if (editState.replyingTo != null ||
|
||||
editState.itemBeingEdited != null) {
|
||||
if (editState.text == null) {
|
||||
commentEditingController.clear();
|
||||
} else {
|
||||
final String text = editState.text!;
|
||||
commentEditingController
|
||||
..text = text
|
||||
..selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: text.length),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: widget.splitViewEnabled
|
||||
? Material(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Positioned.fill(
|
||||
child: mainView,
|
||||
} else {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
},
|
||||
child: widget.splitViewEnabled
|
||||
? Material(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Positioned.fill(
|
||||
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>(
|
||||
buildWhen: (
|
||||
SplitViewState previous,
|
||||
SplitViewState current,
|
||||
) =>
|
||||
previous.expanded != current.expanded,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
SplitViewState state,
|
||||
) {
|
||||
return Positioned(
|
||||
top: Dimens.zero,
|
||||
left: Dimens.zero,
|
||||
right: Dimens.zero,
|
||||
child: CustomAppBar(
|
||||
backgroundColor: Theme.of(context)
|
||||
.canvasColor
|
||||
.withOpacity(0.6),
|
||||
item: widget.item,
|
||||
scrollController: scrollController,
|
||||
onBackgroundTap:
|
||||
onFeatureDiscoveryDismissed,
|
||||
onDismiss: onFeatureDiscoveryDismissed,
|
||||
splitViewEnabled: state.enabled,
|
||||
expanded: state.expanded,
|
||||
onZoomTap:
|
||||
context.read<SplitViewCubit>().zoom,
|
||||
onFontSizeTap: onFontSizeTapped,
|
||||
fontSizeIconButtonKey:
|
||||
fontSizeIconButtonKey,
|
||||
),
|
||||
);
|
||||
),
|
||||
BlocBuilder<SplitViewCubit, SplitViewState>(
|
||||
buildWhen: (
|
||||
SplitViewState previous,
|
||||
SplitViewState current,
|
||||
) =>
|
||||
previous.expanded != current.expanded,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
SplitViewState state,
|
||||
) {
|
||||
return Positioned(
|
||||
top: Dimens.zero,
|
||||
left: Dimens.zero,
|
||||
right: Dimens.zero,
|
||||
child: CustomAppBar(
|
||||
backgroundColor: Theme.of(context)
|
||||
.canvasColor
|
||||
.withOpacity(0.6),
|
||||
item: widget.item,
|
||||
scrollController: scrollController,
|
||||
onBackgroundTap: onFeatureDiscoveryDismissed,
|
||||
onDismiss: onFeatureDiscoveryDismissed,
|
||||
splitViewEnabled: state.enabled,
|
||||
expanded: state.expanded,
|
||||
onZoomTap:
|
||||
context.read<SplitViewCubit>().zoom,
|
||||
onFontSizeTap: onFontSizeTapped,
|
||||
fontSizeIconButtonKey: fontSizeIconButtonKey,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
bottom: Dimens.zero,
|
||||
left: Dimens.zero,
|
||||
right: Dimens.zero,
|
||||
child: ReplyBox(
|
||||
splitViewEnabled: true,
|
||||
focusNode: focusNode,
|
||||
textEditingController: commentEditingController,
|
||||
onSendTapped: onSendTapped,
|
||||
onCloseTapped: () {
|
||||
context.read<EditCubit>().onReplyBoxClosed();
|
||||
commentEditingController.clear();
|
||||
focusNode.unfocus();
|
||||
},
|
||||
onChanged:
|
||||
context.read<EditCubit>().onTextChanged,
|
||||
),
|
||||
Positioned(
|
||||
bottom: Dimens.zero,
|
||||
left: Dimens.zero,
|
||||
right: Dimens.zero,
|
||||
child: ReplyBox(
|
||||
splitViewEnabled: true,
|
||||
focusNode: focusNode,
|
||||
textEditingController: commentEditingController,
|
||||
onSendTapped: onSendTapped,
|
||||
onCloseTapped: () {
|
||||
context.read<EditCubit>().onReplyBoxClosed();
|
||||
commentEditingController.clear();
|
||||
focusNode.unfocus();
|
||||
},
|
||||
onChanged:
|
||||
context.read<EditCubit>().onTextChanged,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: CustomAppBar(
|
||||
backgroundColor:
|
||||
Theme.of(context).canvasColor.withOpacity(0.6),
|
||||
item: widget.item,
|
||||
scrollController: scrollController,
|
||||
onBackgroundTap: onFeatureDiscoveryDismissed,
|
||||
onDismiss: onFeatureDiscoveryDismissed,
|
||||
onFontSizeTap: onFontSizeTapped,
|
||||
fontSizeIconButtonKey: fontSizeIconButtonKey,
|
||||
),
|
||||
body: mainView,
|
||||
bottomSheet: ReplyBox(
|
||||
focusNode: focusNode,
|
||||
textEditingController: commentEditingController,
|
||||
onSendTapped: onSendTapped,
|
||||
onCloseTapped: () {
|
||||
context.read<EditCubit>().onReplyBoxClosed();
|
||||
commentEditingController.clear();
|
||||
focusNode.unfocus();
|
||||
},
|
||||
onChanged: context.read<EditCubit>().onTextChanged,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: CustomAppBar(
|
||||
backgroundColor:
|
||||
Theme.of(context).canvasColor.withOpacity(0.6),
|
||||
item: widget.item,
|
||||
scrollController: scrollController,
|
||||
onBackgroundTap: onFeatureDiscoveryDismissed,
|
||||
onDismiss: onFeatureDiscoveryDismissed,
|
||||
onFontSizeTap: onFontSizeTapped,
|
||||
fontSizeIconButtonKey: fontSizeIconButtonKey,
|
||||
),
|
||||
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(
|
||||
focusNode: focusNode,
|
||||
textEditingController: commentEditingController,
|
||||
onSendTapped: onSendTapped,
|
||||
onCloseTapped: () {
|
||||
context.read<EditCubit>().onReplyBoxClosed();
|
||||
commentEditingController.clear();
|
||||
focusNode.unfocus();
|
||||
},
|
||||
onChanged: context.read<EditCubit>().onTextChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -442,7 +448,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
fontSize: fontSize.fontSize,
|
||||
color:
|
||||
context.read<PreferenceCubit>().state.fontSize == fontSize
|
||||
? Colors.deepOrange
|
||||
? Palette.deepOrange
|
||||
: null,
|
||||
),
|
||||
),
|
||||
|
@ -22,7 +22,6 @@ class MainView extends StatelessWidget {
|
||||
required this.refreshController,
|
||||
required this.commentEditingController,
|
||||
required this.authState,
|
||||
required this.state,
|
||||
required this.focusNode,
|
||||
required this.topPadding,
|
||||
required this.splitViewEnabled,
|
||||
@ -36,7 +35,6 @@ class MainView extends StatelessWidget {
|
||||
final RefreshController refreshController;
|
||||
final TextEditingController commentEditingController;
|
||||
final AuthState authState;
|
||||
final CommentsState state;
|
||||
final FocusNode focusNode;
|
||||
final double topPadding;
|
||||
final bool splitViewEnabled;
|
||||
@ -47,130 +45,274 @@ class MainView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SmartRefresher(
|
||||
scrollController: scrollController,
|
||||
enablePullUp: !state.onlyShowTargetComment,
|
||||
enablePullDown: !state.onlyShowTargetComment,
|
||||
header: WaterDropMaterialHeader(
|
||||
backgroundColor: Palette.orange,
|
||||
offset: topPadding,
|
||||
),
|
||||
footer: CustomFooter(
|
||||
loadStyle: LoadStyle.ShowWhenLoading,
|
||||
builder: (BuildContext context, LoadStatus? mode) {
|
||||
const double height = 55;
|
||||
late final Widget body;
|
||||
|
||||
if (mode == LoadStatus.idle) {
|
||||
body = const Text('');
|
||||
} else if (mode == LoadStatus.loading) {
|
||||
body = const Text('');
|
||||
} else if (mode == LoadStatus.failed) {
|
||||
body = const Text(
|
||||
'',
|
||||
);
|
||||
} else if (mode == LoadStatus.canLoading) {
|
||||
body = const Text(
|
||||
'',
|
||||
);
|
||||
} else {
|
||||
body = const Text('');
|
||||
}
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: Center(child: body),
|
||||
);
|
||||
},
|
||||
),
|
||||
controller: refreshController,
|
||||
onRefresh: () {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
if (context.read<StoriesBloc>().state.offlineReading) {
|
||||
refreshController.refreshCompleted();
|
||||
} else {
|
||||
context.read<CommentsCubit>().refresh();
|
||||
|
||||
if (state.item.isPoll) {
|
||||
context.read<PollCubit>().refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoading: () {
|
||||
if (state.fetchMode == FetchMode.eager) {
|
||||
context.read<CommentsCubit>().loadMore();
|
||||
} else {
|
||||
refreshController.loadComplete();
|
||||
}
|
||||
},
|
||||
child: ListView.builder(
|
||||
primary: false,
|
||||
itemCount: state.comments.length + 2,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (index == 0) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: topPadding,
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
Positioned.fill(
|
||||
child: BlocBuilder<CommentsCubit, CommentsState>(
|
||||
builder: (BuildContext context, CommentsState state) {
|
||||
return SmartRefresher(
|
||||
scrollController: scrollController,
|
||||
enablePullUp: !state.onlyShowTargetComment,
|
||||
enablePullDown: !state.onlyShowTargetComment,
|
||||
header: WaterDropMaterialHeader(
|
||||
backgroundColor: Palette.orange,
|
||||
offset: topPadding,
|
||||
),
|
||||
if (!splitViewEnabled)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: Dimens.pt6),
|
||||
child: OfflineBanner(),
|
||||
),
|
||||
Slidable(
|
||||
startActionPane: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
HapticFeedback.lightImpact();
|
||||
footer: CustomFooter(
|
||||
loadStyle: LoadStyle.ShowWhenLoading,
|
||||
builder: (BuildContext context, LoadStatus? mode) {
|
||||
const double height = 55;
|
||||
late final Widget body;
|
||||
|
||||
if (state.item.id !=
|
||||
if (mode == LoadStatus.idle) {
|
||||
body = const Text('');
|
||||
} else if (mode == LoadStatus.loading) {
|
||||
body = const Text('');
|
||||
} else if (mode == LoadStatus.failed) {
|
||||
body = const Text(
|
||||
'',
|
||||
);
|
||||
} else if (mode == LoadStatus.canLoading) {
|
||||
body = const Text(
|
||||
'',
|
||||
);
|
||||
} else {
|
||||
body = const Text('');
|
||||
}
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: Center(child: body),
|
||||
);
|
||||
},
|
||||
),
|
||||
controller: refreshController,
|
||||
onRefresh: () {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
if (context.read<StoriesBloc>().state.offlineReading) {
|
||||
refreshController.refreshCompleted();
|
||||
} else {
|
||||
context.read<CommentsCubit>().refresh();
|
||||
|
||||
if (state.item.isPoll) {
|
||||
context.read<PollCubit>().refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoading: () {
|
||||
if (state.fetchMode == FetchMode.eager) {
|
||||
context.read<CommentsCubit>().loadMore();
|
||||
} else {
|
||||
refreshController.loadComplete();
|
||||
}
|
||||
},
|
||||
child: ListView.builder(
|
||||
primary: false,
|
||||
itemCount: state.comments.length + 2,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
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(state.item);
|
||||
|
||||
context.read<EditCubit>().onReplyTapped(cmt);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.message,
|
||||
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,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (BuildContext context) =>
|
||||
onMoreTapped(state.item, context.rect),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.more_horiz,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
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(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: topPadding,
|
||||
),
|
||||
if (!splitViewEnabled)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: Dimens.pt6),
|
||||
child: OfflineBanner(),
|
||||
),
|
||||
Slidable(
|
||||
startActionPane: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
if (state.item.id !=
|
||||
context.read<EditCubit>().state.replyingTo?.id) {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
context.read<EditCubit>().onReplyTapped(state.item);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.message,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (BuildContext context) =>
|
||||
onMoreTapped(state.item, context.rect),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.more_horiz,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
state.item.by,
|
||||
style: const TextStyle(
|
||||
color: Palette.orange,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
state.item.postedDate,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
buildWhen: (
|
||||
PreferenceState previous,
|
||||
PreferenceState current,
|
||||
) =>
|
||||
previous.fontSize != current.fontSize,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
PreferenceState prefState,
|
||||
) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
state.item.by,
|
||||
style: const TextStyle(
|
||||
color: Palette.orange,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
state.item.postedDate,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (state.item is Story)
|
||||
InkWell(
|
||||
onTap: () => LinkUtil.launch(
|
||||
@ -189,14 +331,48 @@ class MainView extends StatelessWidget {
|
||||
bottom: Dimens.pt12,
|
||||
top: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
state.item.title,
|
||||
child: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: state.item.url.isNotEmpty
|
||||
? Palette.orange
|
||||
: null,
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
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
|
||||
? Palette.orange
|
||||
: 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -206,239 +382,169 @@ class MainView extends StatelessWidget {
|
||||
height: Dimens.pt6,
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
buildWhen: (
|
||||
PreferenceState previous,
|
||||
PreferenceState current,
|
||||
) =>
|
||||
previous.fontSize != current.fontSize,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
PreferenceState prefState,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt10,
|
||||
),
|
||||
child: SelectableLinkify(
|
||||
text: state.item.text,
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
MediaQuery.of(context).textScaleFactor *
|
||||
context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.fontSize
|
||||
.fontSize,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize:
|
||||
MediaQuery.of(context).textScaleFactor *
|
||||
context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.fontSize
|
||||
.fontSize,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) {
|
||||
if (link.url.isStoryLink) {
|
||||
onStoryLinkTapped(link.url);
|
||||
} else {
|
||||
LinkUtil.launch(link.url);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (state.item.isPoll)
|
||||
BlocProvider<PollCubit>(
|
||||
create: (BuildContext context) =>
|
||||
PollCubit(story: state.item as Story)..init(),
|
||||
child: PollView(
|
||||
onLoginTapped: onLoginTapped,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt10,
|
||||
),
|
||||
child: SelectableLinkify(
|
||||
text: state.item.text,
|
||||
style: TextStyle(
|
||||
fontSize: MediaQuery.of(context).textScaleFactor *
|
||||
context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.fontSize
|
||||
.fontSize,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: MediaQuery.of(context).textScaleFactor *
|
||||
context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.fontSize
|
||||
.fontSize,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) {
|
||||
if (link.url.isStoryLink) {
|
||||
onStoryLinkTapped(link.url);
|
||||
} else {
|
||||
LinkUtil.launch(link.url);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
if (state.item.isPoll)
|
||||
BlocProvider<PollCubit>(
|
||||
create: (BuildContext context) =>
|
||||
PollCubit(story: state.item as Story)..init(),
|
||||
child: PollView(
|
||||
onLoginTapped: onLoginTapped,
|
||||
),
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
],
|
||||
),
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
if (state.onlyShowTargetComment) ...<Widget>[
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () =>
|
||||
context.read<CommentsCubit>().loadAll(state.item as Story),
|
||||
child: const Text('View all comments'),
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
] else ...<Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
if (state.item is Story) ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
if (state.onlyShowTargetComment) ...<Widget>[
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () => context
|
||||
.read<CommentsCubit>()
|
||||
.loadAll(state.item as Story),
|
||||
child: const Text('View all comments'),
|
||||
),
|
||||
Text(
|
||||
'''${state.item.score} karma, ${state.item.descendants} comment${state.item.descendants > 1 ? 's' : ''}''',
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
] else ...<Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
if (state.item is Story) ...<Widget>[
|
||||
const SizedBox(
|
||||
),
|
||||
] else ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: context.read<CommentsCubit>().loadParentThread,
|
||||
child: state.fetchParentStatus == CommentsStatus.loading
|
||||
? const SizedBox(
|
||||
height: Dimens.pt12,
|
||||
width: Dimens.pt12,
|
||||
child: CustomCircularProgressIndicator(
|
||||
strokeWidth: Dimens.pt2,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'View parent thread',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'''${state.item.score} karma, ${state.item.descendants} comment${state.item.descendants > 1 ? 's' : ''}''',
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
if (!state.offlineReading)
|
||||
DropdownButton<FetchMode>(
|
||||
value: state.fetchMode,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: FetchMode.values
|
||||
.map(
|
||||
(FetchMode val) => DropdownMenuItem<FetchMode>(
|
||||
value: val,
|
||||
child: Text(
|
||||
val.description,
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: context.read<CommentsCubit>().onFetchModeChanged,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt6,
|
||||
),
|
||||
DropdownButton<CommentsOrder>(
|
||||
value: state.order,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: CommentsOrder.values
|
||||
.map(
|
||||
(CommentsOrder val) => DropdownMenuItem<CommentsOrder>(
|
||||
value: val,
|
||||
child: Text(
|
||||
val.description,
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
] else ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
TextButton(
|
||||
onPressed:
|
||||
context.read<CommentsCubit>().loadParentThread,
|
||||
child:
|
||||
state.fetchParentStatus == CommentsStatus.loading
|
||||
? const SizedBox(
|
||||
height: Dimens.pt12,
|
||||
width: Dimens.pt12,
|
||||
child: CustomCircularProgressIndicator(
|
||||
strokeWidth: Dimens.pt2,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'View parent thread',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
if (!state.offlineReading)
|
||||
DropdownButton<FetchMode>(
|
||||
value: state.fetchMode,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: FetchMode.values
|
||||
.map(
|
||||
(FetchMode val) => DropdownMenuItem<FetchMode>(
|
||||
value: val,
|
||||
child: Text(
|
||||
val.description,
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged:
|
||||
context.read<CommentsCubit>().onFetchModeChanged,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt6,
|
||||
),
|
||||
DropdownButton<CommentsOrder>(
|
||||
value: state.order,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: CommentsOrder.values
|
||||
.map(
|
||||
(CommentsOrder val) =>
|
||||
DropdownMenuItem<CommentsOrder>(
|
||||
value: val,
|
||||
child: Text(
|
||||
val.description,
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: context.read<CommentsCubit>().onOrderChanged,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
],
|
||||
if (state.comments.isEmpty &&
|
||||
state.status == CommentsStatus.allLoaded) ...<Widget>[
|
||||
const SizedBox(
|
||||
height: 240,
|
||||
),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Nothing yet',
|
||||
style: TextStyle(color: Palette.grey),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
} 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,
|
||||
)
|
||||
.toList(),
|
||||
onChanged: context.read<CommentsCubit>().onOrderChanged,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
],
|
||||
if (state.comments.isEmpty &&
|
||||
state.status == CommentsStatus.allLoaded) ...<Widget>[
|
||||
const SizedBox(
|
||||
height: 240,
|
||||
),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Nothing yet',
|
||||
style: TextStyle(color: Palette.grey),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.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:tuple/tuple.dart';
|
||||
|
||||
@ -122,6 +123,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
return ItemsListView<Item>(
|
||||
showWebPreview: false,
|
||||
showMetadata: false,
|
||||
showUrl: false,
|
||||
useConsistentFontSize: true,
|
||||
refreshController: refreshControllerHistory,
|
||||
items: historyState.submittedItems
|
||||
@ -174,6 +176,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
showWebPreview:
|
||||
preferenceState.showComplexStoryTile,
|
||||
showMetadata: preferenceState.showMetadata,
|
||||
showUrl: preferenceState.showUrl,
|
||||
useCommentTile: true,
|
||||
refreshController: refreshControllerFav,
|
||||
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
|
||||
in preferenceState.preferences
|
||||
.whereType<BooleanPreference>()
|
||||
@ -422,93 +436,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
title: const Text('About'),
|
||||
subtitle:
|
||||
const Text('nothing interesting here.'),
|
||||
onTap: () {
|
||||
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'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
onTap: showAboutHackiDialog,
|
||||
),
|
||||
const SizedBox(
|
||||
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}) {
|
||||
throttle.run(() {
|
||||
locator
|
||||
|
@ -166,6 +166,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
showWebPreview:
|
||||
prefState.showComplexStoryTile,
|
||||
showMetadata: prefState.showMetadata,
|
||||
showUrl: prefState.showUrl,
|
||||
story: e,
|
||||
onTap: () => goToItemScreen(
|
||||
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/extensions/extensions.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/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
@ -46,344 +47,329 @@ class CommentTile extends StatelessWidget {
|
||||
lazy: false,
|
||||
create: (_) => CollapseCubit(
|
||||
commentId: comment.id,
|
||||
commentsCubit: context.tryRead<CommentsCubit>(),
|
||||
collapseCache: context.tryRead<CollapseCache>() ?? CollapseCache(),
|
||||
)..init(),
|
||||
child: BlocBuilder<CollapseCubit, CollapseState>(
|
||||
builder: (BuildContext context, CollapseState state) {
|
||||
child: BlocBuilder3<CollapseCubit, CollapseState, PreferenceCubit,
|
||||
PreferenceState, BlocklistCubit, BlocklistState>(
|
||||
builder: (
|
||||
BuildContext context,
|
||||
CollapseState state,
|
||||
PreferenceState prefState,
|
||||
BlocklistState blocklistState,
|
||||
) {
|
||||
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);
|
||||
final Color color = _getColor(level);
|
||||
const Color orange = Color.fromRGBO(255, 152, 0, 1);
|
||||
final Color color = _getColor(level);
|
||||
|
||||
final Padding child = Padding(
|
||||
padding: EdgeInsets.zero,
|
||||
final Padding child = Padding(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Slidable(
|
||||
startActionPane: actionable
|
||||
? ActionPane(
|
||||
motion: const StretchMotion(),
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) => onReplyTapped?.call(comment),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.message,
|
||||
),
|
||||
if (context.read<AuthBloc>().state.user.id ==
|
||||
comment.by)
|
||||
SlidableAction(
|
||||
onPressed: (_) => onEditTapped?.call(comment),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.edit,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (BuildContext context) =>
|
||||
onMoreTapped?.call(
|
||||
comment,
|
||||
context.rect,
|
||||
),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.more_horiz,
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
endActionPane: actionable
|
||||
? ActionPane(
|
||||
motion: const StretchMotion(),
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) =>
|
||||
onRightMoreTapped?.call(comment),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.av_timer,
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (actionable) {
|
||||
HapticFeedback.selectionClick();
|
||||
context.read<CollapseCubit>().collapse();
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Slidable(
|
||||
startActionPane: actionable
|
||||
? ActionPane(
|
||||
motion: const StretchMotion(),
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) =>
|
||||
onReplyTapped?.call(comment),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.message,
|
||||
),
|
||||
if (context
|
||||
.read<AuthBloc>()
|
||||
.state
|
||||
.user
|
||||
.id ==
|
||||
comment.by)
|
||||
SlidableAction(
|
||||
onPressed: (_) =>
|
||||
onEditTapped?.call(comment),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.edit,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (BuildContext context) =>
|
||||
onMoreTapped?.call(
|
||||
comment,
|
||||
context.rect,
|
||||
),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.more_horiz,
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
endActionPane: actionable
|
||||
? ActionPane(
|
||||
motion: const StretchMotion(),
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) =>
|
||||
onRightMoreTapped?.call(comment),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.av_timer,
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (actionable) {
|
||||
HapticFeedback.selectionClick();
|
||||
context.read<CollapseCubit>().collapse();
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
top: Dimens.pt6,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
comment.by,
|
||||
style: TextStyle(
|
||||
color: prefState.showEyeCandy
|
||||
? orange
|
||||
: color,
|
||||
),
|
||||
),
|
||||
if (comment.by == opUsername)
|
||||
const Text(
|
||||
' - OP',
|
||||
style: TextStyle(
|
||||
color: orange,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
comment.postedDate,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
top: Dimens.pt6,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
comment.by,
|
||||
style: TextStyle(
|
||||
color:
|
||||
prefState.showEyeCandy ? orange : color,
|
||||
),
|
||||
),
|
||||
if (comment.by == opUsername)
|
||||
const Text(
|
||||
' - OP',
|
||||
style: TextStyle(
|
||||
color: orange,
|
||||
),
|
||||
),
|
||||
if (actionable && state.collapsed)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
'collapsed '
|
||||
'(${state.collapsedCount + 1})',
|
||||
style: const TextStyle(
|
||||
color: Palette.orangeAccent,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (comment.deleted)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
'deleted',
|
||||
style: TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (comment.dead)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
'dead',
|
||||
style: TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (blocklistState.blocklist
|
||||
.contains(comment.by))
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
'blocked',
|
||||
style: TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt8,
|
||||
right: Dimens.pt8,
|
||||
top: Dimens.pt6,
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: comment is BuildableComment
|
||||
? SelectableText.rich(
|
||||
key: ValueKey<int>(comment.id),
|
||||
buildTextSpan(
|
||||
(comment as BuildableComment)
|
||||
.elements,
|
||||
style: TextStyle(
|
||||
fontSize: MediaQuery.of(
|
||||
context,
|
||||
).textScaleFactor *
|
||||
prefState.fontSize.fontSize,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: MediaQuery.of(
|
||||
context,
|
||||
).textScaleFactor *
|
||||
prefState.fontSize.fontSize,
|
||||
decoration:
|
||||
TextDecoration.underline,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) {
|
||||
if (link.url.isStoryLink) {
|
||||
onStoryLinkTapped
|
||||
.call(link.url);
|
||||
} else {
|
||||
LinkUtil.launch(link.url);
|
||||
}
|
||||
},
|
||||
),
|
||||
onTap: () => onTextTapped(context),
|
||||
)
|
||||
: SelectableLinkify(
|
||||
key: ValueKey<int>(comment.id),
|
||||
text: comment.text,
|
||||
style: TextStyle(
|
||||
fontSize: MediaQuery.of(context)
|
||||
.textScaleFactor *
|
||||
prefState.fontSize.fontSize,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: MediaQuery.of(context)
|
||||
.textScaleFactor *
|
||||
prefState.fontSize.fontSize,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) {
|
||||
if (link.url.isStoryLink) {
|
||||
onStoryLinkTapped
|
||||
.call(link.url);
|
||||
} else {
|
||||
LinkUtil.launch(link.url);
|
||||
}
|
||||
},
|
||||
onTap: () => onTextTapped(context),
|
||||
),
|
||||
),
|
||||
if (!state.collapsed &&
|
||||
fetchMode == FetchMode.lazy &&
|
||||
comment.kids.isNotEmpty &&
|
||||
!context
|
||||
.read<CommentsCubit>()
|
||||
.state
|
||||
.commentIds
|
||||
.contains(comment.kids.first) &&
|
||||
!context
|
||||
.read<CommentsCubit>()
|
||||
.state
|
||||
.onlyShowTargetComment)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt12,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
context
|
||||
.read<CommentsCubit>()
|
||||
.loadMore(
|
||||
comment: comment,
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'''Load ${comment.kids.length} ${comment.kids.length > 1 ? 'replies' : 'reply'}''',
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
const Spacer(),
|
||||
Text(
|
||||
comment.postedDate,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (actionable && state.collapsed)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
'collapsed '
|
||||
'(${state.collapsedCount + 1})',
|
||||
style: const TextStyle(
|
||||
color: Palette.orangeAccent,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (comment.deleted)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
'deleted',
|
||||
style: TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (comment.dead)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
'dead',
|
||||
style: TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (blocklistState.blocklist.contains(comment.by))
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
'blocked',
|
||||
style: TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt8,
|
||||
right: Dimens.pt8,
|
||||
top: Dimens.pt6,
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: comment is BuildableComment
|
||||
? SelectableText.rich(
|
||||
key: ValueKey<int>(comment.id),
|
||||
buildTextSpan(
|
||||
(comment as BuildableComment).elements,
|
||||
style: TextStyle(
|
||||
fontSize: MediaQuery.of(
|
||||
context,
|
||||
).textScaleFactor *
|
||||
prefState.fontSize.fontSize,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: MediaQuery.of(
|
||||
context,
|
||||
).textScaleFactor *
|
||||
prefState.fontSize.fontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) {
|
||||
if (link.url.isStoryLink) {
|
||||
onStoryLinkTapped.call(link.url);
|
||||
} else {
|
||||
LinkUtil.launch(link.url);
|
||||
}
|
||||
},
|
||||
),
|
||||
onTap: () => onTextTapped(context),
|
||||
)
|
||||
: SelectableLinkify(
|
||||
key: ValueKey<int>(comment.id),
|
||||
text: comment.text,
|
||||
style: TextStyle(
|
||||
fontSize: MediaQuery.of(context)
|
||||
.textScaleFactor *
|
||||
prefState.fontSize.fontSize,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: MediaQuery.of(context)
|
||||
.textScaleFactor *
|
||||
prefState.fontSize.fontSize,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) {
|
||||
if (link.url.isStoryLink) {
|
||||
onStoryLinkTapped.call(link.url);
|
||||
} else {
|
||||
LinkUtil.launch(link.url);
|
||||
}
|
||||
},
|
||||
onTap: () => onTextTapped(context),
|
||||
),
|
||||
),
|
||||
if (!state.collapsed &&
|
||||
fetchMode == FetchMode.lazy &&
|
||||
comment.kids.isNotEmpty &&
|
||||
!context
|
||||
.read<CommentsCubit>()
|
||||
.state
|
||||
.commentIds
|
||||
.contains(comment.kids.first) &&
|
||||
!context
|
||||
.read<CommentsCubit>()
|
||||
.state
|
||||
.onlyShowTargetComment)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt12,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
context.read<CommentsCubit>().loadMore(
|
||||
comment: comment,
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'''Load ${comment.kids.length} ${comment.kids.length > 1 ? 'replies' : 'reply'}''',
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final double commentBackgroundColorOpacity =
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? 0.03
|
||||
: 0.15;
|
||||
|
||||
final Color commentColor = prefState.showEyeCandy
|
||||
? color.withOpacity(commentBackgroundColorOpacity)
|
||||
: Palette.transparent;
|
||||
final bool isMyComment = myUsername == comment.by;
|
||||
|
||||
Widget wrapper = child;
|
||||
|
||||
if (isMyComment && level == 0) {
|
||||
return Container(
|
||||
color: Palette.orange.withOpacity(0.2),
|
||||
child: wrapper,
|
||||
);
|
||||
}
|
||||
|
||||
for (final int i in level.to(0, inclusive: false)) {
|
||||
final Color wrapperBorderColor = _getColor(i);
|
||||
final bool shouldHighlight = isMyComment && i == level;
|
||||
wrapper = Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: const EdgeInsets.only(
|
||||
left: Dimens.pt12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: i != 0
|
||||
? Border(
|
||||
left: BorderSide(
|
||||
color: wrapperBorderColor,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
color: shouldHighlight
|
||||
? Palette.orange.withOpacity(0.2)
|
||||
: commentColor,
|
||||
),
|
||||
child: wrapper,
|
||||
);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final double commentBackgroundColorOpacity =
|
||||
Theme.of(context).brightness == Brightness.dark ? 0.03 : 0.15;
|
||||
|
||||
final Color commentColor = prefState.showEyeCandy
|
||||
? color.withOpacity(commentBackgroundColorOpacity)
|
||||
: Palette.transparent;
|
||||
final bool isMyComment = myUsername == comment.by;
|
||||
|
||||
Widget wrapper = child;
|
||||
|
||||
if (isMyComment && level == 0) {
|
||||
return Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Palette.orange.withOpacity(0.2),
|
||||
),
|
||||
child: wrapper,
|
||||
);
|
||||
}
|
||||
|
||||
for (final int i in level.to(0, inclusive: false)) {
|
||||
final Color wrapperBorderColor = _getColor(i);
|
||||
final bool shouldHighlight = isMyComment && i == level;
|
||||
wrapper = Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: const EdgeInsets.only(
|
||||
left: Dimens.pt8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: i != 0
|
||||
? Border(
|
||||
left: BorderSide(
|
||||
color: wrapperBorderColor,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
color: shouldHighlight
|
||||
? Palette.orange.withOpacity(0.2)
|
||||
: commentColor,
|
||||
),
|
||||
child: wrapper,
|
||||
);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -16,6 +16,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
super.key,
|
||||
required this.showWebPreview,
|
||||
required this.showMetadata,
|
||||
required this.showUrl,
|
||||
required this.items,
|
||||
required this.onTap,
|
||||
required this.refreshController,
|
||||
@ -39,6 +40,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
final bool showCommentBy;
|
||||
final bool showWebPreview;
|
||||
final bool showMetadata;
|
||||
final bool showUrl;
|
||||
final bool enablePullDown;
|
||||
final bool markReadStories;
|
||||
final bool showOfflineBanner;
|
||||
@ -97,6 +99,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
onTap: () => onTap(e),
|
||||
showWebPreview: showWebPreview,
|
||||
showMetadata: showMetadata,
|
||||
showUrl: showUrl,
|
||||
hasRead: markReadStories && hasRead,
|
||||
simpleTileFontSize: useConsistentFontSize
|
||||
? TextDimens.pt14
|
||||
|
@ -15,6 +15,7 @@ class LinkPreview extends StatefulWidget {
|
||||
required this.link,
|
||||
required this.story,
|
||||
required this.showMetadata,
|
||||
required this.showUrl,
|
||||
required this.offlineReading,
|
||||
this.cache = const Duration(days: 30),
|
||||
this.titleStyle,
|
||||
@ -103,6 +104,7 @@ class LinkPreview extends StatefulWidget {
|
||||
final List<BoxShadow>? boxShadow;
|
||||
|
||||
final bool showMetadata;
|
||||
final bool showUrl;
|
||||
final bool offlineReading;
|
||||
|
||||
@override
|
||||
@ -180,6 +182,7 @@ class _LinkPreviewState extends State<LinkPreview> {
|
||||
key: widget.key ?? Key(widget.link),
|
||||
metadata: widget.story.simpleMetadata,
|
||||
url: widget.link,
|
||||
readableUrl: widget.story.readableUrl,
|
||||
title: widget.story.title,
|
||||
description: desc ?? title ?? 'no comment yet.',
|
||||
imageUri: imageUri,
|
||||
@ -194,6 +197,7 @@ class _LinkPreviewState extends State<LinkPreview> {
|
||||
bgColor: widget.backgroundColor,
|
||||
radius: widget.borderRadius ?? 12,
|
||||
showMetadata: widget.showMetadata,
|
||||
showUrl: widget.showUrl,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -8,10 +8,12 @@ class LinkView extends StatelessWidget {
|
||||
super.key,
|
||||
required this.metadata,
|
||||
required this.url,
|
||||
required this.readableUrl,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.onTap,
|
||||
required this.showMetadata,
|
||||
required this.showUrl,
|
||||
this.imageUri,
|
||||
this.imagePath,
|
||||
this.titleTextStyle,
|
||||
@ -30,6 +32,7 @@ class LinkView extends StatelessWidget {
|
||||
|
||||
final String metadata;
|
||||
final String url;
|
||||
final String readableUrl;
|
||||
final String title;
|
||||
final String description;
|
||||
final String? imageUri;
|
||||
@ -44,6 +47,7 @@ class LinkView extends StatelessWidget {
|
||||
final double radius;
|
||||
final Color? bgColor;
|
||||
final bool showMetadata;
|
||||
final bool showUrl;
|
||||
|
||||
double computeTitleFontSize(double width) {
|
||||
double size = width * 0.13;
|
||||
@ -146,6 +150,7 @@ class LinkView extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildTitleContainer(TextStyle _titleTS, int _maxLines) {
|
||||
final bool showUrl = this.showUrl && url.isNotEmpty;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4, 2, 3, 0),
|
||||
child: Column(
|
||||
@ -159,6 +164,22 @@ class LinkView extends StatelessWidget {
|
||||
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,
|
||||
style: _bodyTS,
|
||||
overflow: bodyTextOverflow ?? TextOverflow.ellipsis,
|
||||
maxLines:
|
||||
(bodyMaxLines ?? _maxLines) - (showMetadata ? 1 : 0),
|
||||
maxLines: (bodyMaxLines ?? _maxLines) -
|
||||
(showMetadata ? 1 : 0) -
|
||||
(showUrl && url.isNotEmpty ? 1 : 0),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -68,6 +68,7 @@ class _StoriesListViewState extends State<StoriesListView> {
|
||||
context.read<PreferenceCubit>().state.markReadStories,
|
||||
showWebPreview: preferenceState.showComplexStoryTile,
|
||||
showMetadata: preferenceState.showMetadata,
|
||||
showUrl: preferenceState.showUrl,
|
||||
refreshController: refreshController,
|
||||
items: state.storiesByType[storyType]!,
|
||||
onRefresh: () {
|
||||
|
@ -15,6 +15,7 @@ class StoryTile extends StatelessWidget {
|
||||
this.hasRead = false,
|
||||
required this.showWebPreview,
|
||||
required this.showMetadata,
|
||||
required this.showUrl,
|
||||
required this.story,
|
||||
required this.onTap,
|
||||
this.simpleTileFontSize = 16,
|
||||
@ -22,6 +23,7 @@ class StoryTile extends StatelessWidget {
|
||||
|
||||
final bool showWebPreview;
|
||||
final bool showMetadata;
|
||||
final bool showUrl;
|
||||
final bool hasRead;
|
||||
final Story story;
|
||||
final VoidCallback onTap;
|
||||
@ -42,90 +44,8 @@ class StoryTile extends StatelessWidget {
|
||||
story: story,
|
||||
link: story.url,
|
||||
offlineReading: context.read<StoriesBloc>().state.offlineReading,
|
||||
placeholderWidget: FadeIn(
|
||||
child: SizedBox(
|
||||
height: height,
|
||||
child: Shimmer.fromColors(
|
||||
baseColor: Palette.orange,
|
||||
highlightColor: Palette.orangeAccent,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: Dimens.pt5,
|
||||
bottom: Dimens.pt5,
|
||||
top: Dimens.pt5,
|
||||
),
|
||||
child: Container(
|
||||
height: height,
|
||||
width: height,
|
||||
color: Palette.white,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt4,
|
||||
top: Dimens.pt6,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: Dimens.pt14,
|
||||
color: Palette.white,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: Dimens.pt4,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: Dimens.pt10,
|
||||
color: Palette.white,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: Dimens.pt3,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: Dimens.pt10,
|
||||
color: Palette.white,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: Dimens.pt3,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: Dimens.pt10,
|
||||
color: Palette.white,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: Dimens.pt3,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: Dimens.pt40,
|
||||
height: Dimens.pt10,
|
||||
color: Palette.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
placeholderWidget: _LinkPreviewPlaceholder(
|
||||
height: height,
|
||||
),
|
||||
errorImage: Constants.hackerNewsLogoLink,
|
||||
backgroundColor: Palette.transparent,
|
||||
@ -136,10 +56,11 @@ class StoryTile extends StatelessWidget {
|
||||
titleStyle: TextStyle(
|
||||
color: hasRead
|
||||
? Palette.grey[500]
|
||||
: Theme.of(context).textTheme.subtitle1!.color,
|
||||
: Theme.of(context).textTheme.subtitle1?.color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
showMetadata: showMetadata,
|
||||
showUrl: showUrl,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -158,11 +79,31 @@ class StoryTile extends StatelessWidget {
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
story.title,
|
||||
style: TextStyle(
|
||||
color: hasRead ? Palette.grey[500] : null,
|
||||
fontSize: simpleTileFontSize,
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -193,3 +134,101 @@ class StoryTile extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
height: height,
|
||||
child: Shimmer.fromColors(
|
||||
baseColor: Palette.orange,
|
||||
highlightColor: Palette.orangeAccent,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: Dimens.pt5,
|
||||
bottom: Dimens.pt5,
|
||||
top: Dimens.pt5,
|
||||
),
|
||||
child: Container(
|
||||
height: height,
|
||||
width: height,
|
||||
color: Palette.white,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt4,
|
||||
top: Dimens.pt6,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: Dimens.pt14,
|
||||
color: Palette.white,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: Dimens.pt4,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: Dimens.pt10,
|
||||
color: Palette.white,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: Dimens.pt3,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: Dimens.pt10,
|
||||
color: Palette.white,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: Dimens.pt3,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: Dimens.pt10,
|
||||
color: Palette.white,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: Dimens.pt3,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: Dimens.pt40,
|
||||
height: Dimens.pt10,
|
||||
color: Palette.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
export 'bloc_builder_3.dart';
|
||||
export 'circle_tab_indicator.dart';
|
||||
export 'comment_tile.dart';
|
||||
export 'countdown_reminder.dart';
|
||||
|
@ -15,7 +15,7 @@ class CollapseCache {
|
||||
addIfParentIsHiddenOrCollapsed(commentId, to);
|
||||
}
|
||||
|
||||
int collapse(int commentId) {
|
||||
Set<int> collapse(int commentId) {
|
||||
_collapsed.add(commentId);
|
||||
|
||||
Set<int> findHiddenComments(int commentId) {
|
||||
@ -35,7 +35,7 @@ class CollapseCache {
|
||||
|
||||
_hiddenCommentsSubject.add(_hidden);
|
||||
|
||||
return hiddenComments.length;
|
||||
return hiddenComments;
|
||||
}
|
||||
|
||||
void uncollapse(int commentId) {
|
||||
|
@ -31,6 +31,7 @@ abstract class TextDimens {
|
||||
static const double pt14 = 14;
|
||||
static const double pt15 = 15;
|
||||
static const double pt16 = 16;
|
||||
static const double pt17 = 17;
|
||||
static const double pt18 = 18;
|
||||
static const double pt20 = 20;
|
||||
static const double pt24 = 24;
|
||||
|
@ -2,6 +2,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/main.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
@ -35,7 +36,7 @@ abstract class LinkUtil {
|
||||
}
|
||||
|
||||
Uri rinseLink(String link) {
|
||||
final RegExp regex = RegExp(r'\)|].*$');
|
||||
final RegExp regex = RegExp(RegExpConstants.linkSuffix);
|
||||
if (!link.contains('en.wikipedia.org') && link.contains(regex)) {
|
||||
final String match = regex.stringMatch(link) ?? '';
|
||||
return Uri.parse(link.replaceAll(match, ''));
|
||||
|
14
pubspec.lock
14
pubspec.lock
@ -607,6 +607,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1,6 +1,6 @@
|
||||
name: hacki
|
||||
description: A Hacker News reader.
|
||||
version: 0.2.33+76
|
||||
version: 1.0.2+80
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@ -18,7 +18,6 @@ dependencies:
|
||||
dio: ^4.0.4
|
||||
equatable: ^2.0.5
|
||||
fast_gbk: ^1.0.0
|
||||
# feature_discovery: ^0.14.0
|
||||
feature_discovery:
|
||||
git:
|
||||
url: https://github.com/livinglist/feature_discovery
|
||||
@ -45,11 +44,11 @@ dependencies:
|
||||
hydrated_bloc: ^9.0.0-dev.3
|
||||
intl: ^0.17.0
|
||||
logger: ^1.1.0
|
||||
package_info_plus: ^3.0.2
|
||||
path: ^1.8.0
|
||||
path_provider: ^2.0.8
|
||||
path_provider_android: ^2.0.8
|
||||
path_provider_ios: ^2.0.8
|
||||
# pull_to_refresh: ^2.0.0
|
||||
pull_to_refresh:
|
||||
git:
|
||||
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